<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
$db_file = "F:\\ratings_data\\stories.db";
$db = new PDO("sqlite:" . $db_file);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("PRAGMA foreign_keys = ON;");
$story_id = intval($_GET['story_id'] ?? 0);
// Verify story exists and fetch creator metadata
$stmt = $db->prepare("SELECT s.*, c.name AS creator_name FROM stories s JOIN creators c ON s.creator_id = c.id WHERE s.id = ?");
$stmt->execute([$story_id]);
$story = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$story) {
die("Timeline execution error: Invalid story instance ID requested.");
}
$creator_id = $story['creator_id'];
// SELF-HEALING ENGINE: Guarantee the global 'None' location placeholder exists
$db->exec("PRAGMA foreign_keys = OFF;");
$stmt = $db->prepare("INSERT OR IGNORE INTO locations (id, creator_id, name) VALUES (1, 0, 'None')");
$stmt->execute();
$db->exec("PRAGMA foreign_keys = ON;");
// NEW FEATURE: Automatically deploy an initial Scene 1 if the timeline is completely empty
$sceneCheck = $db->prepare("SELECT COUNT(*) FROM scenes WHERE story_id = ?");
$sceneCheck->execute([$story_id]);
if (intval($sceneCheck->fetchColumn()) === 0) {
$stmt = $db->prepare("INSERT INTO scenes (story_id, scene_number, title, location_id) VALUES (?, 1, 'Initial Scene', 1)");
$stmt->execute([$story_id]);
}
// AJAX API Processing Pipeline
if (isset($_GET['ajax_action'])) {
header('Content-Type: application/json');
$action = $_GET['ajax_action'];
if ($action === 'add_scene') {
$stmt = $db->prepare("SELECT COUNT(*) FROM scenes WHERE story_id = ?");
$stmt->execute([$story_id]);
$next_num = intval($stmt->fetchColumn()) + 1;
$stmt = $db->prepare("INSERT INTO scenes (story_id, scene_number, title, location_id) VALUES (?, ?, '', 1)");
$stmt->execute([$story_id, $next_num]);
echo json_encode(['status' => 'success']);
exit;
}
if ($action === 'update_scene_field') {
$scene_id = intval($_GET['scene_id']);
$field = preg_replace('/[^a-z_]/', '', $_GET['field']);
$value = $_GET['value'];
$stmt = $db->prepare("UPDATE scenes SET {$field} = ? WHERE id = ? AND story_id = ?");
$stmt->execute([$value, $scene_id, $story_id]);
echo json_encode(['status' => 'success']);
exit;
}
if ($action === 'add_scene_location') {
$value = trim($_GET['value']);
// Check or insert at Creator-level
$stmt = $db->prepare("INSERT OR IGNORE INTO locations (creator_id, name) VALUES (?, ?)");
$stmt->execute([$creator_id, $value]);
$stmt = $db->prepare("SELECT id FROM locations WHERE creator_id = ? AND name = ?");
$stmt->execute([$creator_id, $value]);
$loc_id = $stmt->fetchColumn();
echo json_encode(['status' => 'success', 'id' => $loc_id]);
exit;
}
if ($action === 'toggle_scene_character') {
$scene_id = intval($_GET['scene_id']);
$char_id = intval($_GET['char_id']);
$state = intval($_GET['state']); // 1 = add, 0 = remove
if ($state === 1) {
$stmt = $db->prepare("INSERT OR IGNORE INTO scene_characters (scene_id, character_id, character_status) VALUES (?, ?, '')");
$stmt->execute([$scene_id, $char_id]);
} else {
$stmt = $db->prepare("DELETE FROM scene_characters WHERE scene_id = ? AND character_id = ?");
$stmt->execute([$scene_id, $char_id]);
}
echo json_encode(['status' => 'success']);
exit;
}
if ($action === 'update_character_status') {
$scene_id = intval($_GET['scene_id']);
$char_id = intval($_GET['char_id']);
$status = $_GET['status'];
$stmt = $db->prepare("UPDATE scene_characters SET character_status = ? WHERE scene_id = ? AND character_id = ?");
$stmt->execute([$status, $scene_id, $char_id]);
echo json_encode(['status' => 'success']);
exit;
}
if ($action === 'add_relationship_shift') {
$scene_id = intval($_GET['scene_id']);
$c1 = intval($_GET['char1']);
$c2 = intval($_GET['char2']);
$type_id = intval($_GET['type_id']);
$stmt = $db->prepare("INSERT INTO scene_relationship_shifts (scene_id, character_id_1, character_id_2, relationship_type_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$scene_id, $c1, $c2, $type_id]);
echo json_encode(['status' => 'success']);
exit;
}
if ($action === 'delete_scene') {
$scene_id = intval($_GET['scene_id']);
$db->prepare("DELETE FROM scenes WHERE id = ? AND story_id = ?")->execute([$scene_id, $story_id]);
// Re-sequence remaining scenes numbers chronologically
$rem = $db->query("SELECT id FROM scenes WHERE story_id = {$story_id} ORDER BY scene_number ASC")->fetchAll(PDO::FETCH_COLUMN);
$stmt_u = $db->prepare("UPDATE scenes SET scene_number = ? WHERE id = ?");
foreach ($rem as $index => $sid) { $stmt_u->execute([$index + 1, $sid]); }
echo json_encode(['status' => 'success']);
exit;
}
}
// Fetch global locations + specific creator custom locations
$locations = $db->query("SELECT id, name FROM locations WHERE creator_id = 0 OR creator_id = {$creator_id} ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
$characters = $db->query("SELECT id, first_name, last_name, nickname FROM characters WHERE story_id = {$story_id} ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
$rel_types = $db->query("SELECT id, name FROM relationship_types ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Story Timeline & Scene Builder</title>
<style>
body { font-family: Arial, sans-serif; background: #f0f2f5; margin: 0; padding: 20px; color: #1e293b; }
.scene-card { background: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); padding: 20px; margin-bottom: 25px; border-left: 5px solid #0056b3; }
.flex-row { display: flex; gap: 15px; margin-bottom: 15px; flex-wrap: wrap; }
.input-box { display: flex; flex-direction: column; flex: 1; min-width: 200px; }
.input-box label { font-size: 12px; font-weight: bold; color: #475569; margin-bottom: 4px; }
.input-box input, .input-box select, textarea { padding: 8px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 14px; }
.badge-board { background: #f8fafc; border: 1px solid #e2e8f0; padding: 12px; border-radius: 6px; margin-bottom: 15px; }
.badge-btn { display: inline-block; padding: 6px 12px; border-radius: 20px; border: 1px solid #cbd5e1; background: white; font-size: 13px; font-weight: 600; cursor: pointer; margin-right: 8px; margin-bottom: 8px; }
.badge-btn.active { background: #2563eb; color: white; border-color: #2563eb; }
.status-box { display: inline-block; background: #f1f5f9; padding: 6px 10px; border-radius: 4px; border: 1px solid #e2e8f0; margin-top: 5px; margin-right: 10px; font-size: 12px; }
.btn-add-scene { background: #166534; color: white; padding: 12px 24px; border: none; border-radius: 4px; font-size: 16px; font-weight: bold; cursor: pointer; width: 100%; text-align: center; }
.btn-del-scene { background: #dc2626; color: white; border: none; padding: 5px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; float: right; margin-top: -5px; }
</style>
</head>
<body>
<?php include 'nav.php'; ?>
<div style="max-width: 1000px; margin: 0 auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>Chronological Timeline Layout: <?php echo htmlspecialchars($story['title']); ?></h2>
<a href="workspace.php?story_id=<?php echo $story_id; ?>" style="padding:8px 15px; background:white; color:#334155; border:1px solid #cbd5e1; border-radius:4px; font-weight:bold; text-decoration:none; font-size:13px;">← Back to Character Canvas</a>
</div>
<div id="scenesContainer">
<?php
$scenes = $db->query("SELECT * FROM scenes WHERE story_id = {$story_id} ORDER BY scene_number ASC")->fetchAll(PDO::FETCH_ASSOC);
foreach ($scenes as $s):
$sid = $s['id'];
// Fetch characters marked inside this scene
$sc_data = $db->query("SELECT character_id, character_status FROM scene_characters WHERE scene_id = {$sid}")->fetchAll(PDO::FETCH_KEY_PAIR);
// Fetch recorded relationship shifts inside this scene
$shifts = $db->query("SELECT s.*, c1.first_name as n1, c2.first_name as n2, t.name as tname
FROM scene_relationship_shifts s
JOIN characters c1 ON s.character_id_1 = c1.id
JOIN characters c2 ON s.character_id_2 = c2.id
JOIN relationship_types t ON s.relationship_type_id = t.id
WHERE s.scene_id = {$sid}")->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="scene-card" id="scene_block_<?php echo $sid; ?>">
<button class="btn-del-scene" onclick="deleteScene(<?php echo $sid; ?>)">Delete Scene</button>
<h3 style="margin-top:0; color:#0f172a;">Scene <?php echo $s['scene_number']; ?></h3>
<div class="flex-row">
<div class="input-box"><label>Scene Title / Hook</label><input type="text" value="<?php echo htmlspecialchars($s['title']); ?>" onblur="updateSceneField(<?php echo $sid; ?>, 'title', this.value)"></div>
<div class="input-box">
<label>Primary Location Anchor</label>
<select id="loc_sel_<?php echo $sid; ?>" onchange="handleLocationDropdown(<?php echo $sid; ?>, this)">
<?php foreach ($locations as $loc): ?>
<option value="<?php echo $loc['id']; ?>" <?php echo ($s['location_id'] == $loc['id']) ? 'selected' : ''; ?>><?php echo htmlspecialchars($loc['name']); ?></option>
<?php endforeach; ?>
<option value="custom_other">[ + Enter Custom Location... ]</option>
</select>
</div>
<div class="input-box"><label>Sub-Location / Room Context</label><input type="text" value="<?php echo htmlspecialchars($s['sub_location']); ?>" placeholder="e.g. Throne Room, Kitchen" onblur="updateSceneField(<?php echo $sid; ?>, 'sub_location', this.value)"></div>
</div>
<div class="badge-board">
<strong style="font-size:12px; display:block; color:#475569; margin-bottom:8px;">Click to Involve Cast Badges in this Scene:</strong>
<?php foreach ($characters as $char):
$cid = $char['id'];
$cName = trim($char['first_name'] . ' ' . $char['last_name']) ?: "Char #".$cid;
$isActive = isset($sc_data[$cid]);
?>
<button class="badge-btn <?php echo $isActive ? 'active' : ''; ?>" onclick="toggleCastBadge(<?php echo $sid; ?>, <?php echo $cid; ?>, this)">
<?php echo htmlspecialchars($cName); ?>
</button>
<?php endforeach; ?>
<div style="margin-top:10px;" id="status_fields_<?php echo $sid; ?>">
<?php foreach ($characters as $char):
if (!isset($sc_data[$char['id']])) continue;
$cid = $char['id'];
?>
<div class="status-box" id="stat_box_<?php echo $sid; ?>_<?php echo $cid; ?>">
<strong><?php echo htmlspecialchars($char['first_name'] ?: 'Char'); ?>:</strong>
<input type="text" style="padding:2px; font-size:11px; margin-left:5px; border:1px solid #ccc;" value="<?php echo htmlspecialchars($sc_data[$cid]); ?>" placeholder="Current state/status..." onblur="updateCharStatus(<?php echo $sid; ?>, <?php echo $cid; ?>, this.value)">
</div>
<?php endforeach; ?>
</div>
</div>
<div class="input-box" style="margin-bottom:15px;">
<label>Narrative Description / Prompt Context (KoboldCPP Prompt Block)</label>
<textarea rows="4" placeholder="Describe the interactions and what occurs in this scene chronologically..." onblur="updateSceneField(<?php echo $sid; ?>, 'description', this.value)"><?php echo htmlspecialchars($s['description']); ?></textarea>
</div>
<div style="font-size:13px; background:#f8fafc; padding:10px; border-radius:4px; border:1px solid #e2e8f0;">
<strong>Scene Relationship Shifts Logged:</strong>
<div id="shifts_log_<?php echo $sid; ?>" style="margin:5px 0; font-size:12px; color:#475569;">
<?php foreach ($shifts as $sh) { echo "• {$sh['n1']} modified link to {$sh['n2']} → <strong>{$sh['tname']}</strong><br>"; } ?>
</div>
<div style="display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; align-items:center;">
<select id="sf_1_<?php echo $sid; ?>"><?php foreach ($characters as $ch) { echo "<option value='{$ch['id']}'>{$ch['first_name']}</option>"; } ?></select>
<span>altered link to</span>
<select id="sf_2_<?php echo $sid; ?>"><?php foreach ($characters as $ch) { echo "<option value='{$ch['id']}'>{$ch['first_name']}</option>"; } ?></select>
<span>to</span>
<select id="sf_t_<?php echo $sid; ?>"><?php foreach ($rel_types as $rt) { echo "<option value='{$rt['id']}'>{$rt['name']}</option>"; } ?></select>
<button class="badge-btn" style="background:#2563eb; color:white; margin:0; padding:4px 10px;" onclick="logSceneShift(<?php echo $sid; ?>)">Log Shift</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<button class="btn-add-scene" onclick="addSceneRow()">+ Add Additional Story Scene Card</button>
</div>
<script>
const storyId = <?php echo $story_id; ?>;
function addSceneRow() {
fetch('scenes.php?story_id=' + storyId + '&ajax_action=add_scene')
.then(res => res.json()).then(data => { window.location.reload(); })
.catch(err => { window.location.reload(); });
}
function deleteScene(sid) {
if(confirm("Erase this scene card?")) {
fetch('scenes.php?story_id=' + storyId + '&ajax_action=delete_scene&scene_id=' + sid)
.then(() => window.location.reload());
}
}
function updateSceneField(sid, field, value) {
fetch('scenes.php?story_id=' + storyId + '&ajax_action=update_scene_field&scene_id=' + sid + '&field=' + field + '&value=' + encodeURIComponent(value));
}
function updateCharStatus(sid, cid, val) {
fetch('scenes.php?story_id=' + storyId + '&ajax_action=update_character_status&scene_id=' + sid + '&char_id=' + cid + '&status=' + encodeURIComponent(val));
}
function handleLocationDropdown(sid, selectEl) {
if (selectEl.value === 'custom_other') {
const val = prompt("Enter New Custom Macro Location Name (Saves globally to your profile):");
if (!val || !val.trim()) { selectEl.value = "1"; return; }
fetch('scenes.php?story_id=' + storyId + '&ajax_action=add_scene_location&value=' + encodeURIComponent(val.trim()))
.then(res => res.json()).then(data => { if(data.status === 'success') window.location.reload(); });
} else {
updateSceneField(sid, 'location_id', selectEl.value);
}
}
function toggleCastBadge(sid, cid, btn) {
const isAdding = !btn.classList.contains('active');
fetch('scenes.php?story_id=' + storyId + '&ajax_action=toggle_scene_character&scene_id=' + sid + '&char_id=' + cid + '&state=' + (isAdding ? 1 : 0))
.then(() => { btn.classList.toggle('active'); window.location.reload(); });
}
function logSceneShift(sid) {
const c1 = document.getElementById('sf_1_' + sid).value;
const c2 = document.getElementById('sf_2_' + sid).value;
const t = document.getElementById('sf_t_' + sid).value;
fetch('scenes.php?story_id=' + storyId + '&ajax_action=add_relationship_shift&scene_id=' + sid + '&char1=' + c1 + '&char2=' + c2 + '&type_id=' + t)
.then(() => window.location.reload());
}
</script>
</body>
</html>
scenes