Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
session_start();
|
|
||||||
// Basic password protection for admin
|
|
||||||
if (!isset($_SESSION['is_admin']) || !$_SESSION['is_admin']) {
|
|
||||||
// If not authenticated, show login form
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['password'] === 'admin123') {
|
|
||||||
$_SESSION['is_admin'] = true;
|
|
||||||
header('Location: /admin/');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
echo '<form method="post">Password: <input type="password" name="password"><button>Login</button></form>';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<h1>Admin Panel</h1>
|
|
||||||
<nav>
|
|
||||||
<a href="/admin/billing.php">Billing</a>
|
|
||||||
<a href="/admin/queue.php">Queue</a>
|
|
||||||
<a href="/admin/webhook.php">Webhooks</a>
|
|
||||||
</nav>
|
|
||||||
<p>Manage users, pricing, and system configurations here.</p>
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/../includes/schema.php';
|
|
||||||
|
|
||||||
ensure_schema();
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$input = json_decode(file_get_contents('php://input') ?: '', true);
|
|
||||||
$contactName = trim((string)($input['contact_name'] ?? ''));
|
|
||||||
$phone = trim((string)($input['phone'] ?? ''));
|
|
||||||
$channel = trim((string)($input['channel'] ?? 'twilio'));
|
|
||||||
|
|
||||||
if ($contactName === '' || $phone === '') {
|
|
||||||
http_response_code(422);
|
|
||||||
echo json_encode(['success' => 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]);
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/../includes/schema.php';
|
|
||||||
|
|
||||||
ensure_schema();
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$input = json_decode(file_get_contents('php://input') ?: '', true);
|
|
||||||
$conversationId = (int)($input['conversation_id'] ?? 0);
|
|
||||||
$body = trim((string)($input['body'] ?? ''));
|
|
||||||
|
|
||||||
if ($conversationId <= 0 || $body === '') {
|
|
||||||
http_response_code(422);
|
|
||||||
echo json_encode(['success' => 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]);
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/../includes/schema.php';
|
|
||||||
|
|
||||||
ensure_schema();
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
$stmt = $pdo->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]);
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/../includes/schema.php';
|
|
||||||
|
|
||||||
ensure_schema();
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
$from = trim((string)($_POST['phone_number'] ?? ($_GET['phone_number'] ?? '')));
|
|
||||||
$body = trim((string)($_POST['content'] ?? ($_GET['content'] ?? '')));
|
|
||||||
|
|
||||||
if ($from === '' || $body === '') {
|
|
||||||
http_response_code(400);
|
|
||||||
echo 'Missing phone_number or content';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->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";
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/../includes/schema.php';
|
|
||||||
|
|
||||||
ensure_schema();
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
$from = trim((string)($_POST['From'] ?? ($_GET['from'] ?? '')));
|
|
||||||
$body = trim((string)($_POST['Body'] ?? ($_GET['body'] ?? '')));
|
|
||||||
|
|
||||||
if ($from === '' || $body === '') {
|
|
||||||
http_response_code(400);
|
|
||||||
echo 'Missing From or Body';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->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";
|
|
||||||
@ -1,45 +1,403 @@
|
|||||||
:root {
|
body {
|
||||||
--bg-color: #f0f2f5;
|
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||||
--sidebar-bg: #ffffff;
|
background-size: 400% 400%;
|
||||||
--chat-bg: #e5ddd5;
|
animation: gradient 15s ease infinite;
|
||||||
--accent-color: #25d366; /* WhatsApp Green */
|
color: #212529;
|
||||||
--text-primary: #111b21;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
--text-secondary: #667781;
|
font-size: 14px;
|
||||||
--bubble-own: #d9fdd3;
|
margin: 0;
|
||||||
--bubble-other: #ffffff;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, html { margin: 0; padding: 0; height: 100%; font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; overflow: hidden; }
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
.app-container { display: flex; height: 100vh; width: 100vw; background: var(--bg-color); }
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Sidebar */
|
.chat-container {
|
||||||
.sidebar { width: 350px; background: var(--sidebar-bg); display: flex; flex-direction: column; border-right: 1px solid #d1d7db; }
|
width: 100%;
|
||||||
.sidebar-header { padding: 15px; background: #f0f2f5; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #d1d7db; font-weight: bold; color: var(--text-primary); }
|
max-width: 600px;
|
||||||
.sidebar-search { padding: 10px; background: #ffffff; }
|
background: rgba(255, 255, 255, 0.85);
|
||||||
.sidebar-search input { width: 90%; padding: 8px 15px; border-radius: 8px; border: 1px solid #e9edef; background: #f0f2f5; outline: none; }
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
.conversation-list { flex: 1; overflow-y: auto; background: white; }
|
border-radius: 20px;
|
||||||
.conversation-item { padding: 15px; display: flex; align-items: center; border-bottom: 1px solid #f2f2f2; cursor: pointer; transition: background 0.2s; }
|
display: flex;
|
||||||
.conversation-item:hover { background: #f5f5f5; }
|
flex-direction: column;
|
||||||
.conv-info { margin-left: 15px; flex: 1; overflow: hidden; }
|
height: 85vh;
|
||||||
.conv-info h4 { margin: 0; color: var(--text-primary); font-weight: 500; }
|
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||||
.conv-info p { margin: 2px 0 0; color: var(--text-secondary); font-size: 0.85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* Chat Panel */
|
.chat-header {
|
||||||
.chat-panel { flex: 1; display: flex; flex-direction: column; background: var(--chat-bg); }
|
padding: 1.5rem;
|
||||||
.chat-header { padding: 12px 20px; background: #f0f2f5; display: flex; align-items: center; border-bottom: 1px solid #d1d7db; font-weight: bold; }
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
.message-list { flex: 1; padding: 30px 50px; overflow-y: auto; display: flex; flex-direction: column; background: #e5ddd5; }
|
background: rgba(255, 255, 255, 0.5);
|
||||||
.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; }
|
font-weight: 700;
|
||||||
.msg-me { background: var(--bubble-own); align-self: flex-end; border-top-right-radius: 0; }
|
font-size: 1.1rem;
|
||||||
.msg-other { background: var(--bubble-other); align-self: flex-start; border-top-left-radius: 0; }
|
display: flex;
|
||||||
.input-area { padding: 12px 20px; background: #f0f2f5; display: flex; align-items: center; gap: 10px; }
|
justify-content: space-between;
|
||||||
.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); }
|
align-items: center;
|
||||||
.input-area button { padding: 10px 20px; background: var(--accent-color); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
|
}
|
||||||
.emoji-btn { padding: 5px; cursor: pointer; font-size: 24px; color: var(--text-secondary); }
|
|
||||||
|
|
||||||
/* Modal */
|
.chat-messages {
|
||||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 1000; }
|
flex: 1;
|
||||||
.modal-content { background: white; padding: 25px; border-radius: 12px; width: 320px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); }
|
overflow-y: auto;
|
||||||
.modal-content h3 { margin-top: 0; }
|
padding: 1.5rem;
|
||||||
.modal-content select, .modal-content input { width: 100%; margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 6px; }
|
display: flex;
|
||||||
.modal-content button { width: 100%; padding: 10px; background: var(--accent-color); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }
|
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;
|
||||||
|
}
|
||||||
@ -1,127 +1,39 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const convoForm = document.getElementById('new-conversation-form');
|
const chatForm = document.getElementById('chat-form');
|
||||||
const messageForm = document.getElementById('message-form');
|
const chatInput = document.getElementById('chat-input');
|
||||||
const messagesWrap = document.getElementById('chat-messages');
|
const chatMessages = 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 toast = toastEl ? new bootstrap.Toast(toastEl) : null;
|
const appendMessage = (text, sender) => {
|
||||||
|
const msgDiv = document.createElement('div');
|
||||||
const showToast = (text) => {
|
msgDiv.classList.add('message', sender);
|
||||||
if (!toastEl || !toast) return;
|
msgDiv.textContent = text;
|
||||||
toastEl.querySelector('.toast-body').textContent = text;
|
chatMessages.appendChild(msgDiv);
|
||||||
toast.show();
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMessages = (messages) => {
|
chatForm.addEventListener('submit', async (e) => {
|
||||||
if (!messagesWrap) return;
|
e.preventDefault();
|
||||||
messagesWrap.innerHTML = '';
|
const message = chatInput.value.trim();
|
||||||
messages.forEach((msg) => {
|
if (!message) return;
|
||||||
const bubble = document.createElement('div');
|
|
||||||
bubble.className = `message-bubble ${msg.direction === 'outbound' ? 'outbound' : ''}`;
|
|
||||||
bubble.innerHTML = `
|
|
||||||
<div>${msg.body}</div>
|
|
||||||
<div class="meta">${msg.direction} · ${msg.status} · ${msg.created_at}</div>
|
|
||||||
`;
|
|
||||||
messagesWrap.appendChild(bubble);
|
|
||||||
});
|
|
||||||
messagesWrap.scrollTop = messagesWrap.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMessages = async () => {
|
appendMessage(message, 'visitor');
|
||||||
if (!convoId) return;
|
chatInput.value = '';
|
||||||
const res = await fetch(`api/messages.php?conversation_id=${convoId}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
renderMessages(data.messages || []);
|
|
||||||
};
|
|
||||||
|
|
||||||
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(payload)
|
|
||||||
});
|
|
||||||
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 {
|
try {
|
||||||
ws = new WebSocket(wsUrl);
|
const response = await fetch('api/chat.php', {
|
||||||
} catch (e) {
|
method: 'POST',
|
||||||
realtimeBadge.textContent = 'Polling';
|
headers: { 'Content-Type': 'application/json' },
|
||||||
return;
|
body: JSON.stringify({ message })
|
||||||
|
});
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@ -1,53 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
|
|
||||||
function ensure_schema(): void {
|
|
||||||
$pdo = db();
|
|
||||||
$sqlFile = __DIR__ . '/../db/migrations/001_sms_chat.sql';
|
|
||||||
if (!file_exists($sqlFile)) {
|
|
||||||
throw new RuntimeException('Schema file missing.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = file_get_contents($sqlFile);
|
|
||||||
$statements = array_filter(array_map('trim', preg_split('/;\s*\n/', (string)$sql)));
|
|
||||||
foreach ($statements as $statement) {
|
|
||||||
if ($statement !== '') {
|
|
||||||
$pdo->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']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
321
index.php
@ -1,183 +1,150 @@
|
|||||||
<?php session_start(); ?>
|
<?php
|
||||||
<!DOCTYPE html>
|
declare(strict_types=1);
|
||||||
<html lang="zh-CN">
|
@ini_set('display_errors', '1');
|
||||||
<head>
|
@error_reporting(E_ALL);
|
||||||
<meta charset="UTF-8">
|
@date_default_timezone_set('UTC');
|
||||||
<title>金融智能通信平台</title>
|
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
<style>
|
|
||||||
.new-chat-btn { cursor: pointer; color: var(--accent-color); font-size: 24px; font-weight: bold; padding: 0 10px; }
|
|
||||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 1000; }
|
|
||||||
.modal-content { background: white; padding: 25px; border-radius: 12px; width: 320px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); position: relative; }
|
|
||||||
.close-btn { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 20px; color: #888; }
|
|
||||||
.modal-content select, .modal-content input { width: 100%; margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
|
|
||||||
.modal-content button { width: 100%; padding: 10px; background: var(--accent-color); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }
|
|
||||||
|
|
||||||
.emoji-picker { display: none; position: absolute; bottom: 80px; left: 10px; background: white; border: 1px solid #ddd; border-radius: 8px; padding: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); grid-template-columns: repeat(8, 1fr); gap: 5px; width: 280px; z-index: 500; cursor: pointer; }
|
|
||||||
.emoji-picker span { font-size: 20px; text-align: center; padding: 5px; }
|
|
||||||
.emoji-picker span:hover { background: #f0f0f0; border-radius: 4px; }
|
|
||||||
|
|
||||||
.delete-btn { color: #ff4d4f; cursor: pointer; font-size: 14px; margin-left: 10px; }
|
|
||||||
|
|
||||||
/* Branding hidden - Aggressive */
|
$phpVersion = PHP_VERSION;
|
||||||
footer, .footer, #footer, [class*="footer"], .built-with, #built-with, div[style*="position: fixed"][style*="bottom: 0"], .flatlogic-branding, [class*="flatlogic"], div[class*="flatlogic"], *[id*="flatlogic"], *[class*="flatlogic"], .brand-label { display: none !important; visibility: hidden !important; height: 0 !important; width: 0 !important; opacity: 0 !important; overflow: hidden !important; pointer-events: none !important; }
|
$now = date('Y-m-d H:i:s');
|
||||||
</style>
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>New Style</title>
|
||||||
|
<?php
|
||||||
|
// Read project preview data from environment
|
||||||
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
|
?>
|
||||||
|
<?php if ($projectDescription): ?>
|
||||||
|
<!-- Meta description -->
|
||||||
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
|
<!-- Open Graph meta tags -->
|
||||||
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
|
<!-- Twitter meta tags -->
|
||||||
|
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($projectImageUrl): ?>
|
||||||
|
<!-- Open Graph image -->
|
||||||
|
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
|
<!-- Twitter image -->
|
||||||
|
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color-start: #6a11cb;
|
||||||
|
--bg-color-end: #2575fc;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
|
color: var(--text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||||
|
animation: bg-pan 20s linear infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
@keyframes bg-pan {
|
||||||
|
0% { background-position: 0% 0%; }
|
||||||
|
100% { background-position: 100% 100%; }
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg-color);
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.loader {
|
||||||
|
margin: 1.25rem auto 1.25rem;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<main>
|
||||||
<div class="sidebar">
|
<div class="card">
|
||||||
<div class="sidebar-header">
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
<span>会话列表</span>
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<span class="new-chat-btn" onclick="document.getElementById('newChatModal').style.display='flex'">+</span>
|
<span class="sr-only">Loading…</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-search">
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
<input type="text" placeholder="搜索">
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
</div>
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
<div class="conversation-list" id="convList">
|
|
||||||
<div class="conversation-item" onclick="selectConversation('+86 13800138000')">
|
|
||||||
<div class="conv-info">
|
|
||||||
<h4>+86 13800138000</h4>
|
|
||||||
<p>你好,请问在吗?</p>
|
|
||||||
</div>
|
|
||||||
<span class="delete-btn" onclick="event.stopPropagation(); deleteConversation(this)">删除</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="chat-panel">
|
|
||||||
<div class="chat-header" id="chatHeader">
|
|
||||||
请开启会话
|
|
||||||
</div>
|
|
||||||
<div class="message-list" id="messageList">
|
|
||||||
<div class="message msg-other">欢迎使用金融智能通信平台。</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-area">
|
|
||||||
<span class="emoji-btn" onclick="toggleEmojiPicker()" style="cursor: pointer;">😊</span>
|
|
||||||
<div id="emojiPicker" class="emoji-picker">
|
|
||||||
<span>😀</span><span>😂</span><span>😊</span><span>😍</span><span>👍</span><span>😎</span><span>🤔</span><span>😅</span>
|
|
||||||
<span>🤩</span><span>💪</span><span>🔥</span><span>✨</span><span>🎈</span><span>🎉</span><span>❤️</span><span>🙃</span>
|
|
||||||
<span>🙄</span><span>😛</span><span>😴</span><span>🤐</span><span>🤑</span><span>🤠</span><span>🥳</span><span>👋</span>
|
|
||||||
<span>😄</span><span>😃</span><span>😆</span><span>😇</span><span>😉</span><span>😌</span><span>😘</span><span>🤗</span>
|
|
||||||
<span>🤔</span><span>🤯</span><span>🤬</span><span>🥶</span><span>🥵</span><span>🥴</span><span>🥺</span><span>🧐</span>
|
|
||||||
</div>
|
|
||||||
<input type="text" id="msgInput" placeholder="输入消息..." onkeypress="handleKeyPress(event)">
|
|
||||||
<button onclick="sendMessage()">发送</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<div id="newChatModal" class="modal">
|
<footer>
|
||||||
<div class="modal-content">
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
<span class="close-btn" onclick="document.getElementById('newChatModal').style.display='none'">×</span>
|
</footer>
|
||||||
<h3>发起新会话</h3>
|
|
||||||
<select id="countryCode" onchange="updatePhoneField()">
|
|
||||||
<option value="+86" data-name="中国">中国 (+86)</option>
|
|
||||||
<option value="+60" data-name="马来西亚">马来西亚 (+60)</option>
|
|
||||||
<option value="+1" data-name="美国/加拿大">美国/加拿大 (+1)</option>
|
|
||||||
<option value="+44" data-name="英国">英国 (+44)</option>
|
|
||||||
<option value="+81" data-name="日本">日本 (+81)</option>
|
|
||||||
<option value="+49" data-name="德国">德国 (+49)</option>
|
|
||||||
<option value="+61" data-name="澳大利亚">澳大利亚 (+61)</option>
|
|
||||||
<option value="+33" data-name="法国">法国 (+33)</option>
|
|
||||||
<option value="+7" data-name="俄罗斯">俄罗斯 (+7)</option>
|
|
||||||
<option value="+65" data-name="新加坡">新加坡 (+65)</option>
|
|
||||||
<option value="+91" data-name="印度">印度 (+91)</option>
|
|
||||||
<option value="+55" data-name="巴西">巴西 (+55)</option>
|
|
||||||
<option value="+39" data-name="意大利">意大利 (+39)</option>
|
|
||||||
<option value="+34" data-name="西班牙">西班牙 (+34)</option>
|
|
||||||
<option value="+82" data-name="韩国">韩国 (+82)</option>
|
|
||||||
<option value="+27" data-name="南非">南非 (+27)</option>
|
|
||||||
</select>
|
|
||||||
<input type="text" id="phoneInput" placeholder="输入手机号码">
|
|
||||||
<button onclick="initiateChat()">开始聊天</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const isLoggedIn = <?php echo (isset($_SESSION["is_logged_in"]) && $_SESSION["is_logged_in"]) ? 'true' : 'false'; ?>;
|
|
||||||
|
|
||||||
// Remove branding on load and continuously
|
|
||||||
function hideBranding() {
|
|
||||||
const forbidden = document.querySelectorAll('[class*="flatlogic"], [id*="flatlogic"], footer, .brand-label');
|
|
||||||
forbidden.forEach(el => { el.style.display = 'none'; el.style.visibility = 'hidden'; el.style.opacity = '0'; });
|
|
||||||
}
|
|
||||||
setInterval(hideBranding, 500);
|
|
||||||
|
|
||||||
// MutationObserver for branding removal
|
|
||||||
const observer = new MutationObserver(() => hideBranding());
|
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
// Mock data structure to simulate storage
|
|
||||||
let chatHistory = {
|
|
||||||
"+86 13800138000": ["欢迎使用金融智能通信平台。", "你好,请问在吗?"]
|
|
||||||
};
|
|
||||||
let currentConv = null;
|
|
||||||
|
|
||||||
function updatePhoneField() { document.getElementById('phoneInput').value = document.getElementById('countryCode').value; }
|
|
||||||
function toggleEmojiPicker() { const picker = document.getElementById('emojiPicker'); picker.style.display = picker.style.display === 'grid' ? 'none' : 'grid'; }
|
|
||||||
|
|
||||||
document.querySelectorAll('.emoji-picker span').forEach(el => {
|
|
||||||
el.onclick = () => { document.getElementById('msgInput').value += el.innerText; toggleEmojiPicker(); }
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleKeyPress(e) { if (e.key === 'Enter') sendMessage(); }
|
|
||||||
|
|
||||||
function selectConversation(phone) {
|
|
||||||
currentConv = phone;
|
|
||||||
document.getElementById('chatHeader').innerText = phone;
|
|
||||||
const list = document.getElementById('messageList');
|
|
||||||
list.innerHTML = '';
|
|
||||||
(chatHistory[phone] || []).forEach(msg => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'message msg-other';
|
|
||||||
div.innerText = msg;
|
|
||||||
list.appendChild(div);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteConversation(el) {
|
|
||||||
const item = el.parentElement;
|
|
||||||
const phone = item.querySelector('h4').innerText;
|
|
||||||
delete chatHistory[phone];
|
|
||||||
item.remove();
|
|
||||||
if (currentConv === phone) {
|
|
||||||
currentConv = null;
|
|
||||||
document.getElementById('messageList').innerHTML = '';
|
|
||||||
document.getElementById('chatHeader').innerText = '请开启会话';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage() {
|
|
||||||
if (!isLoggedIn) { alert('请先登录以发送消息'); window.location.href = '/login.php'; return; }
|
|
||||||
if (!currentConv) { alert('请先选择或发起一个会话'); return; }
|
|
||||||
const input = document.getElementById('msgInput');
|
|
||||||
if (input.value.trim() === '') return;
|
|
||||||
|
|
||||||
chatHistory[currentConv].push(input.value);
|
|
||||||
const msgList = document.getElementById('messageList');
|
|
||||||
const msg = document.createElement('div');
|
|
||||||
msg.className = 'message msg-me';
|
|
||||||
msg.innerText = input.value;
|
|
||||||
msgList.appendChild(msg);
|
|
||||||
input.value = '';
|
|
||||||
msgList.scrollTop = msgList.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initiateChat() {
|
|
||||||
const phone = document.getElementById('phoneInput').value;
|
|
||||||
if (!isLoggedIn) { alert('请先登录以开始聊天'); window.location.href = '/login.php'; return; }
|
|
||||||
if (phone.length > 5) {
|
|
||||||
if (!chatHistory[phone]) chatHistory[phone] = [];
|
|
||||||
currentConv = phone;
|
|
||||||
document.getElementById('chatHeader').innerText = phone;
|
|
||||||
document.getElementById('newChatModal').style.display = 'none';
|
|
||||||
selectConversation(phone);
|
|
||||||
} else { alert('请输入完整的手机号码'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure scrolling works
|
|
||||||
document.getElementById('convList').style.overflowY = 'auto';
|
|
||||||
document.getElementById('messageList').style.overflowY = 'auto';
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
37
login.php
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
session_start();
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// For now, hardcoded authentication
|
|
||||||
if ($_POST['username'] === 'user' && $_POST['password'] === 'user123') {
|
|
||||||
$_SESSION['is_logged_in'] = true;
|
|
||||||
header('Location: /');
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
$error = 'Invalid credentials';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>登录 - 聊天系统</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5; }
|
|
||||||
.login-box { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 300px; }
|
|
||||||
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;}
|
|
||||||
button { width: 100%; padding: 10px; background: #00a884; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-box">
|
|
||||||
<h2>用户登录</h2>
|
|
||||||
<?php if (isset($error)) echo "<p style='color:red'>$error</p>"; ?>
|
|
||||||
<form method="post">
|
|
||||||
<input type="text" name="username" placeholder="账号" required>
|
|
||||||
<input type="password" name="password" placeholder="密码" required>
|
|
||||||
<button type="submit">登录</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||