odyssey-frontend / src /App.svelte
Fraser's picture
Initial backend setup with Gradio
7ac86fa
<script lang="ts">
import ApiKeyInput from '$lib/components/ApiKeyInput.svelte';
import StoryEngine from '$lib/components/StoryEngine.svelte';
import { apiKey, isGenerating, generationError, resetStory } from '$lib/stores/story';
let showEngine = false;
let errorMessage = '';
function handleApiKeySet() {
showEngine = true;
errorMessage = '';
}
function handleEngineError(error: string) {
errorMessage = error;
}
function handleReset() {
resetStory();
showEngine = false;
errorMessage = '';
}
</script>
<div class="app">
<header class="app-header">
<h1>🎬 Odyssey</h1>
<p class="subtitle">An interactive video-based choose-your-own-adventure</p>
</header>
<main>
{#if !$apiKey}
<ApiKeyInput onApiKeySet={handleApiKeySet} />
{:else if showEngine}
<div class="engine-container">
{#if errorMessage}
<div class="error-banner">
<p>❌ {errorMessage}</p>
<button on:click={handleReset} class="retry-button">
Start Over
</button>
</div>
{/if}
{#if $isGenerating}
<div class="generating-banner">
<p>🎬 Generating your adventure...</p>
</div>
{/if}
<StoryEngine onError={handleEngineError} />
<div class="controls">
<button on:click={handleReset} class="reset-button">
Restart Adventure
</button>
</div>
</div>
{/if}
</main>
<footer class="app-footer">
<p>Powered by OpenAI's GPT-4 and Sora</p>
</footer>
</div>
<style>
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
}
.app-header {
text-align: center;
padding: 2rem 1rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.subtitle {
margin: 0.5rem 0 0;
font-size: 1.1rem;
opacity: 0.95;
}
main {
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 2rem 1rem;
}
.engine-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.error-banner,
.generating-banner {
padding: 1rem 1.5rem;
border-radius: 0.5rem;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.error-banner {
background: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.generating-banner {
background: #e3f2fd;
color: #1565c0;
border: 1px solid #bbdefb;
}
.error-banner p,
.generating-banner p {
margin: 0;
font-weight: 600;
}
.retry-button {
padding: 0.5rem 1rem;
background: white;
color: #c62828;
border: 1px solid #ffcdd2;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.retry-button:hover {
background: #fff5f5;
}
.controls {
display: flex;
justify-content: center;
padding: 2rem 0;
}
.reset-button {
padding: 0.75rem 1.5rem;
background: #6c757d;
color: white;
border: none;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.reset-button:hover {
background: #5a6268;
}
.app-footer {
text-align: center;
padding: 2rem 1rem;
background: white;
border-top: 1px solid #dee2e6;
color: #6c757d;
font-size: 0.9rem;
}
.app-footer p {
margin: 0;
}
@media (max-width: 768px) {
.app-header h1 {
font-size: 2rem;
}
.subtitle {
font-size: 1rem;
}
main {
padding: 1rem 0.5rem;
}
}
</style>