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": "",
"main": "index.js",
"scripts": {
"build-css": "tailwindcss -i ./src/input.css -o ./public/css/tailwind.css --watch",
"build": "tailwindcss -i ./src/input.css -o ./public/css/tailwind.css",
"build-css": "npx tailwindcss -i ./src/input.css -o ./public/css/tailwind.css --watch",
"build": "npx tailwindcss -i ./src/input.css -o ./public/css/tailwind.css",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@ -13,6 +13,6 @@
"license": "ISC",
"type": "commonjs",
"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 ---
// 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';
// Validate image size (base64 data URI should be under 5MB)

View file

@ -35,7 +35,7 @@ try {
$config = [
'azure_client_id' => $env['AZURE_CLIENT_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);

View file

@ -388,3 +388,43 @@ button#login-btn:hover {
.text-white {
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>
</button>
<!-- Authentication Section -->
<div id="auth-section" class="mb-4 text-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;">
Sign In with Microsoft
</button>
<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;">
Sign Out
</button>
<div id="user-info" class="mt-2 text-sm font-medium"></div>
<!-- Login Screen Container -->
<div id="login-screen-container" class="min-h-screen flex flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl md:text-6xl font-bold text-orange-600 mb-8">
Runway Gen4
</h1>
<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;">
Sign In with Microsoft
</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>
<!-- Header -->
<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 App Container (hidden by default) -->
<div id="main-app-container" class="hidden">
<!-- Authentication Section -->
<div id="auth-section" class="mb-4 text-center">
<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;">
Sign Out
</button>
<div id="user-info" class="mt-2 text-sm font-medium"></div>
</div>
<!-- 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">
<!-- Header -->
<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) -->
<div id="input-form-section" class="w-full">
<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">
<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>
<p class="text-sm md:text-base">Drag & Drop 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">Drag & Drop First Frame Image Here or</p>
<p class="text-sm md:text-base font-medium text-orange-600">Click to Upload (Required)</p>
</div>
<p id="image-error-message" class="absolute bottom-2 text-red-500 text-xs mt-2 hidden"></p>
</div>
@ -139,14 +154,15 @@
</button>
</div>
</div>
</main>
</main>
<!-- 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 class="flex flex-col items-center text-white">
<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-sm md:text-base mt-2">This may take a moment.</p>
<!-- 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 class="flex flex-col items-center text-white">
<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-sm md:text-base mt-2">This may take a moment.</p>
</div>
</div>
</div>

View file

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