|
{% extends "base.html" %}
|
|
|
|
{% block title %}Deploy to HuggingFace Spaces{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<div class="hero bg-gradient-to-br from-primary via-secondary to-accent text-primary-content rounded-2xl mb-10 shadow-2xl">
|
|
<div class="hero-content text-center py-20">
|
|
<div class="max-w-2xl">
|
|
<h1 class="text-6xl font-bold mb-6 flex items-center justify-center">
|
|
<i data-lucide="rocket" class="w-16 h-16 mr-4 animate-pulse"></i>
|
|
<span data-i18n="hero.title">One-Click Deploy</span>
|
|
</h1>
|
|
<p class="text-xl opacity-90 mb-8" data-i18n="hero.subtitle">
|
|
Deploy to HuggingFace Spaces instantly
|
|
</p>
|
|
<div class="stats shadow-lg bg-base-100/20 backdrop-blur">
|
|
<div class="stat">
|
|
<div class="stat-figure text-secondary">
|
|
<i data-lucide="zap" class="w-8 h-8"></i>
|
|
</div>
|
|
<div class="stat-title text-primary-content/80" data-i18n="hero.deployTime">Deploy Time</div>
|
|
<div class="stat-value" data-i18n="hero.minutes">2-5min</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-figure text-secondary">
|
|
<i data-lucide="server" class="w-8 h-8"></i>
|
|
</div>
|
|
<div class="stat-title text-primary-content/80" data-i18n="hero.freeHosting">Free Hosting</div>
|
|
<div class="stat-value" data-i18n="hero.percentage">100%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
|
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
|
<div class="card-body items-center text-center">
|
|
<div class="w-16 h-16 bg-warning/20 rounded-full flex items-center justify-center mb-4">
|
|
<i data-lucide="zap" class="w-8 h-8 text-warning"></i>
|
|
</div>
|
|
<h2 class="card-title" data-i18n="feature.fast.title">Lightning Fast</h2>
|
|
<p data-i18n="feature.fast.desc">Deploy from Git in minutes</p>
|
|
</div>
|
|
</div>
|
|
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
|
<div class="card-body items-center text-center">
|
|
<div class="w-16 h-16 bg-success/20 rounded-full flex items-center justify-center mb-4">
|
|
<i data-lucide="shield-check" class="w-8 h-8 text-success"></i>
|
|
</div>
|
|
<h2 class="card-title" data-i18n="feature.secure.title">Secure</h2>
|
|
<p data-i18n="feature.secure.desc">Full control with your token</p>
|
|
</div>
|
|
</div>
|
|
<div class="card bg-base-200 shadow-xl hover:shadow-2xl transition-shadow">
|
|
<div class="card-body items-center text-center">
|
|
<div class="w-16 h-16 bg-info/20 rounded-full flex items-center justify-center mb-4">
|
|
<i data-lucide="activity" class="w-8 h-8 text-info"></i>
|
|
</div>
|
|
<h2 class="card-title" data-i18n="feature.monitor.title">Real-time</h2>
|
|
<p data-i18n="feature.monitor.desc">Live deployment status</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="card bg-base-100 shadow-2xl border border-base-300">
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<h2 class="card-title text-3xl flex items-center">
|
|
<i data-lucide="settings" class="w-8 h-8 mr-3"></i>
|
|
<span data-i18n="form.title">Configuration</span>
|
|
</h2>
|
|
<div class="flex gap-2">
|
|
<button
|
|
type="button"
|
|
onclick="showImportModal()"
|
|
class="btn btn-ghost btn-sm"
|
|
data-i18n-title="config.import"
|
|
title="Import Configuration"
|
|
>
|
|
<i data-lucide="upload" class="w-4 h-4 mr-2"></i>
|
|
<span data-i18n="config.import">Import</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick="showExportModal()"
|
|
class="btn btn-ghost btn-sm"
|
|
data-i18n-title="config.export"
|
|
title="Export Configuration"
|
|
>
|
|
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
|
<span data-i18n="config.export">Export</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
id="deploy-form"
|
|
action="/web/deploy"
|
|
method="POST"
|
|
class="space-y-6"
|
|
x-data="{ showAdvanced: false }"
|
|
>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
<div class="space-y-6">
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-lg">
|
|
<i data-lucide="key" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.token">HF Token</span>
|
|
</span>
|
|
<span class="label-text-alt">
|
|
<a href="https://huggingface.co/settings/tokens" target="_blank" class="link link-primary" data-i18n="form.token.get">
|
|
Get Token
|
|
</a>
|
|
</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
type="password"
|
|
name="hf_token"
|
|
data-i18n-placeholder="form.token.placeholder"
|
|
placeholder="hf_..."
|
|
class="input input-bordered input-primary w-full pr-24"
|
|
required
|
|
x-ref="tokenInput"
|
|
>
|
|
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1" style="z-index: 10;">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-xs p-1"
|
|
@click="$refs.tokenInput.type = $refs.tokenInput.type === 'password' ? 'text' : 'password'"
|
|
data-i18n-title="form.token.toggle"
|
|
title="Toggle visibility"
|
|
>
|
|
<i data-lucide="eye" class="w-4 h-4"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-xs p-1"
|
|
onclick="clearCachedToken()"
|
|
data-i18n-title="form.token.clear"
|
|
title="Clear cached token"
|
|
id="clear-token-btn"
|
|
style="display: none;"
|
|
>
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<label class="label">
|
|
<span class="label-text-alt text-warning">
|
|
<i data-lucide="alert-circle" class="w-3 h-3 inline"></i>
|
|
<span data-i18n="form.token.hint">Write permission required</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-lg">
|
|
<i data-lucide="git-branch" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.repo">Git Repository</span>
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="url"
|
|
name="git_repo_url"
|
|
data-i18n-placeholder="form.repo.placeholder"
|
|
placeholder="https://github.com/user/repo.git"
|
|
class="input input-bordered input-primary w-full"
|
|
required
|
|
>
|
|
<label class="label">
|
|
<span class="label-text-alt" data-i18n="form.repo.hint">GitHub, GitLab, etc.</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="space-y-6">
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-lg">
|
|
<i data-lucide="tag" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.space">Space Name</span>
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="space_name"
|
|
data-i18n-placeholder="form.space.placeholder"
|
|
placeholder="my-app"
|
|
class="input input-bordered input-primary w-full"
|
|
pattern="[a-zA-Z0-9_\-]+"
|
|
required
|
|
>
|
|
<label class="label">
|
|
<span class="label-text-alt" data-i18n="form.space.hint">Letters, numbers, hyphens only</span>
|
|
</label>
|
|
</div>
|
|
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-lg">
|
|
<i data-lucide="file-text" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.desc">Description</span>
|
|
</span>
|
|
</label>
|
|
<textarea
|
|
name="description"
|
|
data-i18n-placeholder="form.desc.placeholder"
|
|
placeholder="Brief description..."
|
|
class="textarea textarea-bordered textarea-primary h-24"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="divider"></div>
|
|
|
|
<div class="collapse collapse-plus bg-base-200 rounded-lg">
|
|
<input type="checkbox" x-model="showAdvanced" />
|
|
<div class="collapse-title text-lg font-medium flex items-center">
|
|
<i data-lucide="sliders" class="w-5 h-5 mr-2"></i>
|
|
<span data-i18n="form.advanced">Advanced</span>
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-4">
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold">
|
|
<i data-lucide="folder-open" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.deployPath">Deploy Path</span>
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="deploy_path"
|
|
value="/"
|
|
data-i18n-placeholder="form.deployPath.placeholder"
|
|
placeholder="/"
|
|
class="input input-bordered w-full"
|
|
pattern="^\.?\/?([\w\-\.\/]*)?$"
|
|
>
|
|
<label class="label">
|
|
<span class="label-text-alt" data-i18n="form.deployPath.hint">Subdirectory to deploy (default: root)</span>
|
|
</label>
|
|
</div>
|
|
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold">
|
|
<i data-lucide="wifi" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.port">Port</span>
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="space_port"
|
|
value="7860"
|
|
min="1"
|
|
max="65535"
|
|
class="input input-bordered w-full"
|
|
>
|
|
</div>
|
|
|
|
|
|
<div class="form-control">
|
|
<label class="label cursor-pointer">
|
|
<span class="label-text font-semibold">
|
|
<i data-lucide="lock" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.private">Private Space</span>
|
|
</span>
|
|
<input type="checkbox" name="private" class="toggle toggle-primary" />
|
|
</label>
|
|
</div>
|
|
|
|
|
|
<div class="form-control lg:col-span-2">
|
|
<label class="label">
|
|
<span class="label-text font-semibold">
|
|
<i data-lucide="terminal" class="w-4 h-4 inline mr-2"></i>
|
|
<span data-i18n="form.env">Environment Variables</span>
|
|
</span>
|
|
</label>
|
|
<textarea
|
|
name="env_vars_text"
|
|
data-i18n-placeholder="form.env.placeholder"
|
|
placeholder="KEY=value"
|
|
class="textarea textarea-bordered h-32 font-mono text-sm"
|
|
></textarea>
|
|
<label class="label">
|
|
<span class="label-text-alt" data-i18n="form.env.hint">One per line</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="form-control mt-8">
|
|
<button type="submit" class="btn btn-primary btn-lg w-full gap-3">
|
|
<i data-lucide="rocket" class="w-6 h-6"></i>
|
|
<span data-i18n="form.submit">Deploy</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="loading-overlay" class="fixed inset-0 bg-base-100/80 backdrop-blur-sm z-50 flex items-center justify-center" style="display: none;">
|
|
<div class="card bg-base-100 shadow-2xl">
|
|
<div class="card-body">
|
|
<div class="flex flex-col items-center space-y-4">
|
|
<div class="loading loading-spinner loading-lg text-primary"></div>
|
|
<h3 class="text-xl font-semibold" data-i18n="loading.title">Deploying...</h3>
|
|
<p class="text-base-content/70" data-i18n="loading.desc">Please wait...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="mt-12 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="card bg-info/10 border border-info/20">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-info">
|
|
<i data-lucide="info" class="w-5 h-5"></i>
|
|
<span data-i18n="req.title">Requirements</span>
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-start">
|
|
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
|
<span data-i18n="req.dockerfile">Repository must contain Dockerfile</span>
|
|
</li>
|
|
<li class="flex items-start">
|
|
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
|
<span data-i18n="req.token">Token needs write permissions</span>
|
|
</li>
|
|
<li class="flex items-start">
|
|
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
|
<span data-i18n="req.time">Deployment takes 2-5 minutes</span>
|
|
</li>
|
|
<li class="flex items-start">
|
|
<i data-lucide="check" class="w-4 h-4 mr-2 text-info mt-0.5"></i>
|
|
<span data-i18n="req.docker">Supports Dockerized apps</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-warning/10 border border-warning/20">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-warning">
|
|
<i data-lucide="lightbulb" class="w-5 h-5"></i>
|
|
<span data-i18n="tips.title">Tips</span>
|
|
</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-start">
|
|
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
|
<span data-i18n="tips.test">Test Dockerfile locally</span>
|
|
</li>
|
|
<li class="flex items-start">
|
|
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
|
<span data-i18n="tips.env">Use env vars for secrets</span>
|
|
</li>
|
|
<li class="flex items-start">
|
|
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
|
<span data-i18n="tips.size">Keep image size small</span>
|
|
</li>
|
|
<li class="flex items-start">
|
|
<i data-lucide="arrow-right" class="w-4 h-4 mr-2 text-warning mt-0.5"></i>
|
|
<span data-i18n="tips.limits">Check HF resource limits</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<dialog id="import-modal" class="modal">
|
|
<div class="modal-box max-w-2xl">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
<i data-lucide="upload" class="w-5 h-5 inline mr-2"></i>
|
|
<span data-i18n="config.import.title">Import Configuration</span>
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div class="alert alert-info">
|
|
<i data-lucide="info" class="w-4 h-4"></i>
|
|
<span data-i18n="config.import.info">Paste your configuration JSON or share configuration URL</span>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text" data-i18n="config.import.label">Configuration JSON</span>
|
|
</label>
|
|
<textarea
|
|
id="import-config-text"
|
|
class="textarea textarea-bordered h-48 font-mono text-sm"
|
|
data-i18n-placeholder="config.import.placeholder"
|
|
placeholder='{"space_name": "my-app", "git_repo_url": "https://github.com/..."}'
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" onclick="importConfig()" class="btn btn-primary">
|
|
<i data-lucide="check" class="w-4 h-4 mr-2"></i>
|
|
<span data-i18n="config.import.apply">Apply Configuration</span>
|
|
</button>
|
|
<form method="dialog">
|
|
<button class="btn btn-ghost" data-i18n="config.cancel">Cancel</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
|
|
<dialog id="export-modal" class="modal">
|
|
<div class="modal-box max-w-2xl">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
<i data-lucide="download" class="w-5 h-5 inline mr-2"></i>
|
|
<span data-i18n="config.export.title">Export Configuration</span>
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div class="alert alert-success">
|
|
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
|
<span data-i18n="config.export.info">Configuration exported successfully</span>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text" data-i18n="config.export.label">Configuration JSON</span>
|
|
<button
|
|
type="button"
|
|
onclick="copyExportConfig()"
|
|
class="btn btn-ghost btn-xs"
|
|
>
|
|
<i data-lucide="copy" class="w-4 h-4 mr-1"></i>
|
|
<span data-i18n="config.copy">Copy</span>
|
|
</button>
|
|
</label>
|
|
<textarea
|
|
id="export-config-text"
|
|
class="textarea textarea-bordered h-48 font-mono text-sm"
|
|
readonly
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text" data-i18n="config.export.url">Share URL</span>
|
|
<button
|
|
type="button"
|
|
onclick="copyShareUrl()"
|
|
class="btn btn-ghost btn-xs"
|
|
>
|
|
<i data-lucide="copy" class="w-4 h-4 mr-1"></i>
|
|
<span data-i18n="config.copy">Copy</span>
|
|
</button>
|
|
</label>
|
|
<input
|
|
id="share-url"
|
|
type="text"
|
|
class="input input-bordered font-mono text-sm"
|
|
readonly
|
|
>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" onclick="saveConfigAsFile()" class="btn btn-primary">
|
|
<i data-lucide="save" class="w-4 h-4 mr-2"></i>
|
|
<span data-i18n="config.export.save">Save as File</span>
|
|
</button>
|
|
<form method="dialog">
|
|
<button class="btn btn-ghost" data-i18n="config.close">Close</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
<script>
|
|
|
|
const TOKEN_CACHE_KEY = 'hf_deploy_token';
|
|
|
|
|
|
function loadCachedToken() {
|
|
try {
|
|
const cachedToken = localStorage.getItem(TOKEN_CACHE_KEY);
|
|
if (cachedToken) {
|
|
const tokenInput = document.querySelector('input[name="hf_token"]');
|
|
if (tokenInput) {
|
|
tokenInput.value = cachedToken;
|
|
|
|
const clearBtn = document.getElementById('clear-token-btn');
|
|
if (clearBtn) {
|
|
clearBtn.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading cached token:', error);
|
|
}
|
|
}
|
|
|
|
|
|
function saveTokenToCache(token) {
|
|
try {
|
|
if (token && token.startsWith('hf_')) {
|
|
localStorage.setItem(TOKEN_CACHE_KEY, token);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving token to cache:', error);
|
|
}
|
|
}
|
|
|
|
|
|
function clearCachedToken() {
|
|
try {
|
|
localStorage.removeItem(TOKEN_CACHE_KEY);
|
|
const tokenInput = document.querySelector('input[name="hf_token"]');
|
|
if (tokenInput) {
|
|
tokenInput.value = '';
|
|
tokenInput.focus();
|
|
}
|
|
|
|
const clearBtn = document.getElementById('clear-token-btn');
|
|
if (clearBtn) {
|
|
clearBtn.style.display = 'none';
|
|
}
|
|
showToast(t('form.token.cleared') || 'Token cache cleared', 'success');
|
|
} catch (error) {
|
|
console.error('Error clearing cached token:', error);
|
|
}
|
|
}
|
|
|
|
|
|
function initTokenCacheHandler() {
|
|
const tokenInput = document.querySelector('input[name="hf_token"]');
|
|
if (tokenInput) {
|
|
let saveTimeout;
|
|
|
|
|
|
tokenInput.addEventListener('input', function() {
|
|
const clearBtn = document.getElementById('clear-token-btn');
|
|
if (clearBtn) {
|
|
clearBtn.style.display = this.value ? 'block' : 'none';
|
|
}
|
|
|
|
|
|
if (saveTimeout) {
|
|
clearTimeout(saveTimeout);
|
|
}
|
|
|
|
|
|
const token = this.value.trim();
|
|
if (token && token.startsWith('hf_')) {
|
|
saveTimeout = setTimeout(() => {
|
|
saveTokenToCache(token);
|
|
|
|
const originalBorder = this.style.borderColor;
|
|
this.style.borderColor = 'var(--success)';
|
|
setTimeout(() => {
|
|
this.style.borderColor = originalBorder;
|
|
}, 1000);
|
|
}, 500);
|
|
}
|
|
});
|
|
|
|
|
|
tokenInput.addEventListener('blur', function() {
|
|
const token = this.value.trim();
|
|
if (token && token.startsWith('hf_')) {
|
|
saveTokenToCache(token);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
const CONFIG_FIELDS = {
|
|
git_repo_url: 'input[name="git_repo_url"]',
|
|
space_name: 'input[name="space_name"]',
|
|
description: 'textarea[name="description"]',
|
|
deploy_path: 'input[name="deploy_path"]',
|
|
space_port: 'input[name="space_port"]',
|
|
private: 'input[name="private"]',
|
|
env_vars_text: 'textarea[name="env_vars_text"]'
|
|
};
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
checkUrlConfig();
|
|
|
|
|
|
loadCachedToken();
|
|
initTokenCacheHandler();
|
|
|
|
|
|
setTimeout(() => {
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
|
|
window.addEventListener('hashchange', function() {
|
|
checkUrlConfig();
|
|
});
|
|
|
|
|
|
function checkUrlConfig() {
|
|
|
|
const hash = window.location.hash;
|
|
|
|
if (hash && hash.startsWith('#config=')) {
|
|
try {
|
|
|
|
const configParam = hash.substring(8);
|
|
|
|
|
|
const configJson = atob(configParam);
|
|
const config = JSON.parse(configJson);
|
|
applyConfiguration(config);
|
|
|
|
|
|
showToast(t('config.import.success'), 'success');
|
|
|
|
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
} catch (error) {
|
|
console.error('Failed to load config from URL:', error);
|
|
showToast(t('config.import.error'), 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function showImportModal() {
|
|
document.getElementById('import-modal').showModal();
|
|
|
|
setTimeout(() => lucide.createIcons(), 100);
|
|
}
|
|
|
|
|
|
function showExportModal() {
|
|
const config = extractConfiguration();
|
|
const configJson = JSON.stringify(config, null, 2);
|
|
|
|
|
|
document.getElementById('export-config-text').value = configJson;
|
|
|
|
|
|
const encoded = btoa(configJson);
|
|
const shareUrl = `${window.location.origin}${window.location.pathname}#config=${encoded}`;
|
|
document.getElementById('share-url').value = shareUrl;
|
|
|
|
document.getElementById('export-modal').showModal();
|
|
|
|
setTimeout(() => lucide.createIcons(), 100);
|
|
}
|
|
|
|
|
|
function extractConfiguration() {
|
|
const config = {};
|
|
|
|
for (const [field, selector] of Object.entries(CONFIG_FIELDS)) {
|
|
const element = document.querySelector(selector);
|
|
if (element) {
|
|
if (element.type === 'checkbox') {
|
|
config[field] = element.checked;
|
|
} else {
|
|
config[field] = element.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
|
|
function applyConfiguration(config) {
|
|
for (const [field, selector] of Object.entries(CONFIG_FIELDS)) {
|
|
const element = document.querySelector(selector);
|
|
if (element && config.hasOwnProperty(field)) {
|
|
if (element.type === 'checkbox') {
|
|
element.checked = config[field];
|
|
} else {
|
|
element.value = config[field];
|
|
}
|
|
|
|
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
|
|
if (config.deploy_path || config.space_port !== 7860 || config.private || config.env_vars_text) {
|
|
const advancedToggle = document.querySelector('.collapse input[type="checkbox"]');
|
|
if (advancedToggle) {
|
|
advancedToggle.checked = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function importConfig() {
|
|
const configText = document.getElementById('import-config-text').value.trim();
|
|
|
|
if (!configText) {
|
|
showToast(t('config.import.empty'), 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const config = JSON.parse(configText);
|
|
applyConfiguration(config);
|
|
|
|
|
|
document.getElementById('import-modal').close();
|
|
|
|
|
|
showToast(t('config.import.success'), 'success');
|
|
} catch (error) {
|
|
console.error('Invalid configuration JSON:', error);
|
|
showToast(t('config.import.invalid'), 'error');
|
|
}
|
|
}
|
|
|
|
|
|
function copyExportConfig() {
|
|
const configText = document.getElementById('export-config-text');
|
|
configText.select();
|
|
navigator.clipboard.writeText(configText.value).then(() => {
|
|
showToast(t('config.copied'), 'success');
|
|
});
|
|
}
|
|
|
|
|
|
function copyShareUrl() {
|
|
const shareUrl = document.getElementById('share-url');
|
|
shareUrl.select();
|
|
navigator.clipboard.writeText(shareUrl.value).then(() => {
|
|
showToast(t('config.url.copied'), 'success');
|
|
});
|
|
}
|
|
|
|
|
|
function saveConfigAsFile() {
|
|
const config = extractConfiguration();
|
|
const configJson = JSON.stringify(config, null, 2);
|
|
|
|
|
|
const blob = new Blob([configJson], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `hf-deploy-config-${config.space_name || 'config'}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
showToast(t('config.export.saved'), 'success');
|
|
}
|
|
|
|
|
|
document.getElementById('deploy-form').addEventListener('submit', function(e) {
|
|
const submitBtn = e.target.querySelector('button[type="submit"]');
|
|
submitBtn.disabled = true;
|
|
|
|
|
|
document.getElementById('loading-overlay').style.display = 'flex';
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
|
|
if (typeof htmx !== 'undefined') {
|
|
htmx.process(document.body);
|
|
}
|
|
|
|
|
|
setTimeout(() => {
|
|
lucide.createIcons();
|
|
}, 100);
|
|
});
|
|
</script>
|
|
{% endblock %} |