Autosave: 20260226-193348

This commit is contained in:
Flatlogic Bot 2026-02-26 19:33:48 +00:00
parent ce5ab804b3
commit 62bc71e2f3
20 changed files with 1935 additions and 420 deletions

127
admin.php Normal file
View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/db/config.php';
// Authentication and Authorization check
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
if (($_SESSION['role'] ?? 'user') !== 'admin') {
header('Location: index.php');
exit;
}
$message = '';
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['logout'])) {
session_destroy();
header('Location: login.php');
exit;
}
$head_ads = $_POST['head_ads'] ?? '';
$body_ads = $_POST['body_ads'] ?? '';
$openai_api_key = $_POST['openai_api_key'] ?? '';
$stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'head_ads'");
$stmt->execute([$head_ads]);
$stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'body_ads'");
$stmt->execute([$body_ads]);
$stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'openai_api_key'");
$stmt->execute([$openai_api_key]);
$message = "Settings updated successfully!";
}
// Fetch current settings
$stmt = $pdo->query("SELECT setting_key, setting_value FROM site_settings");
$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$head_ads = $settings['head_ads'] ?? '';
$body_ads = $settings['body_ads'] ?? '';
$openai_api_key = $settings['openai_api_key'] ?? '';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin Panel - TikTok Live AI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style>
body { font-family: 'Inter', sans-serif; background-color: #0f0f0f; color: #fff; }
.card-admin { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; }
.form-control { background-color: #262626; border-color: #444; color: #fff; }
.form-control:focus { background-color: #2d2d2d; border-color: #00f2ea; color: #fff; box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.25); }
.btn-save { background-color: #00f2ea; color: #000; font-weight: 700; border-radius: 8px; border: none; padding: 12px 24px; }
.btn-save:hover { background-color: #00d8d1; color: #000; }
.text-tiktok-cyan { color: #00f2ea; }
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-black border-bottom border-secondary mb-5">
<div class="container">
<a class="navbar-brand fw-bold" href="/">
<span class="text-tiktok-cyan">TikTok</span> Live Admin
</a>
<div class="d-flex align-items-center">
<span class="text-secondary small me-3">Logged in as <?= htmlspecialchars($_SESSION['username']) ?></span>
<a href="/" class="btn btn-outline-light btn-sm me-2">Back to Home</a>
<form action="admin.php" method="POST" class="m-0 d-inline">
<button type="submit" name="logout" class="btn btn-danger btn-sm">Logout</button>
</form>
</div>
</div>
</nav>
<main class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card-admin p-4 shadow-lg">
<h2 class="mb-4 fw-bold">Site Settings</h2>
<?php if ($message): ?>
<div class="alert alert-success bg-dark text-success border-success"><?= $message ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-4">
<label for="openai_api_key" class="form-label fw-semibold text-secondary small">OpenAI API Key</label>
<input type="password" name="openai_api_key" id="openai_api_key" class="form-control" placeholder="sk-..." value="<?= htmlspecialchars($openai_api_key) ?>">
<div class="form-text text-muted">If left empty, the application will use the Flatlogic AI Proxy.</div>
</div>
<hr class="border-secondary my-4">
<div class="mb-4">
<label for="head_ads" class="form-label fw-semibold text-secondary small">Head Scripts (for JS Ads, Meta tags, etc.)</label>
<textarea name="head_ads" id="head_ads" rows="4" class="form-control" placeholder="Paste your <head> scripts here..."><?= htmlspecialchars($head_ads) ?></textarea>
<div class="form-text text-muted">These scripts will be placed inside the &lt;head&gt; tag.</div>
</div>
<div class="mb-4">
<label for="body_ads" class="form-label fw-semibold text-secondary small">Body Scripts (for floating ads, analytics, etc.)</label>
<textarea name="body_ads" id="body_ads" rows="4" class="form-control" placeholder="Paste your <body> scripts here..."><?= htmlspecialchars($body_ads) ?></textarea>
<div class="form-text text-muted">These scripts will be placed at the bottom of the &lt;body&gt; tag.</div>
</div>
<div class="d-grid mt-5">
<button type="submit" class="btn btn-save">Save All Settings</button>
</div>
</form>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@ -8,20 +8,6 @@
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
// ],
// ]);
// if (!empty($response['success'])) {
// // response['data'] contains full payload, e.g.:
// // {
// // "id": "resp_xxx",
// // "status": "completed",
// // "output": [
// // {"type": "reasoning", "summary": []},
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
// // ]
// // }
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
// }
// Poll settings override:
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
class LocalAIApi
{
@ -55,8 +41,14 @@ class LocalAIApi
];
}
// Use custom model if provided, else use default from config
if (!isset($payload['model']) || $payload['model'] === '') {
$payload['model'] = $cfg['default_model'];
$payload['model'] = !empty($cfg['openai_api_key']) ? 'gpt-4o-mini' : $cfg['default_model'];
}
// If we have a custom OpenAI API key, we call OpenAI directly (synchronously)
if (!empty($cfg['openai_api_key'])) {
return self::directOpenAIRequest($payload, $options, $cfg['openai_api_key']);
}
$initial = self::request($options['path'] ?? null, $payload, $options);
@ -167,6 +159,40 @@ class LocalAIApi
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
}
/**
* Direct request to OpenAI API.
*/
private static function directOpenAIRequest(array $payload, array $options, string $apiKey): array
{
// Translate Flatlogic 'input' to OpenAI 'messages'
if (isset($payload['input'])) {
$payload['messages'] = $payload['input'];
unset($payload['input']);
}
$url = 'https://api.openai.com/v1/chat/completions';
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
];
$timeout = $options['timeout'] ?? 30;
$verifyTls = $options['verify_tls'] ?? true;
$body = json_encode($payload);
$resp = self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
if ($resp['success']) {
return [
'success' => true,
'status' => 200,
'data' => $resp['data']
];
}
return $resp;
}
/**
* Poll AI request status until ready or timeout.
*
@ -359,6 +385,22 @@ class LocalAIApi
if (!is_array($cfg)) {
throw new RuntimeException('Invalid AI config format: expected array');
}
// Try to load custom API key from DB if possible
$dbConfig = __DIR__ . '/../db/config.php';
if (file_exists($dbConfig)) {
try {
require_once $dbConfig;
$pdo = db();
$stmt = $pdo->query("SELECT setting_value FROM site_settings WHERE setting_key = 'openai_api_key' LIMIT 1");
$val = $stmt->fetchColumn();
$cfg['openai_api_key'] = $val ?: null;
} catch (Exception $e) {
// Fail silently, fallback to proxy
$cfg['openai_api_key'] = null;
}
}
self::$configCache = $cfg;
}
@ -490,4 +532,4 @@ class LocalAIApi
// Legacy alias for backward compatibility with the previous class name.
if (!class_exists('OpenAIService')) {
class_alias(LocalAIApi::class, 'OpenAIService');
}
}

40
api/bridge_control.php Normal file
View File

@ -0,0 +1,40 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$action = $_GET['action'] ?? '';
$username = $_GET['username'] ?? '';
if (empty($username)) {
echo json_encode(['success' => false, 'error' => 'Username required']);
exit;
}
if ($action === 'start') {
// 1. Kill any existing bridge for this user
shell_exec("pkill -f \"node tiktok_bridge.js $username\"");
// 2. Start new bridge
$cmd = sprintf(
"node %s %s %s %s %s %s > /dev/null 2>&1 &",
escapeshellarg(__DIR__ . '/../tiktok_bridge.js'),
escapeshellarg($username),
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME)
);
shell_exec($cmd);
echo json_encode(['success' => true, 'message' => "Bridge started for @$username"]);
} elseif ($action === 'stop') {
shell_exec("pkill -f \"node tiktok_bridge.js $username\"");
echo json_encode(['success' => true, 'message' => "Bridge stopped for @$username"]);
} elseif ($action === 'status') {
$output = shell_exec("pgrep -f \"node tiktok_bridge.js $username\"");
echo json_encode(['success' => true, 'running' => !empty($output)]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}

71
api/get_updates.php Normal file
View File

@ -0,0 +1,71 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
$username = $_GET['username'] ?? '';
if (empty($username)) {
echo json_encode(['events' => []]);
exit;
}
try {
// 1. Fetch pending comments (those without AI replies)
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND ai_reply IS NULL ORDER BY created_at ASC LIMIT 5");
$stmt->execute([$username]);
$pending = $stmt->fetchAll(PDO::FETCH_ASSOC);
$results = [];
foreach ($pending as $row) {
$comment_author = $row['comment_author'];
$comment_text = $row['comment_text'];
// 2. Generate AI Reply
$prompt = "You are a helpful and funny AI assistant for a TikTok live stream.
The streamer is '$username'.
The user '$comment_author' just said: '$comment_text'.
Respond to them in a short, engaging, and friendly way (max 20 words).
Keep it suitable for a live stream audience.";
$resp = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'TikTok Live AI Assistant'],
['role' => 'user', 'content' => $prompt],
],
]);
$ai_reply = '';
if (!empty($resp['success'])) {
$ai_reply = LocalAIApi::extractText($resp);
} else {
$ai_reply = "Thanks for your comment, $comment_author!";
}
// 3. Update DB
$update = db()->prepare("UPDATE tiktok_history SET ai_reply = ? WHERE id = ?");
$update->execute([$ai_reply, $row['id']]);
$results[] = [
'id' => $row['id'],
'author' => $comment_author,
'comment' => $comment_text,
'reply' => $ai_reply,
'timestamp' => $row['created_at']
];
}
// 4. Also fetch recently processed comments (last 10 seconds)
// To ensure the frontend shows them even if they were processed by another call
$since = date('Y-m-d H:i:s', time() - 5);
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND ai_reply IS NOT NULL AND created_at >= ? ORDER BY created_at DESC LIMIT 10");
$stmt->execute([$username, $since]);
$processed = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Merge if needed, but for polling, we just return the newly processed ones usually
echo json_encode(['events' => $results]);
} catch (Exception $e) {
error_log("Update Error: " . $e->getMessage());
echo json_encode(['error' => $e->getMessage(), 'events' => []]);
}

51
api/process_comment.php Normal file
View File

@ -0,0 +1,51 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || empty($data['comment']) || empty($data['username'])) {
echo json_encode(['error' => 'Missing required data']);
exit;
}
$username = $data['username'];
$comment_author = $data['author'] ?? 'Anonymous';
$comment_text = $data['comment'];
// Prompt for AI
$prompt = "You are a helpful and funny AI assistant for a TikTok live stream.
The streamer is '$username'.
The user '$comment_author' just said: '$comment_text'.
Respond to them in a short, engaging, and friendly way (max 20 words).
Keep it suitable for a live stream audience.";
$resp = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'TikTok Live AI Assistant'],
['role' => 'user', 'content' => $prompt],
],
]);
$ai_reply = '';
if (!empty($resp['success'])) {
$ai_reply = LocalAIApi::extractText($resp);
} else {
$ai_reply = "Thanks for your comment, $comment_author!";
}
// Persist to DB
try {
$stmt = db()->prepare("INSERT INTO tiktok_history (username, comment_author, comment_text, ai_reply) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $comment_author, $comment_text, $ai_reply]);
} catch (Exception $e) {
// Log error but continue
}
echo json_encode([
'success' => true,
'reply' => $ai_reply,
'author' => $comment_author,
'comment' => $comment_text
]);

View File

@ -1,302 +1,240 @@
:root {
--tiktok-cyan: #00f2ea;
--tiktok-red: #ff0050;
--bg-dark: #121212;
--bg-surface: #1e1e1e;
--border-secondary: rgba(255, 255, 255, 0.1);
--text-light: #f8f9fa;
--text-secondary: #adb5bd;
}
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;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg-dark);
color: var(--text-light);
line-height: 1.6;
}
.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;
.bg-surface {
background-color: var(--bg-surface);
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
.text-tiktok-cyan {
color: var(--tiktok-cyan) !important;
}
.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;
.text-tiktok-red {
color: var(--tiktok-red) !important;
}
.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;
.btn-tiktok-cyan {
background-color: var(--tiktok-cyan);
color: #000;
border: none;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
.btn-tiktok-cyan:hover {
background-color: #00d8d1;
color: #000;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
.btn-outline-tiktok-cyan {
border-color: var(--tiktok-cyan);
color: var(--tiktok-cyan);
}
::-webkit-scrollbar-track {
background: transparent;
.btn-outline-tiktok-cyan:hover {
background-color: var(--tiktok-cyan);
color: #000;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
.bg-secondary {
background-color: var(--text-secondary);
}
.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);
.bg-success {
background-color: #28a745 !important;
}
.comment-feed {
scrollbar-width: thin;
scrollbar-color: var(--border-secondary) transparent;
}
.comment-feed::-webkit-scrollbar {
width: 6px;
}
.comment-feed::-webkit-scrollbar-thumb {
background-color: var(--border-secondary);
border-radius: 10px;
}
.comment-item {
border-bottom: 1px solid var(--border-secondary);
padding: 10px 0;
animation: fadeIn 0.3s ease-out;
}
.comment-author {
font-weight: 600;
color: var(--tiktok-cyan);
margin-right: 8px;
}
.comment-text {
color: var(--text-light);
}
.ai-reply-box {
background-color: rgba(0, 242, 234, 0.05);
border-left: 3px solid var(--tiktok-cyan);
padding: 8px 12px;
margin-top: 5px;
font-size: 0.9rem;
font-style: italic;
color: var(--tiktok-cyan);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
.pulse {
animation: pulse-animation 2s infinite;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
@keyframes pulse-animation {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
.card {
border-radius: 12px;
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
.form-control, .form-select {
border-radius: 8px;
padding: 0.6rem 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;
.form-control:focus, .form-select:focus {
background-color: #1a1a1a;
border-color: var(--tiktok-cyan);
box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.1);
color: #fff;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
.toast {
background-color: var(--bg-surface);
color: var(--text-light);
border: 1px solid var(--border-secondary);
}
.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;
/* AI Avatar Styles Improved */
.ai-avatar-container {
text-align: center;
margin-bottom: 2rem;
position: relative;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
.ai-avatar {
width: 160px;
height: 220px;
border-radius: 80px 80px 20px 20px;
background: linear-gradient(135deg, var(--tiktok-cyan), var(--tiktok-red));
padding: 5px;
display: inline-block;
box-shadow: 0 10px 40px rgba(0, 242, 234, 0.3);
overflow: hidden;
position: relative;
animation: float-sway 6s ease-in-out infinite, breath 4s ease-in-out infinite;
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
.ai-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 75px 75px 15px 15px;
background: #000;
filter: brightness(1.1) contrast(1.1);
}
.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);
.ai-avatar::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to top, rgba(0,0,0,0.5), transparent);
pointer-events: none;
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
.ai-avatar-glow {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: inset 0 0 20px rgba(0, 242, 234, 0.4);
pointer-events: none;
z-index: 2;
animation: glow-pulse 3s infinite;
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
.ai-badge {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
background-color: var(--tiktok-cyan);
color: #000;
font-size: 0.7rem;
font-weight: 700;
padding: 2px 12px;
border-radius: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.4);
z-index: 3;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
@keyframes float-sway {
0% { transform: translateY(0px) rotate(-1deg); }
50% { transform: translateY(-20px) rotate(1deg); }
100% { transform: translateY(0px) rotate(-1deg); }
}
@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); }
@keyframes breath {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
@keyframes glow-pulse {
0% { opacity: 0.3; }
50% { opacity: 1; }
100% { opacity: 0.3; }
}
.admin-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;
font-size: 0.8rem;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s;
display: flex;
align-items: center;
gap: 5px;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
color: var(--tiktok-cyan);
}
/* 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);
}

BIN
assets/images/ai_avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,39 +1,239 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const tiktokUsernameInput = document.getElementById('tiktokUsername');
const connectionStatus = document.getElementById('connectionStatus');
const commentFeed = document.getElementById('commentFeed');
const emptyFeed = document.getElementById('emptyFeed');
const voiceSelect = document.getElementById('voiceSelect');
const rateRange = document.getElementById('rateRange');
const rateValue = document.getElementById('rateValue');
const autoReplyToggle = document.getElementById('autoReplyToggle');
const simulateBtn = document.getElementById('simulateBtn');
const manualCommentInput = document.getElementById('manualComment');
const historyTableBody = document.getElementById('historyTableBody');
const commentCountBadge = document.getElementById('commentCount');
const toastContainer = document.getElementById('toastContainer');
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
let isConnected = false;
let commentCount = 0;
let synth = window.speechSynthesis;
let voices = [];
let pollInterval = null;
let lastEventId = 0;
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
// Load voices
function populateVoiceList() {
voices = synth.getVoices().sort(function (a, b) {
const aname = a.name.toUpperCase();
const bname = b.name.toUpperCase();
if (aname < bname) return -1;
else if (aname > bname) return 1;
return 0;
});
voiceSelect.innerHTML = '';
voices.forEach((voice, i) => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
if (voice.default) option.textContent += ' -- DEFAULT';
option.setAttribute('data-lang', voice.lang);
option.setAttribute('data-name', voice.name);
voiceSelect.appendChild(option);
});
}
appendMessage(message, 'visitor');
chatInput.value = '';
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
rateRange.addEventListener('input', () => {
rateValue.textContent = rateRange.value;
});
async function startBridge(username) {
try {
const response = await fetch('api/chat.php', {
const resp = await fetch(`api/bridge_control.php?action=start&username=${username}`);
const data = await resp.json();
if (data.success) {
isConnected = true;
connectBtn.classList.add('d-none');
disconnectBtn.classList.remove('d-none');
connectionStatus.innerHTML = '<span class="status-dot bg-success me-1 pulse"></span> Live connection active: @' + username;
emptyFeed.classList.add('d-none');
showToast('Connected', `Started listening to @${username}. Looking for comments...`, 'success');
startPolling(username);
} else {
showToast('Bridge Error', data.error || 'Failed to start bridge', 'danger');
}
} catch (err) {
showToast('Network Error', 'Could not reach bridge control.', 'danger');
}
}
async function stopBridge(username) {
try {
await fetch(`api/bridge_control.php?action=stop&username=${username}`);
isConnected = false;
connectBtn.classList.remove('d-none');
disconnectBtn.classList.add('d-none');
connectionStatus.innerHTML = '<span class="status-dot bg-secondary me-1"></span> Disconnected';
showToast('Disconnected', 'Stopped bridge.', 'warning');
if (pollInterval) clearInterval(pollInterval);
} catch (err) {
console.error(err);
}
}
function startPolling(username) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
if (!isConnected) return;
try {
const resp = await fetch(`api/get_updates.php?username=${username}`);
const data = await resp.json();
if (data.events && data.events.length > 0) {
data.events.forEach(event => {
// Only add if not already seen
if (event.id > lastEventId) {
addEventToFeed(event);
lastEventId = event.id;
}
});
}
} catch (err) {
console.error('Polling error:', err);
}
}, 3000); // Poll every 3 seconds
}
function addEventToFeed(event) {
commentCount++;
commentCountBadge.textContent = `${commentCount} events`;
const isSystem = event.comment.includes('sent') && event.comment.includes('x');
const commentItem = document.createElement('div');
commentItem.className = isSystem ? 'comment-item border-start border-4 border-tiktok-red' : 'comment-item';
commentItem.innerHTML = `
<div class="d-flex justify-content-between">
<div>
<span class="comment-author" style="${isSystem ? 'color: var(--tiktok-red)' : ''}">${event.author}:</span>
<span class="comment-text">${event.comment}</span>
</div>
<small class="text-secondary opacity-50" style="font-size: 0.7rem">${new Date(event.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</small>
</div>
<div class="ai-reply-box mt-1">AI: ${event.reply}</div>
`;
commentFeed.prepend(commentItem);
// TTS
speak(event.reply);
// Update history table
updateHistoryTable(event);
}
connectBtn.addEventListener('click', () => {
const username = tiktokUsernameInput.value.trim();
if (!username) {
showToast('Error', 'Please enter a TikTok username.', 'danger');
return;
}
startBridge(username);
});
disconnectBtn.addEventListener('click', () => {
const username = tiktokUsernameInput.value.trim();
stopBridge(username);
});
simulateBtn.addEventListener('click', async () => {
const commentText = manualCommentInput.value.trim();
const username = tiktokUsernameInput.value.trim();
if (!commentText || !username) return;
try {
// Manually insert into DB to test
await fetch('api/process_comment.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
body: JSON.stringify({ author: 'TestUser', comment: commentText, username })
});
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');
manualCommentInput.value = '';
// Polling will pick it up
} catch (err) {
console.error(err);
}
});
});
function speak(text) {
if (!text) return;
const utterThis = new SpeechSynthesisUtterance(text);
const selectedOption = voiceSelect.selectedOptions[0]?.getAttribute('data-name');
if (selectedOption) {
for (let i = 0; i < voices.length; i++) {
if (voices[i].name === selectedOption) {
utterThis.voice = voices[i];
}
}
}
utterThis.pitch = 1;
utterThis.rate = rateRange.value;
synth.speak(utterThis);
}
function updateHistoryTable(event) {
const row = document.createElement('tr');
row.className = 'border-secondary';
const time = new Date(event.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
row.innerHTML = `
<td class="text-secondary small">${time}</td>
<td><strong>${event.author}</strong></td>
<td class="text-secondary">${event.comment}</td>
<td class="text-tiktok-cyan">${event.reply}</td>
`;
if (historyTableBody.firstChild && historyTableBody.firstChild.tagName === 'TR' && historyTableBody.firstChild.innerText.includes('No history yet')) {
historyTableBody.innerHTML = '';
}
historyTableBody.prepend(row);
if (historyTableBody.children.length > 20) {
historyTableBody.removeChild(historyTableBody.lastChild);
}
}
function showToast(title, message, type = 'info') {
const toastId = 'toast-' + Date.now();
const toastHtml = `
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-dark text-light border-secondary">
<strong class="me-auto ${type === 'danger' ? 'text-danger' : (type === 'success' ? 'text-tiktok-cyan' : '')}">${title}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
});

View File

@ -0,0 +1,6 @@
-- Add email and role columns to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255) AFTER username;
ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(50) DEFAULT 'user' AFTER password;
-- Set existing admin user to admin role
UPDATE users SET role = 'admin' WHERE username = 'admin';

View File

@ -0,0 +1 @@
INSERT INTO site_settings (setting_key, setting_value) VALUES ('openai_api_key', '') ON DUPLICATE KEY UPDATE setting_key=setting_key;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS site_settings (
setting_key VARCHAR(255) PRIMARY KEY,
setting_value TEXT
);
INSERT INTO site_settings (setting_key, setting_value) VALUES ('head_ads', '') ON DUPLICATE KEY UPDATE setting_key=setting_key;
INSERT INTO site_settings (setting_key, setting_value) VALUES ('body_ads', '') ON DUPLICATE KEY UPDATE setting_key=setting_key;

9
db/migrations/users.sql Normal file
View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Default admin user (password is 'admin123')
INSERT IGNORE INTO users (username, password) VALUES ('admin', '$2y$10$2jqIQAXSAx9.G80gTO4yjO7gUmhxfGsBpVq9Iv.HJZlTIKy0i2Ypa');

8
db/schema.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS tiktok_history (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL,
comment_author VARCHAR(255) NOT NULL,
comment_text TEXT NOT NULL,
ai_reply TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

331
index.php
View File

@ -1,150 +1,215 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
session_start();
require_once __DIR__ . '/db/config.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$pdo = db();
// Fetch settings
$stmt = $pdo->query("SELECT setting_key, setting_value FROM site_settings");
$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$head_ads = $settings['head_ads'] ?? '';
$body_ads = $settings['body_ads'] ?? '';
?>
<!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 -->
<title>TikTok Live AI Assistant</title>
<?php
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Real-time TikTok Live AI Comment Reader and Auto-Responder with TTS.';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<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; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<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>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<!-- Custom Head Scripts -->
<?= $head_ads ?>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<body class="bg-dark text-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-black border-bottom border-secondary sticky-top">
<div class="container-fluid">
<a class="navbar-brand fw-bold d-flex align-items-center" href="/">
<span class="text-tiktok-cyan me-1">TikTok</span>
<span class="text-tiktok-red me-2">Live</span>
<span class="badge bg-danger rounded-pill pulse">AI LIVE</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarMain">
<div class="ms-auto d-flex align-items-center flex-column flex-lg-row">
<div id="connectionStatus" class="me-lg-3 my-2 my-lg-0 small text-secondary">
<span class="status-dot bg-secondary me-1"></span> Disconnected
</div>
<?php if (isset($_SESSION['user_id'])): ?>
<?php if (($_SESSION['role'] ?? 'user') === 'admin'): ?>
<a href="admin.php" class="nav-link text-tiktok-cyan me-lg-3 my-1 my-lg-0">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-gear-fill me-1" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>
Admin
</a>
<?php endif; ?>
<span class="text-secondary small me-lg-3 my-1 my-lg-0">Hi, <?= htmlspecialchars($_SESSION['username']) ?></span>
<a href="logout.php" class="btn btn-outline-danger btn-sm my-1 my-lg-0">Logout</a>
<?php else: ?>
<a href="login.php" class="nav-link text-light me-lg-3 my-1 my-lg-0">Login</a>
<a href="register.php" class="btn btn-tiktok-cyan btn-sm my-1 my-lg-0">Register</a>
<?php endif; ?>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</div>
</nav>
<main class="container py-4">
<div class="row g-4">
<!-- Left Column: Controls & Input -->
<div class="col-lg-4">
<!-- AI Avatar Improved -->
<div class="ai-avatar-container">
<div class="ai-avatar">
<img src="assets/images/ai_avatar.jpg" alt="AI Avatar">
<div class="ai-avatar-glow"></div>
<div class="ai-badge">AI ACTIVE</div>
</div>
<h4 class="mt-3 fw-bold text-tiktok-cyan">Your AI Assistant</h4>
<p class="text-secondary small">I am here to interact with your live stream audience!</p>
</div>
<div class="card bg-surface border-secondary h-100 shadow-sm">
<div class="card-body">
<h5 class="card-title mb-4 fw-bold">Configuration</h5>
<div class="mb-3">
<label for="tiktokUsername" class="form-label small text-secondary">TikTok Username</label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-secondary">@</span>
<input type="text" id="tiktokUsername" class="form-control bg-dark border-secondary text-light" placeholder="username" value="tiktok_star">
</div>
</div>
<div class="mb-4">
<button id="connectBtn" class="btn btn-tiktok-cyan w-100 fw-bold py-2 shadow-sm">Connect to Live</button>
<button id="disconnectBtn" class="btn btn-outline-danger w-100 fw-bold py-2 mt-2 shadow-sm d-none">Disconnect</button>
</div>
<hr class="border-secondary my-4">
<h6 class="mb-3 fw-bold">TTS Settings</h6>
<div class="mb-3">
<label for="voiceSelect" class="form-label small text-secondary">Voice</label>
<select id="voiceSelect" class="form-select bg-dark border-secondary text-light"></select>
</div>
<div class="mb-3">
<label for="rateRange" class="form-label small text-secondary">Speed: <span id="rateValue">1.0</span></label>
<input type="range" class="form-range" id="rateRange" min="0.5" max="2" step="0.1" value="1.0">
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="autoReplyToggle" checked>
<label class="form-check-label" for="autoReplyToggle">AI Auto-Reply</label>
</div>
</div>
</div>
</div>
<!-- Right Column: Live Feed -->
<div class="col-lg-8">
<div class="card bg-surface border-secondary shadow-sm h-100">
<div class="card-header bg-transparent border-secondary d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">Live Comment Feed</h5>
<span class="badge bg-dark border border-secondary text-secondary" id="commentCount">0 comments</span>
</div>
<div class="card-body p-0">
<div id="commentFeed" class="comment-feed p-3 overflow-auto" style="height: 500px;">
<div class="text-center text-secondary py-5" id="emptyFeed">
<p>Enter a username and connect to start seeing live comments.</p>
</div>
</div>
</div>
<div class="card-footer bg-transparent border-secondary">
<div class="input-group">
<input type="text" id="manualComment" class="form-control bg-dark border-secondary text-light" placeholder="Simulate a comment...">
<button id="simulateBtn" class="btn btn-outline-tiktok-cyan">Simulate</button>
</div>
</div>
</div>
</div>
</div>
<!-- Stats / History -->
<div class="row g-4 mt-4">
<div class="col-12">
<div class="card bg-surface border-secondary shadow-sm">
<div class="card-header bg-transparent border-secondary">
<h5 class="mb-0 fw-bold">Recent AI Interactions</h5>
</div>
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr class="border-secondary">
<th class="border-secondary text-secondary small">Time</th>
<th class="border-secondary text-secondary small">User</th>
<th class="border-secondary text-secondary small">Comment</th>
<th class="border-secondary text-secondary small">AI Reply</th>
</tr>
</thead>
<tbody id="historyTableBody">
<?php
try {
$stmt = db()->query("SELECT * FROM tiktok_history ORDER BY created_at DESC LIMIT 5");
$rows = $stmt->fetchAll();
if (empty($rows)) {
echo '<tr><td colspan="4" class="text-center text-secondary py-4">No history yet.</td></tr>';
} else {
foreach ($rows as $row) {
echo "<tr class='border-secondary'>";
echo "<td class='text-secondary small'>" . date('H:i:s', strtotime($row['created_at'])) . "</td>";
echo "<td><strong>" . htmlspecialchars($row['comment_author']) . "</strong></td>";
echo "<td class='text-secondary'>" . htmlspecialchars($row['comment_text']) . "</td>";
echo "<td class='text-tiktok-cyan'>" . htmlspecialchars($row['ai_reply']) . "</td>";
echo "</tr>";
}
}
} catch (Exception $e) {
echo '<tr><td colspan="4" class="text-center text-danger py-4">Error loading history.</td></tr>';
}
?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
<footer class="text-center py-4 text-secondary small border-top border-secondary mt-5">
<p>&copy; <?= date('Y') ?> TikTok Live AI Assistant. Built with LAMP + OpenAI.</p>
<p><a href="admin.php" class="text-secondary text-decoration-none">Admin Settings</a></p>
</footer>
<div id="toastContainer" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
<!-- Custom Body Scripts -->
<?= $body_ads ?>
</body>
</html>
</html>

98
login.php Normal file
View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/db/config.php';
if (isset($_SESSION['user_id'])) {
if (($_SESSION['role'] ?? 'user') === 'admin') {
header('Location: admin.php');
} else {
header('Location: index.php');
}
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if ($username && $password) {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $username]); // Allow login with username or email
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'] ?? 'user';
if ($_SESSION['role'] === 'admin') {
header('Location: admin.php');
} else {
header('Location: index.php');
}
exit;
} else {
$error = "Invalid credentials.";
}
} else {
$error = "Please fill in all fields.";
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Login - TikTok Live AI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style>
body { font-family: 'Inter', sans-serif; background-color: #0f0f0f; color: #fff; height: 100vh; display: flex; align-items: center; justify-content: center; }
.card-login { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; width: 100%; max-width: 400px; }
.form-control { background-color: #262626; border-color: #444; color: #fff; }
.form-control:focus { background-color: #2d2d2d; border-color: #00f2ea; color: #fff; box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.25); }
.btn-login { background-color: #00f2ea; color: #000; font-weight: 700; border-radius: 8px; border: none; padding: 12px; }
.btn-login:hover { background-color: #00d8d1; color: #000; }
.text-tiktok-cyan { color: #00f2ea; }
</style>
</head>
<body>
<div class="card-login p-4 shadow-lg">
<div class="text-center mb-4">
<h3 class="fw-bold"><span class="text-tiktok-cyan">TikTok</span> AI Login</h3>
<p class="text-secondary small">Please login to continue</p>
</div>
<?php if ($error): ?>
<div class="alert alert-danger bg-dark text-danger border-danger py-2 small"><?= $error ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label small text-secondary">Username or Email</label>
<input type="text" name="username" id="username" class="form-control" required autofocus>
</div>
<div class="mb-4">
<label for="password" class="form-label small text-secondary">Password</label>
<input type="password" name="password" id="password" class="form-control" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-login">Login</button>
</div>
</form>
<div class="text-center mt-4">
<p class="small text-secondary">Don't have an account? <a href="register.php" class="text-tiktok-cyan text-decoration-none">Register here</a></p>
<a href="/" class="text-secondary small text-decoration-none">&larr; Back to Site</a>
</div>
</div>
</body>
</html>

7
logout.php Normal file
View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
session_start();
session_unset();
session_destroy();
header('Location: index.php');
exit;

631
package-lock.json generated Normal file
View File

@ -0,0 +1,631 @@
{
"name": "workspace",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "workspace",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"mysql2": "^3.18.2",
"tiktok-live-connector": "^2.1.1-beta1",
"ws": "^8.19.0"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@eulerstream/euler-api-sdk": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@eulerstream/euler-api-sdk/-/euler-api-sdk-0.1.7.tgz",
"integrity": "sha512-nahq6cTun0XClhpjFqSDOA79GpJXa5wgEEQIRE/q8G5sM83BzkJcRkQPaI0SP1AUgovCuAiXNdhEhqtvdAVf5g==",
"license": "MIT",
"dependencies": {
"axios": "^1.9.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
"integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callable-instance": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-2.0.0.tgz",
"integrity": "sha512-wOBp/J1CRZLsbFxG1alxefJjoG1BW/nocXkUanAe2+leiD/+cVr00j8twSZoDiRy03o5vibq9pbrZc+EDjjUTw==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lru.min": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mysql2": {
"version": "3.18.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz",
"integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.2",
"long": "^5.3.2",
"lru.min": "^1.1.4",
"named-placeholders": "^1.1.6",
"sql-escaper": "^1.3.3"
},
"engines": {
"node": ">= 8.0"
},
"peerDependencies": {
"@types/node": ">= 8"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/protobufjs": {
"version": "6.11.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
"integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/protobufjs/node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/tiktok-live-connector": {
"version": "2.1.1-beta1",
"resolved": "https://registry.npmjs.org/tiktok-live-connector/-/tiktok-live-connector-2.1.1-beta1.tgz",
"integrity": "sha512-K//OhuyMqq9EAb/2S5jHlW14fsFtO2cXQmRG5ZGQe6Cykb89h4qnkZMdMarzujYxb1jaadzfWKlB2dlz6OmCTA==",
"funding": [
"https://buymeacoffee.com/zerody"
],
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"@eulerstream/euler-api-sdk": "0.1.7",
"axios": "^1.12.2",
"callable-instance": "^2.0.0",
"protobufjs": "^6.11.2",
"typed-emitter": "^2.1.0",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
},
"node_modules/typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
"license": "MIT",
"optionalDependencies": {
"rxjs": "*"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "workspace",
"version": "1.0.0",
"description": "",
"main": "tiktok_bridge.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"mysql2": "^3.18.2",
"tiktok-live-connector": "^2.1.1-beta1",
"ws": "^8.19.0"
}
}

113
register.php Normal file
View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/db/config.php';
if (isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if ($username && $email && $password && $confirm_password) {
if ($password !== $confirm_password) {
$error = "Passwords do not match.";
} elseif (strlen($password) < 6) {
$error = "Password must be at least 6 characters.";
} else {
$pdo = db();
// Check if username or email exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
if ($stmt->fetch()) {
$error = "Username or email already exists.";
} else {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, 'user')");
if ($stmt->execute([$username, $email, $hashed_password])) {
$success = "Registration successful! You can now <a href='login.php' class='text-tiktok-cyan'>login</a>.";
} else {
$error = "Registration failed. Please try again.";
}
}
}
} else {
$error = "Please fill in all fields.";
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Register - TikTok Live AI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style>
body { font-family: 'Inter', sans-serif; background-color: #0f0f0f; color: #fff; height: 100vh; display: flex; align-items: center; justify-content: center; }
.card-register { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; width: 100%; max-width: 450px; }
.form-control { background-color: #262626; border-color: #444; color: #fff; }
.form-control:focus { background-color: #2d2d2d; border-color: #00f2ea; color: #fff; box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.25); }
.btn-register { background-color: #00f2ea; color: #000; font-weight: 700; border-radius: 8px; border: none; padding: 12px; }
.btn-register:hover { background-color: #00d8d1; color: #000; }
.text-tiktok-cyan { color: #00f2ea; }
</style>
</head>
<body>
<div class="card-register p-4 shadow-lg">
<div class="text-center mb-4">
<h3 class="fw-bold"><span class="text-tiktok-cyan">Join</span> TikTok AI</h3>
<p class="text-secondary small">Create your account to get started</p>
</div>
<?php if ($error): ?>
<div class="alert alert-danger bg-dark text-danger border-danger py-2 small"><?= $error ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success bg-dark text-success border-success py-2 small"><?= $success ?></div>
<?php else: ?>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label small text-secondary">Username</label>
<input type="text" name="username" id="username" class="form-control" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required autofocus>
</div>
<div class="mb-3">
<label for="email" class="form-label small text-secondary">Email Address</label>
<input type="email" name="email" id="email" class="form-control" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label small text-secondary">Password</label>
<input type="password" name="password" id="password" class="form-control" required>
</div>
<div class="col-md-6 mb-4">
<label for="confirm_password" class="form-label small text-secondary">Confirm Password</label>
<input type="password" name="confirm_password" id="confirm_password" class="form-control" required>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-register">Register Now</button>
</div>
</form>
<?php endif; ?>
<div class="text-center mt-4">
<p class="small text-secondary">Already have an account? <a href="login.php" class="text-tiktok-cyan text-decoration-none">Login here</a></p>
<a href="/" class="text-secondary small text-decoration-none">&larr; Back to Site</a>
</div>
</div>
</body>
</html>

84
tiktok_bridge.js Normal file
View File

@ -0,0 +1,84 @@
const { WebcastPushConnection } = require('tiktok-live-connector');
const mysql = require('mysql2/promise');
const [,, username, dbHost, dbUser, dbPass, dbName] = process.argv;
if (!username) {
console.error('Usage: node tiktok_bridge.js <username> <dbHost> <dbUser> <dbPass> <dbName>');
process.exit(1);
}
async function start() {
console.log(`TikTok Bridge starting for @${username}`);
let connection;
try {
connection = await mysql.createConnection({
host: dbHost || '127.0.0.1',
user: dbUser || 'root',
password: dbPass || '',
database: dbName || 'app_db'
});
console.log('Connected to database');
} catch (err) {
console.error('DB Connection failed:', err);
process.exit(1);
}
const tiktokConnection = new WebcastPushConnection(username);
tiktokConnection.connect().then(state => {
console.log(`Connected to TikTok Live @${username} (Room ID: ${state.roomId})`);
}).catch(err => {
console.error('TikTok Connection failed:', err);
process.exit(1);
});
tiktokConnection.on('chat', async (data) => {
console.log(`[Chat] ${data.uniqueId}: ${data.comment}`);
try {
await connection.execute(
'INSERT INTO tiktok_history (username, comment_author, comment_text, created_at) VALUES (?, ?, ?, NOW())',
[username, data.uniqueId, data.comment]
);
} catch (err) {
console.error('Error inserting chat:', err);
}
});
tiktokConnection.on('gift', async (data) => {
const giftMsg = `sent ${data.repeatCount}x ${data.giftName}!`;
console.log(`[Gift] ${data.uniqueId}: ${giftMsg}`);
try {
await connection.execute(
'INSERT INTO tiktok_history (username, comment_author, comment_text, created_at) VALUES (?, ?, ?, NOW())',
[username, data.uniqueId, giftMsg]
);
} catch (err) {
console.error('Error inserting gift:', err);
}
});
tiktokConnection.on('disconnected', () => {
console.log('TikTok connection disconnected');
process.exit(0);
});
tiktokConnection.on('error', (err) => {
console.error('TikTok error:', err);
});
// Keep alive
setInterval(async () => {
try {
await connection.query('SELECT 1');
} catch (err) {
console.error('DB connection lost, reconnecting...');
connection = await mysql.createConnection({
host: dbHost, user: dbUser, password: dbPass, database: dbName
});
}
}, 30000);
}
start();