|
|
class AIChat extends HTMLElement { |
|
|
connectedCallback() { |
|
|
this.messages = []; |
|
|
this.model = ''; |
|
|
this.apiKey = ''; |
|
|
this.isTyping = false; |
|
|
this.attachShadow({ mode: 'open' }); |
|
|
this.shadowRoot.innerHTML = ` |
|
|
<style> |
|
|
.chat-container { |
|
|
background: white; |
|
|
border-radius: 0.5rem; |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
height: 500px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
border: 1px solid #e5e7eb; |
|
|
} |
|
|
.chat-header { |
|
|
padding: 1rem; |
|
|
border-bottom: 1px solid #e5e7eb; |
|
|
font-weight: 600; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
background: #f9fafb; |
|
|
border-top-left-radius: 0.5rem; |
|
|
border-top-right-radius: 0.5rem; |
|
|
} |
|
|
.chat-header select { |
|
|
background: white; |
|
|
border: 1px solid #e5e7eb; |
|
|
border-radius: 0.375rem; |
|
|
padding: 0.25rem 0.5rem; |
|
|
font-size: 0.875rem; |
|
|
margin-left: 0.5rem; |
|
|
} |
|
|
.chat-messages { |
|
|
flex: 1; |
|
|
overflow-y: auto; |
|
|
padding: 1rem; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 0.75rem; |
|
|
} |
|
|
.message { |
|
|
max-width: 80%; |
|
|
padding: 0.75rem 1rem; |
|
|
border-radius: 1rem; |
|
|
line-height: 1.5; |
|
|
word-wrap: break-word; |
|
|
white-space: pre-wrap; |
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
.message pre { |
|
|
background: rgba(0,0,0,0.05); |
|
|
padding: 0.5rem; |
|
|
border-radius: 0.25rem; |
|
|
overflow-x: auto; |
|
|
margin: 0.5rem 0; |
|
|
} |
|
|
.message code { |
|
|
font-family: monospace; |
|
|
background: rgba(0,0,0,0.05); |
|
|
padding: 0.2rem 0.4rem; |
|
|
border-radius: 0.2rem; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
.user-message { |
|
|
align-self: flex-end; |
|
|
background: #4f46e5; |
|
|
color: white; |
|
|
border-bottom-right-radius: 0.25rem; |
|
|
margin-left: 20%; |
|
|
} |
|
|
.ai-message { |
|
|
align-self: flex-start; |
|
|
background: #f9fafb; |
|
|
color: #111827; |
|
|
border-bottom-left-radius: 0.25rem; |
|
|
margin-right: 20%; |
|
|
border: 1px solid #e5e7eb; |
|
|
} |
|
|
.chat-input { |
|
|
display: flex; |
|
|
padding: 1rem; |
|
|
border-top: 1px solid #e5e7eb; |
|
|
} |
|
|
.chat-input input { |
|
|
flex: 1; |
|
|
padding: 0.75rem 1rem; |
|
|
border: 1px solid #e5e7eb; |
|
|
border-radius: 0.375rem; |
|
|
outline: none; |
|
|
transition: border-color 0.2s; |
|
|
} |
|
|
.chat-input input:focus { |
|
|
border-color: #4f46e5; |
|
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); |
|
|
} |
|
|
.chat-input button { |
|
|
margin-left: 0.5rem; |
|
|
padding: 0.75rem 1.5rem; |
|
|
background: #4f46e5; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 0.375rem; |
|
|
cursor: pointer; |
|
|
transition: background 0.2s, transform 0.1s; |
|
|
} |
|
|
.chat-input button:hover { |
|
|
background: #4338ca; |
|
|
} |
|
|
.chat-input button:active { |
|
|
transform: scale(0.98); |
|
|
} |
|
|
.typing-indicator { |
|
|
display: flex; |
|
|
padding: 0.5rem; |
|
|
align-items: center; |
|
|
color: #6b7280; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
.typing-dots { |
|
|
display: flex; |
|
|
margin-left: 0.5rem; |
|
|
} |
|
|
.typing-dot { |
|
|
width: 0.5rem; |
|
|
height: 0.5rem; |
|
|
background: #9ca3af; |
|
|
border-radius: 50%; |
|
|
margin: 0 0.125rem; |
|
|
animation: typingAnimation 1.4s infinite ease-in-out; |
|
|
} |
|
|
.typing-dot:nth-child(1) { animation-delay: 0s; } |
|
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; } |
|
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; } |
|
|
@keyframes typingAnimation { |
|
|
0%, 60%, 100% { transform: translateY(0); } |
|
|
30% { transform: translateY(-0.25rem); } |
|
|
} |
|
|
</style> |
|
|
<div class="chat-container"> |
|
|
<div class="chat-header"> |
|
|
<i data-feather="message-square" class="mr-2"></i> |
|
|
<span>AI Chat Assistant</span> |
|
|
</div> |
|
|
<div class="chat-messages" id="chat-messages"></div> |
|
|
<div class="chat-input"> |
|
|
<input type="text" id="chat-input" placeholder="Type your message..."> |
|
|
<button id="send-btn"> |
|
|
<i data-feather="send"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
this.messages = []; |
|
|
this.model = ''; |
|
|
this.apiKey = ''; |
|
|
this.isTyping = false; |
|
|
|
|
|
this.shadowRoot.getElementById('send-btn').addEventListener('click', () => this.sendMessage()); |
|
|
this.shadowRoot.getElementById('chat-input').addEventListener('keypress', (e) => { |
|
|
if (e.key === 'Enter') this.sendMessage(); |
|
|
}); |
|
|
} |
|
|
setModel(model, apiKey, modelList = []) { |
|
|
this.model = model; |
|
|
this.apiKey = apiKey; |
|
|
this.modelList = modelList; |
|
|
|
|
|
|
|
|
const header = this.shadowRoot.querySelector('.chat-header'); |
|
|
if (modelList.length > 0) { |
|
|
const select = document.createElement('select'); |
|
|
select.className = 'ml-4 px-2 py-1 border rounded text-sm'; |
|
|
modelList.forEach(m => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = m.value; |
|
|
option.textContent = m.name; |
|
|
if (m.value === model) option.selected = true; |
|
|
select.appendChild(option); |
|
|
}); |
|
|
|
|
|
select.addEventListener('change', (e) => { |
|
|
this.model = e.target.value; |
|
|
this.addMessage('ai', `Switched to ${e.target.selectedOptions[0].text}. How can I help you now?`); |
|
|
}); |
|
|
|
|
|
header.appendChild(select); |
|
|
} |
|
|
|
|
|
this.addMessage('ai', `You are now chatting with ${this.getModelName(model)}. How can I help you with your server?`); |
|
|
} |
|
|
|
|
|
getModelName(modelValue) { |
|
|
const model = this.modelList.find(m => m.value === modelValue); |
|
|
return model ? model.name : modelValue; |
|
|
} |
|
|
addMessage(sender, text) { |
|
|
const messagesContainer = this.shadowRoot.getElementById('chat-messages'); |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = `message ${sender}-message`; |
|
|
|
|
|
|
|
|
const formattedText = this.formatMessage(text); |
|
|
messageDiv.innerHTML = formattedText; |
|
|
|
|
|
messagesContainer.appendChild(messageDiv); |
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight; |
|
|
} |
|
|
|
|
|
formatMessage(text) { |
|
|
|
|
|
let formatted = text |
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') |
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>') |
|
|
.replace(/`([^`]+)`/g, '<code>$1</code>') |
|
|
.replace(/```([^`]+)```/gs, '<pre>$1</pre>') |
|
|
.replace(/\n/g, '<br>'); |
|
|
|
|
|
|
|
|
formatted = formatted.replace( |
|
|
/(https?:\/\/[^\s]+)/g, |
|
|
'<a href="$1" target="_blank" rel="noopener noreferrer" class="text-indigo-600 hover:underline">$1</a>' |
|
|
); |
|
|
|
|
|
return formatted; |
|
|
} |
|
|
showTypingIndicator() { |
|
|
const messagesContainer = this.shadowRoot.getElementById('chat-messages'); |
|
|
const typingDiv = document.createElement('div'); |
|
|
typingDiv.className = 'typing-indicator'; |
|
|
typingDiv.id = 'typing-indicator'; |
|
|
typingDiv.innerHTML = ` |
|
|
<span>AI is typing</span> |
|
|
<div class="typing-dots"> |
|
|
<div class="typing-dot"></div> |
|
|
<div class="typing-dot"></div> |
|
|
<div class="typing-dot"></div> |
|
|
</div> |
|
|
`; |
|
|
messagesContainer.appendChild(typingDiv); |
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight; |
|
|
} |
|
|
|
|
|
hideTypingIndicator() { |
|
|
const typingIndicator = this.shadowRoot.getElementById('typing-indicator'); |
|
|
if (typingIndicator) { |
|
|
typingIndicator.remove(); |
|
|
} |
|
|
} |
|
|
async sendMessage() { |
|
|
|
|
|
const sendBtn = this.shadowRoot.getElementById('send-btn'); |
|
|
const originalBtnHTML = sendBtn.innerHTML; |
|
|
sendBtn.innerHTML = '<i data-feather="loader" class="animate-spin"></i>'; |
|
|
sendBtn.disabled = true; |
|
|
feather.replace(); |
|
|
const input = this.shadowRoot.getElementById('chat-input'); |
|
|
const message = input.value.trim(); |
|
|
if (!message) return; |
|
|
|
|
|
if (!this.model || !this.apiKey) { |
|
|
this.addMessage('ai', 'Please configure your AI settings first (API key and model)'); |
|
|
return; |
|
|
} |
|
|
|
|
|
input.value = ''; |
|
|
this.addMessage('user', message); |
|
|
input.focus(); |
|
|
this.showTypingIndicator(); |
|
|
this.isTyping = true; |
|
|
|
|
|
try { |
|
|
|
|
|
|
|
|
const context = this.messages |
|
|
.slice(-4) |
|
|
.map(msg => `${msg.sender === 'user' ? 'User' : 'Assistant'}: ${msg.text}`) |
|
|
.join('\n'); |
|
|
|
|
|
const response = await fetch(`https://api-inference.huggingface.co/models/${this.model}`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Authorization': `Bearer ${this.apiKey}`, |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
inputs: `Previous conversation:\n${context}\n\nUser: ${message}\nAssistant:`, |
|
|
parameters: { |
|
|
max_new_tokens: 250, |
|
|
temperature: 0.7, |
|
|
repetition_penalty: 1.2 |
|
|
} |
|
|
}) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
this.hideTypingIndicator(); |
|
|
this.isTyping = false; |
|
|
if (data.error) { |
|
|
this.addMessage('ai', `Error: ${data.error}. Please check your API key and model selection.`); |
|
|
} else { |
|
|
let reply = data[0]?.generated_text || "I'm sorry, I couldn't generate a response."; |
|
|
|
|
|
reply = reply.replace(/.*Assistant:/s, '').trim(); |
|
|
this.addMessage('ai', reply); |
|
|
|
|
|
this.messages.push({sender: 'ai', text: reply}); |
|
|
} |
|
|
} catch (error) { |
|
|
this.hideTypingIndicator(); |
|
|
this.isTyping = false; |
|
|
this.addMessage('ai', `Connection error: ${error.message}. Please check your internet connection.`); |
|
|
} finally { |
|
|
|
|
|
sendBtn.innerHTML = originalBtnHTML; |
|
|
sendBtn.disabled = false; |
|
|
feather.replace(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
customElements.define('ai-chat', AIChat); |