Good Versi

This commit is contained in:
Flatlogic Bot 2026-02-26 19:50:11 +00:00
parent 62bc71e2f3
commit 3f421e9a53
17 changed files with 706 additions and 301 deletions

137
admin.php
View File

@ -2,6 +2,7 @@
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'])) {
@ -27,15 +28,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$head_ads = $_POST['head_ads'] ?? '';
$body_ads = $_POST['body_ads'] ?? '';
$openai_api_key = $_POST['openai_api_key'] ?? '';
$site_name = $_POST['site_name'] ?? 'TikTok Live AI Assistant';
$default_language = $_POST['default_language'] ?? 'en';
$stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'head_ads'");
$stmt->execute([$head_ads]);
// Handle File Uploads
$upload_dir = 'assets/images/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0775, true);
}
$stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'body_ads'");
$stmt->execute([$body_ads]);
$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;
}
}
$stmt = $pdo->prepare("UPDATE site_settings SET setting_value = ? WHERE setting_key = 'openai_api_key'");
$stmt->execute([$openai_api_key]);
$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,
'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!";
}
@ -46,21 +77,26 @@ $settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$head_ads = $settings['head_ads'] ?? '';
$body_ads = $settings['body_ads'] ?? '';
$openai_api_key = $settings['openai_api_key'] ?? '';
$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="en">
<html lang="<?= get_lang() ?>">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin Panel - TikTok Live AI</title>
<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 { 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); }
.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; }
@ -74,48 +110,80 @@ $openai_api_key = $settings['openai_api_key'] ?? '';
<span class="text-tiktok-cyan">TikTok</span> Live Admin
</a>
<div class="d-flex align-items-center">
<span class="text-secondary small me-3">Logged in as <?= htmlspecialchars($_SESSION['username']) ?></span>
<a href="/" class="btn btn-outline-light btn-sm me-2">Back to Home</a>
<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>
<button type="submit" name="logout" class="btn btn-danger btn-sm"><?= __('logout') ?></button>
</form>
</div>
</div>
</nav>
<main class="container">
<main class="container mb-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="col-lg-10">
<div class="card-admin p-4 shadow-lg">
<h2 class="mb-4 fw-bold">Site Settings</h2>
<h2 class="mb-4 fw-bold"><?= __('site_settings') ?></h2>
<?php if ($message): ?>
<div class="alert alert-success bg-dark text-success border-success"><?= $message ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-4">
<label for="openai_api_key" class="form-label fw-semibold text-secondary small">OpenAI API Key</label>
<input type="password" name="openai_api_key" id="openai_api_key" class="form-control" placeholder="sk-..." value="<?= htmlspecialchars($openai_api_key) ?>">
<div class="form-text text-muted">If left empty, the application will use the Flatlogic AI Proxy.</div>
</div>
<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>
<hr class="border-secondary my-4">
<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="mb-4">
<label for="head_ads" class="form-label fw-semibold text-secondary small">Head Scripts (for JS Ads, Meta tags, etc.)</label>
<textarea name="head_ads" id="head_ads" rows="4" class="form-control" placeholder="Paste your <head> scripts here..."><?= htmlspecialchars($head_ads) ?></textarea>
<div class="form-text text-muted">These scripts will be placed inside the &lt;head&gt; tag.</div>
</div>
<div class="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="mb-4">
<label for="body_ads" class="form-label fw-semibold text-secondary small">Body Scripts (for floating ads, analytics, etc.)</label>
<textarea name="body_ads" id="body_ads" rows="4" class="form-control" placeholder="Paste your <body> scripts here..."><?= htmlspecialchars($body_ads) ?></textarea>
<div class="form-text text-muted">These scripts will be placed at the bottom of the &lt;body&gt; tag.</div>
<div class="col-12">
<hr class="border-secondary my-2">
</div>
<div class="col-12">
<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="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 All Settings</button>
<button type="submit" class="btn btn-save"><?= __('save_settings') ?></button>
</div>
</form>
</div>
@ -123,5 +191,6 @@ $openai_api_key = $settings['openai_api_key'] ?? '';
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>

View File

@ -1,7 +1,15 @@
<?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'] ?? '';
@ -12,29 +20,29 @@ if (empty($username)) {
if ($action === 'start') {
// 1. Kill any existing bridge for this user
shell_exec("pkill -f \"node tiktok_bridge.js $username\"");
shell_exec("pkill -f \"node tiktok_bridge.js $username $user_id\"");
// 2. Start new bridge
$cmd = sprintf(
"node %s %s %s %s %s %s > /dev/null 2>&1 &",
"node %s %s %s %s %s %s %s > /dev/null 2>&1 &",
escapeshellarg(__DIR__ . '/../tiktok_bridge.js'),
escapeshellarg($username),
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME)
escapeshellarg(DB_NAME),
escapeshellarg($user_id)
);
shell_exec($cmd);
echo json_encode(['success' => true, 'message' => "Bridge started for @$username"]);
} elseif ($action === 'stop') {
shell_exec("pkill -f \"node tiktok_bridge.js $username\"");
shell_exec("pkill -f \"node tiktok_bridge.js $username $user_id\"");
echo json_encode(['success' => true, 'message' => "Bridge stopped for @$username"]);
} elseif ($action === 'status') {
$output = shell_exec("pgrep -f \"node tiktok_bridge.js $username\"");
$output = shell_exec("pgrep -f \"node tiktok_bridge.js $username $user_id\"");
echo json_encode(['success' => true, 'running' => !empty($output)]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}
}

View File

@ -1,18 +1,27 @@
<?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)
$stmt = db()->prepare("SELECT * FROM tiktok_history WHERE username = ? AND ai_reply IS NULL ORDER BY created_at ASC LIMIT 5");
$stmt->execute([$username]);
// 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 = [];
@ -55,11 +64,10 @@ try {
];
}
// 4. Also fetch recently processed comments (last 10 seconds)
// To ensure the frontend shows them even if they were processed by another call
// 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 ai_reply IS NOT NULL AND created_at >= ? ORDER BY created_at DESC LIMIT 10");
$stmt->execute([$username, $since]);
$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
@ -68,4 +76,4 @@ try {
} catch (Exception $e) {
error_log("Update Error: " . $e->getMessage());
echo json_encode(['error' => $e->getMessage(), 'events' => []]);
}
}

View File

@ -1,8 +1,16 @@
<?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'])) {
@ -14,16 +22,40 @@ $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 and funny AI assistant for a TikTok live stream.
$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, engaging, and friendly way (max 20 words).
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'],
['role' => 'system', 'content' => "TikTok Live AI Assistant - Personality: $personality"],
['role' => 'user', 'content' => $prompt],
],
]);
@ -37,8 +69,8 @@ if (!empty($resp['success'])) {
// Persist to DB
try {
$stmt = db()->prepare("INSERT INTO tiktok_history (username, comment_author, comment_text, ai_reply) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $comment_author, $comment_text, $ai_reply]);
$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
}
@ -48,4 +80,4 @@ echo json_encode([
'reply' => $ai_reply,
'author' => $comment_author,
'comment' => $comment_text
]);
]);

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

@ -4,8 +4,9 @@
--bg-dark: #121212;
--bg-surface: #1e1e1e;
--border-secondary: rgba(255, 255, 255, 0.1);
--text-light: #f8f9fa;
--text-secondary: #adb5bd;
--text-light: #ffffff;
--text-secondary: #ced4da; /* Brighter than adb5bd */
--text-muted: #adb5bd;
}
body {
@ -15,6 +16,10 @@ body {
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6, .card-title, .navbar-brand {
color: var(--text-light) !important;
}
.bg-surface {
background-color: var(--bg-surface);
}
@ -27,6 +32,15 @@ body {
color: var(--tiktok-red) !important;
}
.text-secondary {
color: var(--text-secondary) !important;
}
.small.text-secondary {
color: var(--text-secondary) !important;
opacity: 1 !important;
}
.btn-tiktok-cyan {
background-color: var(--tiktok-cyan);
color: #000;
@ -125,13 +139,21 @@ body {
.form-control, .form-select {
border-radius: 8px;
padding: 0.6rem 0.75rem;
background-color: #262626 !important;
border-color: #444 !important;
color: #fff !important;
}
.form-control::placeholder {
color: #888 !important;
opacity: 1;
}
.form-control:focus, .form-select:focus {
background-color: #1a1a1a;
border-color: var(--tiktok-cyan);
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;
color: #fff !important;
}
.toast {
@ -238,3 +260,19 @@ body {
.admin-link:hover {
color: var(--tiktok-cyan);
}
/* Table readability */
.table {
color: var(--text-light) !important;
}
.table thead th {
color: var(--text-secondary) !important;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.text-muted {
color: var(--text-muted) !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 466 KiB

View File

@ -1,5 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
const connectBtn = document.getElementById('connectBtn');
if (!connectBtn) return; // Exit if not logged in
const disconnectBtn = document.getElementById('disconnectBtn');
const tiktokUsernameInput = document.getElementById('tiktokUsername');
const connectionStatus = document.getElementById('connectionStatus');
@ -14,6 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
const historyTableBody = document.getElementById('historyTableBody');
const commentCountBadge = document.getElementById('commentCount');
const toastContainer = document.getElementById('toastContainer');
const aiPersonality = document.getElementById('aiPersonality');
let isConnected = false;
let commentCount = 0;
@ -24,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Load voices
function populateVoiceList() {
if (!synth) return;
voices = synth.getVoices().sort(function (a, b) {
const aname = a.name.toUpperCase();
const bname = b.name.toUpperCase();
@ -31,15 +35,17 @@ document.addEventListener('DOMContentLoaded', () => {
else if (aname > bname) return 1;
return 0;
});
voiceSelect.innerHTML = '';
voices.forEach((voice, i) => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
if (voice.default) option.textContent += ' -- DEFAULT';
option.setAttribute('data-lang', voice.lang);
option.setAttribute('data-name', voice.name);
voiceSelect.appendChild(option);
});
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);
});
}
}
populateVoiceList();
@ -47,9 +53,32 @@ document.addEventListener('DOMContentLoaded', () => {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
rateRange.addEventListener('input', () => {
rateValue.textContent = rateRange.value;
});
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 {
@ -58,9 +87,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.success) {
isConnected = true;
connectBtn.classList.add('d-none');
disconnectBtn.classList.remove('d-none');
connectionStatus.innerHTML = '<span class="status-dot bg-success me-1 pulse"></span> Live connection active: @' + username;
emptyFeed.classList.add('d-none');
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);
@ -77,8 +106,8 @@ document.addEventListener('DOMContentLoaded', () => {
await fetch(`api/bridge_control.php?action=stop&username=${username}`);
isConnected = false;
connectBtn.classList.remove('d-none');
disconnectBtn.classList.add('d-none');
connectionStatus.innerHTML = '<span class="status-dot bg-secondary me-1"></span> Disconnected';
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);
@ -114,7 +143,7 @@ document.addEventListener('DOMContentLoaded', () => {
function addEventToFeed(event) {
commentCount++;
commentCountBadge.textContent = `${commentCount} events`;
if (commentCountBadge) commentCountBadge.textContent = `${commentCount} events`;
const isSystem = event.comment.includes('sent') && event.comment.includes('x');
@ -131,7 +160,7 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="ai-reply-box mt-1">AI: ${event.reply}</div>
`;
commentFeed.prepend(commentItem);
if (commentFeed) commentFeed.prepend(commentItem);
// TTS
speak(event.reply);
@ -149,35 +178,39 @@ document.addEventListener('DOMContentLoaded', () => {
startBridge(username);
});
disconnectBtn.addEventListener('click', () => {
const username = tiktokUsernameInput.value.trim();
stopBridge(username);
});
if (disconnectBtn) {
disconnectBtn.addEventListener('click', () => {
const username = tiktokUsernameInput.value.trim();
stopBridge(username);
});
}
simulateBtn.addEventListener('click', async () => {
const commentText = manualCommentInput.value.trim();
const username = tiktokUsernameInput.value.trim();
if (!commentText || !username) return;
try {
// Manually insert into DB to test
await fetch('api/process_comment.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ author: 'TestUser', comment: commentText, username })
});
manualCommentInput.value = '';
// Polling will pick it up
} catch (err) {
console.error(err);
}
});
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) return;
if (!text || !synth) return;
const utterThis = new SpeechSynthesisUtterance(text);
const selectedOption = voiceSelect.selectedOptions[0]?.getAttribute('data-name');
const selectedOption = voiceSelect?.selectedOptions[0]?.getAttribute('data-name');
if (selectedOption) {
for (let i = 0; i < voices.length; i++) {
if (voices[i].name === selectedOption) {
@ -186,11 +219,12 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
utterThis.pitch = 1;
utterThis.rate = rateRange.value;
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' });
@ -214,6 +248,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
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">
@ -229,11 +264,13 @@ document.addEventListener('DOMContentLoaded', () => {
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
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

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,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;

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;
}

408
index.php
View File

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

43
languages/en.php Normal file
View File

@ -0,0 +1,43 @@
<?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',
'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;
}

43
languages/id.php Normal file
View File

@ -0,0 +1,43 @@
<?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',
'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',
];

View File

@ -1,15 +1,15 @@
const { WebcastPushConnection } = require('tiktok-live-connector');
const mysql = require('mysql2/promise');
const [,, username, dbHost, dbUser, dbPass, dbName] = process.argv;
const [,, username, dbHost, dbUser, dbPass, dbName, userId] = process.argv;
if (!username) {
console.error('Usage: node tiktok_bridge.js <username> <dbHost> <dbUser> <dbPass> <dbName>');
console.error('Usage: node tiktok_bridge.js <username> <dbHost> <dbUser> <dbPass> <dbName> <userId>');
process.exit(1);
}
async function start() {
console.log(`TikTok Bridge starting for @${username}`);
console.log(`TikTok Bridge starting for @${username} (User ID: ${userId})`);
let connection;
try {
@ -38,8 +38,8 @@ async function start() {
console.log(`[Chat] ${data.uniqueId}: ${data.comment}`);
try {
await connection.execute(
'INSERT INTO tiktok_history (username, comment_author, comment_text, created_at) VALUES (?, ?, ?, NOW())',
[username, data.uniqueId, data.comment]
'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);
@ -51,8 +51,8 @@ async function start() {
console.log(`[Gift] ${data.uniqueId}: ${giftMsg}`);
try {
await connection.execute(
'INSERT INTO tiktok_history (username, comment_author, comment_text, created_at) VALUES (?, ?, ?, NOW())',
[username, data.uniqueId, giftMsg]
'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);
@ -81,4 +81,4 @@ async function start() {
}, 30000);
}
start();
start();