Compare commits

...
Sign in to create a new pull request.

6 commits
master ... main

Author SHA1 Message Date
michael
2c0f32d35f added login screen 2025-09-09 09:38:04 -05:00
michael
d889086fb4 Merge branch 'main' of bitbucket.org:zlalani/runway-video 2025-09-05 17:17:31 -05:00
michael
68d1df674a hosted on optical webserver and made various changes and fixes to support that 2025-09-05 17:04:50 -05:00
DJP
613e4fc0ca Merge remote-tracking branch 'origin/main'
- Resolved .gitignore merge conflict
- Combined project-specific ignores with Bitbucket defaults
- Maintained all environment and debug file exclusions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 14:38:16 -04:00
DJP
76bcaccf46 Initial commit: Runway Gen4 Web App
- Complete web application for video generation using Runway Gen4 API
- Frontend: HTML5, CSS3, JavaScript with Tailwind CSS styling
- Backend: PHP API integration with async job processing
- Features: Image upload, drag & drop, progress tracking, dark mode
- Supports multiple aspect ratios, duration settings, and seed control
- Fixed MAMP compatibility issues with backend file access
- Comprehensive documentation and troubleshooting guide

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 14:37:07 -04:00
Dave Porter
a9ab0e87e9 Initial commit 2025-08-22 18:32:58 +00:00
8 changed files with 3373 additions and 82 deletions

1468
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build-css": "tailwindcss -i ./src/input.css -o ./public/css/tailwind.css --watch", "build-css": "npx tailwindcss -i ./src/input.css -o ./public/css/tailwind.css --watch",
"build": "tailwindcss -i ./src/input.css -o ./public/css/tailwind.css", "build": "npx tailwindcss -i ./src/input.css -o ./public/css/tailwind.css",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [], "keywords": [],
@ -13,6 +13,6 @@
"license": "ISC", "license": "ISC",
"type": "commonjs", "type": "commonjs",
"devDependencies": { "devDependencies": {
"tailwindcss": "^4.1.10" "tailwindcss": "^3.4.17"
} }
} }

View file

@ -53,7 +53,12 @@ try {
// --- CONFIGURE THIS SECTION WITH YOUR ACTUAL RUNWAY GEN4 API DETAILS --- // --- CONFIGURE THIS SECTION WITH YOUR ACTUAL RUNWAY GEN4 API DETAILS ---
// Refer to RunwayML's official Gen4 API documentation for the correct model ID and parameters. // Refer to RunwayML's official Gen4 API documentation for the correct model ID and parameters.
// Endpoint for Image to Video as per documentation: // Runway Gen4 Turbo requires both image and text - it's image-to-video only
if (empty($imageBase64)) {
throw new Exception('Runway Gen4 Turbo requires an image input. Please upload a first frame image to generate a video.');
}
// Image to Video (only supported mode)
$runwayApiEndpoint = 'https://api.dev.runwayml.com/v1/image_to_video'; $runwayApiEndpoint = 'https://api.dev.runwayml.com/v1/image_to_video';
// Validate image size (base64 data URI should be under 5MB) // Validate image size (base64 data URI should be under 5MB)

View file

@ -35,7 +35,7 @@ try {
$config = [ $config = [
'azure_client_id' => $env['AZURE_CLIENT_ID'] ?? '', 'azure_client_id' => $env['AZURE_CLIENT_ID'] ?? '',
'azure_tenant_id' => $env['AZURE_TENANT_ID'] ?? '', 'azure_tenant_id' => $env['AZURE_TENANT_ID'] ?? '',
'redirect_uri' => 'http://localhost:3000/runway-video/public/' 'redirect_uri' => 'https://ai-sandbox.oliver.solutions/runway-video/public/'
]; ];
echo json_encode($config); echo json_encode($config);

View file

@ -388,3 +388,43 @@ button#login-btn:hover {
.text-white { .text-white {
color: #ffffff !important; color: #ffffff !important;
} }
/* Login Screen Styles */
#login-screen-container {
background: inherit;
transition: opacity 0.3s ease-in-out;
}
#login-screen-container h1 {
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Main App Container Styles */
#main-app-container {
opacity: 1;
transition: opacity 0.3s ease-in-out;
width: 100%;
}
#main-app-container.hidden {
opacity: 0;
pointer-events: none;
}
/* Loading States */
#login-loading {
transition: opacity 0.3s ease-in-out;
}
/* Dark Mode for Login Screen */
.dark-mode #login-screen-container {
color: #f5f5f5;
}
.dark-mode #login-screen-container h1 {
color: var(--primary-text-color) !important;
}
.dark-mode #login-loading p {
color: #d1d5db !important;
}

1734
public/css/tailwind.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -28,26 +28,41 @@
</span> </span>
</button> </button>
<!-- Authentication Section --> <!-- Login Screen Container -->
<div id="auth-section" class="mb-4 text-center"> <div id="login-screen-container" class="min-h-screen flex flex-col items-center justify-center">
<button id="login-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg shadow-md transition duration-150 ease-in-out hidden" style="background-color: #2563eb !important; color: #ffffff !important;"> <div class="text-center">
Sign In with Microsoft <h1 class="text-4xl md:text-6xl font-bold text-orange-600 mb-8">
</button> Runway Gen4
<button id="logout-btn" class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg shadow-md transition duration-150 ease-in-out hidden" style="background-color: #dc2626 !important; color: #ffffff !important;"> </h1>
Sign Out <button id="login-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg shadow-lg text-lg font-medium transition duration-150 ease-in-out" style="background-color: #2563eb !important; color: #ffffff !important;">
</button> Sign In with Microsoft
<div id="user-info" class="mt-2 text-sm font-medium"></div> </button>
<div id="login-loading" class="mt-4 hidden">
<i class="fas fa-spinner fa-spin fa-2x text-orange-600"></i>
<p class="text-sm text-gray-600 mt-2">Signing in...</p>
</div>
</div>
</div> </div>
<!-- Header --> <!-- Main App Container (hidden by default) -->
<header class="w-full py-6 bg-gray-50 shadow-sm rounded-b-lg"> <div id="main-app-container" class="hidden">
<h1 class="text-3xl md:text-4xl font-bold text-orange-600 text-center px-4"> <!-- Authentication Section -->
Runway Gen4 Web App <div id="auth-section" class="mb-4 text-center">
</h1> <button id="logout-btn" class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg shadow-md transition duration-150 ease-in-out" style="background-color: #dc2626 !important; color: #ffffff !important;">
</header> Sign Out
</button>
<div id="user-info" class="mt-2 text-sm font-medium"></div>
</div>
<!-- Main Content Area --> <!-- Header -->
<main class="w-full max-w-4xl lg:max-w-6xl mx-auto p-4 md:p-8 flex flex-col gap-6 md:gap-8 mt-8"> <header class="w-full py-6 bg-gray-50 shadow-sm rounded-b-lg">
<h1 class="text-3xl md:text-4xl font-bold text-orange-600 text-center px-4">
Runway Gen4 Web App
</h1>
</header>
<!-- Main Content Area -->
<main class="w-full max-w-4xl lg:max-w-6xl mx-auto p-4 md:p-8 flex flex-col gap-6 md:gap-8 mt-8">
<!-- Input Form Section (always visible) --> <!-- Input Form Section (always visible) -->
<div id="input-form-section" class="w-full"> <div id="input-form-section" class="w-full">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
@ -69,8 +84,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg> </svg>
<p class="text-sm md:text-base">Drag & Drop Image Here or</p> <p class="text-sm md:text-base">Drag & Drop First Frame Image Here or</p>
<p class="text-sm md:text-base font-medium text-orange-600">Click to Upload</p> <p class="text-sm md:text-base font-medium text-orange-600">Click to Upload (Required)</p>
</div> </div>
<p id="image-error-message" class="absolute bottom-2 text-red-500 text-xs mt-2 hidden"></p> <p id="image-error-message" class="absolute bottom-2 text-red-500 text-xs mt-2 hidden"></p>
</div> </div>
@ -139,14 +154,15 @@
</button> </button>
</div> </div>
</div> </div>
</main> </main>
<!-- Loading Overlay --> <!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden" role="status" aria-live="polite"> <div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden" role="status" aria-live="polite">
<div class="flex flex-col items-center text-white"> <div class="flex flex-col items-center text-white">
<i class="fas fa-spinner fa-spin fa-3x mb-4"></i> <i class="fas fa-spinner fa-spin fa-3x mb-4"></i>
<p class="text-xl md:text-2xl font-semibold">Generating your video...</p> <p class="text-xl md:text-2xl font-semibold">Generating your video...</p>
<p class="text-sm md:text-base mt-2">This may take a moment.</p> <p class="text-sm md:text-base mt-2">This may take a moment.</p>
</div>
</div> </div>
</div> </div>

View file

@ -9,21 +9,8 @@ let selectedImageFile = null;
// Initialize MSAL or check for existing auth // Initialize MSAL or check for existing auth
async function initializeMSAL() { async function initializeMSAL() {
try { try {
// First, check if we have auth state from landing page // Always start by showing the login screen
const storedToken = localStorage.getItem('runway_access_token'); showLoginScreen();
const storedAuthState = localStorage.getItem('runway_auth_state');
if (storedToken && storedAuthState) {
const tokenData = JSON.parse(storedToken);
const authState = JSON.parse(storedAuthState);
// Check if token is still valid (not expired)
if (Date.now() < tokenData.expires_at) {
currentUser = tokenData.user;
updateAuthUI();
return; // Skip MSAL initialization if we have valid token
}
}
// Initialize MSAL for this page // Initialize MSAL for this page
const configResponse = await fetch('backend/config_client.php'); const configResponse = await fetch('backend/config_client.php');
@ -62,16 +49,13 @@ async function initializeMSAL() {
// Clean up the URL (remove auth parameters) // Clean up the URL (remove auth parameters)
window.history.replaceState({}, document.title, window.location.pathname); window.history.replaceState({}, document.title, window.location.pathname);
} else { // Show main app since user just authenticated
// Check if user was already authenticated updateAuthUI();
const accounts = msalInstance.getAllAccounts(); return;
if (accounts.length > 0) {
currentUser = accounts[0];
msalInstance.setActiveAccount(currentUser);
}
} }
updateAuthUI(); // Don't automatically log in - always show login screen first
currentUser = null;
} catch (error) { } catch (error) {
console.error('MSAL initialization failed:', error); console.error('MSAL initialization failed:', error);
showAuthError(); showAuthError();
@ -80,6 +64,9 @@ async function initializeMSAL() {
// Sign In function // Sign In function
async function signIn() { async function signIn() {
// Show loading state immediately
showLoginLoading();
if (!msalInstance) { if (!msalInstance) {
// If MSAL not initialized, redirect to portal for authentication // If MSAL not initialized, redirect to portal for authentication
window.location.href = 'http://localhost:3000/'; window.location.href = 'http://localhost:3000/';
@ -104,46 +91,95 @@ async function signIn() {
}; };
localStorage.setItem('runway_access_token', JSON.stringify(tokenData)); localStorage.setItem('runway_access_token', JSON.stringify(tokenData));
// Hide loading state and update UI
hideLoginLoading();
updateAuthUI(); updateAuthUI();
} catch (error) { } catch (error) {
console.error("Login failed:", error); console.error("Login failed:", error);
showErrorMessage(generalErrorMessage, "Login failed: " + error.message); hideLoginLoading();
// Could show error message on login screen if needed
const generalErrorMessage = document.getElementById('general-error-message');
if (generalErrorMessage) {
showErrorMessage(generalErrorMessage, "Login failed: " + error.message);
}
} }
} }
// Sign Out function - redirect to portal // Sign Out function - return to login screen
async function signOut() { async function signOut() {
// Clear any stored tokens // Clear any stored tokens
localStorage.removeItem('runway_access_token'); localStorage.removeItem('runway_access_token');
localStorage.removeItem('runway_auth_state'); localStorage.removeItem('runway_auth_state');
// Redirect to portal // Clear current user state
window.location.href = 'http://localhost:3000/'; currentUser = null;
// If MSAL is initialized, logout from Microsoft silently
if (msalInstance && msalInstance.getAllAccounts().length > 0) {
try {
await msalInstance.logoutSilent();
} catch (error) {
console.log('Silent logout failed, continuing with local logout');
}
}
// Update UI to show login screen
updateAuthUI();
}
// Show Login Screen
function showLoginScreen() {
const loginScreen = document.getElementById('login-screen-container');
const mainApp = document.getElementById('main-app-container');
const loginLoading = document.getElementById('login-loading');
if (loginScreen) loginScreen.classList.remove('hidden');
if (mainApp) mainApp.classList.add('hidden');
if (loginLoading) loginLoading.classList.add('hidden');
}
// Show Main App
function showMainApp() {
const loginScreen = document.getElementById('login-screen-container');
const mainApp = document.getElementById('main-app-container');
if (loginScreen) loginScreen.classList.add('hidden');
if (mainApp) mainApp.classList.remove('hidden');
}
// Show Login Loading State
function showLoginLoading() {
const loginLoading = document.getElementById('login-loading');
const loginBtn = document.getElementById('login-btn');
if (loginBtn) loginBtn.classList.add('hidden');
if (loginLoading) loginLoading.classList.remove('hidden');
}
// Hide Login Loading State
function hideLoginLoading() {
const loginLoading = document.getElementById('login-loading');
const loginBtn = document.getElementById('login-btn');
if (loginLoading) loginLoading.classList.add('hidden');
if (loginBtn) loginBtn.classList.remove('hidden');
} }
// Update Authentication UI // Update Authentication UI
function updateAuthUI() { function updateAuthUI() {
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
const userInfo = document.getElementById('user-info'); const userInfo = document.getElementById('user-info');
if (currentUser) { if (currentUser) {
if (loginBtn) loginBtn.classList.add('hidden'); // User is authenticated - show main app
if (logoutBtn) logoutBtn.classList.remove('hidden'); showMainApp();
if (userInfo) { if (userInfo) {
userInfo.textContent = `Welcome, ${currentUser.name || currentUser.username}`; userInfo.textContent = `Welcome, ${currentUser.name || currentUser.username}`;
userInfo.classList.remove('text-red-600'); userInfo.classList.remove('text-red-600');
userInfo.classList.add('text-green-600'); userInfo.classList.add('text-green-600');
} }
} else { } else {
// Show sign in option without auto-redirect // User is not authenticated - show login screen
if (loginBtn) loginBtn.classList.remove('hidden'); showLoginScreen();
if (logoutBtn) logoutBtn.classList.add('hidden');
if (userInfo) {
userInfo.innerHTML = 'Please sign in to generate videos | <a href="http://localhost:3000/" class="text-blue-600 hover:underline">← Back to Portal</a>';
userInfo.classList.remove('text-green-600');
userInfo.classList.add('text-red-600');
}
} }
// Update generate button state only if the function exists // Update generate button state only if the function exists
@ -154,9 +190,9 @@ function updateAuthUI() {
// Show authentication error // Show authentication error
function showAuthError() { function showAuthError() {
const userInfo = document.getElementById('user-info'); // Show login screen with error state
userInfo.innerHTML = 'Authentication initialization failed. <a href="http://localhost:3000/" class="text-blue-600 hover:underline">← Back to Portal</a>'; showLoginScreen();
userInfo.classList.add('text-red-600'); hideLoginLoading();
} }
// Navigate back to portal // Navigate back to portal
@ -852,7 +888,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// --- Video Generation Logic --- // --- Video Generation Logic ---
const generateVideoBtn = document.getElementById('generate-video-btn');
if (generateVideoBtn) { if (generateVideoBtn) {
generateVideoBtn.addEventListener('click', async () => { generateVideoBtn.addEventListener('click', async () => {
// Immediately lock the button to prevent double clicks // Immediately lock the button to prevent double clicks
@ -872,13 +907,14 @@ document.addEventListener('DOMContentLoaded', async () => {
hideErrorMessage(generalErrorMessage); // Clear previous general errors hideErrorMessage(generalErrorMessage); // Clear previous general errors
if (!selectedImageFile) { if (!promptInput.value.trim()) {
showErrorMessage(generalErrorMessage, "Please upload an image."); showErrorMessage(generalErrorMessage, "Please enter a prompt.");
toggleLoadingOverlay(false); // Unlock if validation fails toggleLoadingOverlay(false); // Unlock if validation fails
return; return;
} }
if (!promptInput.value.trim()) {
showErrorMessage(generalErrorMessage, "Please enter a prompt."); if (!selectedImageFile) {
showErrorMessage(generalErrorMessage, "Please upload an image.");
toggleLoadingOverlay(false); // Unlock if validation fails toggleLoadingOverlay(false); // Unlock if validation fails
return; return;
} }