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
162 lines
4.3 KiB
TypeScript
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 }
|