Files
celery/app/templates/effects/list.html
gilesb 585c75e846 Fix item visibility bugs and add effects web UI
- Fix recipe filter to allow owner=None (S-expression compiled recipes)
- Fix media uploads to use category (video/image/audio) not MIME type
- Fix IPFS imports to detect and store correct media type
- Add Effects navigation link between Recipes and Media
- Create effects list and detail templates with upload functionality
- Add cache/not_found.html template (was missing)
- Add type annotations to service classes
- Add tests for item visibility and effects web UI (30 tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 12:01:54 +00:00

135 lines
4.9 KiB
HTML

{% extends "base.html" %}
{% block title %}Effects - Art-DAG L1{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold">Effects</h1>
<label class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded font-medium cursor-pointer">
Upload Effect
<input type="file" accept=".py" class="hidden" id="effect-upload" />
</label>
</div>
<p class="text-gray-400 mb-8">
Effects are Python scripts that process video frames or whole videos.
Each effect is stored in IPFS and can be referenced by CID in recipes.
</p>
{% if effects %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for effect in effects %}
{% set meta = effect.meta or effect %}
<a href="/effects/{{ effect.cid }}"
class="bg-gray-800 border border-gray-700 rounded-lg p-4 hover:border-gray-600 transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-white">{{ meta.name or 'Unnamed' }}</span>
<span class="text-gray-500 text-sm">v{{ meta.version or '1.0.0' }}</span>
</div>
{% if meta.description %}
<p class="text-gray-400 text-sm mb-3 line-clamp-2">{{ meta.description }}</p>
{% endif %}
<div class="flex items-center justify-between text-sm mb-2">
{% if meta.author %}
<span class="text-gray-500">by {{ meta.author }}</span>
{% else %}
<span></span>
{% endif %}
{% if meta.temporal %}
<span class="bg-purple-900 text-purple-300 px-2 py-0.5 rounded text-xs">temporal</span>
{% endif %}
</div>
{% if meta.params %}
<div class="text-gray-500 text-sm">
{{ meta.params | length }} parameter{{ 's' if meta.params | length != 1 else '' }}
</div>
{% endif %}
{% if meta.dependencies %}
<div class="mt-2 flex flex-wrap gap-1">
{% for dep in meta.dependencies[:3] %}
<span class="bg-gray-700 text-gray-300 px-2 py-0.5 rounded text-xs">{{ dep }}</span>
{% endfor %}
{% if meta.dependencies | length > 3 %}
<span class="text-gray-500 text-xs">+{{ meta.dependencies | length - 3 }} more</span>
{% endif %}
</div>
{% endif %}
<div class="mt-3 text-gray-600 text-xs font-mono truncate">
{{ effect.cid[:24] }}...
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="bg-gray-800 border border-gray-700 rounded-lg p-12 text-center">
<p class="text-gray-500 mb-4">No effects uploaded yet.</p>
<p class="text-gray-600 text-sm mb-6">
Effects are Python files with @effect metadata in a docstring.
</p>
<label class="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded font-medium cursor-pointer inline-block">
Upload Your First Effect
<input type="file" accept=".py" class="hidden" id="effect-upload-empty" />
</label>
</div>
{% endif %}
</div>
<div id="upload-result" class="fixed bottom-4 right-4 max-w-sm"></div>
<script>
function handleEffectUpload(input) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
fetch('/effects/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('Upload failed');
return response.json();
})
.then(data => {
const resultDiv = document.getElementById('upload-result');
resultDiv.innerHTML = `
<div class="bg-green-900 border border-green-700 rounded-lg p-4">
<p class="text-green-300 font-medium">Effect uploaded!</p>
<p class="text-green-400 text-sm mt-1">${data.name} v${data.version}</p>
<p class="text-gray-400 text-xs mt-2 font-mono">${data.cid}</p>
</div>
`;
setTimeout(() => {
window.location.reload();
}, 1500);
})
.catch(error => {
const resultDiv = document.getElementById('upload-result');
resultDiv.innerHTML = `
<div class="bg-red-900 border border-red-700 rounded-lg p-4">
<p class="text-red-300 font-medium">Upload failed</p>
<p class="text-red-400 text-sm mt-1">${error.message}</p>
</div>
`;
});
input.value = '';
}
document.getElementById('effect-upload')?.addEventListener('change', function() {
handleEffectUpload(this);
});
document.getElementById('effect-upload-empty')?.addEventListener('change', function() {
handleEffectUpload(this);
});
</script>
{% endblock %}