File size: 4,701 Bytes
8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 e178e44 8edd759 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 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 |