mirror of
https://github.com/Sygil-Dev/sygil-webui.git
synced 2024-12-16 07:41:38 +03:00
Added implementation of the Concepts Library tab made by @Qualzz.
Added implementation of the Concepts Library tab made by @Qualzz.
This commit is contained in:
commit
c66e1a36e8
@ -108,4 +108,10 @@ h1 {
|
||||
}
|
||||
div.gallery:hover {
|
||||
border: 1px solid #777;
|
||||
}
|
||||
.css-dg4u6x p {
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
11
frontend/index.html
Normal file
11
frontend/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Component Template</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
34
frontend/package.json
Normal file
34
frontend/package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@codedealer/streamlit_template_vue",
|
||||
"version": "0.1.0",
|
||||
"description": "Starter template to build Streamlit component with vue 3",
|
||||
"license": "MIT",
|
||||
"author": "Thomas Mello",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/codedealer/streamlit-component-template-vue"
|
||||
},
|
||||
"scripts": {
|
||||
"clean:dist": "rimraf dist",
|
||||
"dev": "vite --mode dev",
|
||||
"build": "yarn clean:dist && vite build --mode prod",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.38",
|
||||
"streamlit-component-lib": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.18.9",
|
||||
"@types/node": "^17.0.21",
|
||||
"@vitejs/plugin-vue": "^2.2.0",
|
||||
"eslint": "^8.11.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.5.4",
|
||||
"vite": "^2.8.0",
|
||||
"vue": "^3.2.38",
|
||||
"vue-tsc": "^0.29.8"
|
||||
}
|
||||
}
|
243
frontend/src/Component.vue
Normal file
243
frontend/src/Component.vue
Normal file
@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="bootstrap-wrapper pt-4">
|
||||
<div class="concept-gallery row">
|
||||
<div v-for="concept in args.concepts"
|
||||
:key="concept.name"
|
||||
class="col-12 col-sm-6 col-md-4 col-lg-4 col-xl-3">
|
||||
|
||||
<div class="concept-card p-4 container-fluid">
|
||||
<div class="concept-card-content-wrapper">
|
||||
<div class="card-header row no-gutters">
|
||||
<div class="col">
|
||||
<h1 class="concept-title pl-1"><span class="token-char pr-0"><</span>{{ concept.name }}<span class="pl-0 token-char">></span></h1>
|
||||
</div>
|
||||
|
||||
<!-- Favorite feature, not implemented yet -->
|
||||
<!-- <div class="col-auto card-favorite" >
|
||||
<img width="24"
|
||||
height="24"
|
||||
class="icon-star"
|
||||
src="./icons/star.svg" />
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="concept-img-wrapper p-0 row no-gutters">
|
||||
|
||||
<div v-for="(img, img_index) in concept.images"
|
||||
:key="'concept_img'+img_index"
|
||||
:class="{
|
||||
'p-1': true,
|
||||
'col-6': concept.images.length % 2 == 0 || img_index < concept.images.length - 1,
|
||||
'col-12': concept.images.length % 2 == 1 && img_index == concept.images.length - 1
|
||||
}">
|
||||
<div class="img-bg" :style="{'background-image': 'url(data:image/png;base64,' + img + ')'}"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="concept.images.length == 0"
|
||||
class="col-12 p-4 no-preview">
|
||||
<svg class="no-preview-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 2 5 L 2 27 L 30 27 L 30 5 Z M 4 7 L 28 7 L 28 20.90625 L 22.71875 15.59375 L 22 14.875 L 17.46875 19.40625 L 11.71875 13.59375 L 11 12.875 L 4 19.875 Z M 24 9 C 22.894531 9 22 9.894531 22 11 C 22 12.105469 22.894531 13 24 13 C 25.105469 13 26 12.105469 26 11 C 26 9.894531 25.105469 9 24 9 Z M 11 15.71875 L 20.1875 25 L 4 25 L 4 22.71875 Z M 22 17.71875 L 28 23.71875 L 28 25 L 23.03125 25 L 18.875 20.8125 Z"/></svg>
|
||||
<p style="opacity: 0.8">No preview available</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="concept-card-footer row no-gutters pt-4">
|
||||
<div class="col pl-1">
|
||||
<div v-if="concept.type"
|
||||
:class="{
|
||||
|
||||
'concept-type-tag': true,
|
||||
'concept-type-style': concept.type.toLowerCase() === 'style',
|
||||
'concept-type-object': concept.type.toLowerCase() === 'object'
|
||||
}
|
||||
">
|
||||
{{ concept.type.toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<!-- Copy to clipboard button -->
|
||||
<button class="button"
|
||||
@click="copyToClipboard(concept.token)">
|
||||
<!-- <svg class="icon-clipboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" ><path d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"/></svg> -->
|
||||
Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { Streamlit, Theme } from "streamlit-component-lib"
|
||||
import { useStreamlit } from "./streamlit"
|
||||
interface IProps {
|
||||
args: any;
|
||||
disabled: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
useStreamlit(); // lifecycle hooks for automatic Streamlit resize
|
||||
const props = defineProps<IProps>();
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
console.log("sending copy to clipboard event", text)
|
||||
// Streamlit.setComponentValue({
|
||||
// action: "copy_to_clipboard",
|
||||
// value: text
|
||||
// })
|
||||
// copy to clipboard
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
// const enable_favorites = ref(false)
|
||||
// const enable_copy_to_clipboard = ref(false)
|
||||
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
/* svg.icon-clipboard {
|
||||
fill: var(--text-color);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
} */
|
||||
|
||||
svg.no-preview-icon {
|
||||
fill: var(--text-color);
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
opacity: 0.2;
|
||||
|
||||
}
|
||||
|
||||
.no-preview {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.concept-card {
|
||||
background-color: var(--secondary-background-color);
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
}
|
||||
|
||||
.concept-card-content-wrapper {
|
||||
flex-direction: column !important;
|
||||
display: flex !important;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.concept-title {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 1em;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.concept-img-wrapper {
|
||||
flex-grow: 1 !important;
|
||||
|
||||
}
|
||||
|
||||
.card-favorite {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.concept-img {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.concept-img img {
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.img-bg {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-origin: content-box;
|
||||
background-repeat: no-repeat;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.icon-star {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.token-char {
|
||||
color: #939393;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.concept-card-footer {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.concept-type-tag {
|
||||
background-color: #898989;
|
||||
border-radius: 16px;
|
||||
padding: 5px 16px;
|
||||
font-size: 0.7em;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.concept-type-style {
|
||||
background-color: #0095ff;
|
||||
}
|
||||
|
||||
.concept-type-object {
|
||||
background-color: #ff9031;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 35px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0px;
|
||||
line-height: 1.6;
|
||||
color: inherit;
|
||||
width: auto;
|
||||
user-select: none;
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid rgba(128, 128, 128, 0.8);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* .button:focus {
|
||||
box-shadow: rgb(var(--primary-color) / 50%) 0px 0px 0px 0.2rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:focus:not(:active) {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
} */
|
||||
</style>
|
52
frontend/src/app.vue
Normal file
52
frontend/src/app.vue
Normal file
File diff suppressed because one or more lines are too long
8
frontend/src/env.d.ts
vendored
Normal file
8
frontend/src/env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
1
frontend/src/icons/clipboard-check-solid.svg
Normal file
1
frontend/src/icons/clipboard-check-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#fff"><path d="M 16 2 C 14.742188 2 13.847656 2.890625 13.40625 4 L 5 4 L 5 29 L 27 29 L 27 4 L 18.59375 4 C 18.152344 2.890625 17.257813 2 16 2 Z M 16 4 C 16.554688 4 17 4.445313 17 5 L 17 6 L 20 6 L 20 8 L 12 8 L 12 6 L 15 6 L 15 5 C 15 4.445313 15.445313 4 16 4 Z M 7 6 L 10 6 L 10 10 L 22 10 L 22 6 L 25 6 L 25 27 L 7 27 Z M 21.28125 13.28125 L 15 19.5625 L 11.71875 16.28125 L 10.28125 17.71875 L 14.28125 21.71875 L 15 22.40625 L 15.71875 21.71875 L 22.71875 14.71875 Z"/></svg>
|
After Width: | Height: | Size: 550 B |
1
frontend/src/icons/clipboard-solid.svg
Normal file
1
frontend/src/icons/clipboard-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#fff"><path d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"/></svg>
|
After Width: | Height: | Size: 481 B |
1
frontend/src/icons/star-active.svg
Normal file
1
frontend/src/icons/star-active.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#3F6078"><path d="M 30.335938 12.546875 L 20.164063 11.472656 L 16 2.132813 L 11.835938 11.472656 L 1.664063 12.546875 L 9.261719 19.394531 L 7.140625 29.398438 L 16 24.289063 L 24.859375 29.398438 L 22.738281 19.394531 Z"/></svg>
|
After Width: | Height: | Size: 296 B |
1
frontend/src/icons/star.svg
Normal file
1
frontend/src/icons/star.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#3F6078"><path d="M 30.335938 12.546875 L 20.164063 11.472656 L 16 2.132813 L 11.835938 11.472656 L 1.664063 12.546875 L 9.261719 19.394531 L 7.140625 29.398438 L 16 24.289063 L 24.859375 29.398438 L 22.738281 19.394531 Z"/></svg>
|
After Width: | Height: | Size: 296 B |
4
frontend/src/main.ts
Normal file
4
frontend/src/main.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './app.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
27
frontend/src/streamlit/StreamlitVue.ts
Normal file
27
frontend/src/streamlit/StreamlitVue.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Vue.js specific composables
|
||||
*/
|
||||
import { onMounted, onUpdated } from "vue"
|
||||
import { Streamlit } from "streamlit-component-lib"
|
||||
|
||||
export function useStreamlit() {
|
||||
/**
|
||||
* Optional Streamlit Vue-based setup.
|
||||
*
|
||||
* You are not required call this function on your Streamlit
|
||||
* component. If you decide not to call it, you should implement the
|
||||
* `onMounted` and `onUpdated` functions in your own component,
|
||||
* so that your plugin properly resizes.
|
||||
*/
|
||||
|
||||
onMounted((): void => {
|
||||
// After we're rendered for the first time, tell Streamlit that our height
|
||||
// has changed.
|
||||
Streamlit.setFrameHeight()
|
||||
})
|
||||
|
||||
onUpdated((): void => {
|
||||
// After we're updated, tell Streamlit that our height may have changed.
|
||||
Streamlit.setFrameHeight()
|
||||
})
|
||||
}
|
83
frontend/src/streamlit/WithStreamlitConnection.vue
Normal file
83
frontend/src/streamlit/WithStreamlitConnection.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Error boundary. If our wrapped component threw an error, display it. -->
|
||||
<div v-if="componentError !== ''">
|
||||
<h1 class="err__title">Component Error</h1>
|
||||
<div class="err__msg">Message: {{ componentError }}</div>
|
||||
</div>
|
||||
<!--
|
||||
Else render the component slot and pass Streamlit event data in `args` props to it.
|
||||
Don't render until we've gotten our first RENDER_EVENT from Streamlit.
|
||||
All components get disabled while the app is being re-run, and become re-enabled when the re-run has finished.
|
||||
-->
|
||||
<slot
|
||||
v-else-if="renderData != null"
|
||||
:args="renderData.args"
|
||||
:theme="renderData.theme"
|
||||
:disabled="renderData.disabled"
|
||||
></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
ref,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
onUnmounted,
|
||||
onErrorCaptured,
|
||||
} from "vue"
|
||||
import { Streamlit, RenderData } from "streamlit-component-lib"
|
||||
|
||||
export default defineComponent({
|
||||
name: "WithStreamlitConnection",
|
||||
setup() {
|
||||
const renderData = ref<RenderData>((undefined as unknown) as RenderData)
|
||||
const componentError = ref("")
|
||||
|
||||
const onRenderEvent = (event: Event): void => {
|
||||
const renderEvent = event as CustomEvent<RenderData>
|
||||
renderData.value = renderEvent.detail
|
||||
componentError.value = ""
|
||||
}
|
||||
|
||||
// Set up event listeners, and signal to Streamlit that we're ready.
|
||||
// We won't render the component until we receive the first RENDER_EVENT.
|
||||
onMounted(() => {
|
||||
Streamlit.events.addEventListener(Streamlit.RENDER_EVENT, onRenderEvent)
|
||||
Streamlit.setComponentReady()
|
||||
})
|
||||
onUpdated(() => {
|
||||
// If our slot threw an error, we display it in render(). In this
|
||||
// case, the slot won't be mounted and therefore won't call
|
||||
// `setFrameHeight` on its own. We do it here so that the rendered
|
||||
// error will be visible.
|
||||
if (componentError.value != "") {
|
||||
Streamlit.setFrameHeight()
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
Streamlit.events.removeEventListener(
|
||||
Streamlit.RENDER_EVENT,
|
||||
onRenderEvent
|
||||
)
|
||||
})
|
||||
onErrorCaptured(err => {
|
||||
componentError.value = String(err)
|
||||
})
|
||||
|
||||
return {
|
||||
renderData,
|
||||
componentError,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.err__title,
|
||||
.err__msg {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
17
frontend/src/streamlit/index.ts
Normal file
17
frontend/src/streamlit/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2018-2020 Streamlit Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { useStreamlit } from "./StreamlitVue"
|
39
frontend/tsconfig.json
Normal file
39
frontend/tsconfig.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"declaration": false,
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"public"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
8
frontend/tsconfig.node.json
Normal file
8
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
25
frontend/vite.config.ts
Normal file
25
frontend/vite.config.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
plugins: [vue()],
|
||||
base: "",
|
||||
server: {
|
||||
port: 3001,
|
||||
},
|
||||
/**
|
||||
* DESC:
|
||||
* defining aliases
|
||||
*/
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: resolve(__dirname, './src'),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
80
scripts/merge.py
Normal file
80
scripts/merge.py
Normal file
@ -0,0 +1,80 @@
|
||||
import torch
|
||||
import sys
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
window = tk.Tk()
|
||||
window.title(string="Model Merger")
|
||||
tk.Label(text = "Model Merger",font=("Arial",25)).pack()
|
||||
tk.Label(text = "GUI by antrobot1234").pack()
|
||||
|
||||
frame1 = tk.Frame()
|
||||
frame2 = tk.Frame()
|
||||
frame3 = tk.Frame()
|
||||
|
||||
frameSlider = tk.Frame()
|
||||
frameButton = tk.Frame()
|
||||
|
||||
tk.Label(frame1,text = "File 1:").pack(side="left")
|
||||
file1text = tk.Entry(frame1,width=40)
|
||||
file1text.pack(side="left")
|
||||
|
||||
tk.Label(frame2,text = "File 2:").pack(side="left")
|
||||
file2text = tk.Entry(frame2,width=40)
|
||||
file2text.pack(side="left")
|
||||
|
||||
tk.Label(frame3,text = "File Out:").pack(side="left")
|
||||
fileOtext = tk.Entry(frame3,width=38)
|
||||
fileOtext.pack(side="left")
|
||||
|
||||
tk.Label(frameSlider,text = "Weight of file 1").pack(side="left")
|
||||
scale = tk.Scale(frameSlider,from_=0, to=100,orient="horizontal",tickinterval=10,length=450)
|
||||
scale.pack(side="left")
|
||||
|
||||
|
||||
|
||||
goButton = tk.Button(frameButton,text="RUN",height=2,width=20,bg="green")
|
||||
def merge(file1,file2,out,a):
|
||||
alpha = (a)/100
|
||||
if not(file1.endswith(".ckpt")):
|
||||
file1 += ".ckpt"
|
||||
if not(file2.endswith(".ckpt")):
|
||||
file2 += ".ckpt"
|
||||
if not(out.endswith(".ckpt")):
|
||||
out += ".ckpt"
|
||||
#Load Models
|
||||
model_0 = torch.load(file1)
|
||||
model_1 = torch.load(file2)
|
||||
theta_0 = model_0['state_dict']
|
||||
theta_1 = model_1['state_dict']
|
||||
|
||||
for key in theta_0.keys():
|
||||
if 'model' in key and key in theta_1:
|
||||
theta_0[key] = (alpha) * theta_0[key] + (1-alpha) * theta_1[key]
|
||||
|
||||
goButton.config(bg="red",text="RUNNING...\n(STAGE 2)")
|
||||
window.update()
|
||||
|
||||
for key in theta_1.keys():
|
||||
if 'model' in key and key not in theta_0:
|
||||
theta_0[key] = theta_1[key]
|
||||
torch.save(model_0, out)
|
||||
|
||||
|
||||
def handleClick(event):
|
||||
goButton.config(bg="red",text="RUNNING...\n(STAGE 1)")
|
||||
window.update()
|
||||
merge(file1text.get(),file2text.get(),fileOtext.get(),scale.get())
|
||||
goButton.config(bg="green",text="RUN")
|
||||
|
||||
goButton.pack()
|
||||
goButton.bind("<Button-1>",handleClick)
|
||||
|
||||
|
||||
frame1.pack()
|
||||
frame2.pack()
|
||||
frame3.pack()
|
||||
frameSlider.pack()
|
||||
frameButton.pack()
|
||||
|
||||
window.mainloop()
|
@ -3,19 +3,18 @@ from webui_streamlit import st
|
||||
from sd_utils import *
|
||||
|
||||
# streamlit imports
|
||||
|
||||
import streamlit.components.v1 as components
|
||||
|
||||
#other imports
|
||||
|
||||
# Temp imports
|
||||
#from sd_concept_browser import *
|
||||
|
||||
# Temp imports
|
||||
|
||||
|
||||
# end of imports
|
||||
#---------------------------------------------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
# this silences the annoying "Some weights of the model checkpoint were not used when initializing..." message at start.
|
||||
from transformers import logging
|
||||
@ -24,126 +23,166 @@ try:
|
||||
except:
|
||||
pass
|
||||
|
||||
# parent_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# build_dir = os.path.join(parent_dir, "frontend/dist")
|
||||
_component_func = components.declare_component("sd-concepts-browser", "./frontend/dist")
|
||||
|
||||
class plugin_info():
|
||||
plugname = "concept_library"
|
||||
description = "Concept Library"
|
||||
displayPriority = 4
|
||||
|
||||
def getLatestGeneratedImagesFromPath():
|
||||
#get the latest images from the generated images folder
|
||||
#get the path to the generated images folder
|
||||
generatedImagesPath = os.path.join(os.getcwd(), st.session_state['defaults'].general.sd_concepts_library_folder)
|
||||
#get all the files from the folders and subfolders
|
||||
files = []
|
||||
ext = ('jpeg', 'jpg', "png")
|
||||
#get the latest 10 images from the output folder without walking the subfolders
|
||||
for r, d, f in os.walk(generatedImagesPath):
|
||||
|
||||
for file in f:
|
||||
if file.endswith(ext):
|
||||
files.append(os.path.join(r, file))
|
||||
#sort the files by date
|
||||
files.sort(reverse=True, key=os.path.getmtime)
|
||||
latest = files
|
||||
latest.reverse()
|
||||
def sdConceptsBrowser(concepts, key=None):
|
||||
component_value = _component_func(concepts=concepts, key=key, default="")
|
||||
return component_value
|
||||
|
||||
# reverse the list so the latest images are first and truncate to
|
||||
# a reasonable number of images, 10 pages worth
|
||||
return [Image.open(f) for f in latest]
|
||||
@st.cache(persist=True, allow_output_mutation=True, show_spinner=False, suppress_st_warning=True)
|
||||
def getConceptsFromPath(page, conceptPerPage, searchText= ""):
|
||||
#print("getConceptsFromPath", "page:", page, "conceptPerPage:", conceptPerPage, "searchText:", searchText)
|
||||
# get the path where the concepts are stored
|
||||
path = os.path.join(os.getcwd(), st.session_state['defaults'].general.sd_concepts_library_folder)
|
||||
acceptedExtensions = ('jpeg', 'jpg', "png")
|
||||
concepts = []
|
||||
|
||||
# List all folders (concepts) in the path
|
||||
folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
|
||||
filteredFolders = folders
|
||||
|
||||
# Filter the folders by the search text
|
||||
if searchText != "":
|
||||
filteredFolders = [f for f in folders if searchText.lower() in f.lower()]
|
||||
|
||||
conceptIndex = 1
|
||||
for folder in filteredFolders:
|
||||
# handle pagination
|
||||
if conceptIndex > (page * conceptPerPage):
|
||||
continue
|
||||
if conceptIndex <= ((page - 1) * conceptPerPage):
|
||||
conceptIndex += 1
|
||||
continue
|
||||
|
||||
|
||||
concept = {
|
||||
"name": folder,
|
||||
"token": "<" + folder + ">",
|
||||
"images": [],
|
||||
"type": ""
|
||||
}
|
||||
|
||||
# type of concept is inside type_of_concept.txt
|
||||
typePath = os.path.join(path, folder, "type_of_concept.txt")
|
||||
binFile = os.path.join(path, folder, "learned_embeds.bin")
|
||||
|
||||
# Continue if the concept is not valid or the download has failed (no type_of_concept.txt or no binFile)
|
||||
if not os.path.exists(typePath) or not os.path.exists(binFile):
|
||||
continue
|
||||
|
||||
with open(typePath, "r") as f:
|
||||
concept["type"] = f.read()
|
||||
|
||||
# List all files in the concept/concept_images folder
|
||||
files = [f for f in os.listdir(os.path.join(path, folder, "concept_images")) if os.path.isfile(os.path.join(path, folder, "concept_images", f))]
|
||||
# Retrieve only the 4 first images
|
||||
for file in files[:4]:
|
||||
if file.endswith(acceptedExtensions):
|
||||
# Add a copy of the image to avoid file locking
|
||||
originalImage = Image.open(os.path.join(path, folder, "concept_images", file))
|
||||
|
||||
# Maintain the aspect ratio (max 200x200)
|
||||
resizedImage = originalImage.copy()
|
||||
resizedImage.thumbnail((200, 200), Image.ANTIALIAS)
|
||||
|
||||
#concept["images"].append(resizedImage)
|
||||
|
||||
concept["images"].append(imageToBase64(resizedImage))
|
||||
# Close original image
|
||||
originalImage.close()
|
||||
|
||||
concepts.append(concept)
|
||||
conceptIndex += 1
|
||||
# print all concepts name
|
||||
#print("Results:", [c["name"] for c in concepts])
|
||||
return concepts
|
||||
|
||||
@st.cache(persist=True, allow_output_mutation=True, show_spinner=False, suppress_st_warning=True)
|
||||
def imageToBase64(image):
|
||||
import io
|
||||
import base64
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format="PNG")
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
return img_str
|
||||
|
||||
@st.cache(persist=True, allow_output_mutation=True, show_spinner=False, suppress_st_warning=True)
|
||||
def getTotalNumberOfConcepts(searchText= ""):
|
||||
# get the path where the concepts are stored
|
||||
path = os.path.join(os.getcwd(), st.session_state['defaults'].general.sd_concepts_library_folder)
|
||||
concepts = []
|
||||
|
||||
# List all folders (concepts) in the path
|
||||
folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
|
||||
filteredFolders = folders
|
||||
|
||||
# Filter the folders by the search text
|
||||
if searchText != "":
|
||||
filteredFolders = [f for f in folders if searchText.lower() in f.lower()]
|
||||
return len(filteredFolders)
|
||||
|
||||
def layout():
|
||||
st.markdown(f"<h1 style='text-align: center; color: white;'>Navigate 300+ Textual-Inversion community trained concepts</h1>", unsafe_allow_html=True)
|
||||
|
||||
latestImages = getLatestGeneratedImagesFromPath()
|
||||
st.session_state['latestImages'] = latestImages
|
||||
|
||||
#with history_tab:
|
||||
##---------------------------------------------------------
|
||||
## image slideshow test
|
||||
## Number of entries per screen
|
||||
#slideshow_N = 9
|
||||
#slideshow_page_number = 0
|
||||
#slideshow_last_page = len(latestImages) // slideshow_N
|
||||
|
||||
## Add a next button and a previous button
|
||||
|
||||
#slideshow_prev, slideshow_image_col , slideshow_next = st.columns([1, 10, 1])
|
||||
|
||||
#with slideshow_image_col:
|
||||
#slideshow_image = st.empty()
|
||||
|
||||
#slideshow_image.image(st.session_state['latestImages'][0])
|
||||
|
||||
#current_image = 0
|
||||
|
||||
#if slideshow_next.button("Next", key=1):
|
||||
##print (current_image+1)
|
||||
#current_image = current_image+1
|
||||
#slideshow_image.image(st.session_state['latestImages'][current_image+1])
|
||||
#if slideshow_prev.button("Previous", key=0):
|
||||
##print ([current_image-1])
|
||||
#current_image = current_image-1
|
||||
#slideshow_image.image(st.session_state['latestImages'][current_image - 1])
|
||||
|
||||
|
||||
#---------------------------------------------------------
|
||||
|
||||
# image gallery
|
||||
# Number of entries per screen
|
||||
gallery_N = 9
|
||||
if not "galleryPage" in st.session_state:
|
||||
st.session_state["galleryPage"] = 0
|
||||
gallery_last_page = len(latestImages) // gallery_N
|
||||
|
||||
# Add a next button and a previous button
|
||||
|
||||
gallery_prev, gallery_refresh, gallery_pagination , gallery_next = st.columns([2, 2, 8, 1])
|
||||
|
||||
# the pagination doesnt work for now so its better to disable the buttons.
|
||||
|
||||
if gallery_refresh.button("Refresh", key=4):
|
||||
st.session_state["galleryPage"] = 0
|
||||
|
||||
if gallery_next.button("Next", key=3):
|
||||
|
||||
if st.session_state["galleryPage"] + 1 > gallery_last_page:
|
||||
st.session_state["galleryPage"] = 0
|
||||
else:
|
||||
st.session_state["galleryPage"] += 1
|
||||
|
||||
if gallery_prev.button("Previous", key=2):
|
||||
|
||||
if st.session_state["galleryPage"] - 1 < 0:
|
||||
st.session_state["galleryPage"] = gallery_last_page
|
||||
else:
|
||||
st.session_state["galleryPage"] -= 1
|
||||
|
||||
#print(st.session_state["galleryPage"])
|
||||
# Get start and end indices of the next page of the dataframe
|
||||
gallery_start_idx = st.session_state["galleryPage"] * gallery_N
|
||||
gallery_end_idx = (1 + st.session_state["galleryPage"]) * gallery_N
|
||||
|
||||
#---------------------------------------------------------
|
||||
# Pagination
|
||||
page = 1
|
||||
conceptPerPage = 12
|
||||
totalNumberOfConcepts = getTotalNumberOfConcepts()
|
||||
if not "cl_page" in st.session_state:
|
||||
st.session_state["cl_page"] = page
|
||||
if not "cl_conceptPerPage" in st.session_state:
|
||||
st.session_state["cl_conceptPerPage"] = conceptPerPage
|
||||
#Search for a concept (totalNumberOfConcepts available)
|
||||
searchInput = st.text_input("","", placeholder= f'Search for a concept ({totalNumberOfConcepts} available)')
|
||||
if searchInput != "":
|
||||
st.session_state["cl_page"] = 1
|
||||
totalNumberOfConcepts = getTotalNumberOfConcepts(searchInput)
|
||||
|
||||
placeholder = st.empty()
|
||||
|
||||
#populate the 3 images per column
|
||||
with placeholder.container():
|
||||
col1, col2, col3 = st.columns(3)
|
||||
col1_cont = st.container()
|
||||
col2_cont = st.container()
|
||||
col3_cont = st.container()
|
||||
# Init session state
|
||||
if not "concepts" in st.session_state:
|
||||
st.session_state['concepts'] = []
|
||||
|
||||
#print (len(st.session_state['latestImages']))
|
||||
images = list(reversed(st.session_state['latestImages']))[gallery_start_idx:(gallery_start_idx+gallery_N)]
|
||||
# Refresh concepts
|
||||
st.session_state['concepts'] = getConceptsFromPath(st.session_state["cl_page"], st.session_state["cl_conceptPerPage"], searchInput)
|
||||
conceptsLenght = len(st.session_state['concepts'])
|
||||
|
||||
with col1_cont:
|
||||
with col1:
|
||||
[st.image(images[index], caption="") for index in [0, 3, 6] if index < len(images)]
|
||||
with col2_cont:
|
||||
with col2:
|
||||
[st.image(images[index]) for index in [1, 4, 7] if index < len(images)]
|
||||
with col3_cont:
|
||||
with col3:
|
||||
[st.image(images[index]) for index in [2, 5, 8] if index < len(images)]
|
||||
|
||||
if (conceptsLenght == 0):
|
||||
if (searchInput == ""):
|
||||
st.write("You don't have any concepts in your library ")
|
||||
# Propose the user to go to "https://github.com/sd-webui/sd-concepts-library"
|
||||
st.markdown("To add concepts to your library, download some from the [sd-concepts-library](https://github.com/sd-webui/sd-concepts-library) \
|
||||
repository and save the content of `sd-concepts-library` into ```./models/custom/sd-concepts-library``` or just create your own concept.", unsafe_allow_html=False)
|
||||
else:
|
||||
st.write("No concepts found in the library matching your search: " + searchInput)
|
||||
|
||||
# print("Number of concept matching the query:", conceptsLenght)
|
||||
sdConceptsBrowser(st.session_state['concepts'], key="clipboard")
|
||||
#
|
||||
# Pagination
|
||||
last_page = math.ceil(getTotalNumberOfConcepts(searchInput) / st.session_state["cl_conceptPerPage"])
|
||||
_prev, _per_page ,_next = st.columns([1, 10, 1])
|
||||
if ("concepts" in st.session_state and len(st.session_state['concepts']) > 0):
|
||||
# The condition doesnt work, it should be fixed
|
||||
|
||||
with _prev:
|
||||
if st.button("Previous", disabled = st.session_state["cl_page"] == 1):
|
||||
st.session_state["cl_page"] -= 1
|
||||
st.session_state['concepts'] = getConceptsFromPath(st.session_state["cl_page"], st.session_state["cl_conceptPerPage"], searchInput)
|
||||
|
||||
with _per_page:
|
||||
st.caption("Page " + str(st.session_state["cl_page"]) + " / " + str(last_page))
|
||||
|
||||
with _next:
|
||||
if st.button("Next", disabled = st.session_state["cl_page"] == last_page):
|
||||
st.session_state["cl_page"] += 1
|
||||
st.session_state['concepts'] = getConceptsFromPath(st.session_state["cl_page"], st.session_state["cl_conceptPerPage"], searchInput)
|
||||
|
||||
return False
|
||||
|
Loading…
Reference in New Issue
Block a user