Skip to content

Commit 605faf0

Browse files
authored
Node preview on focus (#116)
* Basic preview * Adjust position * Fix node display * nit * handle combo default value * nit * Custom AutoComplete
1 parent 3ac793b commit 605faf0

File tree

4 files changed

+314
-4
lines changed

4 files changed

+314
-4
lines changed

src/components/NodePreview.vue

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
<!-- Reference:
2+
https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c683087a3e168db/app/js/functions/sb_fn.js#L149
3+
-->
4+
5+
<template>
6+
<div id="previewDiv">
7+
<div class="sb_table">
8+
<div class="node_header">
9+
<div class="sb_dot headdot"></div>
10+
{{ nodeDef.display_name }}
11+
</div>
12+
<div class="sb_preview_badge">PREVIEW</div>
13+
14+
<!-- Node slot I/O -->
15+
<div
16+
v-for="[slotInput, slotOutput] in _.zip(slotInputDefs, allOutputDefs)"
17+
class="sb_row slot_row"
18+
>
19+
<div class="sb_col">
20+
<div v-if="slotInput" :class="['sb_dot', slotInput.type]"></div>
21+
</div>
22+
<div class="sb_col">{{ slotInput ? slotInput.name : "" }}</div>
23+
<div class="sb_col middle-column"></div>
24+
<div class="sb_col sb_inherit">
25+
{{ slotOutput ? slotOutput.name : "" }}
26+
</div>
27+
<div class="sb_col">
28+
<div v-if="slotOutput" :class="['sb_dot', slotOutput.type]"></div>
29+
</div>
30+
</div>
31+
32+
<!-- Node widget inputs -->
33+
<div v-for="widgetInput in widgetInputDefs" class="sb_row long_field">
34+
<div class="sb_col sb_arrow">&#x25C0;</div>
35+
<div class="sb_col">{{ widgetInput.name }}</div>
36+
<div class="sb_col middle-column"></div>
37+
<div class="sb_col sb_inherit">{{ widgetInput.defaultValue }}</div>
38+
<div class="sb_col sb_arrow">&#x25B6;</div>
39+
</div>
40+
</div>
41+
<div class="sb_description" v-if="nodeDef.description">
42+
{{ nodeDef.description }}
43+
</div>
44+
</div>
45+
</template>
46+
47+
<script setup lang="ts">
48+
import { app } from "@/scripts/app";
49+
import { type ComfyNodeDef } from "@/types/apiTypes";
50+
import _ from "lodash";
51+
import { PropType } from "vue";
52+
53+
const props = defineProps({
54+
nodeDef: {
55+
type: Object as PropType<ComfyNodeDef>,
56+
required: true,
57+
},
58+
// Make sure vue properly re-render the component when the nodeDef changes
59+
key: {
60+
type: String,
61+
required: true,
62+
},
63+
});
64+
65+
const nodeDef = props.nodeDef as ComfyNodeDef;
66+
67+
// --------------------------------------------------
68+
// TODO: Move out to separate file
69+
interface IComfyNodeInputDef {
70+
name: string;
71+
type: string;
72+
widgetType: string | null;
73+
defaultValue: any;
74+
}
75+
76+
interface IComfyNodeOutputDef {
77+
name: string | null;
78+
type: string;
79+
isList: boolean;
80+
}
81+
82+
const allInputs = Object.assign(
83+
{},
84+
nodeDef.input.required || {},
85+
nodeDef.input.optional || {}
86+
);
87+
const allInputDefs: IComfyNodeInputDef[] = Object.entries(allInputs).map(
88+
([inputName, inputData]) => {
89+
return {
90+
name: inputName,
91+
type: inputData[0],
92+
widgetType: app.getWidgetType(inputData, inputName),
93+
defaultValue:
94+
inputData[1]?.default ||
95+
(inputData[0] instanceof Array ? inputData[0][0] : ""),
96+
};
97+
}
98+
);
99+
100+
const allOutputDefs: IComfyNodeOutputDef[] = _.zip(
101+
nodeDef.output,
102+
nodeDef.output_name || [],
103+
nodeDef.output_is_list || []
104+
).map(([outputType, outputName, isList]) => {
105+
return {
106+
name: outputName,
107+
type: outputType instanceof Array ? "COMBO" : outputType,
108+
isList: isList,
109+
};
110+
});
111+
112+
const slotInputDefs = allInputDefs.filter((input) => !input.widgetType);
113+
const widgetInputDefs = allInputDefs.filter((input) => !!input.widgetType);
114+
</script>
115+
116+
<style scoped>
117+
.slot_row {
118+
padding: 2px;
119+
}
120+
121+
/* Original N-SideBar styles */
122+
.sb_dot {
123+
width: 8px;
124+
height: 8px;
125+
border-radius: 50%;
126+
background-color: grey;
127+
}
128+
129+
.node_header {
130+
line-height: 1;
131+
padding: 8px 13px 7px;
132+
background: var(--comfy-input-bg);
133+
margin-bottom: 5px;
134+
font-size: 15px;
135+
text-wrap: nowrap;
136+
overflow: hidden;
137+
display: flex;
138+
align-items: center;
139+
}
140+
141+
.headdot {
142+
width: 10px;
143+
height: 10px;
144+
float: inline-start;
145+
margin-right: 8px;
146+
}
147+
148+
.IMAGE {
149+
background-color: #64b5f6;
150+
}
151+
152+
.VAE {
153+
background-color: #ff6e6e;
154+
}
155+
156+
.LATENT {
157+
background-color: #ff9cf9;
158+
}
159+
160+
.MASK {
161+
background-color: #81c784;
162+
}
163+
164+
.CONDITIONING {
165+
background-color: #ffa931;
166+
}
167+
168+
.CLIP {
169+
background-color: #ffd500;
170+
}
171+
172+
.MODEL {
173+
background-color: #b39ddb;
174+
}
175+
176+
.CONTROL_NET {
177+
background-color: #a5d6a7;
178+
}
179+
180+
#previewDiv {
181+
background-color: var(--comfy-menu-bg);
182+
font-family: "Open Sans", sans-serif;
183+
font-size: small;
184+
color: var(--descrip-text);
185+
border: 1px solid var(--descrip-text);
186+
min-width: 300px;
187+
width: min-content;
188+
height: fit-content;
189+
z-index: 9999;
190+
border-radius: 12px;
191+
overflow: hidden;
192+
font-size: 12px;
193+
padding-bottom: 10px;
194+
}
195+
196+
#previewDiv .sb_description {
197+
margin: 10px;
198+
padding: 6px;
199+
background: var(--border-color);
200+
border-radius: 5px;
201+
font-style: italic;
202+
font-weight: 500;
203+
font-size: 0.9rem;
204+
}
205+
206+
.sb_table {
207+
display: grid;
208+
209+
grid-column-gap: 10px;
210+
/* Spazio tra le colonne */
211+
width: 100%;
212+
/* Imposta la larghezza della tabella al 100% del contenitore */
213+
}
214+
215+
.sb_row {
216+
display: grid;
217+
grid-template-columns: 10px 1fr 1fr 1fr 10px;
218+
grid-column-gap: 10px;
219+
align-items: center;
220+
padding-left: 9px;
221+
padding-right: 9px;
222+
}
223+
224+
.sb_row_string {
225+
grid-template-columns: 10px 1fr 1fr 10fr 1fr;
226+
}
227+
228+
.sb_col {
229+
border: 0px solid #000;
230+
display: flex;
231+
align-items: flex-end;
232+
flex-direction: row-reverse;
233+
flex-wrap: nowrap;
234+
align-content: flex-start;
235+
justify-content: flex-end;
236+
}
237+
238+
.sb_inherit {
239+
display: inherit;
240+
}
241+
242+
.long_field {
243+
background: var(--bg-color);
244+
border: 2px solid var(--border-color);
245+
margin: 5px 5px 0 5px;
246+
border-radius: 10px;
247+
line-height: 1.7;
248+
}
249+
250+
.sb_arrow {
251+
color: var(--fg-color);
252+
}
253+
254+
.sb_preview_badge {
255+
text-align: center;
256+
background: var(--comfy-input-bg);
257+
font-weight: bold;
258+
color: var(--error-text);
259+
}
260+
</style>

src/components/NodeSearchBox.vue

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
<template>
22
<div class="comfy-vue-node-search-container">
3+
<div class="comfy-vue-node-preview-container">
4+
<NodePreview
5+
:nodeDef="hoveredSuggestion"
6+
:key="hoveredSuggestion?.name || ''"
7+
v-if="hoveredSuggestion"
8+
/>
9+
</div>
310
<NodeSearchFilter @addFilter="onAddFilter" />
4-
<AutoComplete
11+
<AutoCompletePlus
512
:model-value="props.filters"
613
class="comfy-vue-node-search-box"
714
scrollHeight="28rem"
@@ -12,6 +19,7 @@
1219
:min-length="0"
1320
@complete="search($event.query)"
1421
@option-select="emit('addNode', $event.value)"
22+
@focused-option-changed="setHoverSuggestion($event)"
1523
complete-on-focus
1624
auto-option-focus
1725
force-selection
@@ -40,13 +48,13 @@
4048
{{ value[1] }}
4149
</Chip>
4250
</template>
43-
</AutoComplete>
51+
</AutoCompletePlus>
4452
</div>
4553
</template>
4654

4755
<script setup lang="ts">
4856
import { computed, inject, onMounted, Ref, ref } from "vue";
49-
import AutoComplete from "primevue/autocomplete";
57+
import AutoCompletePlus from "./primevueOverride/AutoCompletePlus.vue";
5058
import Chip from "primevue/chip";
5159
import Badge from "primevue/badge";
5260
import NodeSearchFilter from "@/components/NodeSearchFilter.vue";
@@ -56,6 +64,7 @@ import {
5664
NodeSearchService,
5765
type FilterAndValue,
5866
} from "@/services/nodeSearchService";
67+
import NodePreview from "./NodePreview.vue";
5968
6069
const props = defineProps({
6170
filters: {
@@ -73,6 +82,7 @@ const nodeSearchService = (
7382
7483
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`;
7584
const suggestions = ref<ComfyNodeDef[]>([]);
85+
const hoveredSuggestion = ref<ComfyNodeDef | null>(null);
7686
const placeholder = computed(() => {
7787
return props.filters.length === 0 ? "Search for nodes" : "";
7888
});
@@ -104,6 +114,14 @@ const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
104114
emit("removeFilter", filterAndValue);
105115
reFocusInput();
106116
};
117+
const setHoverSuggestion = (index: number) => {
118+
if (index === -1) {
119+
hoveredSuggestion.value = null;
120+
return;
121+
}
122+
const value = suggestions.value[index];
123+
hoveredSuggestion.value = value;
124+
};
107125
</script>
108126

109127
<style scoped>
@@ -115,12 +133,18 @@ const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
115133
pointer-events: auto;
116134
}
117135
136+
.comfy-vue-node-preview-container {
137+
position: absolute;
138+
left: -350px;
139+
top: 50px;
140+
}
141+
118142
.comfy-vue-node-search-box {
119143
@apply min-w-96 w-full z-10;
120144
}
121145
122146
.option-container {
123-
@apply flex flex-col px-4 py-2 cursor-pointer overflow-hidden;
147+
@apply flex flex-col px-4 py-2 cursor-pointer overflow-hidden w-full;
124148
}
125149
126150
.option-container:hover .option-description {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!-- Auto complete with extra event "focused-option-changed" -->
2+
<script>
3+
import AutoComplete from "primevue/autocomplete";
4+
5+
export default {
6+
name: "AutoCompletePlus",
7+
extends: AutoComplete,
8+
emits: ["focused-option-changed"],
9+
mounted() {
10+
if (typeof AutoComplete.mounted === "function") {
11+
AutoComplete.mounted.call(this);
12+
}
13+
14+
// Add a watcher on the focusedOptionIndex property
15+
this.$watch(
16+
() => this.focusedOptionIndex,
17+
(newVal, oldVal) => {
18+
// Emit a custom event when focusedOptionIndex changes
19+
this.$emit("focused-option-changed", newVal);
20+
}
21+
);
22+
},
23+
};
24+
</script>

src/types/apiTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ const zComfyNodeDef = z.object({
223223
});
224224

225225
// `/object_info`
226+
export type ComfyInputSpec = z.infer<typeof zInputSpec>;
227+
export type ComfyOutputSpec = z.infer<typeof zComfyOutputSpec>;
226228
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>;
227229

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

0 commit comments

Comments
 (0)