feat(canvas): Add message input to ChatPanel with Claude integration

- Add textarea input with Enter-to-send functionality to ChatPanel
- Pass sendMessage and isConnected props from AtomizerCanvas
- Add disabled states during thinking/disconnected states
- Use Lucide Send icon for submit button
- Show connection status with Sparkles welcome icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 22:45:47 -05:00
parent 22b483a912
commit 94b59fb65f
2 changed files with 74 additions and 23 deletions

View File

@@ -47,6 +47,7 @@ function CanvasFlow() {
executeIntent,
validateIntent,
analyzeIntent,
sendMessage,
} = useCanvasChat({
onError: (error) => console.error('Canvas chat error:', error),
});
@@ -213,6 +214,8 @@ function CanvasFlow() {
<ChatPanel
messages={messages}
isThinking={isThinking || isExecuting}
onSendMessage={sendMessage}
isConnected={isConnected}
/>
</div>
) : selectedNode ? (

View File

@@ -1,48 +1,96 @@
/**
* Chat Panel for Canvas - Displays messages from Claude
* Chat Panel for Canvas - Displays messages from Claude with input
*/
import { useRef, useEffect } from 'react';
import { useRef, useEffect, useState, KeyboardEvent } from 'react';
import { Send, Sparkles } from 'lucide-react';
import { Message, ChatMessage } from '../../chat/ChatMessage';
import { ThinkingIndicator } from '../../chat/ThinkingIndicator';
interface ChatPanelProps {
messages: Message[];
isThinking: boolean;
onSendMessage?: (message: string) => void;
isConnected?: boolean;
}
export function ChatPanel({ messages, isThinking }: ChatPanelProps) {
export function ChatPanel({ messages, isThinking, onSendMessage, isConnected = true }: ChatPanelProps) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [input, setInput] = useState('');
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isThinking]);
const handleSend = () => {
if (input.trim() && onSendMessage && !isThinking) {
onSendMessage(input.trim());
setInput('');
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-dark-900">
{/* Welcome message if no messages */}
{messages.length === 0 && !isThinking && (
<div className="text-center py-8">
<div className="w-12 h-12 rounded-xl bg-primary-500/20 flex items-center justify-center mx-auto mb-4">
<span className="text-2xl">🧠</span>
<div className="flex-1 flex flex-col overflow-hidden">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-dark-900">
{/* Welcome message if no messages */}
{messages.length === 0 && !isThinking && (
<div className="text-center py-8">
<div className="w-12 h-12 rounded-xl bg-primary-500/20 flex items-center justify-center mx-auto mb-4">
<Sparkles size={24} className="text-primary-400" />
</div>
<p className="text-dark-400 text-sm max-w-xs mx-auto">
Use <strong className="text-white">Validate</strong>, <strong className="text-white">Analyze</strong>, or <strong className="text-white">Execute</strong> to interact with Claude about your optimization workflow.
</p>
<p className="text-dark-500 text-xs mt-4">
Or type a message below to ask questions.
</p>
</div>
)}
{/* Messages */}
{messages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
{/* Thinking indicator */}
{isThinking && <ThinkingIndicator />}
{/* Scroll anchor */}
<div ref={messagesEndRef} />
</div>
{/* Input */}
{onSendMessage && (
<div className="p-3 border-t border-dark-700 bg-dark-850">
<div className="flex gap-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={isConnected ? "Ask about your optimization..." : "Connecting..."}
disabled={isThinking || !isConnected}
rows={1}
className="flex-1 px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-500 rounded-lg resize-none focus:border-primary-500 focus:outline-none transition-colors disabled:opacity-50"
/>
<button
onClick={handleSend}
disabled={!input.trim() || isThinking || !isConnected}
className="px-3 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Send size={18} />
</button>
</div>
<p className="text-dark-400 text-sm max-w-xs mx-auto">
Use <strong className="text-white">Validate</strong>, <strong className="text-white">Analyze</strong>, or <strong className="text-white">Execute</strong> to interact with Claude about your optimization workflow.
</p>
</div>
)}
{/* Messages */}
{messages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
{/* Thinking indicator */}
{isThinking && <ThinkingIndicator />}
{/* Scroll anchor */}
<div ref={messagesEndRef} />
</div>
);
}