Files
hermes-workspace/src/components/agent-avatar.tsx
Eric fb17b5f86f feat: v1.0.0 — profiles, knowledge browser, MCP settings, skills hub upgrade, eslint, security contact update
New features:
- Multi-profile management (create, switch, rename, delete)
- Knowledge browser with document viewer
- MCP server settings screen
- Skills hub with marketplace search fallback
- Context usage tracking and display

Improvements:
- eslint added and auto-fixed (69 issues resolved)
- Settings dialog restructured (Agent, Smart Routing, Voice, Display sections)
- Navigation updated with Profiles tab across desktop/mobile
- Security contact updated to GitHub advisories + X DM
- .gitignore hardened (.runtime/, internal dev docs)
- Version bumped to 1.0.0

Build: clean | TypeScript: 0 errors | Tests: 4/4 passing
2026-04-10 01:49:13 -04:00

162 lines
4.3 KiB
TypeScript

'use client'
import { useSyncExternalStore } from 'react'
import {
TooltipContent,
TooltipProvider,
TooltipRoot,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
export type AgentAvatarPreference = 'lobster' | 'logo'
export type AgentAvatarSize = 'sm' | 'md' | 'lg'
export const AGENT_AVATAR_STORAGE_KEY = 'hermes-loader-preference'
const AGENT_AVATAR_EVENT = 'hermes-loader-preference-change'
type AgentAvatarProps = {
size?: AgentAvatarSize
className?: string
iconClassName?: string
}
function getContainerSizeClassName(size: AgentAvatarSize): string {
if (size === 'sm') return 'size-6'
if (size === 'lg') return 'size-10'
return 'size-8'
}
function getEmojiSizeClassName(size: AgentAvatarSize): string {
if (size === 'sm') return 'text-base'
if (size === 'lg') return 'text-2xl'
return 'text-xl'
}
function getLogoSizeClassName(size: AgentAvatarSize): string {
if (size === 'sm') return 'size-4 rounded overflow-hidden'
if (size === 'lg') return 'size-6 rounded-lg overflow-hidden'
return 'size-5 rounded-md overflow-hidden'
}
export function readAgentAvatarPreference(): AgentAvatarPreference {
if (typeof window === 'undefined') return 'lobster'
try {
const stored = window.localStorage.getItem(AGENT_AVATAR_STORAGE_KEY)
if (stored === 'lobster' || stored === 'logo') {
return stored
}
} catch {
// Ignore storage errors
}
return 'lobster'
}
export function writeAgentAvatarPreference(
preference: AgentAvatarPreference,
): void {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(AGENT_AVATAR_STORAGE_KEY, preference)
} catch {
// Ignore storage errors
}
window.dispatchEvent(new Event(AGENT_AVATAR_EVENT))
}
export function toggleAgentAvatarPreference(
currentPreference: AgentAvatarPreference,
): AgentAvatarPreference {
const nextPreference = currentPreference === 'lobster' ? 'logo' : 'lobster'
writeAgentAvatarPreference(nextPreference)
return nextPreference
}
export function subscribeToAgentAvatarPreference(
onStoreChange: () => void,
): () => void {
if (typeof window === 'undefined') {
return function noop() {
return undefined
}
}
function handleStorage(event: StorageEvent) {
if (event.key === AGENT_AVATAR_STORAGE_KEY) {
onStoreChange()
}
}
function handlePreferenceChange() {
onStoreChange()
}
window.addEventListener('storage', handleStorage)
window.addEventListener(AGENT_AVATAR_EVENT, handlePreferenceChange)
return function unsubscribe() {
window.removeEventListener('storage', handleStorage)
window.removeEventListener(AGENT_AVATAR_EVENT, handlePreferenceChange)
}
}
function AgentAvatar({
size = 'md',
className,
iconClassName,
}: AgentAvatarProps) {
const preference = useSyncExternalStore(
subscribeToAgentAvatarPreference,
readAgentAvatarPreference,
function getServerSnapshot() {
return 'lobster'
},
)
return (
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger
type="button"
className={cn(
'inline-flex cursor-pointer items-center justify-center rounded-full border border-primary-300/70 bg-primary-200/70 text-primary-900 transition-transform duration-150 hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/45',
getContainerSizeClassName(size),
className,
)}
aria-label="Toggle agent avatar"
onClick={function handleToggleAvatar(event) {
event.stopPropagation()
toggleAgentAvatarPreference(preference as AgentAvatarPreference)
}}
>
{preference === 'lobster' ? (
<span
className={cn('leading-none', getEmojiSizeClassName(size))}
aria-hidden="true"
>
🦞
</span>
) : (
<img
src="/hermes-avatar.webp"
alt="Hermes"
className={cn(
getLogoSizeClassName(size),
iconClassName,
'rounded-xl',
)}
/>
)}
</TooltipTrigger>
<TooltipContent side="top">Click to switch avatar</TooltipContent>
</TooltipRoot>
</TooltipProvider>
)
}
export { AgentAvatar }