happyrwandan's picture
chat window?
c827077 verified
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;
// Add model selector to chat header
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`;
// Format message with markdown-like support
const formattedText = this.formatMessage(text);
messageDiv.innerHTML = formattedText;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
formatMessage(text) {
// Simple markdown formatting
let formatted = text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // bold
.replace(/\*(.*?)\*/g, '<em>$1</em>') // italic
.replace(/`([^`]+)`/g, '<code>$1</code>') // inline code
.replace(/```([^`]+)```/gs, '<pre>$1</pre>') // code blocks
.replace(/\n/g, '<br>'); // line breaks
// Convert URLs to links
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() {
// Add loading state to send button
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(); // Keep focus on input after sending
this.showTypingIndicator();
this.isTyping = true;
try {
// Simulate API call to HuggingFace
// Add message history context
const context = this.messages
.slice(-4) // Last 2 exchanges (4 messages)
.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.";
// Clean up the response if it includes our prompt
reply = reply.replace(/.*Assistant:/s, '').trim();
this.addMessage('ai', reply);
// Store message in history
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 {
// Restore send button
sendBtn.innerHTML = originalBtnHTML;
sendBtn.disabled = false;
feather.replace();
}
}
}
customElements.define('ai-chat', AIChat);