upgraded ui:

Added personalities zoo page
enhanced Apps zoo page
This commit is contained in:
Saifeddine ALOUI 2024-08-25 04:21:43 +02:00
parent 6596b9ccdd
commit d3a0f3bf14
22 changed files with 544 additions and 524 deletions

View File

@ -3,7 +3,12 @@ class WebAppLocalizer {
this.translations = translations;
this.localStoragePrefix = localStoragePrefix;
this.currentLang = this.loadCurrentLanguage() || Object.keys(translations)[0];
this.languageSelector = languageSelector;
if (typeof languageSelector === 'string') {
this.languageSelector = document.getElementById(languageSelector);
} else {
this.languageSelector = languageSelector;
}
if (this.languageSelector) {
this.initializeLanguageSelector();

@ -1 +1 @@
Subproject commit 62fff368de81efeeee01c8ed2aa0bd973be26ab2
Subproject commit 92109353b8e11d7d35a95c0640ca155f8d818cc7

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
web/dist/assets/index-cb896237.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 224 B

View File

Before

Width:  |  Height:  |  Size: 363 B

After

Width:  |  Height:  |  Size: 363 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

4
web/dist/index.html vendored
View File

@ -6,8 +6,8 @@
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LoLLMS WebUI</title>
<script type="module" crossorigin src="/assets/index-b4757eb4.js"></script>
<link rel="stylesheet" href="/assets/index-057fde26.css">
<script type="module" crossorigin src="/assets/index-4dce9705.js"></script>
<link rel="stylesheet" href="/assets/index-cb896237.css">
</head>
<body>
<div id="app"></div>

View File

@ -1,5 +1,5 @@
<template>
<div ref="container"></div>
<div :id="containerId" ref="container"></div>
</template>
<script>
@ -8,54 +8,60 @@ export default {
props: {
ui: {
type: String,
required: true,
default: "",
required: true
},
instanceId: {
type: String,
required: true
}
},
mounted() {
this.renderUI();
data() {
return {
containerId: `dynamic-ui-${this.instanceId}`
};
},
watch: {
ui: {
handler: 'renderUI',
immediate: true,
},
handler(newValue) {
console.log(`UI prop changed for instance ${this.instanceId}:`, newValue);
this.$nextTick(() => {
this.renderContent();
});
}
}
},
methods: {
renderUI() {
renderContent() {
console.log(`Rendering content for instance ${this.instanceId}...`);
const container = this.$refs.container;
container.innerHTML = '';
// Parse the UI string
const parser = new DOMParser();
const doc = parser.parseFromString(this.ui, 'text/html');
// Extract and apply styles
// Extract and inject CSS
const styles = doc.getElementsByTagName('style');
Array.from(styles).forEach(style => {
const styleElement = document.createElement('style');
styleElement.textContent = style.textContent;
container.appendChild(styleElement);
const scopedStyle = document.createElement('style');
scopedStyle.textContent = this.scopeCSS(style.textContent);
document.head.appendChild(scopedStyle);
});
// Extract and execute scripts
// Extract and inject HTML
container.innerHTML = doc.body.innerHTML;
// Extract and execute JavaScript
const scripts = doc.getElementsByTagName('script');
Array.from(scripts).forEach(script => {
const scriptElement = document.createElement('script');
scriptElement.textContent = script.textContent;
container.appendChild(scriptElement);
const newScript = document.createElement('script');
newScript.textContent = script.textContent;
container.appendChild(newScript);
});
// Append body content
const body = doc.body;
Array.from(body.children).forEach(child => {
container.appendChild(child);
});
// Apply any inline styles from the body
if (body.style.cssText) {
container.style.cssText = body.style.cssText;
}
},
},
scopeCSS(css) {
return css.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g, `#${this.containerId} $1$2`);
}
}
};
</script>

View File

@ -94,13 +94,12 @@
<JsonViewer :jsonFormText="metadata.title" :jsonData="metadata.content" :key="'msgjson-' + message.id" />
</div>
</div>
<DynamicUIRenderer ref="ui" class="w-full" :ui="message.ui" :key="'msgui-' + message.id" />
<DynamicUIRenderer v-if="message.ui" ref="ui" class="w-full" :ui="message.ui" :key="'msgui-' + message.id" />
<audio controls v-if="audio_url!=null" :key="audio_url">
<source :src="audio_url" type="audio/wav" ref="audio_player" >
Your browser does not support the audio element.
</audio>
</audio>
</div>
<!-- MESSAGE CONTROLS -->
<div class="flex-row justify-end mx-2">

View File

@ -28,6 +28,7 @@ const menuItems = ref([]);
const navLinks = [
{ active: true, route: 'discussions', text: 'Discussions' },
{ active: true, route: 'playground', text: 'Playground' },
{ active: true, route: 'PersonalitiesZoo', text: 'Personalities Zoo' },
{ active: true, route: 'AppsZoo', text: 'Apps Zoo' },
{
active: false,

View File

@ -1,109 +1,62 @@
<template>
<div
class=" min-w-96 items-start p-4 hover:bg-primary-light rounded-lg mb-2 shadow-lg border-2 cursor-pointer select-none"
tabindex="-1"
:class="selected_computed ? 'border-2 border-primary-light' : 'border-transparent', isMounted ? 'bg-blue-200 dark:bg-blue-700':''"
<div class="personality-card bg-white border rounded-xl shadow-lg p-6 hover:shadow-xl transition duration-300 ease-in-out flex flex-col h-full"
:class="selected_computed ? 'border-primary-light' : 'border-transparent', isMounted ? 'bg-blue-200 dark:bg-blue-700' : ''"
:title="!personality.installed ? 'Not installed' : ''">
<div class="flex-grow">
<div class="flex items-center mb-4">
<img :src="getImgUrl()" @error="defaultImg($event)" alt="Personality Icon"
class="w-16 h-16 rounded-full border border-gray-300 mr-4 cursor-pointer"
@click="toggleSelected"
@mouseover="showThumbnail" @mousemove="updateThumbnailPosition" @mouseleave="hideThumbnail" />
<div>
<h3 class="font-bold text-xl text-gray-800 cursor-pointer" @click="toggleSelected">{{ personality.name }}</h3>
<p class="text-sm text-gray-600">Author: {{ personality.author }}</p>
<p class="text-sm text-gray-600">Version: {{ personality.version }}</p>
<p v-if="personality.language" class="text-sm text-gray-600">Language: {{ personality.language }}</p>
<p class="text-sm text-gray-600">Category: {{ personality.category }}</p>
</div>
</div>
<div :class="!personality.installed ? 'border-red-500' : ''">
<div class="mb-4">
<h4 class="font-semibold mb-1 text-gray-700">Description:</h4>
<p class="text-sm text-gray-600 h-20 overflow-y-auto" v-html="personality.description"></p>
</div>
<div class="flex flex-row items-center flex-shrink-0 gap-3">
<img @click="toggleSelected" ref="imgElement" :src="getImgUrl()" @error="defaultImg($event)"
class="w-10 h-10 rounded-full object-fill text-red-700 cursor-pointer"
@mouseover="showThumbnail" @mousemove="updateThumbnailPosition" @mouseleave="hideThumbnail">
<h3 @click="toggleSelected" class="font-bold font-large text-lg line-clamp-3 cursor-pointer"
@mouseover="showThumbnail" @mousemove="updateThumbnailPosition" @mouseleave="hideThumbnail">
{{ personality.name }}
</h3>
<button
class="hover:text-secondary duration-75 active:scale-90 font-medium rounded-lg text-sm p-2 text-center inline-flex items-center"
title="Copy link to clipboard" @click.stop="toggleCopyLink()">
<i data-feather="clipboard" class="w-5"></i>
<div v-if="personality.languages && select_language" class="mb-4">
<label for="languages" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Select Language:</label>
<select v-if="!isMounted" id="languages" v-model="personality.language"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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">
<option v-for="(item, index) in personality.languages" :key="index" :selected="item == personality.languages[0]">
{{ item }}
</option>
</select>
</div>
</div>
<div class="mt-auto pt-4 border-t">
<div class="flex justify-between items-center flex-wrap">
<button @click="toggleFavorite" class="text-yellow-500 hover:text-yellow-600 transition duration-300 ease-in-out" :title="isFavorite ? 'Remove from favorites' : 'Add to favorites'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" :fill="isFavorite ? 'currentColor' : 'none'" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
</button>
<div v-if="thumbnailVisible" :style="{ top: thumbnailPosition.y + 'px', left: thumbnailPosition.x + 'px' }"
class="fixed z-50 w-20 h-20 rounded-full overflow-hidden">
<img :src="getImgUrl()" class="w-full h-full object-fill">
</div>
</div>
<div class="">
<div class="">
<div class="flex items-center">
<i data-feather="user" class="w-5 m-1"></i>
<b>Author:&nbsp;</b>
{{ personality.author }}
</div>
<div class="flex items-center">
<i data-feather="git-commit" class="w-5 m-1"></i>
<b>Version:&nbsp;</b>
{{ personality.version }}
</div>
<div v-if="personality.languages && select_language" class="flex items-center">
<i data-feather="globe" class="w-5 m-1"></i>
<b>Languages:&nbsp;</b>
<select v-if="!isMounted" id="languages" v-model ="personality.language"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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">
<option v-for="(item, index) in personality.languages" :key="index"
:selected="item == personality.languages[0]">{{
item
}}
</option>
</select>
</div>
<div v-if="personality.language" class="flex items-center">
<i data-feather="globe" class="w-5 m-1"></i>
<b>Language:&nbsp;</b>
{{ personality.language }}
</div>
<div class="flex items-center">
<i data-feather="bookmark" class="w-5 m-1"></i>
<b>Category:&nbsp;</b>
{{ personality.category }}
</div>
</div>
<div class="flex items-center">
<i data-feather="info" class="w-5 m-1"></i>
<b>Description:&nbsp;</b><br>
</div>
<p class="mx-1 opacity-80 h-20 overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary" :title="personality.description" v-html="personality.description"></p>
</div>
<div class="rounded bg-blue-300">
<button v-if="isMounted" type="button" title="Select"
@click="toggleSelected"
class="hover:text-secondary duration-75 active:scale-90 font-medium rounded-lg text-sm p-2 text-center inline-flex items-center " @click.stop="">
<i
data-feather="check"
></i>
<span class="sr-only">Select</span>
</button>
<button v-if="isMounted" type="button" title="Talk"
@click="toggleTalk"
class="hover:text-secondary duration-75 active:scale-90 font-medium rounded-lg text-sm p-2 text-center inline-flex items-center " @click.stop="">
<i data-feather="send" class="w-5"></i>
<span class="sr-only">Talk</span>
</button>
<button type="button" title="Show_folder"
@click="showFolder"
class="hover:text-secondary duration-75 active:scale-90 font-medium rounded-lg text-sm p-2 text-center inline-flex items-center " @click.stop="">
<i data-feather="folder" class="w-5"></i>
<span class="sr-only">Show Folder</span>
</button>
<InteractiveMenu :commands="commandsList" :force_position=2 title="Menu">
<button v-if="isMounted" @click="toggleSelected" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out" title="Select">
<i data-feather="check" class="h-6 w-6"></i>
</button>
<button v-if="isMounted" @click="toggleTalk" class="text-green-500 hover:text-green-600 transition duration-300 ease-in-out" title="Talk">
<i data-feather="send" class="h-6 w-6"></i>
</button>
<button @click="showFolder" class="text-purple-500 hover:text-purple-600 transition duration-300 ease-in-out" title="Show Folder">
<i data-feather="folder" class="h-6 w-6"></i>
</button>
<InteractiveMenu :commands="commandsList" :force_position="2" title="Menu" class="text-gray-500 hover:text-gray-600 transition duration-300 ease-in-out">
</InteractiveMenu>
</div>
</div>
</div>
<div v-if="thumbnailVisible" :style="{ top: thumbnailPosition.y + 'px', left: thumbnailPosition.x + 'px' }"
class="fixed z-50 w-20 h-20 rounded-full overflow-hidden">
<img :src="getImgUrl()" class="w-full h-full object-fill">
</div>
</div>
</template>

View File

@ -11,6 +11,7 @@ import NodesView from '../views/NodesView.vue'
import ComfyuiView from '../views/ComfyuiView.vue'
import AutoSDView from '../views/AutoSDView.vue'
import AppsZoo from '../views/AppsZoo.vue'
import PersonalitiesZoo from '../views/PersonalitiesZoo.vue'
const router = createRouter({
@ -21,6 +22,11 @@ const router = createRouter({
name: 'AppsZoo',
component: AppsZoo
},
{
path: '/personalities_view/',
name: 'PersonalitiesZoo',
component: PersonalitiesZoo
},
{
path: '/auto_sd_view/',
name: 'AutoSD',

View File

@ -1,6 +1,6 @@
<template>
<div class="app-zoo background-color w-full p-6 overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary">
<nav class="panels-color shadow-lg rounded-lg p-4 max-w-4xl mx-auto">
<nav class="panels-color shadow-lg rounded-lg p-4 max-w-4xl mx-auto mb-8">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center space-x-4">
<button
@ -43,111 +43,83 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<div class="flex items-center space-x-4">
<label for="category-select" class="font-semibold">Category:</label>
<select
id="category-select"
v-model="selectedCategory"
class="border-2 border-gray-300 rounded-md px-2 py-1"
>
<option value="all">All Categories</option>
<option v-for="category in categories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
<div class="flex items-center space-x-4">
<label for="sort-select" class="font-semibold">Sort by:</label>
<select
id="sort-select"
v-model="sortBy"
class="border-2 border-gray-300 rounded-md px-2 py-1"
>
<option value="name">Name</option>
<option value="author">Author</option>
<option value="date">Creation Date</option>
<option value="update">Last Update</option>
</select>
<button @click="toggleSortOrder" class="btn btn-secondary">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</button>
</div>
</div>
</nav>
</nav>
<div v-if="loading" class="flex justify-center items-center space-x-2 my-8" aria-live="polite">
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500"></div>
<span class="text-xl text-gray-700 font-semibold">Loading...</span>
</div>
<div v-for="category in categories" :key="category" class="mb-12">
<h2 class="text-3xl font-bold mb-6 text-gray-800">{{ category }}</h2>
</div>
<div v-else>
<h2 class="text-2xl font-bold mb-4">Favorite Apps</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-8">
<app-card
v-for="app in favoriteApps"
:key="app.uid"
:app="app"
@toggle-favorite="toggleFavorite"
@install="installApp"
@uninstall="uninstallApp"
@delete="deleteApp"
@edit="editApp"
@download="downloadApp"
@view="handleAppClick"
@open="openApp"
@start-server="startServer"
/>
</div>
<h2 class="text-2xl font-bold mb-4">{{ currentCategoryName }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div
v-for="app in filteredApps.filter(a => a.category === category)"
:key="app.uid"
class="app-card bg-white border rounded-xl shadow-lg p-6 hover:shadow-xl transition duration-300 ease-in-out flex flex-col h-full"
>
<div class="flex-grow">
<div class="flex items-center mb-4">
<img :src="app.icon" alt="App Icon" class="w-16 h-16 rounded-full border border-gray-300 mr-4" />
<div>
<h3 class="font-bold text-xl text-gray-800">{{ app.name }}</h3>
<p class="text-sm text-gray-600">Author: {{ app.author }}</p>
<p class="text-sm text-gray-600">Version: {{ app.version }}</p>
<p class="text-sm text-gray-600">Creation date: {{ app.creation_date }}</p>
<p class="text-sm text-gray-600">Last update date: {{ app.last_update_date }}</p>
<!-- New section for app visibility -->
<p class="text-sm" :class="app.is_public ? 'text-green-600' : 'text-orange-600'">
{{ app.is_public ? 'Public App' : 'Local App' }}
</p>
<app-card
v-for="app in sortedAndFilteredApps"
:key="app.uid"
:app="app"
@toggle-favorite="toggleFavorite"
@install="installApp"
@uninstall="uninstallApp"
@delete="deleteApp"
@edit="editApp"
@download="downloadApp"
@view="handleAppClick"
@open="openApp"
@start-server="startServer"
/>
</div>
</div>
<div class="mb-4">
<h4 class="font-semibold mb-1 text-gray-700">Description:</h4>
<p class="text-sm text-gray-600 h-20 overflow-y-auto">{{ app.description }}</p>
</div>
<p class="text-sm text-gray-600 mb-2">AI Model: {{ app.model_name }}</p>
<div v-if="app.disclaimer && app.disclaimer.trim() !== ''" class="mb-4">
<h4 class="font-semibold mb-1 text-gray-700">Disclaimer:</h4>
<p class="text-xs text-gray-500 italic h-16 overflow-y-auto">{{ app.disclaimer }}</p>
</div>
</div>
<div class="mt-auto pt-4 border-t">
<div class="flex justify-between items-center flex-wrap">
<button v-if="app.installed" @click="uninstallApp(app.folder_name)" class="text-red-500 hover:text-red-600 transition duration-300 ease-in-out" title="Uninstall">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<button v-else-if="app.existsInFolder" @click="deleteApp(app.name)" class="text-yellow-500 hover:text-yellow-600 transition duration-300 ease-in-out" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<button v-else @click="installApp(app.folder_name)" class="text-blue-500 hover:text-blue-600 transition duration-300 ease-in-out" title="Install">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
<button v-if="app.installed" @click="editApp(app)" class="text-purple-500 hover:text-purple-600 transition duration-300 ease-in-out" title="Edit">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button @click="downloadApp(app.folder_name)" class="text-green-500 hover:text-green-600 transition duration-300 ease-in-out" title="Download">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
<button @click="handleAppClick(app)" class="text-gray-500 hover:text-gray-600 transition duration-300 ease-in-out" title="View">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button v-if="app.installed" @click="openApp(app)" class="text-indigo-500 hover:text-indigo-600 transition duration-300 ease-in-out" title="Open">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</button>
<!-- New button for starting the server -->
<button v-if="app.has_server && app.installed" @click="startServer(app.folder_name)" class="text-teal-500 hover:text-teal-600 transition duration-300 ease-in-out" title="Start Server">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7" />
</svg>
</button>
<!-- New button for updating the app -->
<button v-if="app.has_update" @click="installApp(app.folder_name)" class="relative text-yellow-500 hover:text-yellow-600 transition duration-300 ease-in-out animate-pulse" title="Update Available">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span class="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 transform translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full">!</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- App details modal -->
<div v-if="selectedApp" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-11/12 h-5/6 flex flex-col">
<div class="flex justify-between items-center mb-4">
@ -159,6 +131,7 @@
</div>
</div>
<!-- Toast message -->
<div v-if="message" class="fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-md" :class="{ 'bg-green-100 text-green-800': successMessage, 'bg-red-100 text-red-800': !successMessage }">
{{ message }}
</div>
@ -167,12 +140,18 @@
<script>
import axios from 'axios';
import AppCard from '@/components/AppCard.vue'; // Create this component for individual app cards
export default {
components: {
AppCard,
},
data() {
return {
apps: [],
githubApps: [],
favorites: [],
selectedCategory: 'all',
selectedApp: null,
appCode: '',
loading: false,
@ -181,11 +160,15 @@ export default {
searchQuery: '',
selectedFile: null,
isUploading: false,
message: '',
error: ''
error: '',
sortBy: 'name',
sortOrder: 'asc',
};
},
computed: {
currentCategoryName() {
return this.selectedCategory === 'all' ? 'All Apps' : this.selectedCategory;
},
combinedApps() {
const installedAppNames = this.apps.map(app => app.name);
const localAppsMap = new Map(this.apps.map(app => [app.name, { ...app, installed: true, existsInFolder: true }]));
@ -198,17 +181,66 @@ export default {
return Array.from(localAppsMap.values());
},
filteredApps() {
return this.combinedApps.filter(app =>
app.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
app.description.toLowerCase().includes(this.searchQuery.toLowerCase())
);
},
categories() {
return [...new Set(this.combinedApps.map(app => app.category))];
}
},
filteredApps() {
return this.combinedApps.filter(app => {
const matchesSearch = app.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
app.description.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
app.author.toLowerCase().includes(this.searchQuery.toLowerCase());
const matchesCategory = this.selectedCategory === 'all' || app.category === this.selectedCategory;
return matchesSearch && matchesCategory;
});
},
sortedAndFilteredApps() {
return this.filteredApps.sort((a, b) => {
let comparison = 0;
switch (this.sortBy) {
case 'name':
comparison = a.name.localeCompare(b.name);
break;
case 'author':
comparison = a.author.localeCompare(b.author);
break;
case 'date':
comparison = new Date(a.creation_date) - new Date(b.creation_date);
break;
case 'update':
comparison = new Date(a.last_update_date) - new Date(b.last_update_date);
break;
}
return this.sortOrder === 'asc' ? comparison : -comparison;
});
},
favoriteApps() {
return this.combinedApps.filter(app => this.favorites.includes(app.uid));
},
},
methods: {
toggleSortOrder() {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
},
toggleFavorite(appUid) {
const index = this.favorites.indexOf(appUid);
if (index === -1) {
this.favorites.push(appUid);
} else {
this.favorites.splice(index, 1);
}
this.saveFavoritesToLocalStorage();
},
saveFavoritesToLocalStorage() {
localStorage.setItem('appZooFavorites', JSON.stringify(this.favorites));
},
loadFavoritesFromLocalStorage() {
const savedFavorites = localStorage.getItem('appZooFavorites');
console.log("savedFavorites",savedFavorites)
if (savedFavorites) {
this.favorites = JSON.parse(savedFavorites);
}
},
startServer(appName) {
const payload = {
client_id: this.$store.state.client_id, // Assuming you have a clientId property in your component
@ -450,6 +482,7 @@ export default {
},
mounted() {
this.fetchGithubApps();
this.loadFavoritesFromLocalStorage();
},
};
</script>

View File

@ -4979,10 +4979,8 @@ export default {
console.log("category")
//await this.getPersonalitiesArr()
this.personality_category = this.configFile.personality_category
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.configFile.personality_category)
this.personalitiesFiltered = this.$store.state.personalities.filter((item) => item.category === this.configFile.personality_category)
// this.personalitiesFiltered.sort()
//mountedPersArr
this.modelsFiltered = []
@ -5716,7 +5714,7 @@ export default {
this.api_get_req("list_personalities_categories").then((cats)=>{
console.log("cats",cats)
this.persCatgArr = cats
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.personality_category)
this.personalitiesFiltered = this.$store.state.personalities.filter((item) => item.category === this.personality_category)
this.personalitiesFiltered.sort()
})
@ -6038,11 +6036,10 @@ export default {
},
async getPersonalitiesArr() {
this.isLoading = true
this.personalities = []
this.$store.state.personalities = []
const dictionary = await this.api_get_req("get_all_personalities")
const config = this.$store.state.config
//console.log('asdas',config)
// console.log("all_personalities")
console.log("recovering all_personalities")
// console.log(dictionary)
const catkeys = Object.keys(dictionary); // returns categories folder names
for (let j = 0; j < catkeys.length; j++) {
@ -6064,15 +6061,15 @@ export default {
})
if (this.personalities.length == 0) {
this.personalities = modPersArr
if (this.$store.state.personalities.length == 0) {
this.$store.state.personalities = modPersArr
} else {
this.personalities = this.personalities.concat(modPersArr)
this.$store.state.personalities = this.$store.state.personalities.concat(modPersArr)
}
}
this.personalities.sort((a, b) => a.name.localeCompare(b.name))
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.configFile.personality_category)
this.$store.state.personalities.sort((a, b) => a.name.localeCompare(b.name))
this.personalitiesFiltered = this.$store.state.personalities.filter((item) => item.category === this.configFile.personality_category)
this.personalitiesFiltered.sort()
console.log('per filtered', this.personalitiesFiltered)
this.isLoading = false
@ -6080,13 +6077,13 @@ export default {
},
async filterPersonalities() {
if (!this.searchPersonality) {
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.configFile.personality_category )
this.personalitiesFiltered = this.$store.state.personalities.filter((item) => item.category === this.configFile.personality_category )
this.personalitiesFiltered.sort()
this.searchPersonalityInProgress = false
return
}
const searchTerm = this.searchPersonality.toLowerCase()
const seachedPersonalities = this.personalities.filter((item) => {
const seachedPersonalities = this.$store.state.personalities.filter((item) => {
if (item.name && item.name.toLowerCase().includes(searchTerm) || item.description && item.description.toLowerCase().includes(searchTerm) || item.full_path && item.full_path.toLowerCase().includes(searchTerm)) {
return item
@ -6099,7 +6096,7 @@ export default {
if (seachedPersonalities.length > 0) {
this.personalitiesFiltered = seachedPersonalities.sort()
} else {
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.configFile.personality_category)
this.personalitiesFiltered = this.$store.state.personalities.filter((item) => item.category === this.configFile.personality_category)
this.personalitiesFiltered.sort()
}
this.searchPersonalityInProgress = false
@ -6265,12 +6262,12 @@ export default {
if (res.status) {
this.configFile.personalities = res.personalities
this.$store.state.toast.showToast("Personality unmounted", 4, true)
const persId = this.personalities.findIndex(item => item.full_path == pers.full_path)
const persId = this.$store.state.personalities.findIndex(item => item.full_path == pers.full_path)
const persFilteredId = this.personalitiesFiltered.findIndex(item => item.full_path == pers.full_path)
const persIdZoo = this.$refs.personalitiesZoo.findIndex(item => item.full_path == pers.full_path)
console.log('ppp', this.personalities[persId])
console.log('ppp', this.$store.state.personalities[persId])
this.personalities[persId].isMounted = false
this.$store.state.personalities[persId].isMounted = false
if (persFilteredId > -1) {
this.personalitiesFiltered[persFilteredId].isMounted = false
@ -6866,9 +6863,9 @@ export default {
if (!this.isMounted) {
return null
}
const index = this.personalities.findIndex(item => item.full_path === this.configFile.personalities[this.configFile.active_personality_id])
const index = this.$store.state.personalities.findIndex(item => item.full_path === this.configFile.personalities[this.configFile.active_personality_id])
if (index > -1) {
return this.personalities[index].name
return this.$store.state.personalities[index].name
} else {
return null

@ -1 +1 @@
Subproject commit b1adf519ca590c1b1bdfce02bc2ae87a16fc869d
Subproject commit 31a63be27ac43e6a344220f40aeab559394d8eb5