| import { Loader2, MessageSquare, Send, Sparkles } from 'lucide-react' |
| import { useRef, useState } from 'react' |
|
|
| interface ChatMessage { |
| id: string |
| role: 'assistant' | 'user' |
| text: string |
| } |
|
|
| const SUGGESTIONS = [ |
| 'List warehouse assets', |
| 'Publish the current pipeline as a private space', |
| 'Configure 8 steps, guidance 1.5, space "wan-demo-1"', |
| 'What can you do?', |
| ] |
|
|
| function ChatPanel({ onLog }: { onLog: (msg: string) => void }) { |
| const [messages, setMessages] = useState<ChatMessage[]>([ |
| { |
| id: 'm0', |
| role: 'assistant', |
| text: "Hey - I'm your Workshop build assistant. I can configure the Wan 2.2 I2V pipeline, attach LoRAs from your reference library, and publish a private Space. What are we building?", |
| }, |
| ]) |
| const [input, setInput] = useState('') |
| const [thinking, setThinking] = useState(false) |
| const scrollRef = useRef<HTMLDivElement>(null) |
|
|
| const send = async (raw: string) => { |
| const text = raw.trim() |
| if (!text || thinking) return |
| const userMsg: ChatMessage = { id: `u-${Date.now()}`, role: 'user', text } |
| setMessages(prev => [...prev, userMsg]) |
| setInput('') |
| setThinking(true) |
| onLog('Assistant: ' + text) |
| try { |
| const res = await fetch('/api/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ messages: [...messages, userMsg].map(m => ({ role: m.role, content: m.text })) }), |
| }) |
| const data = await res.json() |
| if (!res.ok) throw new Error(data.detail ?? 'chat failed') |
| setMessages(prev => [...prev, { id: `a-${Date.now()}`, role: 'assistant', text: data.content }]) |
| } catch (e) { |
| onLog('Assistant error: ' + e) |
| setMessages(prev => [...prev, { id: `a-${Date.now()}`, role: 'assistant', text: 'Something went wrong talking to the assistant: ' + e }]) |
| } finally { |
| setThinking(false) |
| } |
| } |
|
|
| return ( |
| <div className="bg-[#191919] border border-[#2E2E2E] rounded-xl flex flex-col h-full min-h-0"> |
| <div className="flex items-center gap-2 px-4 py-3 border-b border-[#2E2E2E]"> |
| <MessageSquare size={16} className="text-[#FF9D00]" /> |
| <span className="text-sm font-semibold text-white">Workshop Assistant</span> |
| {thinking && <Loader2 size={14} className="text-[#FF9D00] animate-spin ml-auto" />} |
| </div> |
| <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-3 space-y-3"> |
| {messages.map(msg => ( |
| <div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}> |
| <div |
| className={`max-w-[85%] rounded-lg px-3 py-2 text-sm leading-relaxed whitespace-pre-wrap ${ |
| msg.role === 'user' |
| ? 'bg-[#FF9D00] text-black' |
| : 'bg-[#0F0F0F] text-[#E0E0E0] border border-[#2E2E2E]' |
| }`} |
| > |
| {msg.role === 'assistant' ? ( |
| <span className="flex items-start gap-1.5"> |
| <Sparkles size={13} className="text-[#FF9D00] mt-0.5 flex-shrink-0" /> |
| <span>{msg.text}</span> |
| </span> |
| ) : ( |
| msg.text |
| )} |
| </div> |
| </div> |
| ))} |
| {thinking && ( |
| <div className="flex justify-start"> |
| <div className="bg-[#0F0F0F] border border-[#2E2E2E] rounded-lg px-3 py-2 text-sm text-[#8B8B8B] flex items-center gap-2"> |
| <Loader2 size={14} className="animate-spin" /> thinking… |
| </div> |
| </div> |
| )} |
| </div> |
| <div className="px-3 pt-0.5 flex flex-wrap gap-1.5"> |
| {SUGGESTIONS.map(s => ( |
| <button key={s} onClick={() => send(s)} |
| className="text-[10px] text-[#8B8B8B] hover:text-[#FF9D00] border border-[#2E2E2E] rounded-full px-2.5 py-1 transition-colors"> |
| {s} |
| </button> |
| ))} |
| </div> |
| <div className="flex items-center gap-2 p-3 border-t border-[#2E2E2E]"> |
| <input |
| value={input} |
| onChange={e => setInput(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && send(input)} |
| placeholder="Describe the pipeline to build…" |
| className="flex-1 bg-[#0F0F0F] border border-[#2E2E2E] rounded-lg px-3 py-2 text-sm text-white placeholder:text-[#6B6B6B] outline-none focus:border-[#FF9D00]" |
| /> |
| <button onClick={() => send(input)} disabled={thinking || !input.trim()} |
| className="bg-[#FF9D00] hover:bg-[#FFB93E] text-black p-2 rounded-lg transition-colors disabled:opacity-40"> |
| <Send size={16} /> |
| </button> |
| </div> |
| </div> |
| ) |
| } |
|
|
| export default ChatPanel |