| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 14 | 14 | ||
| 15 | 15 | - `timeFormat` fix for pure time stamps | |
| 16 | 16 | - page input in list table now autogrows in large values | |
| 17 | + - plugin was not able to use more then one package in `custom` folder package.json | ||
| 18 | + - express server proxy now has `blobStream()` method which returns stream for blob data e.g. for piping | ||
| 17 | 19 | ||
| 18 | 20 | ||
| 19 | 21 | ## [1.3.48] - 2024-10-02 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -538,13 +538,12 @@ class CodeInjector implements ICodeInjector { | |||
| 538 | 538 | } | |
| 539 | 539 | ||
| 540 | 540 | await this.runNpmShell({command: 'ci', cwd: CodeInjector.SPA_TMP_PATH}); | |
| 541 | - | ||
| 542 | 541 | ||
| 543 | 542 | if (iconPackageNames.length) { | |
| 544 | 543 | const npmInstallCommand = `install ${[ | |
| 545 | 544 | ...iconPackageNames, | |
| 546 | 545 | ...usersPackages, | |
| 547 | - ...pluginPackages.map(({ packages }) => packages) | ||
| 546 | + ...pluginPackages.map(({ packages }) => packages.join(' ')), | ||
| 548 | 547 | ].join(' ')}`; | |
| 549 | 548 | await this.runNpmShell({command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH}); | |
| 550 | 549 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,7 +6,7 @@ | |||
| 6 | 6 | <!-- Modal content --> | |
| 7 | 7 | <div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700"> | |
| 8 | 8 | <!-- Modal header --> | |
| 9 | - <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"> | ||
| 9 | + <div class="flex items-center justify-between p-3 md:p-4 border-b rounded-t dark:border-gray-600"> | ||
| 10 | 10 | <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> | |
| 11 | 11 | Generate image with AI | |
| 12 | 12 | </h3> | |
@@ -21,10 +21,10 @@ | |||
| 21 | 21 | </div> | |
| 22 | 22 | <!-- Modal body --> | |
| 23 | 23 | <div class="p-4 md:p-5 space-y-4"> | |
| 24 | - | ||
| 25 | - <label for="message" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your prompt</label> | ||
| 26 | - <textarea id="message" rows="4" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" | ||
| 27 | - placeholder="Prompt which will be passed to AI network" v-model="prompt"></textarea> | ||
| 24 | + <textarea id="message" rows="3" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" | ||
| 25 | + placeholder="Prompt which will be passed to AI network" v-model="prompt" | ||
| 26 | + title="Prompt which will be passed to AI network" | ||
| 27 | + ></textarea> | ||
| 28 | 28 | ||
| 29 | 29 | <div class="flex items-center justify-center w-full relative"> | |
| 30 | 30 | <div | |
@@ -39,7 +39,7 @@ | |||
| 39 | 39 | ||
| 40 | 40 | <div id="gallery" class="relative w-full" data-carousel="static"> | |
| 41 | 41 | <!-- Carousel wrapper --> | |
| 42 | - <div class="relative h-56 overflow-hidden rounded-lg md:h-96"> | ||
| 42 | + <div class="relative h-56 overflow-hidden rounded-lg md:h-72"> | ||
| 43 | 43 | <!-- Item 1 --> | |
| 44 | 44 | <div v-for="(img, index) in images" :key="index" class="hidden duration-700 ease-in-out" data-carousel-item> | |
| 45 | 45 | <img :src="img" class="absolute block max-w-full h-auto -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2" alt=""> | |
@@ -48,12 +48,10 @@ | |||
| 48 | 48 | <div v-if="images.length === 0" class="flex items-center justify-center w-full h-full"> | |
| 49 | 49 | ||
| 50 | 50 | <button @click="generateImages" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 | |
| 51 | - focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center | ||
| 52 | - dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">Generate images</button> | ||
| 51 | + focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center | ||
| 52 | + dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">Generate images</button> | ||
| 53 | 53 | ||
| 54 | 54 | </div> | |
| 55 | - | ||
| 56 | - | ||
| 57 | 55 | ||
| 58 | 56 | </div> | |
| 59 | 57 | <!-- Slider controls --> | |
@@ -84,9 +82,15 @@ | |||
| 84 | 82 | </div> | |
| 85 | 83 | <!-- Modal footer --> | |
| 86 | 84 | <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600"> | |
| 87 | - <button type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Use image</button> | ||
| 85 | + <button type="button" @click="confirmImage" | ||
| 86 | + :disabled="loading" | ||
| 87 | + class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center | ||
| 88 | + dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 | ||
| 89 | + disabled:opacity-50 disabled:cursor-not-allowed" | ||
| 90 | + >Use image</button> | ||
| 88 | 91 | <button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" | |
| 89 | - @click="emit('close')">Cancel</button> | ||
| 92 | + @click="emit('close')" | ||
| 93 | + >Cancel</button> | ||
| 90 | 94 | </div> | |
| 91 | 95 | </div> | |
| 92 | 96 | </div> | |
@@ -100,34 +104,36 @@ | |||
| 100 | 104 | <script setup lang="ts"> | |
| 101 | 105 | ||
| 102 | 106 | import { ref, onMounted } from 'vue' | |
| 103 | - import { Carousel, initFlowbite } from 'flowbite'; | ||
| 104 | - import internal from 'stream'; | ||
| 107 | + import { Carousel } from 'flowbite'; | ||
| 108 | + import { callAdminForthApi } from '@/utils'; | ||
| 105 | 109 | ||
| 106 | 110 | const prompt = ref(''); | |
| 107 | - const emit = defineEmits(['close']); | ||
| 111 | + const emit = defineEmits(['close', 'uploadImage']); | ||
| 108 | 112 | const props = defineProps(['meta', 'record']); | |
| 109 | 113 | const images = ref([]); | |
| 110 | 114 | const loading = ref(false); | |
| 111 | 115 | ||
| 112 | - function minifyRecord(rec: object) { | ||
| 113 | - // for each key make it string and make sure each field has not more then 100 characters | ||
| 114 | - // if more add ... at the end | ||
| 115 | - return Object.keys(rec).reduce((acc, key) => { | ||
| 116 | - if (!rec[key]) return acc; | ||
| 117 | - acc[key] = rec[key].toString().slice(0, 100) | ||
| 118 | - if (rec[key].length > 100) { | ||
| 119 | - acc[key] += '...' | ||
| 120 | - } | ||
| 121 | - return acc | ||
| 122 | - }, {}) | ||
| 116 | + function minifyField(field: string): string { | ||
| 117 | + if (field.length > 100) { | ||
| 118 | + return field.slice(0, 100) + '...'; | ||
| 119 | + } | ||
| 120 | + return field; | ||
| 123 | 121 | } | |
| 124 | 122 | ||
| 125 | 123 | const caurosel = ref(null); | |
| 126 | 124 | onMounted(() => { | |
| 127 | 125 | // Initialize carousel | |
| 128 | - prompt.value = `Generate image for field "${props.meta.pathColumnLabel}" in ${props.meta.resourceLabel}.${ | ||
| 129 | - Object.values(props.record || {}).length ? 'Additional context: ' + JSON.stringify(minifyRecord(props.record)) : '' | ||
| 130 | - }` | ||
| 126 | + let additionalContext = null; | ||
| 127 | + if (props.meta.fieldsForContext) { | ||
| 128 | + additionalContext = props.meta.fieldsForContext.filter((field: string) => props.record[field]).map((field: string) => { | ||
| 129 | + return `${field}: ${minifyField(props.record[field])}`; | ||
| 130 | + }).join('\n'); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + prompt.value = `Generate image for field "${props.meta.pathColumnLabel}" in ${props.meta.resourceLabel}. No text should be on image.` | ||
| 134 | + if (additionalContext) { | ||
| 135 | + prompt.value += ` ${additionalContext}`; | ||
| 136 | + } | ||
| 131 | 137 | ||
| 132 | 138 | }) | |
| 133 | 139 | ||
@@ -145,30 +151,53 @@ async function slide(direction: number) { | |||
| 145 | 151 | } | |
| 146 | 152 | } | |
| 147 | 153 | ||
| 148 | - async function generateImages() { | ||
| 154 | + async function confirmImage() { | ||
| 149 | 155 | loading.value = true; | |
| 156 | + | ||
| 150 | 157 | const currentIndex = caurosel.value?.getActiveItem()?.position || 0; | |
| 158 | + const img = images.value[currentIndex]; | ||
| 159 | + // read url to base64 and send it to the parent component | ||
| 160 | + const imgBlob = await fetch( | ||
| 161 | + `/adminapi/v1/plugin/${props.meta.pluginInstanceId}/cors-proxy?url=${encodeURIComponent(img)}` | ||
| 162 | + ).then(res => { return res.blob() }); | ||
| 151 | 163 | ||
| 152 | - await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 * 2)); | ||
| 153 | - if (!images.value.length) { | ||
| 154 | - images.value = [ | ||
| 155 | - 'https://flowbite.s3.amazonaws.com/docs/gallery/square/image-1.jpg', | ||
| 156 | - 'https://flowbite.s3.amazonaws.com/docs/gallery/square/image-2.jpg', | ||
| 157 | - 'https://flowbite.s3.amazonaws.com/docs/gallery/square/image-3.jpg', | ||
| 158 | - ]; | ||
| 159 | - } else { | ||
| 160 | - images.value = [ | ||
| 161 | - ...images.value, | ||
| 162 | - 'https://flowbite.s3.amazonaws.com/docs/gallery/square/image-4.jpg', | ||
| 163 | - 'https://flowbite.s3.amazonaws.com/docs/gallery/square/image-5.jpg', | ||
| 164 | - ]; | ||
| 164 | + emit('uploadImage', imgBlob); | ||
| 165 | + emit('close'); | ||
| 166 | + | ||
| 167 | + loading.value = false; | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + async function generateImages() { | ||
| 171 | + loading.value = true; | ||
| 172 | + const currentIndex = caurosel.value?.getActiveItem()?.position || 0; | ||
| 173 | + const resp = await callAdminForthApi({ | ||
| 174 | + path: `/plugin/${props.meta.pluginInstanceId}/generate_images`, | ||
| 175 | + method: 'POST', | ||
| 176 | + body: { | ||
| 177 | + prompt: prompt.value, | ||
| 178 | + }, | ||
| 179 | + }); | ||
| 180 | + | ||
| 181 | + if (resp.error) { | ||
| 182 | + window.adminforth.alert({ | ||
| 183 | + message: `error: ${JSON.stringify(resp.error)}`, | ||
| 184 | + variant: 'danger', | ||
| 185 | + timeout: 15, | ||
| 186 | + }); | ||
| 187 | + return; | ||
| 165 | 188 | } | |
| 189 | + | ||
| 190 | + images.value = [ | ||
| 191 | + ...images.value, | ||
| 192 | + ...resp.images.map(im => im.data[0].url), | ||
| 193 | + ]; | ||
| 194 | + | ||
| 195 | + // images.value = [ | ||
| 196 | + // 'https://via.placeholder.com/600x400?text=Image+1', | ||
| 197 | + // 'https://via.placeholder.com/600x400?text=Image+2', | ||
| 198 | + // ]; | ||
| 166 | 199 | await new Promise(resolve => setTimeout(resolve, 0)); | |
| 167 | 200 | ||
| 168 | - console.log(images.value.map((img, index) => ({ | ||
| 169 | - el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`), | ||
| 170 | - position: index, | ||
| 171 | - }))) | ||
| 172 | 201 | caurosel.value = new Carousel( | |
| 173 | 202 | document.getElementById('gallery'), | |
| 174 | 203 | images.value.map((img, index) => ({ | |
| Back | FazBrowse Home | New Git URL |
0 commit comments