diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 708dd4e..5987c8e 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -8,6 +8,7 @@ export {} declare module 'vue' { export interface GlobalComponents { About: typeof import('./src/components/settings/About.vue')['default'] + AgentStackList: typeof import('./src/components/AgentStackList.vue')['default'] Appearance: typeof import('./src/components/settings/Appearance.vue')['default'] ArrayInput: typeof import('./src/components/ArrayInput.vue')['default'] ArraySelect: typeof import('./src/components/ArraySelect.vue')['default'] diff --git a/frontend/src/components/AgentStackList.vue b/frontend/src/components/AgentStackList.vue new file mode 100644 index 0000000..a4ce76d --- /dev/null +++ b/frontend/src/components/AgentStackList.vue @@ -0,0 +1,46 @@ +<template> + <div class="wrapper"> + <div class="group-header"> + {{ agentName }} + </div> + + <slot /> + </div> + + <hr> +</template> + +<script setup lang="ts"> +interface AgentStackListProps { + agentName: string; +} + +defineProps<AgentStackListProps>(); +</script> + +<style lang="scss" scoped> +@import "../styles/vars.scss"; + +.group-header { + border-bottom: 1px solid #dee2e6; + border-radius: 10px; + margin-bottom: 10px; + font-size: 1.25rem; + font-weight: 500; + padding: 10px; + + .dark & { + background-color: darken($color: $dark-header-bg, $amount: 1.90); + border-bottom: 0; + } +} + +@media (max-width: 770px) { + .group-header { + margin: -20px; + margin-bottom: 10px; + padding: 5px; + } +} + +</style> diff --git a/frontend/src/components/StackList.vue b/frontend/src/components/StackList.vue index 6b00cb2..19ffdcf 100644 --- a/frontend/src/components/StackList.vue +++ b/frontend/src/components/StackList.vue @@ -42,7 +42,22 @@ </span> </div> </div> - <div ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle"> + + <div v-if="searchText === '' && $root.agentCount > 1" ref="stackList" :style="stackListStyle" class="stack-list"> + <AgentStackList v-for="[agentName, stacks] in stackListByAgent" :agentName="agentName" :key="agentName"> + <StackListItem + v-for="(stack, index) in stacks" + :key="index" + :stack="stack" + :isSelectMode="selectMode" + :isSelected="isSelected" + :select="select" + :deselect="deselect" + /> + </AgentStackList> + </div> + + <div v-else ref="stackList" class="stack-list" :class="{ scrollbar: scrollbar }" :style="stackListStyle"> <div v-if="Object.keys(sortedStackList).length === 0" class="text-center mt-3"> <router-link to="/compose">{{ $t("addFirstStackMsg") }}</router-link> </div> @@ -189,7 +204,30 @@ export default { return result; }, + /** + * Groups all stacks by it's agent + * @returns {Map<string, object} A map containing all agents with their stacks + */ + stackListByAgent() { + const stacksByAgent = new Map(); + const stacks = this.$root.completeStackList; + for (const key of Object.keys(stacks)) { + // Handle stacks with no suffix (from the current endpoint) + let [ stackName, agent ] = key.split("_"); + const stackHasEndpoint = agent !== ""; + agent = stackHasEndpoint ? agent : this.$t("currentEndpoint"); + + if (!stacksByAgent.has(agent)) { + stacksByAgent.set(agent, []); + } + + const stack = stacks[!stackHasEndpoint ? `${stackName}_` : `${stackName}_${agent}`]; + stacksByAgent.get(agent).push(stack); + } + + return stacksByAgent; + }, isDarkTheme() { return document.body.classList.contains("dark"); },