Compare commits

..

5 Commits

Author SHA1 Message Date
Flatlogic Bot
fefe28601c Autosave: 20260226-203025 2026-02-26 20:30:25 +00:00
Flatlogic Bot
70c4865817 Save 2026-02-26 20:17:29 +00:00
Flatlogic Bot
0c23b971b3 Mantap 2026-02-26 19:54:27 +00:00
Flatlogic Bot
3f421e9a53 Good Versi 2026-02-26 19:50:11 +00:00
Flatlogic Bot
62bc71e2f3 Autosave: 20260226-193348 2026-02-26 19:33:48 +00:00
36 changed files with 2588 additions and 432 deletions

272
admin.php Normal file
View File

@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/languages/helper.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'] ?? '';
$openai_model = $_POST['openai_model'] ?? 'gpt-4o-mini';
$site_name = $_POST['site_name'] ?? 'TikTok Live AI Assistant';
$default_language = $_POST['default_language'] ?? 'en';
// Handle File Uploads
$upload_dir = 'assets/images/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0775, true);
}
$site_icon = $_POST['current_site_icon'] ?? 'assets/images/logo.png';
if (!empty($_FILES['site_icon']['name'])) {
$icon_name = 'logo_' . time() . '.' . pathinfo($_FILES['site_icon']['name'], PATHINFO_EXTENSION);
if (move_uploaded_file($_FILES['site_icon']['tmp_name'], $upload_dir . $icon_name)) {
$site_icon = $upload_dir . $icon_name;
}
}
$site_favicon = $_POST['current_site_favicon'] ?? 'favicon.ico';
if (!empty($_FILES['site_favicon']['name'])) {
$fav_name = 'favicon_' . time() . '.' . pathinfo($_FILES['site_favicon']['name'], PATHINFO_EXTENSION);
if (move_uploaded_file($_FILES['site_favicon']['tmp_name'], $upload_dir . $fav_name)) {
$site_favicon = $upload_dir . $fav_name;
}
}
$settings_to_update = [
'head_ads' => $head_ads,
'body_ads' => $body_ads,
'openai_api_key' => $openai_api_key,
'openai_model' => $openai_model,
'site_name' => $site_name,
'site_icon' => $site_icon,
'site_favicon' => $site_favicon,
'default_language' => $default_language
];
foreach ($settings_to_update as $key => $value) {
$stmt = $pdo->prepare("INSERT INTO site_settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
$stmt->execute([$key, $value, $value]);
}
$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'] ?? '';
$openai_model = $settings['openai_model'] ?? 'gpt-4o-mini';
$site_name = $settings['site_name'] ?? 'TikTok Live AI Assistant';
$site_icon = $settings['site_icon'] ?? 'assets/images/logo.png';
$site_favicon = $settings['site_favicon'] ?? 'favicon.ico';
$default_language = $settings['default_language'] ?? 'en';
?>
<!doctype html>
<html lang="<?= get_lang() ?>">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= __('admin') ?> - <?= htmlspecialchars($site_name) ?></title>
<link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($site_favicon) ?>">
<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, .form-select { background-color: #262626; border-color: #444; color: #fff; }
.form-control:focus, .form-select: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"><?= __('hi') ?>, <?= 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 mb-5">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card-admin p-4 shadow-lg mb-4">
<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" enctype="multipart/form-data">
<div class="row g-4">
<div class="col-md-6">
<label for="site_name" class="form-label fw-semibold text-secondary small"><?= __('site_name') ?></label>
<input type="text" name="site_name" id="site_name" class="form-control" value="<?= htmlspecialchars($site_name) ?>">
</div>
<div class="col-md-6">
<label for="default_language" class="form-label fw-semibold text-secondary small"><?= __('language') ?></label>
<select name="default_language" id="default_language" class="form-select">
<option value="en" <?= $default_language === 'en' ? 'selected' : '' ?>>English</option>
<option value="id" <?= $default_language === 'id' ? 'selected' : '' ?>>Indonesia</option>
</select>
</div>
<div class="col-md-6">
<label for="site_icon" class="form-label fw-semibold text-secondary small"><?= __('site_icon') ?></label>
<input type="file" name="site_icon" id="site_icon" class="form-control">
<input type="hidden" name="current_site_icon" value="<?= htmlspecialchars($site_icon) ?>">
<?php if ($site_icon): ?>
<div class="mt-2 small text-secondary">Current: <img src="<?= $site_icon ?>" height="30" class="ms-2"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label for="site_favicon" class="form-label fw-semibold text-secondary small"><?= __('site_favicon') ?></label>
<input type="file" name="site_favicon" id="site_favicon" class="form-control">
<input type="hidden" name="current_site_favicon" value="<?= htmlspecialchars($site_favicon) ?>">
<?php if ($site_favicon): ?>
<div class="mt-2 small text-secondary">Current: <img src="<?= $site_favicon ?>" height="20" class="ms-2"></div>
<?php endif; ?>
</div>
<div class="col-12">
<hr class="border-secondary my-2">
</div>
<div class="col-md-6">
<label for="openai_api_key" class="form-label fw-semibold text-secondary small"><?= __('openai_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>
<div class="col-md-6">
<label for="openai_model" class="form-label fw-semibold text-secondary small"><?= __('openai_model') ?></label>
<select name="openai_model" id="openai_model" class="form-select">
<option value="gpt-4o-mini" <?= $openai_model === 'gpt-4o-mini' ? 'selected' : '' ?>>GPT-4o Mini (Default)</option>
<option value="o3-mini" <?= $openai_model === 'o3-mini' ? 'selected' : '' ?>>o3-mini (Reasoning)</option>
<option value="gpt-4o" <?= $openai_model === 'gpt-4o' ? 'selected' : '' ?>>GPT-4o (High Quality)</option>
</select>
<div class="form-text text-muted">Choose which model to use with your API Key.</div>
</div>
<div class="col-md-6">
<label for="head_ads" class="form-label fw-semibold text-secondary small"><?= __('head_scripts') ?></label>
<textarea name="head_ads" id="head_ads" rows="4" class="form-control" placeholder="Paste your <head> scripts here..."><?= htmlspecialchars($head_ads) ?></textarea>
</div>
<div class="col-md-6">
<label for="body_ads" class="form-label fw-semibold text-secondary small"><?= __('body_scripts') ?></label>
<textarea name="body_ads" id="body_ads" rows="4" class="form-control" placeholder="Paste your <body> scripts here..."><?= htmlspecialchars($body_ads) ?></textarea>
</div>
</div>
<div class="d-grid mt-5">
<button type="submit" class="btn btn-save"><?= __('save_settings') ?></button>
</div>
</form>
</div>
<!-- Diagnostics Card -->
<div class="card-admin p-4 shadow-lg border-warning border-opacity-25">
<h4 class="mb-3 fw-bold text-warning"><i class="fa-solid fa-microchip me-2"></i> System Diagnostics</h4>
<p class="text-secondary small">Check if your hosting (cPanel/Other) is compatible with the TikTok Bridge.</p>
<div id="diagnosticsOutput" class="p-3 bg-black rounded border border-secondary mb-3" style="font-family: monospace; font-size: 0.85rem; max-height: 300px; overflow-y: auto;">
Loading diagnostics...
</div>
<button class="btn btn-outline-warning btn-sm" id="runDiagnosticsBtn">
<i class="fa-solid fa-rotate me-1"></i> Run Diagnostics
</button>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const diagOutput = document.getElementById('diagnosticsOutput');
const runBtn = document.getElementById('runDiagnosticsBtn');
async function runDiagnostics() {
diagOutput.innerHTML = '<span class="text-secondary">Running diagnostics...</span>';
try {
const resp = await fetch('api/bridge_control.php?action=debug&username=admin'); // username doesn't matter for debug
const data = await resp.json();
if (data.success && data.env) {
let html = '<table class="table table-dark table-sm table-borderless mb-0">';
for (const [key, value] of Object.entries(data.env)) {
let valDisplay = value;
let colorClass = '';
if (value === true) { valDisplay = '<span class="text-success">YES</span>'; }
else if (value === false) { valDisplay = '<span class="text-danger">NO</span>'; }
else if (!value) { valDisplay = '<span class="text-secondary">N/A</span>'; }
html += `<tr><td class="text-secondary" style="width: 40%">${key}:</td><td>${valDisplay}</td></tr>`;
}
html += '</table>';
if (!data.env.shell_exec_enabled) {
html += '<div class="mt-3 text-danger small"><strong>Warning:</strong> shell_exec is disabled. The TikTok bridge cannot start. Please enable it in cPanel MultiPHP INI Editor or contact your host.</div>';
}
if (!data.env.node_path || data.env.node_path === 'node') {
html += '<div class="mt-2 text-warning small"><strong>Notice:</strong> Node binary not explicitly found. Ensure "node" is in your system PATH.</div>';
}
if (!data.env.node_modules_exists) {
html += '<div class="mt-2 text-danger small"><strong>Warning:</strong> node_modules folder is missing. Run "npm install" on your server.</div>';
}
diagOutput.innerHTML = html;
} else {
diagOutput.innerHTML = '<span class="text-danger">Error fetching diagnostics: ' + (data.error || 'Unknown error') + '</span>';
}
} catch (err) {
diagOutput.innerHTML = '<span class="text-danger">Network Error: Could not reach bridge control API.</span>';
}
}
runBtn.addEventListener('click', runDiagnostics);
runDiagnostics(); // Run on load
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
</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 site_settings or config
if (!isset($payload['model']) || $payload['model'] === '') {
$payload['model'] = $cfg['default_model'];
$payload['model'] = !empty($cfg['openai_model']) ? $cfg['openai_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,26 @@ class LocalAIApi
if (!is_array($cfg)) {
throw new RuntimeException('Invalid AI config format: expected array');
}
// Try to load custom API key and model from DB if possible
$dbConfig = __DIR__ . '/../db/config.php';
if (file_exists($dbConfig)) {
try {
require_once $dbConfig;
$pdo = db();
// Fetch all relevant settings in one go
$stmt = $pdo->query("SELECT setting_key, setting_value FROM site_settings WHERE setting_key IN ('openai_api_key', 'openai_model')");
while ($row = $stmt->fetch()) {
$cfg[$row['setting_key']] = $row['setting_value'] ?: null;
}
} catch (Exception $e) {
// Fail silently, fallback to defaults
$cfg['openai_api_key'] = null;
$cfg['openai_model'] = null;
}
}
self::$configCache = $cfg;
}
@ -490,4 +536,4 @@ class LocalAIApi
// Legacy alias for backward compatibility with the previous class name.
if (!class_exists('OpenAIService')) {
class_alias(LocalAIApi::class, 'OpenAIService');
}
}

View File

@ -24,7 +24,7 @@ if (
if ($key === '') {
continue;
}
$value = trim($value, "\"' ");
$value = trim($value, "' ");
if (getenv($key) === false || getenv($key) === '') {
putenv("{$key}={$value}");
}
@ -46,7 +46,7 @@ return [
'project_id' => $projectId,
'project_uuid' => $projectUuid,
'project_header' => 'project-uuid',
'default_model' => 'gpt-5-mini',
'default_model' => 'gpt-4o-mini',
'timeout' => 30,
'verify_tls' => true,
];
];

161
api/bridge_control.php Normal file
View File

@ -0,0 +1,161 @@
<?php
header('Content-Type: application/json');
session_start();
require_once __DIR__ . '/../db/config.php';
// Authentication check
if (!isset($_SESSION['user_id'])) {
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$user_id = $_SESSION['user_id'];
$action = $_GET['action'] ?? '';
$username = $_GET['username'] ?? '';
if (empty($username)) {
echo json_encode(['success' => false, 'error' => 'Username required']);
exit;
}
// Helper to check if a function is enabled
function is_enabled($func) {
return function_exists($func) && !in_array($func, array_map('trim', explode(',', ini_get('disable_functions'))));
}
if (!is_enabled('shell_exec')) {
echo json_encode(['success' => false, 'error' => 'PHP function shell_exec is disabled on this server. Contact your hosting provider.']);
exit;
}
// Find node path
function get_node_path() {
$path = trim((string)shell_exec('which node'));
if ($path && file_exists($path)) return $path;
$common_paths = ['/usr/local/bin/node', '/usr/bin/node', '/bin/node', '/opt/node/bin/node'];
foreach ($common_paths as $p) {
if (file_exists($p)) return $p;
}
return 'node'; // Fallback to PATH
}
$node = get_node_path();
// Create logs directory if not exists
$logs_dir = __DIR__ . '/../logs';
if (!is_dir($logs_dir)) {
@mkdir($logs_dir, 0755, true);
}
$pid_file = "$logs_dir/bridge_{$username}_{$user_id}.pid";
$log_file = "$logs_dir/bridge_{$username}_{$user_id}.log";
function is_running($pid_file) {
if (!file_exists($pid_file)) return false;
$pid = (int)file_get_contents($pid_file);
if ($pid <= 0) return false;
// Check if process exists (posix_getpgid is safer but might be disabled)
if (function_exists('posix_getpgid')) {
return @posix_getpgid($pid) !== false;
}
// Fallback to pgrep or ps
$output = shell_exec("ps -p $pid");
return (strpos($output, (string)$pid) !== false);
}
function kill_process($pid_file, $username, $user_id) {
if (file_exists($pid_file)) {
$pid = (int)file_get_contents($pid_file);
if ($pid > 0) {
shell_exec("kill -9 $pid > /dev/null 2>&1");
}
@unlink($pid_file);
}
// Also try pkill as backup
shell_exec("pkill -f \"node .*tiktok_bridge.js $username .* $user_id\"");
}
if ($action === 'start') {
// 1. Kill any existing bridge
kill_process($pid_file, $username, $user_id);
// 2. Start new bridge
$bridge_script = __DIR__ . '/../tiktok_bridge.js';
// Check if node_modules exist
if (!is_dir(__DIR__ . '/../node_modules')) {
echo json_encode(['success' => false, 'error' => 'node_modules folder not found. Please run "npm install" on your server.']);
exit;
}
$cmd = sprintf(
"%s %s %s %s %s %s %s %s > %s 2>&1 & echo $!",
$node,
escapeshellarg($bridge_script),
escapeshellarg($username),
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME),
escapeshellarg($user_id),
escapeshellarg($log_file)
);
$pid = trim((string)shell_exec($cmd));
if ($pid > 0) {
file_put_contents($pid_file, $pid);
echo json_encode([
'success' => true,
'message' => "Bridge started for @$username",
'pid' => $pid,
'log' => "logs/" . basename($log_file)
]);
} else {
echo json_encode([
'success' => false,
'error' => "Failed to start bridge. Check $log_file for errors.",
'node_path' => $node
]);
}
} elseif ($action === 'stop') {
kill_process($pid_file, $username, $user_id);
echo json_encode(['success' => true, 'message' => "Bridge stopped for @$username"]);
} elseif ($action === 'status') {
$running = is_running($pid_file);
// Check log for errors if not running but was supposed to
$last_log = "";
if (!$running && file_exists($log_file)) {
$last_log = trim((string)shell_exec("tail -n 5 " . escapeshellarg($log_file)));
}
echo json_encode([
'success' => true,
'running' => $running,
'last_log' => $last_log
]);
} elseif ($action === 'debug') {
// Special action to check environment
$env = [
'php_version' => PHP_VERSION,
'shell_exec_enabled' => is_enabled('shell_exec'),
'node_path' => $node,
'node_version' => trim((string)shell_exec("$node -v")),
'npm_version' => trim((string)shell_exec("npm -v")),
'node_modules_exists' => is_dir(__DIR__ . '/../node_modules'),
'db_host' => DB_HOST,
'db_user' => DB_USER,
'os' => PHP_OS,
];
echo json_encode(['success' => true, 'env' => $env]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}

View File

@ -32,7 +32,7 @@ try {
// 3. Call AI API
$response = LocalAIApi::createResponse([
'model' => 'gpt-4o-mini',
// Model is handled by LocalAIApi defaults/site_settings if omitted
'input' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $message],
@ -61,4 +61,4 @@ try {
} catch (Exception $e) {
error_log("Chat Error: " . $e->getMessage());
echo json_encode(['reply' => "An internal error occurred."]);
}
}

79
api/get_updates.php Normal file
View File

@ -0,0 +1,79 @@
<?php
header('Content-Type: application/json');
session_start();
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
// Authentication check
if (!isset($_SESSION['user_id'])) {
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$user_id = $_SESSION['user_id'];
$username = $_GET['username'] ?? '';
if (empty($username)) {
echo json_encode(['events' => []]);
exit;
}
try {
// 1. Fetch pending comments (those without AI replies) for this specific user
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND user_id = ? AND ai_reply IS NULL ORDER BY created_at ASC LIMIT 5");
$stmt->execute([$username, $user_id]);
$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 5 seconds) for this user
$since = date('Y-m-d H:i:s', time() - 5);
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND user_id = ? AND ai_reply IS NOT NULL AND created_at >= ? ORDER BY created_at DESC LIMIT 10");
$stmt->execute([$username, $user_id, $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' => []]);
}

83
api/process_comment.php Normal file
View File

@ -0,0 +1,83 @@
<?php
header('Content-Type: application/json');
session_start();
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
// Authentication check
if (!isset($_SESSION['user_id'])) {
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$user_id = $_SESSION['user_id'];
$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'];
// Fetch user personality
$personality = 'funny';
try {
$stmt = db()->prepare("SELECT ai_personality FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$personality = $stmt->fetchColumn() ?: 'funny';
} catch (Exception $e) {
// Fallback to funny
}
$personality_instructions = "";
switch ($personality) {
case 'serious':
$personality_instructions = "Respond in a serious, professional, and formal tone.";
break;
case 'expert':
$personality_instructions = "Respond as an expert, providing highly informative, accurate, and insightful information.";
break;
case 'funny':
default:
$personality_instructions = "Respond in a funny, engaging, and lighthearted way, using jokes or playful language.";
break;
}
// Prompt for AI
$prompt = "You are a helpful AI assistant for a TikTok live stream. $personality_instructions
The streamer is '$username'.
The user '$comment_author' just said: '$comment_text'.
Respond to them in a short way (max 20 words).
Keep it suitable for a live stream audience.";
$resp = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => "TikTok Live AI Assistant - Personality: $personality"],
['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, user_id) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$username, $comment_author, $comment_text, $ai_reply, $user_id]);
} catch (Exception $e) {
// Log error but continue
}
echo json_encode([
'success' => true,
'reply' => $ai_reply,
'author' => $comment_author,
'comment' => $comment_text
]);

View File

@ -19,7 +19,7 @@ if (empty($text)) {
}
// Get Telegram Token from DB
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
$stmt = db()->query("SELECT setting_value FROM site_settings WHERE setting_key = 'telegram_token'");
$token = $stmt->fetchColumn();
if (!$token) {
@ -64,7 +64,7 @@ try {
// 2. Call AI
$response = LocalAIApi::createResponse([
'model' => 'gpt-4o-mini',
// Model is handled by LocalAIApi defaults/site_settings if omitted
'input' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $text],
@ -88,4 +88,4 @@ try {
} catch (Exception $e) {
error_log("Telegram Webhook Error: " . $e->getMessage());
}
}

View File

@ -0,0 +1,25 @@
<?php
header('Content-Type: application/json');
session_start();
require_once __DIR__ . '/../db/config.php';
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$personality = $data['personality'] ?? 'funny';
$allowed_personalities = ['funny', 'serious', 'expert'];
if (!in_array($personality, $allowed_personalities)) {
$personality = 'funny';
}
try {
$stmt = db()->prepare("UPDATE users SET ai_personality = ? WHERE id = ?");
$stmt->execute([$personality, $_SESSION['user_id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -1,302 +1,278 @@
:root {
--tiktok-cyan: #00f2ea;
--tiktok-red: #ff0050;
--bg-dark: #121212;
--bg-surface: #1e1e1e;
--border-secondary: rgba(255, 255, 255, 0.1);
--text-light: #ffffff;
--text-secondary: #ced4da; /* Brighter than adb5bd */
--text-muted: #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;
h1, h2, h3, h4, h5, h6, .card-title, .navbar-brand {
color: var(--text-light) !important;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
.bg-surface {
background-color: var(--bg-surface);
}
.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-cyan {
color: var(--tiktok-cyan) !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;
.text-tiktok-red {
color: var(--tiktok-red) !important;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
.text-secondary {
color: var(--text-secondary) !important;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
.small.text-secondary {
color: var(--text-secondary) !important;
opacity: 1 !important;
}
::-webkit-scrollbar-track {
background: transparent;
.btn-tiktok-cyan {
background-color: var(--tiktok-cyan);
color: #000;
border: none;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
.btn-tiktok-cyan:hover {
background-color: #00d8d1;
color: #000;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
.btn-outline-tiktok-cyan {
border-color: var(--tiktok-cyan);
color: var(--tiktok-cyan);
}
.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);
.btn-outline-tiktok-cyan:hover {
background-color: var(--tiktok-cyan);
color: #000;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.bg-secondary {
background-color: var(--text-secondary);
}
.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;
background-color: #262626 !important;
border-color: #444 !important;
color: #fff !important;
}
.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::placeholder {
color: #888 !important;
opacity: 1;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
.form-control:focus, .form-select:focus {
background-color: #2d2d2d !important;
border-color: var(--tiktok-cyan) !important;
box-shadow: 0 0 0 0.25rem rgba(0, 242, 234, 0.1);
color: #fff !important;
}
.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;
.toast {
background-color: var(--bg-surface);
color: var(--text-light);
border: 1px solid var(--border-secondary);
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
/* AI Avatar Styles Improved */
.ai-avatar-container {
text-align: center;
margin-bottom: 2rem;
position: relative;
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
.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;
}
.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 img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 75px 75px 15px 15px;
background: #000;
filter: brightness(1.1) contrast(1.1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
.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-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
.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-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
.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;
}
@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 float-sway {
0% { transform: translateY(0px) rotate(-1deg); }
50% { transform: translateY(-20px) rotate(1deg); }
100% { transform: translateY(0px) rotate(-1deg); }
}
@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;
}
/* 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;
color: var(--tiktok-cyan);
}
/* Table readability */
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
color: var(--text-light) !important;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
.table thead th {
color: var(--text-secondary) !important;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
.text-muted {
color: var(--text-muted) !important;
}
.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: 466 KiB

View File

@ -1,39 +1,276 @@
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');
if (!connectBtn) return; // Exit if not logged in
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
const 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 aiPersonality = document.getElementById('aiPersonality');
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
let isConnected = false;
let commentCount = 0;
let synth = window.speechSynthesis;
let voices = [];
let pollInterval = null;
let lastEventId = 0;
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
// Load voices
function populateVoiceList() {
if (!synth) return;
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;
});
if (voiceSelect) {
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);
});
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');
}
}
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
if (rateRange) {
rateRange.addEventListener('input', () => {
if (rateValue) rateValue.textContent = rateRange.value;
});
}
if (aiPersonality) {
aiPersonality.addEventListener('change', async () => {
const personality = aiPersonality.value;
try {
const resp = await fetch('api/update_personality.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ personality })
});
const data = await resp.json();
if (data.success) {
showToast('Settings Saved', 'AI personality updated successfully.', 'success');
} else {
showToast('Error', data.error || 'Failed to update personality', 'danger');
}
} catch (err) {
showToast('Network Error', 'Could not reach settings API.', 'danger');
}
});
}
async function startBridge(username) {
try {
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');
if (disconnectBtn) disconnectBtn.classList.remove('d-none');
if (connectionStatus) connectionStatus.innerHTML = '<span class="status-dot bg-success me-1 pulse"></span> Live connection active: @' + username;
if (emptyFeed) 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');
if (disconnectBtn) disconnectBtn.classList.add('d-none');
if (connectionStatus) 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++;
if (commentCountBadge) 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>
`;
if (commentFeed) 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);
});
if (disconnectBtn) {
disconnectBtn.addEventListener('click', () => {
const username = tiktokUsernameInput.value.trim();
stopBridge(username);
});
}
if (simulateBtn) {
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({ author: 'TestUser', comment: commentText, username })
});
manualCommentInput.value = '';
// Polling will pick it up
} catch (err) {
console.error(err);
}
});
}
function speak(text) {
if (!text || !synth) 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 || 1.0;
synth.speak(utterThis);
}
function updateHistoryTable(event) {
if (!historyTableBody) return;
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') {
if (!toastContainer) return;
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);
if (toastElement) {
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -0,0 +1,2 @@
-- Add ai_personality column to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS ai_personality VARCHAR(50) DEFAULT 'funny' AFTER role;

View File

@ -0,0 +1 @@
INSERT INTO site_settings (setting_key, setting_value) VALUES ('openai_model', 'gpt-4o-mini') ON DUPLICATE KEY UPDATE setting_key=setting_key;

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,13 @@
CREATE TABLE IF NOT EXISTS faqs (
id INT AUTO_INCREMENT PRIMARY KEY,
keywords TEXT NOT NULL,
answer TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_message TEXT NOT NULL,
ai_response TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

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;

View File

@ -0,0 +1,4 @@
INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_name', 'TikTok Live AI Assistant') ON DUPLICATE KEY UPDATE setting_key=setting_key;
INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_icon', 'assets/images/logo.png') ON DUPLICATE KEY UPDATE setting_key=setting_key;
INSERT INTO site_settings (setting_key, setting_value) VALUES ('site_favicon', 'favicon.ico') ON DUPLICATE KEY UPDATE setting_key=setting_key;
INSERT INTO site_settings (setting_key, setting_value) VALUES ('default_language', 'en') 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
);

27
includes/pexels.php Normal file
View File

@ -0,0 +1,27 @@
<?php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
$data = file_get_contents($srcUrl);
if ($data === false) return false;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
return file_put_contents($destPath, $data) !== false;
}

391
index.php
View File

@ -1,150 +1,259 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
// index.php - Main Dashboard
session_start();
require_once 'db/config.php';
require_once 'languages/helper.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}
$user_id = $_SESSION['user_id'];
$stmt = db()->prepare("SELECT username, role, ai_personality FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
if (!$user) {
session_destroy();
header("Location: login.php");
exit();
}
$is_admin = ($user['role'] ?? 'user') === 'admin';
$ai_personality = $user['ai_personality'] ?? 'Expert';
// Simple SEO meta
$page_title = __('welcome') . " - AI Assistant";
$meta_description = "AI Dashboard for TikTok automation and more.";
?>
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="<?= $lang ?>">
<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>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $page_title ?></title>
<meta name="description" content="<?= $meta_description ?>">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<style>
/* Minor overrides for layout */
.ai-avatar {
width: 140px;
height: 140px;
border-radius: 50%;
border: 4px solid var(--tiktok-cyan);
overflow: hidden;
background: #000;
box-shadow: 0 0 20px rgba(0, 242, 234, 0.4);
margin: 0 auto;
}
.ai-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.status-dot.pulse {
animation: pulse-animation 1.5s infinite;
}
@keyframes pulse-animation {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.5; }
100% { transform: scale(1); opacity: 1; }
}
</style>
</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>
<nav class="navbar navbar-expand-lg navbar-dark bg-surface shadow-sm sticky-top">
<div class="container">
<a class="navbar-brand fw-bold" href="index.php">
<span class="text-tiktok-red">AI</span> <span class="text-tiktok-cyan">DASHBOARD</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" href="index.php"><i class="fa-solid fa-house me-1"></i> Dashboard</a>
</li>
<?php if ($is_admin): ?>
<li class="nav-item">
<a class="nav-link" href="admin.php"><i class="fa-solid fa-user-shield me-1"></i> Admin</a>
</li>
<?php endif; ?>
</ul>
<div class="navbar-nav align-items-center">
<span class="nav-item me-3 text-secondary small">
<i class="fa-solid fa-user me-1"></i> <?= htmlspecialchars($user['username']) ?>
</span>
<div class="nav-item dropdown me-2">
<a class="nav-link dropdown-toggle" href="#" id="langDropdown" role="button" data-bs-toggle="dropdown">
<i class="fa-solid fa-globe"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end bg-surface border-secondary shadow">
<li><a class="dropdown-item text-light" href="?lang=id">Indonesia</a></li>
<li><a class="dropdown-item text-light" href="?lang=en">English</a></li>
</ul>
</div>
<a href="logout.php" class="btn btn-outline-danger btn-sm rounded-pill px-3">
<i class="fa-solid fa-right-from-bracket me-1"></i> Logout
</a>
</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-5">
<div class="text-center mb-5">
<h1 class="display-5 fw-bold mb-2"><?= __('welcome') ?>, <span class="text-tiktok-red"><?= htmlspecialchars($user['username']) ?></span>!</h1>
<p class="lead text-secondary"><?= __('hero_text') ?></p>
</div>
<div class="row g-4">
<!-- Left Column: Controls & AI personality -->
<div class="col-lg-4">
<!-- AI Avatar Display -->
<div class="card bg-surface border-secondary mb-4 shadow-sm py-4">
<div class="card-body text-center">
<div class="ai-avatar mb-3">
<img src="assets/images/ai_avatar.jpg?v=<?php echo time(); ?>" alt="AI Avatar">
</div>
<h5 class="fw-bold text-tiktok-cyan mb-1"><?= __('welcome') ?></h5>
<div id="connectionStatus" class="small text-secondary mb-3">
<span class="status-dot bg-secondary me-1"></span> Disconnected
</div>
<hr class="border-secondary opacity-25">
<div class="text-start px-3">
<label class="form-label text-secondary small fw-bold mb-1"><?= __('personality') ?></label>
<select class="form-select bg-dark text-light border-secondary" id="aiPersonality">
<option value="Funny" <?= $ai_personality == 'Funny' ? 'selected' : '' ?>><?= __('personality_funny') ?></option>
<option value="Serious" <?= $ai_personality == 'Serious' ? 'selected' : '' ?>><?= __('personality_serious') ?></option>
<option value="Expert" <?= $ai_personality == 'Expert' ? 'selected' : '' ?>><?= __('personality_expert') ?></option>
</select>
</div>
</div>
</div>
<!-- Config Card -->
<div class="card bg-surface border-secondary mb-4 shadow-sm">
<div class="card-body">
<h5 class="card-title mb-4 fw-bold"><i class="fa-brands fa-tiktok me-2"></i> <?= __('config') ?></h5>
<div class="mb-3">
<label for="tiktokUsername" class="form-label text-secondary small fw-bold"><?= __('tiktok_username') ?></label>
<div class="input-group">
<span class="input-group-text bg-dark border-secondary text-secondary">@</span>
<input type="text" class="form-control bg-dark text-light border-secondary" id="tiktokUsername" placeholder="username">
</div>
<div class="form-text text-secondary mt-1 small">Enter your TikTok username that is currently live.</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-tiktok-cyan text-dark fw-bold" id="connectBtn">
<i class="fa-solid fa-plug me-2"></i> <?= __('connect') ?>
</button>
<button class="btn btn-outline-danger fw-bold d-none" id="disconnectBtn">
<i class="fa-solid fa-power-off me-2"></i> <?= __('disconnect') ?>
</button>
</div>
</div>
</div>
<!-- TTS Settings -->
<div class="card bg-surface border-secondary shadow-sm">
<div class="card-body">
<h5 class="card-title mb-3 fw-bold"><i class="fa-solid fa-volume-high me-2"></i> <?= __('tts_settings') ?></h5>
<div class="mb-3">
<label class="form-label text-secondary small fw-bold"><?= __('voice') ?></label>
<select class="form-select bg-dark text-light border-secondary" id="voiceSelect"></select>
</div>
<div class="mb-3">
<label class="form-label text-secondary small fw-bold d-flex justify-content-between">
<span><?= __('speed') ?></span>
<span id="rateValue" class="text-tiktok-cyan">1.0</span>
</label>
<input type="range" class="form-range" id="rateRange" min="0.5" max="2" step="0.1" value="1">
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="autoReplyToggle" checked>
<label class="form-check-label text-secondary small fw-bold" for="autoReplyToggle"><?= __('auto_reply') ?></label>
</div>
</div>
</div>
</div>
<!-- Right Column: Live Feed & History -->
<div class="col-lg-8">
<!-- Comment Feed -->
<div class="card bg-dark border-secondary mb-4 shadow-sm" style="min-height: 500px;">
<div class="card-header bg-surface border-secondary d-flex justify-content-between align-items-center py-3">
<h5 class="mb-0 fw-bold"><i class="fa-solid fa-stream me-2 text-tiktok-cyan"></i> <?= __('live_feed') ?></h5>
<span class="badge bg-tiktok-red" id="commentCount">0 <?= __('comments') ?></span>
</div>
<div class="card-body p-0 d-flex flex-column">
<div id="commentFeed" class="p-3 flex-grow-1" style="height: 400px; overflow-y: auto;">
<div id="emptyFeed" class="text-center py-5 opacity-50">
<i class="fa-solid fa-satellite-dish fa-3x mb-3 text-secondary"></i>
<p><?= __('no_history') ?></p>
</div>
</div>
<div class="p-3 bg-surface border-top border-secondary mt-auto">
<div class="input-group">
<input type="text" class="form-control bg-dark text-light border-secondary" id="manualComment" placeholder="Simulate a comment...">
<button class="btn btn-outline-tiktok-cyan fw-bold" id="simulateBtn"><?= __('simulate') ?></button>
</div>
</div>
</div>
</div>
<!-- History -->
<div class="card bg-surface border-secondary shadow-sm">
<div class="card-header bg-transparent border-secondary py-3">
<h5 class="mb-0 fw-bold"><i class="fa-solid fa-history me-2 text-secondary"></i> <?= __('recent_interactions') ?></h5>
</div>
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr class="border-secondary text-secondary">
<th class="small fw-bold"><?= __('time') ?></th>
<th class="small fw-bold"><?= __('user') ?></th>
<th class="small fw-bold"><?= __('comment') ?></th>
<th class="small fw-bold"><?= __('ai_reply') ?></th>
</tr>
</thead>
<tbody id="historyTableBody">
<tr id="noHistoryRow">
<td colspan="4" class="text-center py-4 text-secondary"><?= __('no_history') ?></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
<footer class="py-4 mt-5 border-top border-secondary">
<div class="container text-center">
<p class="text-secondary small mb-0"><?= __('footer_text') ?></p>
</div>
</footer>
<!-- Toast Container -->
<div id="toastContainer" class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1100"></div>
<!-- Scripts -->
<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=<?php echo time(); ?>"></script>
</body>
</html>
</html>

44
languages/en.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
'welcome' => 'Welcome to TikTok Live AI',
'hero_text' => 'Enhance your live stream with AI-powered interactions. Connect with your audience like never before.',
'login_to_start' => 'Please login or register to connect to TikTok Live.',
'config' => 'Configuration',
'tiktok_username' => 'TikTok Username',
'connect' => 'Connect to Live',
'disconnect' => 'Disconnect',
'tts_settings' => 'TTS Settings',
'voice' => 'Voice',
'speed' => 'Speed',
'auto_reply' => 'AI Auto-Reply',
'live_feed' => 'Live Comment Feed',
'comments' => 'comments',
'simulate' => 'Simulate',
'recent_interactions' => 'Recent AI Interactions',
'time' => 'Time',
'user' => 'User',
'comment' => 'Comment',
'ai_reply' => 'AI Reply',
'no_history' => 'No history yet.',
'hi' => 'Hi',
'logout' => 'Logout',
'login' => 'Login',
'register' => 'Register',
'admin' => 'Admin',
'footer_text' => 'TikTok Live AI Assistant. Built with LAMP + OpenAI.',
'back_to_home' => 'Back to Home',
'save_settings' => 'Save All Settings',
'site_settings' => 'Site Settings',
'openai_key' => 'OpenAI API Key',
'openai_model' => 'OpenAI Model',
'head_scripts' => 'Head Scripts',
'body_scripts' => 'Body Scripts',
'site_name' => 'Site Name',
'site_icon' => 'Site Icon',
'site_favicon' => 'Site Favicon',
'language' => 'Language',
'personality' => 'AI Personality',
'personality_funny' => 'Funny',
'personality_serious' => 'Serious',
'personality_expert' => 'Expert',
];

25
languages/helper.php Normal file
View File

@ -0,0 +1,25 @@
<?php
function get_lang() {
if (!isset($_SESSION['lang'])) {
$pdo = db();
$stmt = $pdo->query("SELECT setting_value FROM site_settings WHERE setting_key = 'default_language'");
$default_lang = $stmt->fetchColumn() ?: 'en';
$_SESSION['lang'] = $default_lang;
}
if (isset($_GET['lang']) && in_array($_GET['lang'], ['en', 'id'])) {
$_SESSION['lang'] = $_GET['lang'];
}
return $_SESSION['lang'];
}
function __($key) {
static $translations = null;
if ($translations === null) {
$lang = get_lang();
$translations = require __DIR__ . "/$lang.php";
}
return $translations[$key] ?? $key;
}

44
languages/id.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
'welcome' => 'Selamat datang di TikTok Live AI',
'hero_text' => 'Tingkatkan siaran langsung Anda dengan interaksi berbasis AI. Terhubung dengan audiens Anda tidak seperti sebelumnya.',
'login_to_start' => 'Silakan login atau daftar untuk terhubung ke TikTok Live.',
'config' => 'Konfigurasi',
'tiktok_username' => 'Username TikTok',
'connect' => 'Hubungkan ke Live',
'disconnect' => 'Putuskan Koneksi',
'tts_settings' => 'Pengaturan TTS',
'voice' => 'Suara',
'speed' => 'Kecepatan',
'auto_reply' => 'Balasan Otomatis AI',
'live_feed' => 'Umpan Komentar Langsung',
'comments' => 'komentar',
'simulate' => 'Simulasi',
'recent_interactions' => 'Interaksi AI Terbaru',
'time' => 'Waktu',
'user' => 'Pengguna',
'comment' => 'Komentar',
'ai_reply' => 'Balasan AI',
'no_history' => 'Belum ada riwayat.',
'hi' => 'Halo',
'logout' => 'Keluar',
'login' => 'Masuk',
'register' => 'Daftar',
'admin' => 'Admin',
'footer_text' => 'Asisten AI TikTok Live. Dibuat dengan LAMP + OpenAI.',
'back_to_home' => 'Kembali ke Beranda',
'save_settings' => 'Simpan Semua Pengaturan',
'site_settings' => 'Pengaturan Situs',
'openai_key' => 'Kunci API OpenAI',
'openai_model' => 'Model OpenAI',
'head_scripts' => 'Skrip Head',
'body_scripts' => 'Skrip Body',
'site_name' => 'Nama Situs',
'site_icon' => 'Ikon Situs',
'site_favicon' => 'Favicon Situs',
'language' => 'Bahasa',
'personality' => 'Kepribadian AI',
'personality_funny' => 'Lucu',
'personality_serious' => 'Serius',
'personality_expert' => 'Pakar',
];

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>

111
tiktok_bridge.js Normal file
View File

@ -0,0 +1,111 @@
const { WebcastPushConnection } = require('tiktok-live-connector');
const mysql = require('mysql2/promise');
const [,, username, dbHost, dbUser, dbPass, dbName, userId] = process.argv;
if (!username) {
console.error('Usage: node tiktok_bridge.js <username> <dbHost> <dbUser> <dbPass> <dbName> <userId>');
process.exit(1);
}
async function start() {
console.log(`[${new Date().toISOString()}] TikTok Bridge starting for @${username} (User ID: ${userId})`);
let connection;
try {
connection = await mysql.createConnection({
host: dbHost || '127.0.0.1',
user: dbUser || 'root',
password: dbPass || '',
database: dbName || 'app_db'
});
console.log(`[${new Date().toISOString()}] Connected to database: ${dbName} at ${dbHost}`);
} catch (err) {
console.error(`[${new Date().toISOString()}] DB Connection failed:`, err.message);
process.exit(1);
}
// Create a new connection instance
let tiktokConnection = new WebcastPushConnection(username, {
processInitialData: false,
enableExtendedGiftInfo: true,
requestPollingIntervalMs: 2000
});
tiktokConnection.connect().then(state => {
console.log(`[${new Date().toISOString()}] Connected to TikTok Live @${username} (Room ID: ${state.roomId})`);
}).catch(err => {
console.error(`[${new Date().toISOString()}] TikTok Connection failed:`, err.message);
console.error('Check if the username is correct and the user is currently LIVE.');
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, user_id, created_at) VALUES (?, ?, ?, ?, NOW())',
[username, data.uniqueId, data.comment, userId || null]
);
} catch (err) {
console.error('Error inserting chat:', err.message);
}
});
tiktokConnection.on('gift', async (data) => {
if (data.giftType === 1 && !data.repeatEnd) {
// Wait for repeatEnd for streaks
return;
}
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, user_id, created_at) VALUES (?, ?, ?, ?, NOW())',
[username, data.uniqueId, giftMsg, userId || null]
);
} catch (err) {
console.error('Error inserting gift:', err.message);
}
});
tiktokConnection.on('member', (data) => {
console.log(`[Join] ${data.uniqueId} joined the stream`);
});
tiktokConnection.on('disconnected', () => {
console.log(`[${new Date().toISOString()}] TikTok connection disconnected`);
process.exit(0);
});
tiktokConnection.on('streamEnd', () => {
console.log(`[${new Date().toISOString()}] Stream ended by host`);
process.exit(0);
});
tiktokConnection.on('error', (err) => {
console.error(`[${new Date().toISOString()}] TikTok error:`, err.message);
});
// Keep DB connection alive
setInterval(async () => {
try {
await connection.query('SELECT 1');
} catch (err) {
console.log(`[${new Date().toISOString()}] DB connection lost, reconnecting...`);
try {
connection = await mysql.createConnection({
host: dbHost, user: dbUser, password: dbPass, database: dbName
});
} catch (reconnectErr) {
console.error('Reconnection failed:', reconnectErr.message);
}
}
}, 30000);
}
// Handle termination signals
process.on('SIGINT', () => process.exit(0));
process.on('SIGTERM', () => process.exit(0));
start();