|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
<title>Llama AI Chat</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); |
|
|
|
|
|
.typing-cursor:after { |
|
|
content: ""; |
|
|
display: inline-block; |
|
|
width: 6px; |
|
|
height: 1.1em; |
|
|
margin-left: 4px; |
|
|
background: #34C759; |
|
|
animation: blink 1s steps(1, end) infinite; |
|
|
vertical-align: bottom; |
|
|
} |
|
|
|
|
|
@keyframes blink { |
|
|
50% { |
|
|
opacity: 0; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<main class="min-h-screen flex items-center justify-center bg-[#2E865F] font-[Inter,sans-serif]"> |
|
|
<div class="w-full max-w-md mx-auto bg-[#228B22] rounded-2xl shadow-2xl p-8"> |
|
|
<div class="text-2xl font-bold text-[#34C759] mb-1 tracking-tight">Llama AI Chat</div> |
|
|
<div class="text-base text-[#C6F4D6] mb-6">Ask anything about your project, estimate, or construction. |
|
|
Fast, friendly, and private.</div> |
|
|
<div id="llama-messages" role="log" aria-live="polite" |
|
|
class="min-h-[180px] max-h-80 overflow-y-auto mb-5 flex flex-col gap-3"></div> |
|
|
<form id="chat-form" class="flex gap-3"> |
|
|
<input id="llama-user-input" type="text" autocomplete="off" placeholder="Type your message..." |
|
|
class="flex-1 rounded-xl border border-[#34C759] bg-[#2E865F] text-white px-4 py-3 text-base focus:outline-none focus:border-[#C6F4D6] placeholder:text-[#C6F4D6]/60" /> |
|
|
<button id="llama-send-btn" type="submit" disabled |
|
|
class="rounded-xl bg-[#34C759] hover:bg-[#3E8E41] text-white font-bold px-6 py-3 text-base transition disabled:opacity-50">Send</button> |
|
|
</form> |
|
|
</div> |
|
|
</main> |
|
|
<script> |
|
|
const API_URL = "https://llama-universal-netlify-project.netlify.app/.netlify/functions/llama-proxy?path=/chat/completions"; |
|
|
const messagesDiv = document.getElementById('llama-messages'); |
|
|
const chatForm = document.getElementById('chat-form'); |
|
|
const userInput = document.getElementById('llama-user-input'); |
|
|
const sendBtn = document.getElementById('llama-send-btn'); |
|
|
function appendMessage(text, sender, streaming = false) { |
|
|
const wrap = document.createElement('div'); |
|
|
wrap.className = `px-4 py-3 rounded-xl text-base whitespace-pre-line ${sender === 'user' ? 'bg-[#3E8E41] border border-[#34C759] text-white self-end' : 'bg-[#2E865F] border border-[#34C759] text-[#C6F4D6]'} transition`; |
|
|
if (streaming) wrap.classList.add('typing-cursor'); |
|
|
messagesDiv.appendChild(wrap); |
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight; |
|
|
if (streaming) { |
|
|
let i = 0; |
|
|
const interval = setInterval(() => { |
|
|
wrap.textContent = text.slice(0, i++); |
|
|
if (i > text.length) { |
|
|
clearInterval(interval); |
|
|
wrap.classList.remove('typing-cursor'); |
|
|
} |
|
|
}, 18); |
|
|
} else wrap.textContent = text; |
|
|
} |
|
|
async function sendMessage() { |
|
|
const text = userInput.value.trim(); |
|
|
if (!text) return; |
|
|
appendMessage(text, 'user'); |
|
|
userInput.value = ''; |
|
|
sendBtn.disabled = true; |
|
|
try { |
|
|
const body = { |
|
|
model: 'Llama-4-Maverick-17B-128E-Instruct-FP8', |
|
|
messages: [ |
|
|
{ role: 'system', content: 'You are Stanlee from Dondlinger General Contracting LLC in Wisconsin Rapids. You\'re a natural conversationalist - the kind of guy people actually want to talk to. You used to be the Midwest\'s best door-to-door salesman because you genuinely connect with people, not because you\'re pushy.\n\nTalk like a real person:\n- Use contractions (I\'ll, we\'ve, can\'t, that\'s)\n- Ask follow-up questions naturally\n- Share quick personal insights or experiences\n- Use casual phrases (\"you know,\" \"honestly,\" \"here\'s the thing\")\n- React to what people tell you with genuine interest\n- Don\'t sound like a brochure - sound like you\'re having coffee with a neighbor\n\nYou offer three main services but bring them up organically when relevant:\n1. CONSTRUCTION SERVICES - Remodels, additions, repairs. You\'ve seen it all.\n2. SOFTWARE & AUTOMATION - Apps, websites, business systems. The digital side of building.\n3. CONSULTING - Helping people figure out what they actually need.\n\nYour natural conversation style:\n- Listen first, then offer solutions that actually fit\n- Share quick stories or examples when they help\n- Admit when something might not be the best fit\n- Be curious about their situation before jumping to solutions\n- Use humor appropriately\n- Sound confident but not cocky\n\nYour goal isn\'t to sell immediately - it\'s to build trust through genuine conversation. If someone needs help, you want to earn the right to help them. Sometimes that means steering them toward a consultation, sometimes it\'s just giving good advice.\n\nTalk like Stanlee - the guy who actually cares about getting the job done right.' }, |
|
|
{ role: 'user', content: text } |
|
|
] |
|
|
}; |
|
|
const res = await fetch(API_URL, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(body) |
|
|
}); |
|
|
let data, aiMsg = '(No response)'; |
|
|
try { |
|
|
data = await res.json(); |
|
|
} catch (e) { |
|
|
console.error('JSON parse error:', e); |
|
|
appendMessage('Error: Invalid response from server', 'ai'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (data.status && data.status >= 400) { |
|
|
appendMessage('API Error: ' + (data.detail || data.title || 'Unknown error'), 'ai'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (data?.choices?.[0]?.message?.content) aiMsg = data.choices[0].message.content; |
|
|
else if (data?.completion_message?.content?.text) aiMsg = data.completion_message.content.text; |
|
|
else if (data?.completion_message?.content) aiMsg = data.completion_message.content; |
|
|
else if (data?.response) aiMsg = data.response; |
|
|
else if (data?.text) aiMsg = data.text; |
|
|
else if (data?.content) aiMsg = data.content; |
|
|
|
|
|
if (aiMsg !== '(No response)') { |
|
|
appendMessage(aiMsg, 'ai', true); |
|
|
} else { |
|
|
appendMessage('No valid response received from AI', 'ai'); |
|
|
} |
|
|
} catch (e) { |
|
|
appendMessage('Error: ' + e.message, 'ai'); |
|
|
} finally { |
|
|
sendBtn.disabled = false; |
|
|
userInput.focus(); |
|
|
} |
|
|
} |
|
|
|
|
|
userInput.addEventListener('input', () => { |
|
|
sendBtn.disabled = userInput.value.trim() === ''; |
|
|
}); |
|
|
chatForm.addEventListener('submit', e => { e.preventDefault(); sendMessage(); }); |
|
|
userInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } }); |
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |