[codex] add customizable profile avatars (#870)
* add customizable profile avatars * keep profile avatar visible when sidebar collapses * simplify collapsed profile avatar styling * force managed gateway startup in docker * limit gateway autostart to active profile * restore all profile gateway autostart * fix managed gateway runtime detection
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { ref, computed, nextTick, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage, NInput, NButton, NSpace, NSelect, NPopover, NPopconfirm, NInputNumber } from 'naive-ui'
|
||||
import multiavatar from '@multiavatar/multiavatar'
|
||||
import { useGroupChatStore } from '@/stores/hermes/group-chat'
|
||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||
import { updateRoomConfig, forceCompress } from '@/api/hermes/group-chat'
|
||||
import GroupMessageList from './GroupMessageList.vue'
|
||||
import GroupChatInput from './GroupChatInput.vue'
|
||||
import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue'
|
||||
import type { Attachment } from '@/stores/hermes/chat'
|
||||
import type { RoomAgent } from '@/api/hermes/group-chat'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
@@ -33,13 +34,13 @@ const profileOptions = computed(() =>
|
||||
profilesStore.profiles.map(p => ({ label: p.name, value: p.name }))
|
||||
)
|
||||
|
||||
const avatarCache = new Map<string, string>()
|
||||
function profileAvatarFor(profileName?: string) {
|
||||
if (!profileName) return null
|
||||
return profilesStore.profiles.find(profile => profile.name === profileName)?.avatar || null
|
||||
}
|
||||
|
||||
function agentAvatarUrl(name: string): string {
|
||||
if (avatarCache.has(name)) return avatarCache.get(name)!
|
||||
const uri = multiavatar(name)
|
||||
avatarCache.set(name, uri)
|
||||
return uri
|
||||
function agentAvatarName(agent: RoomAgent): string {
|
||||
return agent.profile || agent.name || agent.agentId
|
||||
}
|
||||
|
||||
const hasRoom = computed(() => !!store.currentRoomId)
|
||||
@@ -146,6 +147,12 @@ async function handleAddAgent() {
|
||||
showAddAgentModal.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (profilesStore.profiles.length === 0) {
|
||||
void profilesStore.fetchProfiles()
|
||||
}
|
||||
})
|
||||
|
||||
async function confirmAddAgent() {
|
||||
if (!selectedProfile.value || !store.currentRoomId) return
|
||||
try {
|
||||
@@ -311,7 +318,7 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
<div class="avatar-stack-inner">
|
||||
<!-- User avatar first -->
|
||||
<span class="avatar-stack-item" :style="{ zIndex: store.agents.length + 1 }">
|
||||
<span class="agent-avatar" v-html="agentAvatarUrl(store.userName || store.userId)" />
|
||||
<ProfileAvatar class="agent-avatar" :name="store.userName || store.userId" :size="28" />
|
||||
</span>
|
||||
<span
|
||||
v-for="(agent, index) in store.agents.slice(-4)"
|
||||
@@ -319,14 +326,14 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
class="avatar-stack-item"
|
||||
:style="{ zIndex: store.agents.length - index }"
|
||||
>
|
||||
<span class="agent-avatar" v-html="agentAvatarUrl(agent.name)" />
|
||||
<ProfileAvatar class="agent-avatar" :name="agentAvatarName(agent)" :avatar="profileAvatarFor(agent.profile)" :size="28" />
|
||||
</span>
|
||||
<span v-if="store.agents.length > 4" class="avatar-stack-more">+{{ store.agents.length - 4 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="agent-popover">
|
||||
<div class="agent-popover-item" style="margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--n-border-color, #efeff5);">
|
||||
<span class="agent-avatar" v-html="agentAvatarUrl(store.userName || store.userId)" />
|
||||
<ProfileAvatar class="agent-avatar" :name="store.userName || store.userId" :size="28" />
|
||||
<div class="agent-popover-info">
|
||||
<span class="agent-popover-name">{{ store.userName || 'You' }}</span>
|
||||
<span class="agent-popover-profile">{{ t('groupChat.you') }}</span>
|
||||
@@ -334,7 +341,7 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
</div>
|
||||
<div class="agent-popover-title">{{ t('groupChat.agents') }} ({{ store.agents.length }})</div>
|
||||
<div v-for="agent in store.agents" :key="agent.id" class="agent-popover-item">
|
||||
<span class="agent-avatar" v-html="agentAvatarUrl(agent.name)" />
|
||||
<ProfileAvatar class="agent-avatar" :name="agentAvatarName(agent)" :avatar="profileAvatarFor(agent.profile)" :size="28" />
|
||||
<div class="agent-popover-info">
|
||||
<span class="agent-popover-name">{{ agent.name }}</span>
|
||||
<span class="agent-popover-profile">{{ agent.profile }}</span>
|
||||
@@ -348,7 +355,7 @@ watch(() => store.sortedMessages.length, async () => {
|
||||
<!-- Only user avatar, no agents -->
|
||||
<div v-else-if="store.userName" class="avatar-stack-inner">
|
||||
<span class="avatar-stack-item">
|
||||
<span class="agent-avatar" v-html="agentAvatarUrl(store.userName || store.userId)" />
|
||||
<ProfileAvatar class="agent-avatar" :name="store.userName || store.userId" :size="28" />
|
||||
</span>
|
||||
</div>
|
||||
<button class="icon-btn" :title="t('groupChat.addAgent')" @click="handleAddAgent">
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import multiavatar from '@multiavatar/multiavatar'
|
||||
import MarkdownRenderer from '../chat/MarkdownRenderer.vue'
|
||||
import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue'
|
||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||
import {
|
||||
copyTextToClipboard,
|
||||
handleCodeBlockCopyClick,
|
||||
@@ -32,6 +33,7 @@ const props = defineProps<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useMessage()
|
||||
const profilesStore = useProfilesStore()
|
||||
const speech = useGlobalSpeech()
|
||||
const voiceSettings = useVoiceSettings()
|
||||
const previewUrl = ref<string | null>(null)
|
||||
@@ -52,9 +54,8 @@ const timeStr = computed(() => {
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
})
|
||||
|
||||
const avatarSvg = computed(() => {
|
||||
return multiavatar(props.message.senderName || props.message.senderId)
|
||||
})
|
||||
const avatarProfileName = computed(() => agentInfo.value?.profile || props.message.senderName || props.message.senderId)
|
||||
const avatarProfile = computed(() => profilesStore.profiles.find(profile => profile.name === agentInfo.value?.profile))
|
||||
|
||||
const mentionNames = computed(() => ['all', ...props.agents.map(a => a.name).filter(Boolean)])
|
||||
const parsedThinking = computed(() => parseThinking(props.message.content || '', { streaming: !!props.message.isStreaming }))
|
||||
@@ -384,7 +385,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div v-if="isToolMessage" class="group-message tool-message">
|
||||
<div class="avatar">
|
||||
<span v-html="avatarSvg" />
|
||||
<ProfileAvatar :name="avatarProfileName" :avatar="avatarProfile?.avatar" :size="36" />
|
||||
</div>
|
||||
|
||||
<div class="msg-body">
|
||||
@@ -430,7 +431,7 @@ onBeforeUnmount(() => {
|
||||
<div v-else class="group-message" :class="{ agent: isAgent, self: isSelf }">
|
||||
<!-- Avatar -->
|
||||
<div class="avatar">
|
||||
<span v-html="avatarSvg" />
|
||||
<ProfileAvatar :name="avatarProfileName" :avatar="avatarProfile?.avatar" :size="36" />
|
||||
</div>
|
||||
|
||||
<div class="msg-body">
|
||||
@@ -674,11 +675,6 @@ onBeforeUnmount(() => {
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(svg) {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
|
||||
Reference in New Issue
Block a user