codemirror option for text widgets & danbooru tag autocomplete

This commit is contained in:
space-nuko
2023-05-24 11:47:08 -05:00
parent 6bbd18a261
commit 18c63c9f2e
15 changed files with 100697 additions and 25 deletions

View File

@@ -42,6 +42,13 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.3.0",
"@codemirror/commands": "^6.1.2",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.2.2",
"@codemirror/state": "^6.1.2",
"@codemirror/view": "^6.4.1",
"@gradio/accordion": "workspace:*", "@gradio/accordion": "workspace:*",
"@gradio/atoms": "workspace:*", "@gradio/atoms": "workspace:*",
"@gradio/button": "workspace:*", "@gradio/button": "workspace:*",
@@ -67,6 +74,11 @@
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^4.0.1",
"@zerodevx/svelte-json-view": "^1.0.5", "@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-light": "^0.2.0",
"codemirror": "^6.0.1",
"csv": "^6.3.0",
"csv-parse": "^5.3.10",
"events": "^3.3.0", "events": "^3.3.0",
"framework7": "^8.0.3", "framework7": "^8.0.3",
"framework7-svelte": "^8.0.3", "framework7-svelte": "^8.0.3",
@@ -74,7 +86,9 @@
"klecks": "workspace:*", "klecks": "workspace:*",
"pollen-css": "^4.6.2", "pollen-css": "^4.6.2",
"radix-icons-svelte": "^1.2.1", "radix-icons-svelte": "^1.2.1",
"style-mod": "^4.0.3",
"svelte-bootstrap-icons": "^2.3.1", "svelte-bootstrap-icons": "^2.3.1",
"svelte-codemirror-editor": "^1.1.0",
"svelte-feather-icons": "^4.0.0", "svelte-feather-icons": "^4.0.0",
"svelte-preprocess": "^5.0.3", "svelte-preprocess": "^5.0.3",
"svelte-select": "^5.5.3", "svelte-select": "^5.5.3",

127
pnpm-lock.yaml generated
View File

@@ -4,6 +4,27 @@ importers:
.: .:
dependencies: dependencies:
'@codemirror/autocomplete':
specifier: ^6.3.0
version: 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
'@codemirror/commands':
specifier: ^6.1.2
version: 6.2.4
'@codemirror/language':
specifier: ^6.6.0
version: 6.6.0
'@codemirror/lint':
specifier: ^6.0.0
version: 6.2.1
'@codemirror/search':
specifier: ^6.2.2
version: 6.4.0
'@codemirror/state':
specifier: ^6.1.2
version: 6.2.0
'@codemirror/view':
specifier: ^6.4.1
version: 6.11.0
'@gradio/accordion': '@gradio/accordion':
specifier: workspace:* specifier: workspace:*
version: link:gradio/js/accordion version: link:gradio/js/accordion
@@ -79,6 +100,21 @@ importers:
canvas-to-svg: canvas-to-svg:
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3 version: 1.0.3
cm6-theme-basic-dark:
specifier: ^0.2.0
version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
cm6-theme-basic-light:
specifier: ^0.2.0
version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
codemirror:
specifier: ^6.0.1
version: 6.0.1
csv:
specifier: ^6.3.0
version: 6.3.0
csv-parse:
specifier: ^5.3.10
version: 5.3.10
events: events:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.0 version: 3.3.0
@@ -100,9 +136,15 @@ importers:
radix-icons-svelte: radix-icons-svelte:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
style-mod:
specifier: ^4.0.3
version: 4.0.3
svelte-bootstrap-icons: svelte-bootstrap-icons:
specifier: ^2.3.1 specifier: ^2.3.1
version: 2.3.1 version: 2.3.1
svelte-codemirror-editor:
specifier: ^1.1.0
version: 1.1.0(codemirror@6.0.1)
svelte-feather-icons: svelte-feather-icons:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@@ -1341,6 +1383,19 @@ packages:
commander: 2.20.3 commander: 2.20.3
dev: false dev: false
/@codemirror/autocomplete@6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0):
resolution: {integrity: sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==}
peerDependencies:
'@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
dependencies:
'@codemirror/language': 6.6.0
'@codemirror/state': 6.2.0
'@codemirror/view': 6.11.0
'@lezer/common': 1.0.2
dev: false
/@codemirror/autocomplete@6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2): /@codemirror/autocomplete@6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2):
resolution: {integrity: sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==} resolution: {integrity: sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==}
peerDependencies: peerDependencies:
@@ -3872,6 +3927,19 @@ packages:
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
dev: false dev: false
/cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0):
resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==}
peerDependencies:
'@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
'@lezer/highlight': ^1.0.0
dependencies:
'@codemirror/language': 6.6.0
'@codemirror/state': 6.2.0
'@codemirror/view': 6.11.0
dev: false
/cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4): /cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4):
resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==} resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==}
peerDependencies: peerDependencies:
@@ -3886,6 +3954,19 @@ packages:
'@lezer/highlight': 1.1.4 '@lezer/highlight': 1.1.4
dev: false dev: false
/cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0):
resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==}
peerDependencies:
'@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
'@lezer/highlight': ^1.0.0
dependencies:
'@codemirror/language': 6.6.0
'@codemirror/state': 6.2.0
'@codemirror/view': 6.11.0
dev: false
/cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4): /cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4):
resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==} resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==}
peerDependencies: peerDependencies:
@@ -3909,6 +3990,18 @@ packages:
resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==} resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==}
dev: false dev: false
/codemirror@6.0.1:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
dependencies:
'@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
'@codemirror/commands': 6.2.4
'@codemirror/language': 6.6.0
'@codemirror/lint': 6.2.1
'@codemirror/search': 6.4.0
'@codemirror/state': 6.2.0
'@codemirror/view': 6.11.0
dev: false
/codemirror@6.0.1(@lezer/common@1.0.2): /codemirror@6.0.1(@lezer/common@1.0.2):
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
dependencies: dependencies:
@@ -4133,6 +4226,28 @@ packages:
rrweb-cssom: 0.6.0 rrweb-cssom: 0.6.0
dev: true dev: true
/csv-generate@4.2.6:
resolution: {integrity: sha512-VtnYqhWLcsUocA346ewFOk+rrqcoT663j11vXzD2uelXq9WguQ3QzDeVD8ISso7hhVtkDSHcWl9psdemeiEHDA==}
dev: false
/csv-parse@5.3.10:
resolution: {integrity: sha512-cTXY6iy0gN5Ha/cGILeDgQE+nKiKDU2m0DjSRdJhr86BN3cM7oefBsTk2aH0LQeaYtL7Z7HvW+jYoadmdhzeDA==}
dev: false
/csv-stringify@6.4.0:
resolution: {integrity: sha512-HQsw0QXiN5fdlO+R8/JzCZnR3Fqp8E87YVnhHlaPtNGJjt6ffbV0LpOkieIb1x6V1+xt878IYq77SpXHWAqKkA==}
dev: false
/csv@6.3.0:
resolution: {integrity: sha512-wXakaMNIz6qe/a15AXQ6JA9urN2lEx08J/3xpxv7Ke3GOrio300Ikbqzrlg1ML0fuEOULHhswvSxtN5h/72sHg==}
engines: {node: '>= 0.1.90'}
dependencies:
csv-generate: 4.2.6
csv-parse: 5.3.10
csv-stringify: 6.4.0
stream-transform: 3.2.6
dev: false
/d3-array@3.2.2: /d3-array@3.2.2:
resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==} resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -7801,6 +7916,10 @@ packages:
/std-env@3.3.3: /std-env@3.3.3:
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
/stream-transform@3.2.6:
resolution: {integrity: sha512-/pyOvaCQFqYTmrFhmMbnAEVo3SsTx1H39eUVPOtYeAgbEUc+rDo7GoP8LbHJgU83mKtzJe/7Nq/ipaAnUOHgJQ==}
dev: false
/streamsearch@1.1.0: /streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -7978,6 +8097,14 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-codemirror-editor@1.1.0(codemirror@6.0.1):
resolution: {integrity: sha512-wFdMIsZds5qzn3x2NbFUxDVU6Cn3rwFdq0035ypaFVgzTjJ90bnPm6IbrFA4OJz1ngIyfbIuPAPDjm7rJIr0gg==}
peerDependencies:
codemirror: ^6.0.0
dependencies:
codemirror: 6.0.1
dev: false
/svelte-dnd-action@0.9.22(svelte@3.58.0): /svelte-dnd-action@0.9.22(svelte@3.58.0):
resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==} resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==}
peerDependencies: peerDependencies:

100000
public/extra/danbooru.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import { download } from "./utils";
* components they represent in the UI. * components they represent in the UI.
*/ */
export type ComfyBoxTemplate = { export type ComfyBoxTemplate = {
version: 1,
nodes: LGraphNode[], nodes: LGraphNode[],
links: LLink[], links: LLink[],
container?: DragItemEntry container?: DragItemEntry
@@ -20,6 +21,8 @@ export type ComfyBoxTemplate = {
* components they represent in the UI. * components they represent in the UI.
*/ */
export type SerializedComfyBoxTemplate = { export type SerializedComfyBoxTemplate = {
version: 1,
/* /*
* Serialized nodes * Serialized nodes
*/ */
@@ -270,6 +273,7 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
[nodes, links] = pruneDetachedLinks(nodes, links); [nodes, links] = pruneDetachedLinks(nodes, links);
let comfyBoxTemplate: SerializedComfyBoxTemplate = { let comfyBoxTemplate: SerializedComfyBoxTemplate = {
version: 1,
nodes: nodes, nodes: nodes,
links: links, links: links,
layout: layout layout: layout
@@ -320,6 +324,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
} }
return { return {
version: 1,
nodes: nodes, nodes: nodes,
links: links, links: links,
container: container container: container
@@ -328,6 +333,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
else { else {
// No UI to serialize. // No UI to serialize.
return { return {
version: 1,
nodes: nodes, nodes: nodes,
links: links, links: links,
} }

198
src/lib/DanbooruTags.ts Normal file
View File

@@ -0,0 +1,198 @@
import { parse } from 'csv-parse/browser/esm/sync';
import { timeExecutionMs } from './utils';
import { insertCompletionText, type Completion, type CompletionContext, type CompletionResult, type CompletionSource, type CompletionConfig, autocompletion } from '@codemirror/autocomplete';
import { syntaxTree } from '@codemirror/language';
import type { Extension, TransactionSpec } from '@codemirror/state';
import type { EditorView } from '@codemirror/view';
import type { StyleSpec } from "style-mod"
export enum DanbooruTagCategory {
General = 0,
Artist = 1,
Copyright = 3,
Character = 4,
}
export type DanbooruTagCategoryData = {
name: string,
color: string
}
const TAG_CATEGORY_DATA: Record<DanbooruTagCategory, DanbooruTagCategoryData> = {
[DanbooruTagCategory.General]: {
name: "general",
color: "lightblue"
},
[DanbooruTagCategory.Artist]: {
name: "artist",
color: "red",
},
[DanbooruTagCategory.Copyright]: {
name: "copyright",
color: "lightpurple"
},
[DanbooruTagCategory.Character]: {
name: "character",
color: "green"
}
}
export const TAG_CATEGORY_COLORS: StyleSpec = Object.values(TAG_CATEGORY_DATA)
.flatMap(d => {
return [
[`.cm-autocompletion-${d.name}`, { color: d.color + " !important" }],
]
})
.reduce((dict, el) => (dict[el[0]] = el[1], dict), {})
export type DanbooruTag = {
text: string,
category: DanbooruTagCategory,
count: number,
aliases: string[]
}
function escapeRegExp(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function formatPostCount(postCount: number): string {
if (!postCount || !isNaN(postCount))
return ""
let formatter: Intl.NumberFormat;
// Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k
if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000))
formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 });
else
formatter = Intl.NumberFormat("en", { notation: "compact" });
return formatter.format(postCount);
}
export default class DanbooruTags {
private static _instance: DanbooruTags;
tags: DanbooruTag[] = [];
tagsByCategory: Record<string, DanbooruTag> = {}
aliases: Record<string, number> = {}
static get instance(): DanbooruTags {
if (!DanbooruTags._instance)
DanbooruTags._instance = new DanbooruTags()
return DanbooruTags._instance
}
async load(force: boolean = false) {
console.log("Parsing danbooru tags CSV...")
if (this.tags.length > 0 && !force) {
console.info("Danbooru tags already parsed")
return;
}
this.tags = []
const transformTag = (data: string[]): DanbooruTag => {
return {
text: data[0],
category: parseInt(data[1]),
count: parseInt(data[2]),
aliases: data[3].split(",")
}
}
const time = await timeExecutionMs(async () => {
const resp = await fetch("/extra/danbooru.csv");
const csv = await resp.text();
const raw: string[][] = parse(csv, {
delimiter: ','
});
const refined = raw.map(transformTag);
this.tags = refined
})
console.log(`Parsed ${this.tags.length} tags in ${time / 1000}ms.`)
console.error(this.tags[0])
}
autocomplete(context: CompletionContext): CompletionResult {
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
let weightBefore = /:[0-9.]+$/.exec(textBefore)
if (weightBefore) return null
let tagBefore = /\b[a-zA-Z0-9_()-]+$/.exec(textBefore)
if (!tagBefore) return null
let tagword = tagBefore[0]
console.warn(tagword)
let searchRegex: RegExp;
if (tagword.startsWith("*")) {
tagword = tagword.slice(1);
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
} else {
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
}
const sanitize = (rawTag: string): string => {
let sanitized = rawTag.replaceAll("_", " ");
// TODO config
const escapeParentheses = true;
const isTagType = true;
if (escapeParentheses && isTagType) {
sanitized = sanitized
.replaceAll("(", "\\(")
.replaceAll(")", "\\)")
.replaceAll("[", "\\[")
.replaceAll("]", "\\]");
}
return sanitized
}
const apply = (view: EditorView, completion: Completion, from: number, to: number) => {
const sanitized = sanitize(completion.label);
view.dispatch(insertCompletionText(view.state, sanitized, from, to));
}
const filter = (x: DanbooruTag) => x.text.toLowerCase().search(searchRegex) > -1;
const options: Completion[] = this.tags.filter(filter).map(t => {
const categoryName = TAG_CATEGORY_DATA[t.category]?.name || "unknown";
return {
label: t.text,
apply,
detail: formatPostCount(t.count),
type: categoryName,
section: "Tags"
}
})
return {
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
options,
validFor: /^\b([\w_()-]+)?$/
}
}
static getCompletionExt(): Extension {
const source: CompletionSource = DanbooruTags.instance.autocomplete.bind(DanbooruTags.instance)
const optionClass = (completion: Completion): string => {
return `cm-autocompletion-${completion.type}`
}
return autocompletion({
override: [source],
interactionDelay: 250,
optionClass
})
}
}

View File

@@ -6,7 +6,7 @@ export class ImageViewer {
currentImages: string[] = [] currentImages: string[] = []
selectedIndex: number = -1; selectedIndex: number = -1;
currentGallery: HTMLDivElement | null = null; currentGallery: HTMLDivElement | null = null;
static _instance: ImageViewer; private static _instance: ImageViewer;
static get instance(): ImageViewer { static get instance(): ImageViewer {
if (!ImageViewer._instance) if (!ImageViewer._instance)

View File

@@ -34,6 +34,7 @@ import { tick } from "svelte";
import { type SvelteComponentDev } from "svelte/internal"; import { type SvelteComponentDev } from "svelte/internal";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer"; import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
import DanbooruTags from "$lib/DanbooruTags";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -232,6 +233,8 @@ export default class ComfyApp {
await this.updateHistoryAndQueue(); await this.updateHistoryAndQueue();
await this.initFrontendFeatures();
// await this.#invokeExtensionsAsync("setup"); // await this.#invokeExtensionsAsync("setup");
// Ensure the canvas fills the window // Ensure the canvas fills the window
@@ -586,6 +589,10 @@ export default class ComfyApp {
}); });
} }
private async initFrontendFeatures() {
await DanbooruTags.instance.load();
}
private async updateHistoryAndQueue() { private async updateHistoryAndQueue() {
const queue = await this.api.getQueue(); const queue = await this.api.getQueue();
const history = await this.api.getHistory(); const history = await this.api.getHistory();

View File

@@ -3,8 +3,9 @@ import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
import layoutStates from './stores/layoutStates'; import layoutStates from './stores/layoutStates';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import workflowState from './stores/workflowState'; import workflowState from './stores/workflowState';
import DanbooruTags from './DanbooruTags';
export function configureLitegraph(isMobile: boolean = false) { function configureLitegraph(isMobile: boolean = false) {
LiteGraph.catch_exceptions = false; LiteGraph.catch_exceptions = false;
// Must be enabled, otherwise subgraphs won't work (because of non-unique node/link IDs) // Must be enabled, otherwise subgraphs won't work (because of non-unique node/link IDs)
@@ -26,10 +27,18 @@ export function configureLitegraph(isMobile: boolean = false) {
} }
Subgraph.default_lgraph_factory = () => new ComfyGraph; Subgraph.default_lgraph_factory = () => new ComfyGraph;
}
(window as any).LiteGraph = LiteGraph;
(window as any).LGraphCanvas = LGraphCanvas; function configureGlobals() {
(window as any).layoutStates = layoutStates; const win = window as any
(window as any).workflowState = workflowState; win.LiteGraph = LiteGraph;
(window as any).svelteGet = get; win.LGraphCanvas = LGraphCanvas;
win.layoutStates = layoutStates;
win.workflowState = workflowState;
win.svelteGet = get;
}
export default async function init(isMobile: boolean = false) {
configureLitegraph(isMobile);
configureGlobals();
} }

View File

@@ -405,7 +405,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "" defaultValue: ""
}, },
// Editor // Image Editor
{ {
name: "variant", name: "variant",
type: "enum", type: "enum",
@@ -430,6 +430,16 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
}, },
// Text // Text
{
name: "variant",
type: "enum",
location: "widget",
editable: true,
validNodeTypes: ["ui/text"],
values: ["text", "code"],
defaultValue: "text",
refreshPanelOnChange: true
},
{ {
name: "multiline", name: "multiline",
type: "boolean", type: "boolean",

View File

@@ -52,6 +52,14 @@ export function* enumerate<T>(iterable: Iterable<T>): Iterable<[number, T]> {
} }
} }
export async function timeExecutionMs(fn: (...any) => Promise<any>, ...args: any[]): Promise<number> {
const start = new Date().getTime();
await fn.apply(null, args)
return new Date().getTime() - start;
}
export function download(filename: string, text: string, type: string = "text/plain") { export function download(filename: string, text: string, type: string = "text/plain") {
const blob = new Blob([text], { type: type }); const blob = new Blob([text], { type: type });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);

View File

@@ -6,6 +6,7 @@
import type { ComfyTextNode } from "$lib/nodes/widgets"; import type { ComfyTextNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
import TextWidgetCodeVariant from "./TextWidgetCodeVariant.svelte"
let node: ComfyTextNode | null = null; let node: ComfyTextNode | null = null;
let nodeValue: Writable<string> | null = null; let nodeValue: Writable<string> | null = null;
@@ -31,6 +32,9 @@
<div class="wrapper gradio-textbox"> <div class="wrapper gradio-textbox">
{#if node !== null && nodeValue !== null} {#if node !== null && nodeValue !== null}
{#if widget.attrs.variant === "code"}
<TextWidgetCodeVariant {widget} {node} {nodeValue} />
{:else}
<TextBox <TextBox
bind:value={$nodeValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
@@ -44,6 +48,7 @@
on:select on:select
/> />
{/if} {/if}
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -0,0 +1,233 @@
<script lang="ts">
import type { ComfyTextNode } from "$lib/nodes/widgets";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { writable, type Writable } from "svelte/store";
import type { ViewUpdate } from "@codemirror/view";
import { EditorView, keymap, placeholder as placeholderExt } from "@codemirror/view";
import { StateEffect, EditorState, type Extension } from "@codemirror/state";
import { basicDark } from "cm6-theme-basic-dark";
import { basicLight } from "cm6-theme-basic-light";
import { basicSetup } from "./TextWidgetCodeVariant";
import { createEventDispatcher, onMount } from "svelte";
import { TAG_CATEGORY_COLORS } from "$lib/DanbooruTags";
export let widget: WidgetLayout;
export let node: ComfyTextNode;
export let nodeValue: Writable<string> = writable("");
export let extraExtensions: Extension[] = [];
let lines = 5;
let classNames = ""
let element: HTMLDivElement;
let view: EditorView;
const dispatch = createEventDispatcher<{ change: string }>();
$: lines = node?.properties?.lines || 5;
let BaseTheme: Extension = EditorView.theme({
"&": {
width: "100%",
maxWidth: "100%",
height: "12rem",
fontSize: "var(--text-sm)",
backgroundColor: "var(--input-background-fill)"
},
".cm-content": {
paddingTop: "5px",
paddingBottom: "5px",
color: "var(--body-text-color)",
fontFamily: "var(--font-mono)",
minHeight: "100%"
},
".cm-gutters": {
marginRight: "1px",
borderRight: "1px solid var(--border-color-primary)",
backgroundColor: "transparent",
color: "var(--body-text-color-subdued)"
},
".cm-focused": {
outline: "none"
},
".cm-scroller": {
height: "auto"
},
".cm-cursor": {
borderLeftColor: "var(--body-text-color)"
},
".cm-selectionBackground": {
backgroundColor: "var(--secondary-600) !important",
},
".cm-tooltip": {
backgroundColor: "var(--panel-background-fill) !important",
border: "1px solid var(--panel-border-color) !important",
},
".cm-tooltip-autocomplete": {
color: "var(--body-text-color) !important",
},
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
color: "unset"
},
...TAG_CATEGORY_COLORS
})
onMount(() => {
view = createEditorView();
return () => view?.destroy();
});
$: reconfigure()
$: setDoc($nodeValue);
$: updateLines(lines);
function reconfigure(): void {
view?.dispatch({
effects: StateEffect.reconfigure.of(getExtensions())
});
}
function setDoc(newDoc: string) {
if (view && newDoc !== view.state.doc.toString()) {
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: newDoc
}
});
}
}
function updateLines(newLines: number) {
if (view) {
view.requestMeasure({ read: updateGutters });
}
}
function getGutterLineHeight(view: EditorView): string | null {
let elements = view.dom.querySelectorAll<HTMLElement>(".cm-gutterElement");
if (elements.length === 0) {
return null;
}
for (var i = 0; i < elements.length; i++) {
let node = elements[i];
let height = getComputedStyle(node)?.height ?? "0px";
if (height != "0px") {
return height;
}
}
return null;
}
function updateGutters(view: EditorView): any {
let gutters = view.dom.querySelectorAll<HTMLElement>(".cm-gutter");
let _lines = lines + 1;
let lineHeight = getGutterLineHeight(view);
if (!lineHeight) {
return null;
}
// for (var i = 0; i < gutters.length; i++) {
// let node = gutters[i];
// node.style.minHeight = `calc(${lineHeight} * ${_lines})`;
// }
return null;
}
function handleChange(vu: ViewUpdate): void {
if (vu.docChanged) {
const doc = vu.state.doc;
const text = doc.toString();
$nodeValue = text;
dispatch("change", text);
}
view.requestMeasure({ read: updateGutters });
}
function getBaseExtensions(readonly: boolean, placeholder: string | null): Extension[] {
const extensions: Extension[] = [
EditorView.editable.of(!readonly),
EditorState.readOnly.of(readonly)
];
extensions.push(basicSetup);
if (placeholder) {
extensions.push(placeholderExt(placeholder));
}
extensions.push(EditorView.updateListener.of(handleChange));
return extensions
}
function getTheme(dark_mode: boolean): Extension[] {
const extensions: Extension[] = [];
if (dark_mode) {
extensions.push(basicDark);
} else {
extensions.push(basicLight);
}
return extensions;
}
function getExtensions(): Extension[] {
// TODO
const readonly = false;
const placeholder = "Placeholder..."
const dark_mode = true;
const stateExtensions: Extension[] = [
...getBaseExtensions(
readonly,
placeholder,
),
BaseTheme,
...getTheme(dark_mode),
...extraExtensions
];
return stateExtensions;
}
function createEditorState(value: string | null | undefined): EditorState {
return EditorState.create({
doc: value ?? undefined,
extensions: getExtensions()
});
}
function createEditorView(): EditorView {
return new EditorView({
parent: element,
state: createEditorState($nodeValue)
});
}
</script>
<div class="wrap">
<div class="codemirror-wrapper {classNames}" bind:this={element} />
</div>
<!-- <CodeMirror bind:value={$nodeValue} {styles} /> -->
<style lang="scss">
.wrap {
display: flex;
flex-direction: column;
flex-flow: column;
margin: 0;
padding: 0;
height: 100%;
border: 1px solid var(--input-border-color);
.codemirror-wrapper {
height: 100%;
overflow: auto;
}
}
:global(.cm-scroller) {
height: 100% !important;
}
</style>

View File

@@ -0,0 +1,55 @@
import type { Extension } from "@codemirror/state";
import {
lineNumbers,
highlightSpecialChars,
drawSelection,
rectangularSelection,
crosshairCursor,
keymap
} from "@codemirror/view";
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import {
foldGutter,
indentOnInput,
syntaxHighlighting,
defaultHighlightStyle,
foldKeymap
} from "@codemirror/language";
import { history, defaultKeymap, historyKeymap } from "@codemirror/commands";
import {
closeBrackets,
closeBracketsKeymap,
completionKeymap
} from "@codemirror/autocomplete";
import { lintKeymap } from "@codemirror/lint";
import {
type CompletionSource, autocompletion, CompletionContext, startCompletion,
currentCompletions, completionStatus, completeFromList, acceptCompletion
} from "@codemirror/autocomplete"
import DanbooruTags from "$lib/DanbooruTags";
export const basicSetup: Extension = /*@__PURE__*/ (() => [
lineNumbers(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
closeBrackets(),
rectangularSelection(),
crosshairCursor(),
EditorView.lineWrapping,
DanbooruTags.getCompletionExt(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
])
])();

View File

@@ -2,10 +2,10 @@
import "$lib/nodeImports"; import "$lib/nodeImports";
import ComfyApp from '$lib/components/ComfyApp'; import ComfyApp from '$lib/components/ComfyApp';
import { configureLitegraph } from '$lib/init'; import init from '$lib/init';
import App from './App.svelte'; import App from './App.svelte';
configureLitegraph() await init();
const comfyApp = new ComfyApp(); const comfyApp = new ComfyApp();
(window as any).app = comfyApp; (window as any).app = comfyApp;

View File

@@ -9,11 +9,11 @@ import ComfyApp from '$lib/components/ComfyApp';
import uiState from '$lib/stores/uiState'; import uiState from '$lib/stores/uiState';
import { LiteGraph } from '@litegraph-ts/core'; import { LiteGraph } from '@litegraph-ts/core';
import ComfyGraph from '$lib/ComfyGraph'; import ComfyGraph from '$lib/ComfyGraph';
import { configureLitegraph } from '$lib/init'; import init from '$lib/init';
Framework7.use(Framework7Svelte); Framework7.use(Framework7Svelte);
configureLitegraph(true); await init(true);
const comfyApp = new ComfyApp(); const comfyApp = new ComfyApp();
(window as any).app = comfyApp; (window as any).app = comfyApp;