initial commit

This commit is contained in:
Renjaya Raga Zenta 2025-07-27 16:02:56 +07:00
commit c92383721d
16 changed files with 786 additions and 0 deletions

View file

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oh My Stitcher!</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f4f7f9;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 2rem;
}
.container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
}
h1 {
text-align: center;
color: #2c3e50;
}
form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem 1.5rem;
}
.full-width {
grid-column: 1 / -1;
}
label {
font-weight: 600;
margin-bottom: 0.25rem;
display: block;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #dcdcdc;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
button {
grid-column: 1 / -1;
padding: 1rem;
font-size: 1.1rem;
font-weight: 600;
color: white;
background-color: #3498db;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
#output-container {
margin-top: 1.5rem;
text-align: center;
width: 100%;
}
#output-container img {
max-width: 100%;
max-height: 80vh;
height: auto;
width: auto;
border: 1px solid #dcdcdc;
border-radius: 4px;
background-color: #f8f9fa;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
#status {
margin-top: 1rem;
width: 100%;
max-width: 500px;
box-sizing: border-box;
text-align: center;
font-weight: 500;
padding: 0.75rem;
}
.status-error {
color: #c0392b;
background-color: #f9e2e2;
}
.status-info {
color: #2980b9;
}
.status-success {
color: #27ae60;
background-color: #eaf7f0;
}
</style>
</head>
<body>
<div class="container">
<h1>Oh My Stitcher!</h1>
<form id="generator-form">
<div class="full-width">
<label for="canvasRect">Canvas Rect (e.g., A1:H12 or B5)</label>
<input type="text" id="canvasRect" value="A1:H12" required>
</div>
<div>
<label for="cropOffsetX">Crop Offset X</label>
<input type="number" id="cropOffsetX" value="0.25" step="0.01" min="0" max="1">
</div>
<div>
<label for="cropOffsetY">Crop Offset Y</label>
<input type="number" id="cropOffsetY" value="0.25" step="0.01" min="0" max="1">
</div>
<div>
<label for="cropSizeWidth">Crop Size Width</label>
<input type="number" id="cropSizeWidth" value="0.5" step="0.01" min="0" max="1">
</div>
<div>
<label for="cropSizeHeight">Crop Size Height</label>
<input type="number" id="cropSizeHeight" value="0.5" step="0.01" min="0" max="1">
</div>
<div class="full-width">
<label for="outputScale">Output Scale</label>
<input type="number" id="outputScale" value="0.05" step="0.01" min="0.01" max="0.05">
</div>
<button type="submit" id="generate-btn">Generate Image</button>
</form>
</div>
<div id="status"></div>
<div id="output-container"></div>
<script>
const form = document.getElementById('generator-form');
const generateBtn = document.getElementById('generate-btn');
const statusDiv = document.getElementById('status');
const outputContainer = document.getElementById('output-container');
form.addEventListener('submit', async (event) => {
event.preventDefault();
generateBtn.disabled = true;
generateBtn.textContent = 'Generating...';
statusDiv.textContent = 'Sending request...';
statusDiv.className = 'status-info';
const existingImg = outputContainer.querySelector('img');
if (existingImg) existingImg.remove();
const payload = {
canvas_rect: document.getElementById('canvasRect').value,
crop_offset: [
parseFloat(document.getElementById('cropOffsetX').value),
parseFloat(document.getElementById('cropOffsetY').value)
],
crop_size: [
parseFloat(document.getElementById('cropSizeWidth').value),
parseFloat(document.getElementById('cropSizeHeight').value)
],
output_scale: parseFloat(document.getElementById('outputScale').value)
};
const startTime = performance.now();
try {
const response = await fetch('/api/image/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const headersReceivedTime = performance.now();
const timeToFirstByte = (headersReceivedTime - startTime).toFixed(1);
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Server error (${response.status}): ${errorData.title || 'Unknown Error'}`);
}
statusDiv.textContent = `Headers received in ${timeToFirstByte} ms. Downloading image...`;
const imageBlob = await response.blob();
const endTime = performance.now();
const totalDuration = (endTime - startTime).toFixed(1);
const imageUrl = URL.createObjectURL(imageBlob);
const img = document.createElement('img');
img.src = imageUrl;
img.onload = () => URL.revokeObjectURL(img.src);
outputContainer.appendChild(img);
const sizeInMB = (imageBlob.size / 1024 / 1024).toFixed(2);
statusDiv.textContent = `Success! Image (${sizeInMB} MB) received in ${totalDuration} ms.`;
statusDiv.className = 'status-success';
} catch (error) {
console.error('Fetch error:', error);
statusDiv.textContent = error.message;
statusDiv.className = 'status-error';
} finally {
generateBtn.disabled = false;
generateBtn.textContent = 'Generate Image';
}
});
</script>
</body>
</html>