Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Model library sidebar tab #837

Merged
merged 40 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
57b138d
basic/empty model library sidebar tab
mcmonkey4eva Sep 15, 2024
9df8e33
make it actually list out models
mcmonkey4eva Sep 15, 2024
1a9e9b9
extremely primitive search impl
mcmonkey4eva Sep 15, 2024
2f7ff2e
list out available folders
mcmonkey4eva Sep 15, 2024
486db95
load list dynamically
mcmonkey4eva Sep 15, 2024
07cacec
nice lil loading icon
mcmonkey4eva Sep 15, 2024
6ffd685
that's not doing anything
mcmonkey4eva Sep 15, 2024
45fdb39
run autoformatter
mcmonkey4eva Sep 15, 2024
d8c641c
fix up some absolute vue shenanigans
mcmonkey4eva Sep 15, 2024
773b4cc
swap to pi-box
mcmonkey4eva Sep 15, 2024
d0731d8
is_fake_object
mcmonkey4eva Sep 15, 2024
c192818
i think apply the tailwind thingo
mcmonkey4eva Sep 15, 2024
4163af3
trim '.safetensors' from end of display title
mcmonkey4eva Sep 17, 2024
6dd9491
oop
mcmonkey4eva Sep 17, 2024
036bfc2
after load, retain title if no new title is given
mcmonkey4eva Sep 17, 2024
d9bae3b
is_load_requested to prevent duplication
mcmonkey4eva Sep 17, 2024
d2cd56c
dirty initial model metadata load & preview
mcmonkey4eva Sep 17, 2024
2d2ad1c
Merge branch 'main' into model-library-sidebar-tab
mcmonkey4eva Sep 17, 2024
9d9d776
update model store tests
mcmonkey4eva Sep 17, 2024
f3a4e61
initial image icon for model lib
mcmonkey4eva Sep 17, 2024
9b3f8ab
i hate this
mcmonkey4eva Sep 17, 2024
82ec4b2
better empty spacer
mcmonkey4eva Sep 17, 2024
e2a8878
Merge branch 'main' into model-library-sidebar-tab
mcmonkey4eva Sep 21, 2024
b2ff7b4
add api handler for '/models'
mcmonkey4eva Sep 21, 2024
600c087
load model folders list instead of hardcoding
mcmonkey4eva Sep 21, 2024
1ab37ef
add a 'no content' placeholder for empty folders
mcmonkey4eva Sep 21, 2024
2926b30
autoformat
mcmonkey4eva Sep 21, 2024
32c249b
autoload model metadata
mcmonkey4eva Sep 21, 2024
b8ff73a
error handling on metadata loading
mcmonkey4eva Sep 21, 2024
c72a0af
larger model icons
mcmonkey4eva Sep 21, 2024
2c42e52
click a model to spawn a node for it
mcmonkey4eva Sep 21, 2024
2b47da0
draggable model nodes
mcmonkey4eva Sep 21, 2024
ab8ed33
add a setting for whether to autoload or not
mcmonkey4eva Sep 21, 2024
120dc7f
autoformat will be the death of me
mcmonkey4eva Sep 21, 2024
305cc82
cleanup promise code
mcmonkey4eva Sep 21, 2024
a1a5e0d
make the model preview actually half-decent
mcmonkey4eva Sep 21, 2024
3a6d179
Merge branch 'main' into model-library-sidebar-tab
mcmonkey4eva Sep 22, 2024
2708688
revert bad unchecked change
mcmonkey4eva Sep 22, 2024
0375d42
Merge remote-tracking branch 'origin/dev1.3' into model-library-sideb…
mcmonkey4eva Sep 23, 2024
7ebe10f
put registration back
mcmonkey4eva Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/common/TreeExplorerTreeNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
]"
ref="container"
>
<div class="node-content truncate">
<span class="node-label text-sm">
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="props.node"></slot>
<EditableText
:modelValue="node.label"
Expand Down
20 changes: 19 additions & 1 deletion src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import { useCanvasStore } from '@/stores/graphStore'
import { ComfyModelDef } from '@/stores/modelStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
import { applyOpacity } from '@/utils/colorUtil'
import { getColorPalette } from '@/extensions/core/colorPalette'
import { debounce } from 'lodash'
Expand All @@ -52,7 +54,7 @@ const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore()
const canvasStore = useCanvasStore()

const modelToNodeStore = useModelToNodeStore()
const betaMenuEnabled = computed(
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
Expand Down Expand Up @@ -153,6 +155,22 @@ onMounted(async () => {
loc.clientY
])
comfyApp.addNodeOnGraph(nodeDef, { pos })
} else if (node.data instanceof ComfyModelDef) {
const model = node.data
const provider = modelToNodeStore.getNodeProvider(model.directory)
if (provider) {
const pos = comfyApp.clientPosToCanvasPos([
loc.clientX - 20,
loc.clientY
])
const node = comfyApp.addNodeOnGraph(provider.nodeDef, { pos })
const widget = node.widgets.find(
(widget) => widget.name === provider.key
)
if (widget) {
widget.value = model.name
}
}
}
}
}
Expand Down
209 changes: 209 additions & 0 deletions src/components/sidebar/tabs/ModelLibrarySidebarTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<template>
<SidebarTabTemplate :title="$t('sideToolbar.modelLibrary')">
<template #tool-buttons> </template>
<template #body>
<div class="flex flex-col h-full">
<div class="flex-shrink-0">
<SearchBox
class="model-lib-search-box mx-4 mt-4"
v-model:modelValue="searchQuery"
@search="handleSearch"
:placeholder="$t('searchModels') + '...'"
/>
</div>
<div class="flex-grow overflow-y-auto">
<TreeExplorer
class="model-lib-tree-explorer mt-1"
:roots="renderedRoot.children"
v-model:expandedKeys="expandedKeys"
@nodeClick="handleNodeClick"
>
<template #node="{ node }">
<ModelTreeLeaf :node="node" />
</template>
</TreeExplorer>
</div>
</div>
</template>
</SidebarTabTemplate>
<div id="model-library-model-preview-container" />
</template>

<script setup lang="ts">
import SearchBox from '@/components/common/SearchBox.vue'
import { useI18n } from 'vue-i18n'
import TreeExplorer from '@/components/common/TreeExplorer.vue'
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
import ModelTreeLeaf from '@/components/sidebar/tabs/modelLibrary/ModelTreeLeaf.vue'
import { ComfyModelDef, useModelStore } from '@/stores/modelStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
import { useSettingStore } from '@/stores/settingStore'
import { useTreeExpansion } from '@/hooks/treeHooks'
import type {
RenderedTreeExplorerNode,
TreeExplorerNode
} from '@/types/treeExplorerTypes'
import { computed, ref, type ComputedRef, watch, toRef } from 'vue'
import type { TreeNode } from 'primevue/treenode'
import { app } from '@/scripts/app'
import { buildTree } from '@/utils/treeUtil'
const { t } = useI18n()
const modelStore = useModelStore()
const modelToNodeStore = useModelToNodeStore()
const settingStore = useSettingStore()
const searchQuery = ref<string>('')
const expandedKeys = ref<Record<string, boolean>>({})
const { toggleNodeOnEvent } = useTreeExpansion(expandedKeys)

const root: ComputedRef<TreeNode> = computed(() => {
let modelList: ComfyModelDef[] = []
if (!modelStore.modelFolders.length) {
modelStore.getModelFolders()
}
if (settingStore.get('Comfy.ModelLibrary.AutoLoadAll')) {
for (let folder of modelStore.modelFolders) {
modelStore.getModelsInFolderCached(folder)
}
}
for (let folder of modelStore.modelFolders) {
const models = modelStore.modelStoreMap[folder]
if (models) {
if (Object.values(models.models).length) {
modelList.push(...Object.values(models.models))
} else {
const fakeModel = new ComfyModelDef('(No Content)', folder)
fakeModel.is_fake_object = true
modelList.push(fakeModel)
}
} else {
const fakeModel = new ComfyModelDef('Loading', folder)
fakeModel.is_fake_object = true
modelList.push(fakeModel)
}
}
if (searchQuery.value) {
const search = searchQuery.value.toLocaleLowerCase()
modelList = modelList.filter((model: ComfyModelDef) => {
return model.name.toLocaleLowerCase().includes(search)
})
}
const tree: TreeNode = buildTree(modelList, (model: ComfyModelDef) => {
return [model.directory, ...model.name.replaceAll('\\', '/').split('/')]
mcmonkey4eva marked this conversation as resolved.
Show resolved Hide resolved
})
return tree
})

const renderedRoot = computed<TreeExplorerNode<ComfyModelDef>>(() => {
const fillNodeInfo = (node: TreeNode): TreeExplorerNode<ComfyModelDef> => {
const children = node.children?.map(fillNodeInfo)
const model: ComfyModelDef | null =
node.leaf && node.data ? node.data : null
if (model?.is_fake_object) {
if (model.name === '(No Content)') {
return {
key: node.key,
label: t('noContent'),
leaf: true,
data: node.data,
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
return 'pi pi-file'
},
children: []
}
} else {
return {
key: node.key,
label: t('loading') + '...',
leaf: true,
data: node.data,
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
return 'pi pi-spin pi-spinner'
},
children: []
}
}
}

return {
key: node.key,
label: model ? model.title : node.label,
leaf: node.leaf,
data: node.data,
getIcon: (node: TreeExplorerNode<ComfyModelDef>) => {
if (node.leaf) {
if (node.data && node.data.image) {
return 'pi pi-fake-spacer'
}
return 'pi pi-file'
}
},
children,
draggable: node.leaf,
handleClick: (
node: RenderedTreeExplorerNode<ComfyModelDef>,
e: MouseEvent
) => {
if (node.leaf) {
const provider = modelToNodeStore.getNodeProvider(model.directory)
if (provider) {
const node = app.addNodeOnGraph(provider.nodeDef, {
pos: app.getCanvasCenter()
})
const widget = node.widgets.find(
(widget) => widget.name === provider.key
)
if (widget) {
widget.value = model.name
}
}
}
}
}
}
return fillNodeInfo(root.value)
})

const handleSearch = (query: string) => {
// TODO
}

const handleNodeClick = (
node: RenderedTreeExplorerNode<ComfyModelDef>,
e: MouseEvent
) => {
if (node.leaf) {
// TODO
} else {
toggleNodeOnEvent(e, node)
}
}

watch(
toRef(expandedKeys, 'value'),
(newExpandedKeys) => {
Object.entries(newExpandedKeys).forEach(([key, isExpanded]) => {
if (isExpanded) {
const folderPath = key.split('/').slice(1).join('/')
if (folderPath && !folderPath.includes('/')) {
// Trigger (async) load of model data for this folder
modelStore.getModelsInFolderCached(folderPath)
}
}
})
},
{ deep: true }
)
</script>

<style>
.pi-fake-spacer {
height: 1px;
width: 16px;
}
</style>

<style scoped>
:deep(.comfy-vue-side-bar-body) {
background: var(--p-tree-background);
}
</style>
89 changes: 89 additions & 0 deletions src/components/sidebar/tabs/modelLibrary/ModelPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<div class="model_preview">
<div class="model_preview_title">
{{ modelDef.title }}
</div>
<div class="model_preview_top_container">
<div class="model_preview_architecture" v-if="modelDef.architecture_id">
<span class="model_preview_prefix">Architecture: </span>
{{ modelDef.architecture_id }}
</div>
<div class="model_preview_author" v-if="modelDef.author">
<span class="model_preview_prefix">Author: </span>
{{ modelDef.author }}
</div>
</div>
<div class="model_preview_image" v-if="modelDef.image">
<img :src="modelDef.image" />
</div>
<div class="model_preview_usage_hint" v-if="modelDef.usage_hint">
<span class="model_preview_prefix">Usage hint: </span>
{{ modelDef.usage_hint }}
</div>
<div class="model_preview_trigger_phrase" v-if="modelDef.trigger_phrase">
<span class="model_preview_prefix">Trigger phrase: </span>
{{ modelDef.trigger_phrase }}
</div>
<div class="model_preview_description" v-if="modelDef.description">
<span class="model_preview_prefix">Description: </span>
{{ modelDef.description }}
</div>
</div>
</template>

<script setup lang="ts">
import { ComfyModelDef } from '@/stores/modelStore'

const props = defineProps({
modelDef: {
type: ComfyModelDef,
required: true
}
})

const modelDef = props.modelDef
</script>
<style scoped>
.model_preview {
background-color: var(--comfy-menu-bg);
font-family: 'Open Sans', sans-serif;
color: var(--descrip-text);
border: 1px solid var(--descrip-text);
min-width: 300px;
max-width: 500px;
width: fit-content;
height: fit-content;
z-index: 9999;
border-radius: 12px;
overflow: hidden;
font-size: 12px;
padding: 10px;
}
.model_preview_image {
margin: auto;
width: fit-content;
}
.model_preview_image img {
max-width: 100%;
max-height: 150px;
object-fit: contain;
}
.model_preview_title {
font-weight: bold;
text-align: center;
font-size: 14px;
}
.model_preview_top_container {
text-align: center;
}
.model_preview_author,
.model_preview_architecture {
display: inline-block;
text-align: center;
margin: 5px;
font-size: 10px;
}
.model_preview_prefix {
font-weight: bold;
}
</style>
Loading
Loading