diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..fd977d0 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,21 @@ +Password: '; + exit; +} +?> +

Admin Panel

+ +

Manage users, pricing, and system configurations here.

\ No newline at end of file diff --git a/api/conversations.php b/api/conversations.php new file mode 100644 index 0000000..d6ea72b --- /dev/null +++ b/api/conversations.php @@ -0,0 +1,33 @@ + false, 'error' => 'Contact name and phone are required.']); + exit; + } + + $stmt = $pdo->prepare("INSERT INTO conversations (contact_name, phone, channel, created_at) VALUES (?, ?, ?, NOW())"); + $stmt->execute([$contactName, $phone, $channel]); + $conversationId = (int)$pdo->lastInsertId(); + + echo json_encode(['success' => true, 'conversation_id' => $conversationId]); + exit; +} + +$rows = $pdo->query("SELECT * FROM conversations ORDER BY created_at DESC")->fetchAll(); +echo json_encode(['success' => true, 'conversations' => $rows]); diff --git a/api/messages.php b/api/messages.php new file mode 100644 index 0000000..75a4587 --- /dev/null +++ b/api/messages.php @@ -0,0 +1,65 @@ + false, 'error' => 'Conversation and message body are required.']); + exit; + } + + $stmt = $pdo->prepare("SELECT channel FROM conversations WHERE id = ?"); + $stmt->execute([$conversationId]); + $channel = (string)($stmt->fetchColumn() ?: 'twilio'); + + $pdo->beginTransaction(); + try { + $msgStmt = $pdo->prepare("INSERT INTO messages (conversation_id, direction, body, status, created_at) VALUES (?, 'outbound', ?, 'queued', NOW())"); + $msgStmt->execute([$conversationId, $body]); + $messageId = (int)$pdo->lastInsertId(); + + $payload = json_encode([ + 'conversation_id' => $conversationId, + 'message_id' => $messageId, + 'body' => $body + ], JSON_UNESCAPED_UNICODE); + + $queueStmt = $pdo->prepare("INSERT INTO sms_queue (message_id, provider, payload, status, attempts, created_at, updated_at) VALUES (?, ?, ?, 'queued', 0, NOW(), NOW())"); + $queueStmt->execute([$messageId, $channel, $payload]); + + $pdo->commit(); + } catch (Throwable $e) { + $pdo->rollBack(); + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Failed to queue message.']); + exit; + } + + echo json_encode(['success' => true, 'message_id' => $messageId]); + exit; +} + +$conversationId = isset($_GET['conversation_id']) ? (int)$_GET['conversation_id'] : 0; +if ($conversationId <= 0) { + http_response_code(422); + echo json_encode(['success' => false, 'error' => 'conversation_id is required.']); + exit; +} + +$stmt = $pdo->prepare("SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at ASC"); +$stmt->execute([$conversationId]); +$rows = $stmt->fetchAll(); + +echo json_encode(['success' => true, 'messages' => $rows]); diff --git a/api/queue_process.php b/api/queue_process.php new file mode 100644 index 0000000..1e96257 --- /dev/null +++ b/api/queue_process.php @@ -0,0 +1,48 @@ +prepare("SELECT value FROM settings WHERE name = ?"); +$stmt->execute(['sms_unit_price']); +$unitPrice = (float)($stmt->fetchColumn() ?: 0.05); + +$queueItems = $pdo->query("SELECT q.id, q.message_id FROM sms_queue q WHERE q.status = 'queued' ORDER BY q.id ASC LIMIT 20")->fetchAll(); +if (!$queueItems) { + echo json_encode(['success' => true, 'processed' => 0]); + exit; +} + +$processed = 0; +$pdo->beginTransaction(); +try { + $updateQueue = $pdo->prepare("UPDATE sms_queue SET status = 'sent', attempts = attempts + 1 WHERE id = ?"); + $updateMessage = $pdo->prepare("UPDATE messages SET status = 'sent' WHERE id = ?"); + $getDirection = $pdo->prepare("SELECT direction FROM messages WHERE id = ?"); + $billStmt = $pdo->prepare("INSERT INTO billing_events (message_id, units, unit_price, total_cost, created_at) VALUES (?, 1, ?, ?, NOW())"); + + foreach ($queueItems as $item) { + $updateQueue->execute([$item['id']]); + $updateMessage->execute([$item['message_id']]); + $getDirection->execute([$item['message_id']]); + $direction = (string)($getDirection->fetchColumn() ?: ''); + if ($direction === 'outbound') { + $billStmt->execute([$item['message_id'], $unitPrice, $unitPrice]); + } + $processed++; + } + $pdo->commit(); +} catch (Throwable $e) { + $pdo->rollBack(); + http_response_code(500); + echo json_encode(['success' => false, 'error' => 'Queue processing failed.']); + exit; +} + +echo json_encode(['success' => true, 'processed' => $processed]); diff --git a/api/webhook_aliyun.php b/api/webhook_aliyun.php new file mode 100644 index 0000000..e668338 --- /dev/null +++ b/api/webhook_aliyun.php @@ -0,0 +1,33 @@ +prepare("SELECT id FROM conversations WHERE phone = ? LIMIT 1"); +$stmt->execute([$from]); +$conversationId = (int)($stmt->fetchColumn() ?: 0); + +if ($conversationId === 0) { + $insert = $pdo->prepare("INSERT INTO conversations (contact_name, phone, channel, created_at) VALUES (?, ?, 'aliyun', NOW())"); + $insert->execute([$from, $from]); + $conversationId = (int)$pdo->lastInsertId(); +} + +$msgStmt = $pdo->prepare("INSERT INTO messages (conversation_id, direction, body, status, created_at) VALUES (?, 'inbound', ?, 'received', NOW())"); +$msgStmt->execute([$conversationId, $body]); + +header('Content-Type: text/plain'); +echo "OK"; diff --git a/api/webhook_twilio.php b/api/webhook_twilio.php new file mode 100644 index 0000000..21f63aa --- /dev/null +++ b/api/webhook_twilio.php @@ -0,0 +1,33 @@ +prepare("SELECT id FROM conversations WHERE phone = ? LIMIT 1"); +$stmt->execute([$from]); +$conversationId = (int)($stmt->fetchColumn() ?: 0); + +if ($conversationId === 0) { + $insert = $pdo->prepare("INSERT INTO conversations (contact_name, phone, channel, created_at) VALUES (?, ?, 'twilio', NOW())"); + $insert->execute([$from, $from]); + $conversationId = (int)$pdo->lastInsertId(); +} + +$msgStmt = $pdo->prepare("INSERT INTO messages (conversation_id, direction, body, status, created_at) VALUES (?, 'inbound', ?, 'received', NOW())"); +$msgStmt->execute([$conversationId, $body]); + +header('Content-Type: text/plain'); +echo "OK"; diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..b7367b8 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,37 @@ -body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; +:root { + --bg-color: #f0f2f5; + --sidebar-bg: #ffffff; + --chat-bg: #e5ddd5; + --accent-color: #00a884; + --text-primary: #111b21; + --text-secondary: #667781; + --bubble-own: #d9fdd3; + --bubble-other: #ffffff; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} +body, html { margin: 0; padding: 0; height: 100%; font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; overflow: hidden; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} +.app-container { display: flex; height: 100vh; width: 100vw; background: var(--bg-color); } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} +/* Sidebar */ +.sidebar { width: 350px; background: var(--sidebar-bg); display: flex; flex-direction: column; border-right: 1px solid #d1d7db; } +.sidebar-header { padding: 15px; background: #f0f2f5; display: flex; align-items: center; border-bottom: 1px solid #d1d7db; font-weight: bold; color: var(--text-primary); } +.sidebar-search { padding: 10px; background: #ffffff; } +.sidebar-search input { width: 90%; padding: 8px 15px; border-radius: 8px; border: 1px solid #e9edef; background: #f0f2f5; outline: none; } +.conversation-list { flex: 1; overflow-y: auto; background: white; } +.conversation-item { padding: 15px; display: flex; align-items: center; border-bottom: 1px solid #f2f2f2; cursor: pointer; transition: background 0.2s; } +.conversation-item:hover { background: #f5f5f5; } +.conv-info { margin-left: 15px; flex: 1; overflow: hidden; } +.conv-info h4 { margin: 0; color: var(--text-primary); font-weight: 500; } +.conv-info p { margin: 2px 0 0; color: var(--text-secondary); font-size: 0.85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file +/* Chat Panel */ +.chat-panel { flex: 1; display: flex; flex-direction: column; background: var(--chat-bg); } +.chat-header { padding: 12px 20px; background: #f0f2f5; display: flex; align-items: center; border-bottom: 1px solid #d1d7db; font-weight: bold; } +.message-list { flex: 1; padding: 30px 50px; overflow-y: auto; display: flex; flex-direction: column; background: #e5ddd5; } +.message { margin-bottom: 8px; padding: 8px 12px; border-radius: 8px; max-width: 60%; position: relative; box-shadow: 0 1px 1px rgba(0,0,0,0.1); font-size: 0.95em; } +.msg-me { background: var(--bubble-own); align-self: flex-end; border-top-right-radius: 0; } +.msg-other { background: var(--bubble-other); align-self: flex-start; border-top-left-radius: 0; } +.input-area { padding: 12px 20px; background: #f0f2f5; display: flex; align-items: center; gap: 10px; } +.input-area input { flex: 1; padding: 12px 15px; border-radius: 8px; border: none; outline: none; box-shadow: 0 1px 1px rgba(0,0,0,0.1); } +.input-area button { padding: 10px 20px; background: var(--accent-color); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; } diff --git a/assets/js/main.js b/assets/js/main.js index d349598..39d0856 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,127 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + const convoForm = document.getElementById('new-conversation-form'); + const messageForm = document.getElementById('message-form'); + const messagesWrap = document.getElementById('chat-messages'); + const realtimeBadge = document.getElementById('realtime-status'); + const toastEl = document.getElementById('app-toast'); + const convoId = messagesWrap ? Number(messagesWrap.dataset.conversationId) : 0; - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; + const toast = toastEl ? new bootstrap.Toast(toastEl) : null; + + const showToast = (text) => { + if (!toastEl || !toast) return; + toastEl.querySelector('.toast-body').textContent = text; + toast.show(); }; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + const renderMessages = (messages) => { + if (!messagesWrap) return; + messagesWrap.innerHTML = ''; + messages.forEach((msg) => { + const bubble = document.createElement('div'); + bubble.className = `message-bubble ${msg.direction === 'outbound' ? 'outbound' : ''}`; + bubble.innerHTML = ` +
${msg.body}
+
${msg.direction} · ${msg.status} · ${msg.created_at}
+ `; + messagesWrap.appendChild(bubble); + }); + messagesWrap.scrollTop = messagesWrap.scrollHeight; + }; - appendMessage(message, 'visitor'); - chatInput.value = ''; + const fetchMessages = async () => { + if (!convoId) return; + const res = await fetch(`api/messages.php?conversation_id=${convoId}`); + if (!res.ok) return; + const data = await res.json(); + renderMessages(data.messages || []); + }; - try { - const response = await fetch('api/chat.php', { + if (convoForm) { + convoForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(convoForm); + const payload = { + contact_name: formData.get('contact_name'), + phone: formData.get('phone'), + channel: formData.get('channel') + }; + const res = await fetch('api/conversations.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) + body: JSON.stringify(payload) }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + const data = await res.json(); + if (data.success) { + window.location = `index.php?conversation_id=${data.conversation_id}`; + } else { + showToast(data.error || 'Unable to create conversation.'); + } + }); + } + + if (messageForm) { + messageForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(messageForm); + const body = String(formData.get('body') || '').trim(); + if (!body) return; + const res = await fetch('api/messages.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ conversation_id: convoId, body }) + }); + const data = await res.json(); + if (data.success) { + messageForm.reset(); + fetchMessages(); + showToast('Message queued for delivery.'); + } else { + showToast(data.error || 'Message failed.'); + } + }); + } + + const connectWebSocket = () => { + if (!realtimeBadge || !window.WebSocket) return; + const wsUrl = `ws://${window.location.hostname}:8081`; + let ws; + try { + ws = new WebSocket(wsUrl); + } catch (e) { + realtimeBadge.textContent = 'Polling'; + return; } - }); + ws.addEventListener('open', () => { + realtimeBadge.textContent = 'WebSocket'; + }); + ws.addEventListener('message', (event) => { + if (event.data === 'refresh') { + fetchMessages(); + } + }); + ws.addEventListener('close', () => { + realtimeBadge.textContent = 'Polling'; + }); + ws.addEventListener('error', () => { + realtimeBadge.textContent = 'Polling'; + }); + }; + + if (messagesWrap) { + fetchMessages(); + setInterval(fetchMessages, 5000); + connectWebSocket(); + } + + const processBtn = document.getElementById('process-queue'); + if (processBtn) { + processBtn.addEventListener('click', async () => { + processBtn.disabled = true; + const res = await fetch('api/queue_process.php', { method: 'POST' }); + const data = await res.json(); + showToast(`Processed ${data.processed || 0} queued messages.`); + setTimeout(() => window.location.reload(), 800); + }); + } }); diff --git a/assets/pasted-20260318-061231-e9000a90.png b/assets/pasted-20260318-061231-e9000a90.png new file mode 100644 index 0000000..c3c2dac Binary files /dev/null and b/assets/pasted-20260318-061231-e9000a90.png differ diff --git a/assets/pasted-20260318-062820-7044d79f.png b/assets/pasted-20260318-062820-7044d79f.png new file mode 100644 index 0000000..13c928f Binary files /dev/null and b/assets/pasted-20260318-062820-7044d79f.png differ diff --git a/assets/pasted-20260318-063828-81c18553.png b/assets/pasted-20260318-063828-81c18553.png new file mode 100644 index 0000000..035f35f Binary files /dev/null and b/assets/pasted-20260318-063828-81c18553.png differ diff --git a/db/migrations/001_sms_chat.sql b/db/migrations/001_sms_chat.sql new file mode 100644 index 0000000..12c1042 --- /dev/null +++ b/db/migrations/001_sms_chat.sql @@ -0,0 +1,53 @@ +-- SMS Chat MVP schema +CREATE TABLE IF NOT EXISTS conversations ( + id INT AUTO_INCREMENT PRIMARY KEY, + contact_name VARCHAR(120) NOT NULL, + phone VARCHAR(32) NOT NULL, + channel VARCHAR(20) NOT NULL DEFAULT 'twilio', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + conversation_id INT NOT NULL, + direction VARCHAR(12) NOT NULL, + body TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'queued', + channel_message_id VARCHAR(120) DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_messages_convo (conversation_id), + CONSTRAINT fk_messages_convo FOREIGN KEY (conversation_id) REFERENCES conversations(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS sms_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + message_id INT NOT NULL, + provider VARCHAR(20) NOT NULL, + payload TEXT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'queued', + attempts INT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_queue_status (status), + CONSTRAINT fk_queue_message FOREIGN KEY (message_id) REFERENCES messages(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS billing_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + message_id INT NOT NULL, + units INT NOT NULL DEFAULT 1, + unit_price DECIMAL(10,4) NOT NULL DEFAULT 0.0000, + total_cost DECIMAL(10,4) NOT NULL DEFAULT 0.0000, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_billing_message (message_id), + CONSTRAINT fk_billing_message FOREIGN KEY (message_id) REFERENCES messages(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(64) NOT NULL UNIQUE, + value TEXT NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/includes/schema.php b/includes/schema.php new file mode 100644 index 0000000..5c1b1a5 --- /dev/null +++ b/includes/schema.php @@ -0,0 +1,44 @@ +exec($statement); + } + } + + $stmt = $pdo->prepare("INSERT INTO settings (name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = value"); + $stmt->execute(['sms_unit_price', '0.05']); + + $count = (int)$pdo->query("SELECT COUNT(*) FROM conversations")->fetchColumn(); + if ($count === 0) { + $convStmt = $pdo->prepare("INSERT INTO conversations (contact_name, phone, channel, created_at) VALUES (?, ?, ?, NOW())"); + $msgStmt = $pdo->prepare("INSERT INTO messages (conversation_id, direction, body, status, created_at) VALUES (?, ?, ?, ?, NOW())"); + $billStmt = $pdo->prepare("INSERT INTO billing_events (message_id, units, unit_price, total_cost, created_at) VALUES (?, ?, ?, ?, NOW())"); + + $convStmt->execute(['Horizon Logistics', '+1 415 555 0118', 'twilio']); + $convA = (int)$pdo->lastInsertId(); + $msgStmt->execute([$convA, 'outbound', 'Hello! Your shipment is scheduled for pickup today at 3 PM.', 'sent']); + $msgA = (int)$pdo->lastInsertId(); + $billStmt->execute([$msgA, 1, 0.05, 0.05]); + $msgStmt->execute([$convA, 'inbound', 'Great, please confirm the driver ID when ready.', 'received']); + + $convStmt->execute(['Nova Care Clinic', '+1 212 555 0199', 'aliyun']); + $convB = (int)$pdo->lastInsertId(); + $msgStmt->execute([$convB, 'outbound', 'Reminder: your appointment is tomorrow at 9:00 AM.', 'sent']); + $msgB = (int)$pdo->lastInsertId(); + $billStmt->execute([$msgB, 1, 0.05, 0.05]); + $msgStmt->execute([$convB, 'inbound', 'Confirmed. Thank you!', 'received']); + } +} diff --git a/index.php b/index.php index 7205f3d..c5a5e6d 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,45 @@ - - - + + + - - - New Style - - - - - - - - - - - - - - - - - - - + + WhatsApp 风格聊天 + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+ + + + +
+
+ 13800138000 +
+
+
你好,请问在吗?
+
您好,请问有什么可以帮您?
+
+
+ + +
+
-
- - + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..d7569c0 --- /dev/null +++ b/login.php @@ -0,0 +1,37 @@ + + + + + + 登录 - 聊天系统 + + + +
+

用户登录

+ $error

"; ?> +
+ + + +
+
+ +