GLMPilot / packages /client /src /components /ai /AIChatPanel.tsx
E5K7's picture
Initial commit: Rebranded to GLMPilot and migrated to GLM-5 API
c2c8c8d
import { useState, useRef, useEffect } from 'react';
import { ArrowUp, Square, Sparkles, Bug, TestTube2, Zap } from 'lucide-react';
import { useGLMChat } from '@/hooks/useGLMChat';
import { useAIStore } from '@/stores/aiStore';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { MarkdownMessage } from './MarkdownMessage';
export default function AIChatPanel() {
const { sendMessage, stopGeneration, isStreaming } = useGLMChat();
const messages = useAIStore((s) => s.messages);
const streamingMessage = useAIStore((s) => s.streamingMessage);
const [input, setInput] = useState('');
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, streamingMessage]);
const handleSend = () => {
if (!input.trim() || isStreaming) return;
const history = messages.map((m) => ({ role: m.role, content: m.content }));
sendMessage(input.trim(), history);
setInput('');
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
handleSend();
}
};
const quickActions = [
{ icon: Sparkles, label: 'Explain', prefix: 'Explain this code:\n' },
{ icon: Bug, label: 'Fix', prefix: 'Fix any issues in this code:\n' },
{ icon: TestTube2, label: 'Tests', prefix: 'Write unit tests for this code:\n' },
{ icon: Zap, label: 'Optimize', prefix: 'Optimize this code for performance:\n' },
];
return (
<div className="flex flex-col h-full bg-card/50">
{/* Header */}
<div className="px-4 py-3 border-b border-border">
<h3 className="text-sm font-semibold flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
AI Assistant
</h3>
<p className="text-xs text-muted-foreground mt-0.5">Powered by GLM-5</p>
</div>
{/* Messages */}
<div ref={scrollRef} className="flex-1 overflow-auto p-4 space-y-4 min-h-0">
{messages.length === 0 && !streamingMessage && (
<div className="text-center text-muted-foreground text-sm mt-8">
<Sparkles className="w-8 h-8 mx-auto mb-3 text-primary/40" />
<p className="font-medium">How can I help?</p>
<p className="text-xs mt-1">Ask about your code, request fixes, or get explanations.</p>
</div>
)}
{messages.map((msg) => (
<div
key={msg.id}
className={cn(
'max-w-[100%] text-sm',
msg.role === 'user' ? 'ml-auto' : 'mr-auto w-full'
)}
>
<div
className={cn(
'px-4 py-3 rounded-2xl',
msg.role === 'user'
? 'bg-primary/10 rounded-br-md whitespace-pre-wrap break-words max-w-[90%] float-right'
: 'liquid-glass rounded-bl-md w-full'
)}
>
{msg.role === 'user' ? msg.content : <MarkdownMessage content={msg.content} />}
</div>
<div className="clear-both"></div>
</div>
))}
{streamingMessage && (
<div className="mr-auto w-full text-sm">
<div className="liquid-glass px-4 py-3 rounded-2xl rounded-bl-md">
<MarkdownMessage content={streamingMessage + '▌'} />
</div>
</div>
)}
</div>
{/* Quick Actions */}
<div className="px-4 py-2 flex gap-1.5 border-t border-border/50">
{quickActions.map((action) => (
<button
key={action.label}
onClick={() => setInput(action.prefix)}
className="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:text-foreground hover:bg-secondary rounded-md transition-colors"
>
<action.icon className="w-3 h-3" />
{action.label}
</button>
))}
</div>
{/* Input */}
<div className="p-3 border-t border-border">
<div className="flex gap-2">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask anything... (⌘+Enter to send)"
rows={1}
className="min-h-[40px] max-h-[120px] resize-none text-sm"
/>
{isStreaming ? (
<Button size="icon" variant="ghost" onClick={stopGeneration}>
<Square className="w-4 h-4" />
</Button>
) : (
<Button
size="icon"
variant="default"
onClick={handleSend}
disabled={!input.trim()}
className="bg-primary hover:bg-primary/90 shrink-0"
>
<ArrowUp className="w-4 h-4" />
</Button>
)}
</div>
</div>
</div>
);
}