Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0f32d35f | ||
|
|
d889086fb4 | ||
|
|
68d1df674a | ||
|
|
613e4fc0ca | ||
|
|
76bcaccf46 | ||
|
|
a9ab0e87e9 |
8 changed files with 3373 additions and 82 deletions
1468
package-lock.json
generated
1468
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
1734
public/css/tailwind.css
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue