Skip to content

Commit

Permalink
Node preview on focus (#116)
Browse files Browse the repository at this point in the history
* Basic preview

* Adjust position

* Fix node display

* nit

* handle combo default value

* nit

* Custom AutoComplete
  • Loading branch information
huchenlei authored Jul 12, 2024
1 parent 3ac793b commit 605faf0
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 4 deletions.
260 changes: 260 additions & 0 deletions src/components/NodePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
<!-- Reference:
https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c683087a3e168db/app/js/functions/sb_fn.js#L149
-->

<template>
<div id="previewDiv">
<div class="sb_table">
<div class="node_header">
<div class="sb_dot headdot"></div>
{{ nodeDef.display_name }}
</div>
<div class="sb_preview_badge">PREVIEW</div>

<!-- Node slot I/O -->
<div
v-for="[slotInput, slotOutput] in _.zip(slotInputDefs, allOutputDefs)"
class="sb_row slot_row"
>
<div class="sb_col">
<div v-if="slotInput" :class="['sb_dot', slotInput.type]"></div>
</div>
<div class="sb_col">{{ slotInput ? slotInput.name : "" }}</div>
<div class="sb_col middle-column"></div>
<div class="sb_col sb_inherit">
{{ slotOutput ? slotOutput.name : "" }}
</div>
<div class="sb_col">
<div v-if="slotOutput" :class="['sb_dot', slotOutput.type]"></div>
</div>
</div>

<!-- Node widget inputs -->
<div v-for="widgetInput in widgetInputDefs" class="sb_row long_field">
<div class="sb_col sb_arrow">&#x25C0;</div>
<div class="sb_col">{{ widgetInput.name }}</div>
<div class="sb_col middle-column"></div>
<div class="sb_col sb_inherit">{{ widgetInput.defaultValue }}</div>
<div class="sb_col sb_arrow">&#x25B6;</div>
</div>
</div>
<div class="sb_description" v-if="nodeDef.description">
{{ nodeDef.description }}
</div>
</div>
</template>

<script setup lang="ts">
import { app } from "@/scripts/app";
import { type ComfyNodeDef } from "@/types/apiTypes";
import _ from "lodash";
import { PropType } from "vue";

const props = defineProps({
nodeDef: {
type: Object as PropType<ComfyNodeDef>,
required: true,
},
// Make sure vue properly re-render the component when the nodeDef changes
key: {
type: String,
required: true,
},
});

const nodeDef = props.nodeDef as ComfyNodeDef;

// --------------------------------------------------
// TODO: Move out to separate file
interface IComfyNodeInputDef {
name: string;
type: string;
widgetType: string | null;
defaultValue: any;
}

interface IComfyNodeOutputDef {
name: string | null;
type: string;
isList: boolean;
}

const allInputs = Object.assign(
{},
nodeDef.input.required || {},
nodeDef.input.optional || {}
);
const allInputDefs: IComfyNodeInputDef[] = Object.entries(allInputs).map(
([inputName, inputData]) => {
return {
name: inputName,
type: inputData[0],
widgetType: app.getWidgetType(inputData, inputName),
defaultValue:
inputData[1]?.default ||
(inputData[0] instanceof Array ? inputData[0][0] : ""),
};
}
);

const allOutputDefs: IComfyNodeOutputDef[] = _.zip(
nodeDef.output,
nodeDef.output_name || [],
nodeDef.output_is_list || []
).map(([outputType, outputName, isList]) => {
return {
name: outputName,
type: outputType instanceof Array ? "COMBO" : outputType,
isList: isList,
};
});

const slotInputDefs = allInputDefs.filter((input) => !input.widgetType);
const widgetInputDefs = allInputDefs.filter((input) => !!input.widgetType);
</script>

<style scoped>
.slot_row {
padding: 2px;
}

/* Original N-SideBar styles */
.sb_dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: grey;
}

.node_header {
line-height: 1;
padding: 8px 13px 7px;
background: var(--comfy-input-bg);
margin-bottom: 5px;
font-size: 15px;
text-wrap: nowrap;
overflow: hidden;
display: flex;
align-items: center;
}

.headdot {
width: 10px;
height: 10px;
float: inline-start;
margin-right: 8px;
}

.IMAGE {
background-color: #64b5f6;
}

.VAE {
background-color: #ff6e6e;
}

.LATENT {
background-color: #ff9cf9;
}

.MASK {
background-color: #81c784;
}

.CONDITIONING {
background-color: #ffa931;
}

.CLIP {
background-color: #ffd500;
}

.MODEL {
background-color: #b39ddb;
}

.CONTROL_NET {
background-color: #a5d6a7;
}

#previewDiv {
background-color: var(--comfy-menu-bg);
font-family: "Open Sans", sans-serif;
font-size: small;
color: var(--descrip-text);
border: 1px solid var(--descrip-text);
min-width: 300px;
width: min-content;
height: fit-content;
z-index: 9999;
border-radius: 12px;
overflow: hidden;
font-size: 12px;
padding-bottom: 10px;
}

#previewDiv .sb_description {
margin: 10px;
padding: 6px;
background: var(--border-color);
border-radius: 5px;
font-style: italic;
font-weight: 500;
font-size: 0.9rem;
}

.sb_table {
display: grid;

grid-column-gap: 10px;
/* Spazio tra le colonne */
width: 100%;
/* Imposta la larghezza della tabella al 100% del contenitore */
}

.sb_row {
display: grid;
grid-template-columns: 10px 1fr 1fr 1fr 10px;
grid-column-gap: 10px;
align-items: center;
padding-left: 9px;
padding-right: 9px;
}

.sb_row_string {
grid-template-columns: 10px 1fr 1fr 10fr 1fr;
}

.sb_col {
border: 0px solid #000;
display: flex;
align-items: flex-end;
flex-direction: row-reverse;
flex-wrap: nowrap;
align-content: flex-start;
justify-content: flex-end;
}

.sb_inherit {
display: inherit;
}

.long_field {
background: var(--bg-color);
border: 2px solid var(--border-color);
margin: 5px 5px 0 5px;
border-radius: 10px;
line-height: 1.7;
}

.sb_arrow {
color: var(--fg-color);
}

.sb_preview_badge {
text-align: center;
background: var(--comfy-input-bg);
font-weight: bold;
color: var(--error-text);
}
</style>
32 changes: 28 additions & 4 deletions src/components/NodeSearchBox.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
<template>
<div class="comfy-vue-node-search-container">
<div class="comfy-vue-node-preview-container">
<NodePreview
:nodeDef="hoveredSuggestion"
:key="hoveredSuggestion?.name || ''"
v-if="hoveredSuggestion"
/>
</div>
<NodeSearchFilter @addFilter="onAddFilter" />
<AutoComplete
<AutoCompletePlus
:model-value="props.filters"
class="comfy-vue-node-search-box"
scrollHeight="28rem"
Expand All @@ -12,6 +19,7 @@
:min-length="0"
@complete="search($event.query)"
@option-select="emit('addNode', $event.value)"
@focused-option-changed="setHoverSuggestion($event)"
complete-on-focus
auto-option-focus
force-selection
Expand Down Expand Up @@ -40,13 +48,13 @@
{{ value[1] }}
</Chip>
</template>
</AutoComplete>
</AutoCompletePlus>
</div>
</template>

<script setup lang="ts">
import { computed, inject, onMounted, Ref, ref } from "vue";
import AutoComplete from "primevue/autocomplete";
import AutoCompletePlus from "./primevueOverride/AutoCompletePlus.vue";
import Chip from "primevue/chip";
import Badge from "primevue/badge";
import NodeSearchFilter from "@/components/NodeSearchFilter.vue";
Expand All @@ -56,6 +64,7 @@ import {
NodeSearchService,
type FilterAndValue,
} from "@/services/nodeSearchService";
import NodePreview from "./NodePreview.vue";
const props = defineProps({
filters: {
Expand All @@ -73,6 +82,7 @@ const nodeSearchService = (
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`;
const suggestions = ref<ComfyNodeDef[]>([]);
const hoveredSuggestion = ref<ComfyNodeDef | null>(null);
const placeholder = computed(() => {
return props.filters.length === 0 ? "Search for nodes" : "";
});
Expand Down Expand Up @@ -104,6 +114,14 @@ const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
emit("removeFilter", filterAndValue);
reFocusInput();
};
const setHoverSuggestion = (index: number) => {
if (index === -1) {
hoveredSuggestion.value = null;
return;
}
const value = suggestions.value[index];
hoveredSuggestion.value = value;
};
</script>

<style scoped>
Expand All @@ -115,12 +133,18 @@ const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
pointer-events: auto;
}
.comfy-vue-node-preview-container {
position: absolute;
left: -350px;
top: 50px;
}
.comfy-vue-node-search-box {
@apply min-w-96 w-full z-10;
}
.option-container {
@apply flex flex-col px-4 py-2 cursor-pointer overflow-hidden;
@apply flex flex-col px-4 py-2 cursor-pointer overflow-hidden w-full;
}
.option-container:hover .option-description {
Expand Down
24 changes: 24 additions & 0 deletions src/components/primevueOverride/AutoCompletePlus.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!-- Auto complete with extra event "focused-option-changed" -->
<script>
import AutoComplete from "primevue/autocomplete";

export default {
name: "AutoCompletePlus",
extends: AutoComplete,
emits: ["focused-option-changed"],
mounted() {
if (typeof AutoComplete.mounted === "function") {
AutoComplete.mounted.call(this);
}

// Add a watcher on the focusedOptionIndex property
this.$watch(
() => this.focusedOptionIndex,
(newVal, oldVal) => {
// Emit a custom event when focusedOptionIndex changes
this.$emit("focused-option-changed", newVal);
}
);
},
};
</script>
2 changes: 2 additions & 0 deletions src/types/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ const zComfyNodeDef = z.object({
});

// `/object_info`
export type ComfyInputSpec = z.infer<typeof zInputSpec>;
export type ComfyOutputSpec = z.infer<typeof zComfyOutputSpec>;
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>;

// TODO: validate `/object_info` API endpoint responses.

0 comments on commit 605faf0

Please sign in to comment.