feat: Add host-based assistant avatars (Neko/Mochi)
- Add avatar field to hosts.json config for Neko and Mochi - Create HostContext to provide host config to components - Display host avatar and name in chat messages instead of generic Claude - Add user avatar and first name to user messages - Include avatar in /api/hosts endpoint response - Fix appendFileSync import for debug logging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
BIN
frontend/public/mochi-avatar.png
Normal file
BIN
frontend/public/mochi-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
frontend/public/neko-avatar.png
Normal file
BIN
frontend/public/neko-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { SessionProvider, useSessionManager } from './contexts/SessionContext';
|
||||
import { HostProvider } from './contexts/HostContext';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { TabBar } from './components/TabBar';
|
||||
import { ChatPanel } from './components/ChatPanel';
|
||||
@@ -128,9 +129,11 @@ function AuthenticatedApp() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<AppContent />
|
||||
</SessionProvider>
|
||||
<HostProvider>
|
||||
<SessionProvider>
|
||||
<AppContent />
|
||||
</SessionProvider>
|
||||
</HostProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -235,6 +235,7 @@ export const ChatPanel = memo(function ChatPanel({ sessionId }) {
|
||||
messages={session.messages || []}
|
||||
isProcessing={session.isProcessing}
|
||||
onSendMessage={handleSendMessage}
|
||||
hostId={session.host}
|
||||
/>
|
||||
) : (
|
||||
<WelcomeScreen session={session} onStart={start} />
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useHosts } from '../contexts/HostContext';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
@@ -117,7 +119,10 @@ function SystemHints({ reminders, inline = false }) {
|
||||
);
|
||||
}
|
||||
|
||||
export const MessageList = memo(function MessageList({ messages, isProcessing, onSendMessage }) {
|
||||
// Not using memo here - needs to re-render when HostContext updates
|
||||
export function MessageList({ messages, isProcessing, onSendMessage, hostId }) {
|
||||
const { hosts } = useHosts();
|
||||
const hostConfig = hosts[hostId] || null;
|
||||
const containerRef = useRef(null);
|
||||
const messagesEndRef = useRef(null);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
@@ -228,7 +233,7 @@ export const MessageList = memo(function MessageList({ messages, isProcessing, o
|
||||
className="h-full overflow-y-auto p-4 space-y-4"
|
||||
>
|
||||
{processedMessages.map((message, index) => (
|
||||
<Message key={`${message.type}-${message._originalIndex}-${message.timestamp || index}`} message={message} onSendMessage={onSendMessage} />
|
||||
<Message key={`${message.type}-${message._originalIndex}-${message.timestamp || index}-${hostConfig?.avatar || 'no-avatar'}`} message={message} onSendMessage={onSendMessage} hostConfig={hostConfig} />
|
||||
))}
|
||||
|
||||
{/* Processing indicator */}
|
||||
@@ -260,10 +265,15 @@ export const MessageList = memo(function MessageList({ messages, isProcessing, o
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const Message = memo(function Message({ message, onSendMessage }) {
|
||||
function Message({ message, onSendMessage, hostConfig }) {
|
||||
const { type, content, tool, input, timestamp, toolUseId, attachments } = message;
|
||||
const { user } = useAuth();
|
||||
|
||||
// Assistant name and avatar from host config
|
||||
const assistantName = hostConfig?.name || 'Claude';
|
||||
const assistantAvatar = hostConfig?.avatar || '🤖';
|
||||
|
||||
// Memoize parsed system reminders and thinking for assistant messages
|
||||
const parsedContent = useMemo(() => {
|
||||
@@ -287,11 +297,19 @@ const Message = memo(function Message({ message, onSendMessage }) {
|
||||
const fileCount = hasAttachments ? attachments.length - imageCount : 0;
|
||||
return (
|
||||
<div className="flex gap-3 message-enter">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-4 h-4" />
|
||||
</div>
|
||||
{user?.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.name || 'You'}
|
||||
className="w-8 h-8 rounded-lg flex-shrink-0 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center flex-shrink-0">
|
||||
<User className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w-[85%]">
|
||||
<div className="text-xs text-dark-500 mb-1">You</div>
|
||||
<div className="text-xs text-dark-500 mb-1">{user?.name?.split(' ')[0] || 'You'}</div>
|
||||
<div className="bg-dark-800 rounded-lg rounded-tl-none p-3 text-dark-100 inline-block">
|
||||
{/* Attachment badge */}
|
||||
{hasAttachments && (
|
||||
@@ -323,11 +341,19 @@ const Message = memo(function Message({ message, onSendMessage }) {
|
||||
const { content: cleanContent, reminders, thinking } = parsedContent;
|
||||
return (
|
||||
<div className="flex gap-3 message-enter">
|
||||
<div className="w-8 h-8 rounded-lg bg-orange-600 flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-4 h-4" />
|
||||
</div>
|
||||
{assistantAvatar?.startsWith('/') ? (
|
||||
<img
|
||||
src={assistantAvatar}
|
||||
alt={assistantName}
|
||||
className="w-8 h-8 rounded-lg object-cover flex-shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-lg bg-orange-600 flex items-center justify-center flex-shrink-0 text-lg">
|
||||
{assistantAvatar}
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w-[85%]">
|
||||
<div className="text-xs text-dark-500 mb-1">Claude</div>
|
||||
<div className="text-xs text-dark-500 mb-1">{assistantName}</div>
|
||||
<div className="bg-dark-850 rounded-lg rounded-tl-none p-4 border border-dark-700 claude-message inline-block">
|
||||
<ThinkingBlock thinking={thinking} />
|
||||
<ReactMarkdown
|
||||
@@ -408,7 +434,7 @@ const Message = memo(function Message({ message, onSendMessage }) {
|
||||
};
|
||||
|
||||
return <div>{renderContent()}</div>;
|
||||
});
|
||||
}
|
||||
|
||||
// Tool configuration with icons, colors, and display logic
|
||||
const TOOL_CONFIG = {
|
||||
|
||||
44
frontend/src/contexts/HostContext.jsx
Normal file
44
frontend/src/contexts/HostContext.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const HostContext = createContext(null);
|
||||
|
||||
export function HostProvider({ children }) {
|
||||
const [hosts, setHosts] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/hosts', { credentials: 'include' })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const hostsMap = {};
|
||||
(data.hosts || []).forEach(host => {
|
||||
hostsMap[host.id] = host;
|
||||
});
|
||||
setHosts(hostsMap);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load hosts:', err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HostContext.Provider value={{ hosts, loading }}>
|
||||
{children}
|
||||
</HostContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useHosts() {
|
||||
const context = useContext(HostContext);
|
||||
if (!context) {
|
||||
return { hosts: {}, loading: false };
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useHost(hostId) {
|
||||
const { hosts } = useHosts();
|
||||
return hosts[hostId] || null;
|
||||
}
|
||||
Reference in New Issue
Block a user