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