status
This commit is contained in:
265
public/support.html
Normal file
265
public/support.html
Normal file
@@ -0,0 +1,265 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Odoo-style Chat</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--odoo-purple: #875A7B;
|
||||
--odoo-dark: #5a3d52;
|
||||
--bg: #f6f7fb;
|
||||
--card: #ffffff;
|
||||
--muted: #7b7f87;
|
||||
--accent: #6bbf59;
|
||||
--radius: 14px;
|
||||
font-family: 'Rubik', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:linear-gradient(180deg,var(--bg),#eef2f8);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:28px}
|
||||
|
||||
.app{width:960px;max-width:96vw;background:transparent;display:grid;grid-template-columns:320px 1fr;gap:20px}
|
||||
|
||||
/* Left column - user list / details */
|
||||
.sidebar{background:linear-gradient(180deg,rgba(255,255,255,0.9),var(--card));border-radius:var(--radius);padding:20px;box-shadow:0 6px 20px rgba(24,39,75,0.06);overflow:hidden}
|
||||
.brand{display:flex;gap:12px;align-items:center}
|
||||
.logo{width:44px;height:44px;border-radius:9px;background:linear-gradient(135deg,var(--odoo-purple),#b07fa9);display:flex;align-items:center;justify-content:center;color:white;font-weight:700;box-shadow:0 6px 18px rgba(135,90,123,0.12)}
|
||||
.brand h1{margin:0;font-size:18px;color:var(--odoo-dark)}
|
||||
.brand p{margin:0;font-size:12px;color:var(--muted)}
|
||||
|
||||
.sidebar .meta{margin-top:18px;font-size:13px;color:var(--muted)}
|
||||
.sidebar .meta strong{color:var(--odoo-dark)}
|
||||
|
||||
.sidebar .hint{margin-top:14px;padding:10px;border-radius:10px;background:#fbf8ff;color:#5b4160;font-size:13px}
|
||||
|
||||
/* Chat area */
|
||||
.chat-card{background:var(--card);border-radius:var(--radius);display:flex;flex-direction:column;height:70vh;box-shadow:0 10px 30px rgba(24,39,75,0.06);overflow:hidden}
|
||||
.chat-header{display:flex;align-items:center;gap:12px;padding:18px;border-bottom:1px solid #eef1f6}
|
||||
.chat-header .title{font-weight:600;color:var(--odoo-dark)}
|
||||
.chat-header .sub{font-size:13px;color:var(--muted)}
|
||||
|
||||
.messages{flex:1;padding:22px;overflow:auto;background:linear-gradient(180deg,#fbfcff, #f7f9fc)}
|
||||
.messages-inner{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:12px}
|
||||
|
||||
.msg{display:flex;gap:10px;align-items:flex-end}
|
||||
.msg.me{justify-content:flex-end}
|
||||
.bubble{max-width:68%;padding:12px 14px;border-radius:12px;background:white;border:1px solid #eef1f6;font-size:14px;color:#222}
|
||||
.bubble.me{background:linear-gradient(90deg,var(--odoo-purple),#9b6fa0);color:white;border:none}
|
||||
.meta-time{font-size:11px;color:var(--muted);margin-top:6px}
|
||||
|
||||
.avatar{width:36px;height:36px;border-radius:10px;background:#eef1f6;display:flex;align-items:center;justify-content:center;color:var(--odoo-dark);font-weight:600}
|
||||
|
||||
.composer{padding:14px;border-top:1px solid #eef1f6;display:flex;gap:12px;align-items:center}
|
||||
.input{flex:1;background:#f2f6fb;border-radius:12px;padding:8px 12px;display:flex;gap:8px;align-items:center}
|
||||
.input input{border:0;background:transparent;outline:none;padding:10px;font-size:14px;width:100%}
|
||||
.send{background:var(--odoo-purple);color:white;border:none;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer;box-shadow:0 8px 18px rgba(135,90,123,0.14)}
|
||||
.send[disabled]{opacity:0.5;cursor:default}
|
||||
|
||||
.small{font-size:12px;color:var(--muted)}
|
||||
|
||||
/* typing indicator */
|
||||
.typing{display:flex;gap:6px;align-items:center;padding:6px 10px;border-radius:10px;background:#fff6fb;color:#6b3a55;width:max-content}
|
||||
.dot{width:7px;height:7px;border-radius:50%;background:#d6a0c8;animation:blink 1.2s linear infinite}
|
||||
.dot:nth-child(2){animation-delay:0.2s}
|
||||
.dot:nth-child(3){animation-delay:0.4s}
|
||||
@keyframes blink{0%{opacity:0.2}50%{opacity:1}100%{opacity:0.2}}
|
||||
|
||||
/* small screens */
|
||||
@media (max-width:760px){
|
||||
.app{grid-template-columns:1fr;}
|
||||
.sidebar{display:none}
|
||||
.chat-card{height:82vh}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="logo">OD</div>
|
||||
<div>
|
||||
<h1>Odoo Chat</h1>
|
||||
<p>Connected: <span id="status" class="small">disconnected</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">Backend: <strong>Custom n8n Chat Webhook</strong></div>
|
||||
<div class="hint">This is a lightweight Odoo-styled chat UI. Messages are posted to your webhook and responses (JSON) are displayed here.</div>
|
||||
<div style="margin-top:18px;font-size:13px;color:var(--muted)">
|
||||
Usage tips:
|
||||
<ul style="padding-left:18px;margin:8px 0;color:var(--muted)">
|
||||
<li>Type a message and press <strong>Enter</strong> or click Send.</li>
|
||||
<li>Expect JSON response with a simple text field (see docs).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="chat-card" role="main">
|
||||
<div class="chat-header">
|
||||
<div style="display:flex;flex-direction:column">
|
||||
<div class="title">Chat with N8N Backend</div>
|
||||
<div class="sub">Webhook: <span id="webhook" class="small">https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/702862fd-dd17-4a34-8efb-e9056d2c50df/chat</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="messages" id="messages" aria-live="polite">
|
||||
<div class="messages-inner" id="messagesInner">
|
||||
<!-- messages go here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="composer">
|
||||
<div class="input" role="search">
|
||||
<input id="messageInput" placeholder="Write a message..." aria-label="Message input" />
|
||||
</div>
|
||||
<button id="sendBtn" class="send">Send</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Configuration - use your webhook
|
||||
const WEBHOOK = 'https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/702862fd-dd17-4a34-8efb-e9056d2c50df/chat';
|
||||
|
||||
const messagesEl = document.getElementById('messagesInner');
|
||||
const statusEl = document.getElementById('status');
|
||||
const input = document.getElementById('messageInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
document.getElementById('webhook').textContent = WEBHOOK;
|
||||
|
||||
// Generate UUID once per page load
|
||||
function generateUUID(){
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
const CLIENT_UUID = generateUUID();
|
||||
|
||||
function formatTime(date){
|
||||
return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
|
||||
function appendMessage(text, who='them'){
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'msg' + (who==='me' ? ' me' : '');
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'avatar';
|
||||
avatar.textContent = who==='me' ? 'ME' : 'AI';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble' + (who==='me' ? ' me' : '');
|
||||
bubble.innerHTML = text.replace(/\n/g,'<br>');
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'meta-time';
|
||||
meta.textContent = formatTime(new Date());
|
||||
|
||||
const col = document.createElement('div');
|
||||
col.style.display = 'flex';
|
||||
col.style.flexDirection = 'column';
|
||||
col.appendChild(bubble);
|
||||
col.appendChild(meta);
|
||||
|
||||
if(who==='me'){
|
||||
wrap.appendChild(col);
|
||||
wrap.appendChild(avatar);
|
||||
} else {
|
||||
wrap.appendChild(avatar);
|
||||
wrap.appendChild(col);
|
||||
}
|
||||
|
||||
messagesEl.appendChild(wrap);
|
||||
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(connected){
|
||||
statusEl.textContent = connected ? 'ready' : 'disconnected';
|
||||
statusEl.style.color = connected ? 'var(--accent)' : 'var(--muted)';
|
||||
}
|
||||
|
||||
async function sendMessage(){
|
||||
const text = input.value.trim();
|
||||
if(!text) return;
|
||||
// show user message
|
||||
appendMessage(text, 'me');
|
||||
input.value = '';
|
||||
sendBtn.disabled = true;
|
||||
setStatus(false);
|
||||
|
||||
// show typing indicator
|
||||
const typingEl = document.createElement('div');
|
||||
typingEl.className = 'msg';
|
||||
typingEl.id = 'typing';
|
||||
typingEl.innerHTML = `
|
||||
<div class="avatar">AI</div>
|
||||
<div class="typing" style="margin-left:6px"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
||||
`;
|
||||
messagesEl.appendChild(typingEl);
|
||||
messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
|
||||
|
||||
try{
|
||||
// Post to webhook - expecting a JSON response. Adapt to your backend.
|
||||
const payload = {text: text, uuid: CLIENT_UUID};
|
||||
const res = await fetch(WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
let data;
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if(ct.includes('application/json')) data = await res.json();
|
||||
else data = {text: await res.text()};
|
||||
|
||||
// remove typing indicator
|
||||
const t = document.getElementById('typing');
|
||||
if(t) t.remove();
|
||||
|
||||
// Decide where message text is
|
||||
// Common patterns: {message: '...'}, {text: '...'}, {reply: '...'}, plain text
|
||||
let replyText = '';
|
||||
if(typeof data === 'string') replyText = data;
|
||||
else if(data === null) replyText = 'No response';
|
||||
else if(data.reply) replyText = data.reply;
|
||||
else if(data.message) replyText = data.message;
|
||||
else if(data.text) replyText = data.text;
|
||||
else if(data.output) replyText = data.output;
|
||||
else replyText = JSON.stringify(data);
|
||||
|
||||
appendMessage(replyText, 'them');
|
||||
setStatus(true);
|
||||
|
||||
}catch(err){
|
||||
const t = document.getElementById('typing');
|
||||
if(t) t.remove();
|
||||
appendMessage('Error: ' + (err && err.message ? err.message : String(err)), 'them');
|
||||
setStatus(false);
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// wire events
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
input.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMessage(); } });
|
||||
|
||||
// optimistic check to see if webhook is reachable via HEAD
|
||||
(async function ping(){
|
||||
try{
|
||||
const r = await fetch(WEBHOOK, {method:'HEAD'});
|
||||
setStatus(r.ok);
|
||||
}catch(e){ setStatus(false); }
|
||||
})();
|
||||
|
||||
// friendly welcome
|
||||
appendMessage('Hello! This chat will post your message to the configured webhook and show the response here. Try sending a message. Your session id is ' + CLIENT_UUID, 'them');
|
||||
setStatus(false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user