first commit
This commit is contained in:
110
public_web/templates/dev/index.html
Normal file
110
public_web/templates/dev/index.html
Normal file
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dev Tools - Code of Conquest{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.dev-banner {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dev-container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.dev-section {
|
||||
background: rgba(30, 30, 40, 0.9);
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dev-section h2 {
|
||||
color: #f59e0b;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #4a4a5a;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dev-link {
|
||||
display: block;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dev-link:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.dev-link-disabled {
|
||||
background: #4a4a5a;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dev-link small {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dev-banner">
|
||||
DEV MODE - Testing Tools (Not available in production)
|
||||
</div>
|
||||
|
||||
<div class="dev-container">
|
||||
<h1>Development Testing Tools</h1>
|
||||
|
||||
<div class="dev-section">
|
||||
<h2>Story System</h2>
|
||||
<a href="{{ url_for('dev.story_hub') }}" class="dev-link">
|
||||
Story Gameplay Tester
|
||||
<small>Create sessions, test actions, view AI responses</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="dev-section">
|
||||
<h2>Quest System</h2>
|
||||
<span class="dev-link dev-link-disabled">
|
||||
Quest Tester (Coming Soon)
|
||||
<small>Test quest offering, acceptance, and completion</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="dev-section">
|
||||
<h2>API Debug</h2>
|
||||
<span class="dev-link dev-link-disabled">
|
||||
API Inspector (Coming Soon)
|
||||
<small>View raw API requests and responses</small>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="dev-section">
|
||||
<h2>Quick Links</h2>
|
||||
<p style="color: #9ca3af; margin: 0;">
|
||||
<strong>API Docs:</strong> <a href="http://localhost:5000/api/v1/docs" target="_blank" style="color: #60a5fa;">localhost:5000/api/v1/docs</a><br>
|
||||
<strong>Characters:</strong> <a href="{{ url_for('character_views.list_characters') }}" style="color: #60a5fa;">Character List</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
public_web/templates/dev/partials/dm_response.html
Normal file
27
public_web/templates/dev/partials/dm_response.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{# DM response after job completes #}
|
||||
<div class="dm-response-content">
|
||||
{{ dm_response | safe }}
|
||||
</div>
|
||||
|
||||
{# Debug info #}
|
||||
<div class="debug-panel" style="margin-top: 1rem; padding: 0.5rem; font-size: 0.7rem;">
|
||||
<div class="debug-toggle" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">
|
||||
[+] Raw API Response
|
||||
</div>
|
||||
<div style="display: none; margin-top: 0.5rem; white-space: pre; color: #a3e635; max-height: 150px; overflow: auto;">
|
||||
{{ raw_result | tojson(indent=2) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Trigger state and history refresh #}
|
||||
<div hx-get="{{ url_for('dev.get_state', session_id=session_id) }}"
|
||||
hx-trigger="load"
|
||||
hx-target="#state-content"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none;"></div>
|
||||
|
||||
<div hx-get="{{ url_for('dev.get_history', session_id=session_id) }}"
|
||||
hx-trigger="load"
|
||||
hx-target="#history-content"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none;"></div>
|
||||
21
public_web/templates/dev/partials/history.html
Normal file
21
public_web/templates/dev/partials/history.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{# History entries partial #}
|
||||
{% if history %}
|
||||
{% for entry in history|reverse %}
|
||||
<div class="history-entry">
|
||||
<div class="history-turn">Turn {{ entry.turn }}</div>
|
||||
<div class="history-action">{{ entry.action }}</div>
|
||||
<div class="history-response">{{ entry.dm_response[:150] }}{% if entry.dm_response|length > 150 %}...{% endif %}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination and pagination.has_more %}
|
||||
<button hx-get="{{ url_for('dev.get_history', session_id=session_id) }}?offset={{ pagination.offset + pagination.limit }}"
|
||||
hx-target="#history-content"
|
||||
hx-swap="innerHTML"
|
||||
style="width: 100%; padding: 0.5rem; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem;">
|
||||
Load More
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p style="color: #9ca3af; text-align: center; font-size: 0.85rem;">No history yet.</p>
|
||||
{% endif %}
|
||||
29
public_web/templates/dev/partials/job_status.html
Normal file
29
public_web/templates/dev/partials/job_status.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{# Job status polling partial - polls every 2 seconds until complete #}
|
||||
<div class="loading"
|
||||
hx-get="{{ url_for('dev.job_status', job_id=job_id) }}?session_id={{ session_id }}"
|
||||
hx-trigger="load delay:2s"
|
||||
hx-swap="outerHTML">
|
||||
<div style="margin-bottom: 0.5rem;">
|
||||
<span class="spinner"></span>
|
||||
Processing your action...
|
||||
</div>
|
||||
<div style="font-size: 0.75rem; color: #9ca3af;">
|
||||
Job: {{ job_id[:8] }}... | Status: {{ status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #4a4a5a;
|
||||
border-top-color: #60a5fa;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
129
public_web/templates/dev/partials/npc_dialogue.html
Normal file
129
public_web/templates/dev/partials/npc_dialogue.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{#
|
||||
NPC Dialogue partial - displays conversation history with current exchange.
|
||||
|
||||
Expected context:
|
||||
- npc_name: Name of the NPC
|
||||
- character_name: Name of the player character
|
||||
- conversation_history: List of previous exchanges [{player_line, npc_response}, ...]
|
||||
- player_line: What the player just said
|
||||
- dialogue: NPC's current response
|
||||
- session_id: For any follow-up actions
|
||||
#}
|
||||
|
||||
<div class="npc-conversation">
|
||||
<div class="npc-conversation-header">
|
||||
<span class="npc-conversation-title">Conversation with {{ npc_name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="npc-conversation-history">
|
||||
{# Show previous exchanges #}
|
||||
{% if conversation_history %}
|
||||
{% for exchange in conversation_history %}
|
||||
<div class="dialogue-exchange dialogue-exchange-past">
|
||||
<div class="dialogue-player">
|
||||
<span class="dialogue-speaker">{{ character_name }}:</span>
|
||||
<span class="dialogue-text">{{ exchange.player_line }}</span>
|
||||
</div>
|
||||
<div class="dialogue-npc">
|
||||
<span class="dialogue-speaker">{{ npc_name }}:</span>
|
||||
<span class="dialogue-text">{{ exchange.npc_response }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# Show current exchange (highlighted) #}
|
||||
<div class="dialogue-exchange dialogue-exchange-current">
|
||||
<div class="dialogue-player">
|
||||
<span class="dialogue-speaker">{{ character_name }}:</span>
|
||||
<span class="dialogue-text">{{ player_line }}</span>
|
||||
</div>
|
||||
<div class="dialogue-npc">
|
||||
<span class="dialogue-speaker">{{ npc_name }}:</span>
|
||||
<span class="dialogue-text">{{ dialogue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.npc-conversation {
|
||||
background: #1a1a2a;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.npc-conversation-header {
|
||||
background: #2a2a3a;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #4a4a5a;
|
||||
}
|
||||
|
||||
.npc-conversation-title {
|
||||
color: #f59e0b;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.npc-conversation-history {
|
||||
padding: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialogue-exchange {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #3a3a4a;
|
||||
}
|
||||
|
||||
.dialogue-exchange:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dialogue-exchange-past {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.dialogue-exchange-current {
|
||||
background: #2a2a3a;
|
||||
margin: 0 -1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialogue-player {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dialogue-npc {
|
||||
padding-left: 0.5rem;
|
||||
border-left: 2px solid #f59e0b;
|
||||
}
|
||||
|
||||
.dialogue-speaker {
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.dialogue-player .dialogue-speaker {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dialogue-npc .dialogue-speaker {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.dialogue-text {
|
||||
color: #e5e7eb;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.dialogue-exchange-past .dialogue-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
32
public_web/templates/dev/partials/session_state.html
Normal file
32
public_web/templates/dev/partials/session_state.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{# Session state partial #}
|
||||
<div class="state-item">
|
||||
<div class="state-label">Session ID</div>
|
||||
<div class="state-value">{{ session_id[:12] }}...</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Turn</div>
|
||||
<div class="state-value">{{ session.turn_number }}</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Location</div>
|
||||
<div class="state-value">{{ session.game_state.current_location }}</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Type</div>
|
||||
<div class="state-value">{{ session.game_state.location_type }}</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Active Quests</div>
|
||||
<div class="state-value">{{ session.game_state.active_quests|length }}</div>
|
||||
</div>
|
||||
|
||||
{% if session.game_state.active_quests %}
|
||||
<div class="state-item" style="margin-top: 0.5rem;">
|
||||
<div class="state-label">Quest IDs</div>
|
||||
<div class="state-value" style="font-size: 0.75rem;">
|
||||
{% for quest_id in session.game_state.active_quests %}
|
||||
{{ quest_id[:10] }}...{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
31
public_web/templates/dev/partials/travel_modal.html
Normal file
31
public_web/templates/dev/partials/travel_modal.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{# Travel Modal Partial - displays available travel destinations #}
|
||||
<div class="modal-overlay" onclick="this.remove()">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<h3>Travel to...</h3>
|
||||
|
||||
{% if locations %}
|
||||
{% for location in locations %}
|
||||
<button class="location-btn"
|
||||
hx-post="{{ url_for('dev.do_travel', session_id=session_id) }}"
|
||||
hx-vals='{"location_id": "{{ location.location_id }}"}'
|
||||
hx-target="#dm-response"
|
||||
hx-swap="innerHTML">
|
||||
<div class="location-name">{{ location.name }}</div>
|
||||
<div class="location-type">{{ location.location_type | default('Unknown') }}</div>
|
||||
{% if location.description %}
|
||||
<div class="location-desc" style="font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;">
|
||||
{{ location.description[:80] }}{% if location.description|length > 80 %}...{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: #9ca3af; text-align: center;">No locations available to travel to.</p>
|
||||
<p style="color: #6b7280; font-size: 0.85rem; text-align: center;">
|
||||
Discover new locations by exploring and talking to NPCs.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
199
public_web/templates/dev/story.html
Normal file
199
public_web/templates/dev/story.html
Normal file
@@ -0,0 +1,199 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Story Tester - Dev Tools{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.dev-banner {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.story-container {
|
||||
max-width: 900px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.story-section {
|
||||
background: rgba(30, 30, 40, 0.9);
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.story-section h2 {
|
||||
color: #f59e0b;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #4a4a5a;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.character-select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #2a2a3a;
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-create {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-create:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-create:disabled {
|
||||
background: #4a4a5a;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #2a2a3a;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.session-item a {
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.session-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.session-meta {
|
||||
color: #9ca3af;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fecaca;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.no-characters {
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.no-characters a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
#create-result {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #065f46;
|
||||
color: #a7f3d0;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dev-banner">
|
||||
DEV MODE - Story Gameplay Tester
|
||||
</div>
|
||||
|
||||
<div class="story-container">
|
||||
<h1>Story System Tester</h1>
|
||||
<p style="color: #9ca3af;"><a href="{{ url_for('dev.index') }}" style="color: #60a5fa;">← Back to Dev Tools</a></p>
|
||||
|
||||
{% if error %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="story-section">
|
||||
<h2>Create New Session</h2>
|
||||
|
||||
{% if characters %}
|
||||
<form hx-post="{{ url_for('dev.create_session') }}"
|
||||
hx-target="#create-result"
|
||||
hx-swap="innerHTML">
|
||||
<select name="character_id" class="character-select" required>
|
||||
<option value="">-- Select a Character --</option>
|
||||
{% for char in characters %}
|
||||
<option value="{{ char.character_id }}">
|
||||
{{ char.name }} ({{ char.class_name }} Lvl {{ char.level }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn-create">
|
||||
Create Story Session
|
||||
</button>
|
||||
</form>
|
||||
<div id="create-result"></div>
|
||||
{% else %}
|
||||
<div class="no-characters">
|
||||
<p>No characters found. <a href="{{ url_for('character_views.create_origin') }}">Create a character</a> first.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="story-section">
|
||||
<h2>Existing Sessions</h2>
|
||||
|
||||
{% if sessions %}
|
||||
<ul class="session-list">
|
||||
{% for session in sessions %}
|
||||
<li class="session-item">
|
||||
<div>
|
||||
<a href="{{ url_for('dev.story_session', session_id=session.session_id) }}">
|
||||
Session {{ session.session_id[:8] }}...
|
||||
</a>
|
||||
<div class="session-meta">
|
||||
Turn {{ session.turn_number }} | {{ session.game_state.current_location }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="session-meta">
|
||||
{{ session.character_id[:8] }}...
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p style="color: #9ca3af; text-align: center;">No active sessions. Create one above.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
669
public_web/templates/dev/story_session.html
Normal file
669
public_web/templates/dev/story_session.html
Normal file
@@ -0,0 +1,669 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Story Session - Dev Tools{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.dev-banner {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.session-container {
|
||||
max-width: 1200px;
|
||||
margin: 1rem auto;
|
||||
padding: 0 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 300px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.session-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(30, 30, 40, 0.9);
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel h3 {
|
||||
color: #f59e0b;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
border-bottom: 1px solid #4a4a5a;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Left sidebar - State */
|
||||
.state-panel {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.state-item {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.state-label {
|
||||
color: #9ca3af;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.state-value {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Main area */
|
||||
.main-panel {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
#dm-response {
|
||||
background: #1a1a2a;
|
||||
border-radius: 6px;
|
||||
padding: 1.5rem;
|
||||
min-height: 200px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
background: #4a4a5a;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.action-btn.action-premium {
|
||||
background: #8b5cf6;
|
||||
}
|
||||
|
||||
.action-btn.action-premium:hover {
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
.action-btn.action-elite {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.action-btn.action-elite:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
/* Right sidebar - History */
|
||||
.history-panel {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-entry {
|
||||
padding: 0.75rem;
|
||||
background: #2a2a3a;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.history-turn {
|
||||
color: #f59e0b;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.history-action {
|
||||
color: #60a5fa;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.history-response {
|
||||
color: #d1d5db;
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Debug panel */
|
||||
.debug-panel {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #1a1a2a;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.debug-toggle {
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
margin-top: 0.5rem;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
color: #a3e635;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fecaca;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* NPC Sidebar */
|
||||
.npc-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #4a4a5a;
|
||||
}
|
||||
|
||||
.npc-section h4 {
|
||||
color: #f59e0b;
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.npc-card {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
margin: 0.25rem 0;
|
||||
background: #2a2a3a;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.npc-card:hover {
|
||||
background: #3a3a4a;
|
||||
border-color: #5a5a6a;
|
||||
}
|
||||
|
||||
.npc-name {
|
||||
font-weight: 500;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.npc-role {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.npc-empty {
|
||||
color: #6b7280;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Travel Section */
|
||||
.travel-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #4a4a5a;
|
||||
}
|
||||
|
||||
.btn-travel {
|
||||
width: 100%;
|
||||
background: #059669;
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-travel:hover {
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1a1a2a;
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
color: #f59e0b;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.location-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
background: #2a2a3a;
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.location-btn:hover {
|
||||
background: #3a3a4a;
|
||||
border-color: #5a5a6a;
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-weight: 500;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.location-type {
|
||||
font-size: 0.8rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #6b7280;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* NPC Dialogue Result */
|
||||
.npc-dialogue {
|
||||
background: #2a2a3a;
|
||||
border-left: 3px solid #f59e0b;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.npc-dialogue-header {
|
||||
color: #f59e0b;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* NPC Chat Form Styles */
|
||||
.npc-card-wrapper {
|
||||
margin: 0.25rem 0;
|
||||
background: #2a2a3a;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.npc-card-wrapper:hover {
|
||||
background: #3a3a4a;
|
||||
border-color: #5a5a6a;
|
||||
}
|
||||
|
||||
.npc-card-header {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.npc-chat-form {
|
||||
padding: 0.5rem;
|
||||
padding-top: 0;
|
||||
border-top: 1px solid #4a4a5a;
|
||||
}
|
||||
|
||||
.npc-chat-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #1a1a2a;
|
||||
border: 1px solid #4a4a5a;
|
||||
border-radius: 4px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.npc-chat-input:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.npc-chat-input::placeholder {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.npc-chat-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-npc-send {
|
||||
flex: 1;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: #f59e0b;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #1a1a2a;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-npc-send:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.btn-npc-send:disabled {
|
||||
background: #4a4a5a;
|
||||
color: #9ca3af;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.btn-npc-greet {
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: #3b3b4b;
|
||||
border: 1px solid #5a5a6a;
|
||||
border-radius: 4px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-npc-greet:hover {
|
||||
background: #4b4b5b;
|
||||
border-color: #6a6a7a;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="dev-banner">
|
||||
DEV MODE - Session {{ session_id[:8] }}...
|
||||
</div>
|
||||
|
||||
<div class="session-container">
|
||||
<!-- Left sidebar: State -->
|
||||
<div class="panel state-panel">
|
||||
<h3>
|
||||
Session State
|
||||
<button class="btn-refresh"
|
||||
hx-get="{{ url_for('dev.get_state', session_id=session_id) }}"
|
||||
hx-target="#state-content"
|
||||
hx-swap="innerHTML">
|
||||
Refresh
|
||||
</button>
|
||||
</h3>
|
||||
<div id="state-content">
|
||||
<div class="state-item">
|
||||
<div class="state-label">Session ID</div>
|
||||
<div class="state-value">{{ session_id[:12] }}...</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Turn</div>
|
||||
<div class="state-value">{{ session.turn_number }}</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Location</div>
|
||||
<div class="state-value">{{ session.game_state.current_location }}</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Type</div>
|
||||
<div class="state-value">{{ session.game_state.location_type }}</div>
|
||||
</div>
|
||||
<div class="state-item">
|
||||
<div class="state-label">Active Quests</div>
|
||||
<div class="state-value">{{ session.game_state.active_quests|length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NPC Section -->
|
||||
<div class="npc-section">
|
||||
<h4>NPCs Here</h4>
|
||||
<div id="npc-list">
|
||||
{% if npcs_present %}
|
||||
{% for npc in npcs_present %}
|
||||
<div class="npc-card-wrapper">
|
||||
<div class="npc-card-header"
|
||||
onclick="toggleNpcChat('{{ npc.npc_id }}')">
|
||||
<div class="npc-name">{{ npc.name }}</div>
|
||||
<div class="npc-role">{{ npc.role }}</div>
|
||||
</div>
|
||||
<div class="npc-chat-form" id="npc-chat-{{ npc.npc_id }}" style="display: none;">
|
||||
<form hx-post="{{ url_for('dev.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-target="#dm-response"
|
||||
hx-swap="innerHTML">
|
||||
<input type="text"
|
||||
name="player_response"
|
||||
placeholder="Say something..."
|
||||
class="npc-chat-input"
|
||||
maxlength="500"
|
||||
autocomplete="off">
|
||||
<div class="npc-chat-buttons">
|
||||
<button type="submit" class="btn-npc-send" hx-disabled-elt="this">Send</button>
|
||||
<button type="button"
|
||||
class="btn-npc-greet"
|
||||
hx-post="{{ url_for('dev.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-vals='{"topic": "greeting"}'
|
||||
hx-target="#dm-response"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this">Greet</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="npc-empty">No one here to talk to.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Travel Section -->
|
||||
<div class="travel-section">
|
||||
<button class="btn-travel"
|
||||
hx-get="{{ url_for('dev.travel_modal', session_id=session_id) }}"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML">
|
||||
Travel to...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #4a4a5a;">
|
||||
<a href="{{ url_for('dev.story_hub') }}" style="color: #60a5fa; font-size: 0.85rem;">← Back to Sessions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main area: DM Response & Actions -->
|
||||
<div class="panel main-panel">
|
||||
<h3>Story Gameplay</h3>
|
||||
|
||||
<!-- DM Response Area -->
|
||||
<div id="dm-response">
|
||||
{% if history %}
|
||||
{{ history[-1].dm_response if history else 'Take an action to begin your adventure...' }}
|
||||
{% else %}
|
||||
Take an action to begin your adventure...
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="actions-grid" id="action-buttons">
|
||||
{% if session.available_actions %}
|
||||
{% for action in session.available_actions %}
|
||||
<button class="action-btn {% if action.category == 'special' %}action-elite{% elif action.category in ['gather_info', 'travel'] and action.prompt_id in ['investigate_suspicious', 'follow_lead', 'make_camp'] %}action-premium{% endif %}"
|
||||
hx-post="{{ url_for('dev.take_action', session_id=session_id) }}"
|
||||
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
||||
hx-target="#dm-response"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this"
|
||||
title="{{ action.description }}">
|
||||
{{ action.display_text }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: #9ca3af; text-align: center; grid-column: 1 / -1;">
|
||||
No actions available for your tier/location. Try changing location or upgrading tier.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Debug Panel -->
|
||||
<div class="debug-panel">
|
||||
<div class="debug-toggle" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">
|
||||
[+] Debug Info (click to toggle)
|
||||
</div>
|
||||
<div class="debug-content" style="display: none;">
|
||||
Session ID: {{ session_id }}
|
||||
Character ID: {{ session.character_id }}
|
||||
Turn: {{ session.turn_number }}
|
||||
Game State: {{ session.game_state | tojson }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right sidebar: History -->
|
||||
<div class="panel history-panel">
|
||||
<h3>
|
||||
History
|
||||
<button class="btn-refresh"
|
||||
hx-get="{{ url_for('dev.get_history', session_id=session_id) }}"
|
||||
hx-target="#history-content"
|
||||
hx-swap="innerHTML">
|
||||
Refresh
|
||||
</button>
|
||||
</h3>
|
||||
<div id="history-content">
|
||||
{% if history %}
|
||||
{% for entry in history|reverse %}
|
||||
<div class="history-entry">
|
||||
<div class="history-turn">Turn {{ entry.turn }}</div>
|
||||
<div class="history-action">{{ entry.action }}</div>
|
||||
<div class="history-response">{{ entry.dm_response[:150] }}{% if entry.dm_response|length > 150 %}...{% endif %}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: #9ca3af; text-align: center; font-size: 0.85rem;">No history yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Container for Travel/NPC dialogs -->
|
||||
<div id="modal-container"></div>
|
||||
|
||||
<script>
|
||||
// Toggle NPC chat form visibility
|
||||
function toggleNpcChat(npcId) {
|
||||
const chatForm = document.getElementById('npc-chat-' + npcId);
|
||||
if (!chatForm) return;
|
||||
|
||||
// Close all other NPC chat forms
|
||||
document.querySelectorAll('.npc-chat-form').forEach(form => {
|
||||
if (form.id !== 'npc-chat-' + npcId) {
|
||||
form.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle the clicked NPC's chat form
|
||||
if (chatForm.style.display === 'none') {
|
||||
chatForm.style.display = 'block';
|
||||
// Focus the input field
|
||||
const input = chatForm.querySelector('.npc-chat-input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
} else {
|
||||
chatForm.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Clear input after successful form submission
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
// Check if this was an NPC chat form submission
|
||||
const form = event.detail.elt.closest('.npc-chat-form form');
|
||||
if (form && event.detail.successful) {
|
||||
const input = form.querySelector('.npc-chat-input');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user