feat: responsive side-by-side input/output layout

- Desktop: input left, output right
- Mobile (<600px): stacked vertically

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-07 14:15:32 +00:00
parent dc9c13ffd9
commit 73908b318d

123
server.py
View File

@@ -406,9 +406,16 @@ UI_HTML = """
.status.running { background: #4d4d1a; color: #facc15; }
.status.failed { background: #4d1a1a; color: #f87171; }
.status.pending { background: #333; color: #888; }
.media-container { margin-top: 12px; }
.media-row { display: flex; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
.media-box { flex: 1; min-width: 200px; }
.media-box label { font-size: 11px; color: #666; display: block; margin-bottom: 4px; }
.media-container { }
.media-container img, .media-container video {
max-width: 100%; max-height: 400px; border-radius: 4px;
max-width: 100%; max-height: 300px; border-radius: 4px;
}
@media (max-width: 600px) {
.media-row { flex-direction: column; }
.media-box { min-width: 100%; }
}
.hash { font-family: monospace; font-size: 11px; color: #666; }
.info { font-size: 13px; color: #aaa; }
@@ -466,32 +473,44 @@ async def ui_runs():
</div>
''')
# Show input
if run.inputs:
input_hash = run.inputs[0]
html_parts.append(f'<div class="hash">Input: {input_hash[:32]}...</div>')
input_cache_path = CACHE_DIR / input_hash
if input_cache_path.exists():
input_media_type = detect_media_type(input_cache_path)
html_parts.append('<div class="media-container">')
if input_media_type == "video":
html_parts.append(f'<video src="/cache/{input_hash}" controls muted loop style="max-height:200px;"></video>')
elif input_media_type == "image":
html_parts.append(f'<img src="/cache/{input_hash}" alt="input" style="max-height:200px;">')
html_parts.append('</div>')
# Show input and output side by side
has_input = run.inputs and (CACHE_DIR / run.inputs[0]).exists()
has_output = run.status == "completed" and run.output_hash and (CACHE_DIR / run.output_hash).exists()
# Show output if completed
if run.status == "completed" and run.output_hash:
cache_path = CACHE_DIR / run.output_hash
if cache_path.exists():
media_type = detect_media_type(cache_path)
html_parts.append(f'<div class="hash">Output: {run.output_hash[:32]}...</div>')
html_parts.append('<div class="media-container">')
if media_type == "video":
html_parts.append(f'<video src="/cache/{run.output_hash}" controls autoplay muted loop></video>')
elif media_type == "image":
html_parts.append(f'<img src="/cache/{run.output_hash}" alt="{run.output_name}">')
html_parts.append('</div>')
if has_input or has_output:
html_parts.append('<div class="media-row">')
# Input box
if has_input:
input_hash = run.inputs[0]
input_media_type = detect_media_type(CACHE_DIR / input_hash)
html_parts.append(f'''
<div class="media-box">
<label>Input: {input_hash[:24]}...</label>
<div class="media-container">
''')
if input_media_type == "video":
html_parts.append(f'<video src="/cache/{input_hash}" controls muted loop></video>')
elif input_media_type == "image":
html_parts.append(f'<img src="/cache/{input_hash}" alt="input">')
html_parts.append('</div></div>')
# Output box
if has_output:
output_hash = run.output_hash
output_media_type = detect_media_type(CACHE_DIR / output_hash)
html_parts.append(f'''
<div class="media-box">
<label>Output: {output_hash[:24]}...</label>
<div class="media-container">
''')
if output_media_type == "video":
html_parts.append(f'<video src="/cache/{output_hash}" controls autoplay muted loop></video>')
elif output_media_type == "image":
html_parts.append(f'<img src="/cache/{output_hash}" alt="output">')
html_parts.append('</div></div>')
html_parts.append('</div>')
# Show error if failed
if run.status == "failed" and run.error:
@@ -544,30 +563,34 @@ async def ui_run_detail(run_id: str):
</div>
'''
if run.inputs:
input_hash = run.inputs[0]
html += f'<div class="hash">Input: {input_hash[:32]}...</div>'
input_cache_path = CACHE_DIR / input_hash
if input_cache_path.exists():
input_media_type = detect_media_type(input_cache_path)
html += '<div class="media-container">'
if input_media_type == "video":
html += f'<video src="/cache/{input_hash}" controls muted loop style="max-height:200px;"></video>'
elif input_media_type == "image":
html += f'<img src="/cache/{input_hash}" alt="input" style="max-height:200px;">'
html += '</div>'
# Show input and output side by side
has_input = run.inputs and (CACHE_DIR / run.inputs[0]).exists()
has_output = run.status == "completed" and run.output_hash and (CACHE_DIR / run.output_hash).exists()
if run.status == "completed" and run.output_hash:
cache_path = CACHE_DIR / run.output_hash
if cache_path.exists():
media_type = detect_media_type(cache_path)
html += f'<div class="hash">Output: {run.output_hash[:32]}...</div>'
html += '<div class="media-container">'
if media_type == "video":
html += f'<video src="/cache/{run.output_hash}" controls autoplay muted loop></video>'
elif media_type == "image":
html += f'<img src="/cache/{run.output_hash}" alt="{run.output_name}">'
html += '</div>'
if has_input or has_output:
html += '<div class="media-row">'
if has_input:
input_hash = run.inputs[0]
input_media_type = detect_media_type(CACHE_DIR / input_hash)
html += f'<div class="media-box"><label>Input: {input_hash[:24]}...</label><div class="media-container">'
if input_media_type == "video":
html += f'<video src="/cache/{input_hash}" controls muted loop></video>'
elif input_media_type == "image":
html += f'<img src="/cache/{input_hash}" alt="input">'
html += '</div></div>'
if has_output:
output_hash = run.output_hash
output_media_type = detect_media_type(CACHE_DIR / output_hash)
html += f'<div class="media-box"><label>Output: {output_hash[:24]}...</label><div class="media-container">'
if output_media_type == "video":
html += f'<video src="/cache/{output_hash}" controls autoplay muted loop></video>'
elif output_media_type == "image":
html += f'<img src="/cache/{output_hash}" alt="output">'
html += '</div></div>'
html += '</div>'
if run.status == "failed" and run.error:
html += f'<div class="info" style="color: #f87171;">Error: {run.error}</div>'