12export const PROMPT_IMAGE_LIMIT = 5;
34export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
5const imageFiles = files.filter(file => file.type.startsWith('image/'));
6const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
78if (filesToProcess.length === 0) return;
910const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
11setImages(newImages);
1213const processedImages = await Promise.all(
14filesToProcess.map(async (file) => {
15return await readFileAsDataURL(file);
17);
1819const updatedImages = [...images];
20processedImages.forEach((dataUrl, index) => {
21updatedImages[images.length + index] = dataUrl;
22});
2324setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
25};
2630reader.onload = () => {
31const result = reader.result as string;
32console.log("Image loaded, size:", result.length, "bytes");
33resolve(result);
34};
Towniefavicon.http.tsx1 match
13return new Response(svg, {
14headers: {
15"Content-Type": "image/svg+xml",
16},
17});
TownieChatRoute.tsx13 matches
10import { useUsageStats } from "../hooks/useUsageStats.ts";
11import { Messages } from "./Messages.tsx";
12import { InputBox, ImageDropContainer } from "./InputBox.tsx";
13import { PreviewFrame } from "./PreviewFrame.tsx";
14import { BranchSelect } from "./BranchSelect.tsx";
64}) {
65const { token, anthropicApiKey } = useAuth();
66const [images, setImages] = useState<(string|null)[]>([]);
67const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
68const { audio } = useContext(AppContext);
84bearerToken: token,
85selectedFiles,
86images,
87soundEnabled: audio,
88});
108109return (
110<ImageDropContainer
111running={running}
112images={images}
113setImages={setImages}>
114<div
115className="chat-container container">
131onSubmit={e => {
132handleSubmit(e);
133setImages([]);
134}}
135onCancel={handleStop}
136running={running}
137error={error}
138images={images}
139setImages={setImages}
140/>
141</div>
149rel="norefferer"
150className="block-link text-link lockup">
151{project.imageUrl ? (
152<img src={project.imageUrl} className="image-thumbnail" />
153) : (
154<div className="image-placeholder" />
155)}
156<div>
173</div>
174<pre hidden>{JSON.stringify(messages, null, 2)}</pre>
175</ImageDropContainer>
176);
177}
adportalcontinue.html149 matches
44cursor: not-allowed;
45}
46/* Image upload styles */
47#imagePreviewContainer {
48position: relative;
49display: inline-block;
50margin-top: 0.5rem;
51}
52#imagePreview {
53border: 1px solid #e5e7eb;
54border-radius: 0.375rem;
58line-height: 1;
59}
60#imagesList img {
61transition: transform 0.2s ease-in-out;
62}
63#imagesList img:hover {
64transform: scale(1.05);
65}
66.image-card {
67box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
68transition: box-shadow 0.2s ease-in-out;
69}
70.image-card:hover {
71box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
72}
136
137<div class="mt-6 border-t pt-6">
138<label class="block text-lg font-medium text-gray-800 mb-2">Upload Images</label>
139
140<div id="imageUploadLog" class="mb-4 p-3 bg-gray-50 border border-gray-200 rounded-md text-sm max-h-32 overflow-y-auto">
141<div class="text-gray-500">Image upload log:</div>
142</div>
143
144<div class="mb-4">
145<div class="flex items-center space-x-2">
146<label for="imageUpload" class="px-4 py-2 bg-blue-100 hover:bg-blue-200 rounded-md cursor-pointer text-sm font-medium border border-blue-300">
147Select Image
148</label>
149<span id="selectedFileName" class="text-sm text-gray-500">No file selected</span>
151<input
152type="file"
153id="imageUpload"
154accept="image/*"
155class="hidden"
156>
157</div>
158
159<div id="imagePreviewContainer" class="hidden mb-4">
160<div class="relative">
161<img id="imagePreview" class="max-w-full h-auto max-h-64 rounded-md border-2 border-blue-300" src="" alt="Preview">
162<button id="cancelUpload" class="absolute top-2 right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center">×</button>
163</div>
166<button
167type="button"
168id="uploadImageButton"
169class="px-4 py-2 bg-blue-500 text-white rounded-md disabled:bg-blue-300 disabled:cursor-not-allowed"
170disabled
171>
172Upload Image
173</button>
174
176</div>
177
178<div id="imagesContainer" class="mt-6 mb-6 p-4 bg-gray-50 border border-gray-200 rounded-md">
179<div class="flex justify-between items-center mb-3">
180<h3 class="text-lg font-medium text-gray-800">Image Gallery</h3>
181<div class="flex items-center space-x-2">
182<span id="imageCount" class="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">0 images</span>
183<button id="refreshImages" class="px-2 py-1 bg-green-100 text-green-800 rounded-md text-sm">Refresh</button>
184<button id="testBlobStorage" class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded-md text-sm">Test Storage</button>
185</div>
186</div>
187<div id="noImagesMessage" class="text-gray-500 text-center py-4">No images uploaded yet</div>
188<div id="imagesList" class="grid grid-cols-2 gap-4"></div>
189</div>
190
217const submissionId = document.getElementById('submissionId').value;
218
219// Image upload elements
220const imageUploadInput = document.getElementById('imageUpload');
221const selectedFileName = document.getElementById('selectedFileName');
222const imagePreviewContainer = document.getElementById('imagePreviewContainer');
223const imagePreview = document.getElementById('imagePreview');
224const cancelUploadButton = document.getElementById('cancelUpload');
225const uploadImageButton = document.getElementById('uploadImageButton');
226const uploadStatus = document.getElementById('uploadStatus');
227const imagesContainer = document.getElementById('imagesContainer');
228const imagesList = document.getElementById('imagesList');
229const imageCount = document.getElementById('imageCount');
230const noImagesMessage = document.getElementById('noImagesMessage');
231const imageUploadLog = document.getElementById('imageUploadLog');
232
233// Helper function to log to UI
237logEntry.className = 'text-xs text-gray-700 mb-1';
238logEntry.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
239imageUploadLog.appendChild(logEntry);
240imageUploadLog.scrollTop = imageUploadLog.scrollHeight;
241}
242
251logToUI(`Page loaded for submission ID: ${submissionId}`);
252
253// Load existing images
254loadImages();
255
256// Add refresh button handler
257document.getElementById('refreshImages').addEventListener('click', () => {
258logToUI('Manual refresh requested');
259loadImages();
260});
261
274logToUI(`Test key worked: ${result.data.testKeyWorked}`);
275
276if (result.data.imageKeys && result.data.imageKeys.length > 0) {
277logToUI(`Found ${result.data.imageKeys.length} image keys:`);
278result.data.imageKeys.forEach(key => {
279logToUI(`- ${key}`);
280});
281} else {
282logToUI('No image keys found');
283}
284}
290
291// Handle file selection
292imageUploadInput.addEventListener('change', (e) => {
293const file = e.target.files[0];
294if (!file) {
295logToUI('File selection canceled');
296resetImageUpload();
297return;
298}
300logToUI(`File selected: ${file.name} (${file.type}, ${formatFileSize(file.size)})`);
301selectedFileName.textContent = file.name;
302uploadImageButton.disabled = false;
303
304// Show image preview
305const reader = new FileReader();
306reader.onload = (e) => {
307imagePreview.src = e.target.result;
308imagePreviewContainer.classList.remove('hidden');
309logToUI('Preview generated');
310};
315cancelUploadButton.addEventListener('click', () => {
316logToUI('Upload canceled by user');
317resetImageUpload();
318});
319
320// Upload image
321uploadImageButton.addEventListener('click', async () => {
322const file = imageUploadInput.files[0];
323if (!file) return;
324
325uploadImageButton.disabled = true;
326uploadStatus.textContent = 'Uploading...';
327uploadStatus.className = 'mt-2 text-sm text-blue-500';
337logToUI(`Sending file to server...`);
338
339const response = await fetch('/upload-image', {
340method: 'POST',
341body: formData
346
347if (response.ok && result.success) {
348logToUI(`Upload successful! Image ID: ${result.data.id}`);
349
350// Log debug info if available
351if (result.debug) {
352logToUI(`Debug info: Found ${result.debug.imagesFound} images`);
353if (result.debug.imageIds && result.debug.imageIds.length > 0) {
354result.debug.imageIds.forEach(id => {
355logToUI(`Debug: Image ID found: ${id}`);
356});
357}
358}
359
360uploadStatus.textContent = 'Image uploaded successfully!';
361uploadStatus.className = 'mt-2 text-sm text-green-500';
362resetImageUpload();
363loadImages(); // Refresh the images list
364} else {
365logToUI(`Upload failed: ${result.message || 'Unknown error'}`);
367logToUI(`Error details: ${result.error}`);
368}
369uploadStatus.textContent = result.message || 'Failed to upload image';
370uploadStatus.className = 'mt-2 text-sm text-red-500';
371uploadImageButton.disabled = false;
372}
373} catch (error) {
374console.error('Error uploading image:', error);
375logToUI(`Upload error: ${error.message}`);
376uploadStatus.textContent = 'An error occurred while uploading';
377uploadStatus.className = 'mt-2 text-sm text-red-500';
378uploadImageButton.disabled = false;
379}
380});
381
382// Reset image upload form
383function resetImageUpload() {
384imageUploadInput.value = '';
385selectedFileName.textContent = 'No file selected';
386imagePreviewContainer.classList.add('hidden');
387uploadImageButton.disabled = true;
388uploadStatus.textContent = '';
389logToUI('Upload form reset');
390}
391
392// Load images for this submission
393async function loadImages() {
394logToUI(`Loading images for submission: ${submissionId}`);
395try {
396logToUI(`Fetching from /list-images?submissionId=${submissionId}`);
397const response = await fetch(`/list-images?submissionId=${submissionId}`);
398
399if (!response.ok) {
400logToUI(`Error response: ${response.status} ${response.statusText}`);
401imagesContainer.classList.remove('hidden');
402noImagesMessage.classList.remove('hidden');
403noImagesMessage.textContent = `Error: ${response.status} ${response.statusText}`;
404return;
405}
409
410if (result.success) {
411const imageCount = result.data.length;
412logToUI(`Found ${imageCount} images`);
413
414if (result.data && Array.isArray(result.data)) {
415result.data.forEach((img, idx) => {
416logToUI(`Image ${idx + 1}: ID=${img.id}, Name=${img.originalName}, Type=${img.contentType}`);
417});
418}
419
420// Update image count display
421document.getElementById('imageCount').textContent = `${imageCount} image${imageCount !== 1 ? 's' : ''}`;
422
423if (imageCount > 0) {
424imagesContainer.classList.remove('hidden');
425noImagesMessage.classList.add('hidden');
426renderImages(result.data);
427} else {
428imagesContainer.classList.remove('hidden');
429noImagesMessage.classList.remove('hidden');
430imagesList.innerHTML = '';
431logToUI('No images found for this submission');
432}
433} else {
434logToUI(`Error in response: ${result.message || 'Unknown error'}`);
435imagesContainer.classList.remove('hidden');
436noImagesMessage.classList.remove('hidden');
437noImagesMessage.textContent = result.message || 'Error loading images';
438}
439} catch (error) {
440console.error('Error loading images:', error);
441logToUI(`Error loading images: ${error.message}`);
442imagesContainer.classList.remove('hidden');
443noImagesMessage.classList.remove('hidden');
444noImagesMessage.textContent = 'Error loading images';
445}
446}
447
448// Render images in the list
449function renderImages(images) {
450logToUI(`Rendering ${images.length} images in gallery`);
451imagesList.innerHTML = '';
452
453images.forEach((image, index) => {
454logToUI(`Rendering image ${index + 1}: ${image.originalName} (${image.id})`);
455
456const imageCard = document.createElement('div');
457imageCard.className = 'relative border rounded-md overflow-hidden image-card';
458imageCard.dataset.imageId = image.id;
459
460const img = document.createElement('img');
461img.src = `/image?id=${image.id}`;
462img.alt = image.originalName;
463img.className = 'w-full h-32 object-cover';
464img.onerror = () => {
465logToUI(`Failed to load image: ${image.id}`);
466img.src = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22286%22%20height%3D%22180%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20286%20180%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_17a3f093956%20text%20%7B%20fill%3A%23999%3Bfont-weight%3Anormal%3Bfont-family%3A-apple-system%2CBlinkMacSystemFont%2C%26quot%3BSegoe%20UI%26quot%3B%2CRoboto%2C%26quot%3BHelvetica%20Neue%26quot%3B%2CArial%2C%26quot%3BNoto%20Sans%26quot%3B%2Csans-serif%2C%26quot%3BApple%20Color%20Emoji%26quot%3B%2C%26quot%3BSegoe%20UI%20Emoji%26quot%3B%2C%26quot%3BSegoe%20UI%20Symbol%26quot%3B%2C%26quot%3BNoto%20Color%20Emoji%26quot%3B%2C%20monospace%3Bfont-size%3A14pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_17a3f093956%22%3E%3Crect%20width%3D%22286%22%20height%3D%22180%22%20fill%3D%22%23373940%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22108.5390625%22%20y%3D%2296.3%22%3EImage%20Error%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E';
467};
468img.onload = () => {
469logToUI(`Image loaded successfully: ${image.id}`);
470};
471
473deleteButton.className = 'absolute top-2 right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center';
474deleteButton.textContent = '×';
475deleteButton.addEventListener('click', () => deleteImage(image.id));
476
477const caption = document.createElement('div');
478caption.className = 'p-2 text-xs truncate';
479caption.textContent = image.originalName;
480
481const size = document.createElement('div');
482size.className = 'px-2 pb-2 text-xs text-gray-500';
483size.textContent = formatFileSize(image.size);
484
485imageCard.appendChild(img);
486imageCard.appendChild(deleteButton);
487imageCard.appendChild(caption);
488imageCard.appendChild(size);
489
490imagesList.appendChild(imageCard);
491});
492}
493
494// Delete an image
495async function deleteImage(imageId) {
496if (!confirm('Are you sure you want to delete this image?')) {
497logToUI(`Deletion canceled for image: ${imageId}`);
498return;
499}
500
501logToUI(`Deleting image: ${imageId}`);
502
503try {
504const response = await fetch('/image', {
505method: 'DELETE',
506headers: {
508},
509body: JSON.stringify({
510imageId,
511submissionId
512})
516
517if (response.ok && result.success) {
518logToUI(`Image deleted successfully: ${imageId}`);
519
520// Remove the image from the list
521const imageElement = imagesList.querySelector(`[data-image-id="${imageId}"]`);
522if (imageElement) {
523imageElement.remove();
524logToUI('Image removed from gallery');
525}
526
527// Update image count
528const remainingImages = imagesList.children.length;
529document.getElementById('imageCount').textContent = `${remainingImages} image${remainingImages !== 1 ? 's' : ''}`;
530
531// If no images left, show the no images message
532if (remainingImages === 0) {
533noImagesMessage.classList.remove('hidden');
534logToUI('No images remaining in gallery');
535}
536} else {
537logToUI(`Failed to delete image: ${result.message || 'Unknown error'}`);
538alert(result.message || 'Failed to delete image');
539}
540} catch (error) {
541console.error('Error deleting image:', error);
542logToUI(`Error deleting image: ${error.message}`);
543alert('An error occurred while deleting the image');
544}
545}
6import { generateUniqueId } from "./utils.ts";
7import {
8storeImage,
9getImage,
10getImageMetadata,
11listSubmissionImages,
12deleteImage,
13ImageMetadata,
14testBlobStorage
15} from "./image-storage.ts";
16import { blob } from "https://esm.town/v/std/blob";
17103
104// Remove id from the fields to update
105const { id: _, images: __, ...fieldsToUpdate } = body;
106
107// Update the submission in the database
119});
120121// Upload an image
122app.post("/upload-image", async (c) => {
123try {
124console.log("[SERVER] Received image upload request");
125
126// Parse the multipart form data
162console.log(`[SERVER] File read as Uint8Array, length: ${uint8Array.length} bytes`);
163
164// Store the image
165console.log(`[SERVER] Calling storeImage function`);
166const metadata = await storeImage(
167uint8Array,
168file.name,
171);
172
173console.log(`[SERVER] Image stored successfully with ID: ${metadata.id}`);
174
175// Immediately try to list images to verify
176console.log(`[SERVER] Verifying image listing immediately after upload`);
177const images = await listSubmissionImages(submissionId);
178console.log(`[SERVER] Verification found ${images.length} images`);
179
180return c.json({
181success: true,
182message: "Image uploaded successfully",
183data: metadata,
184debug: {
185imagesFound: images.length,
186imageIds: images.map(img => img.id)
187}
188});
189} catch (error) {
190console.error("[SERVER] Error uploading image:", error);
191return c.json({
192success: false,
193message: "An error occurred uploading your image",
194error: error.toString()
195}, 500);
197});
198199// Get an image by ID
200app.get("/image", async (c) => {
201try {
202const imageId = c.req.query("id");
203console.log(`[SERVER] Image request received for ID: ${imageId}`);
204
205if (!imageId) {
206console.log("[SERVER] Missing image ID in request");
207return c.json({ success: false, message: "Image ID is required" }, 400);
208}
209
210// Get image metadata
211const metadata = await getImageMetadata(imageId);
212if (!metadata) {
213console.log(`[SERVER] Metadata not found for image: ${imageId}`);
214return c.json({ success: false, message: "Image not found" }, 404);
215}
216
217console.log(`[SERVER] Metadata retrieved for image: ${imageId}`);
218console.log(`[SERVER] Image details: name=${metadata.originalName}, type=${metadata.contentType}, size=${metadata.size} bytes`);
219
220// Get the image data
221const imageData = await getImage(imageId);
222if (!imageData) {
223console.log(`[SERVER] Image data not found for ID: ${imageId}`);
224return c.json({ success: false, message: "Image data not found" }, 404);
225}
226
227console.log(`[SERVER] Image data retrieved successfully, size: ${imageData.length} bytes`);
228
229// Return the image with proper content type
230console.log(`[SERVER] Serving image with content type: ${metadata.contentType}`);
231return new Response(imageData, {
232headers: {
233"Content-Type": metadata.contentType,
236});
237} catch (error) {
238console.error("[SERVER] Error getting image:", error);
239return c.json({ success: false, message: "An error occurred getting the image" }, 500);
240}
241});
242243// List images for a submission
244app.get("/list-images", async (c) => {
245try {
246const submissionId = c.req.query("submissionId");
247console.log(`[SERVER] List images request for submission: ${submissionId}`);
248
249if (!submissionId) {
250console.log("[SERVER] Missing submission ID in list images request");
251return c.json({ success: false, message: "Submission ID is required" }, 400);
252}
253
254// Get all images for this submission
255const images = await listSubmissionImages(submissionId);
256console.log(`[SERVER] Found ${images.length} images for submission: ${submissionId}`);
257
258// Log details of each image
259images.forEach((img, index) => {
260console.log(`[SERVER] Image ${index + 1}: id=${img.id}, name=${img.originalName}, type=${img.contentType}, size=${img.size} bytes`);
261});
262
263return c.json({
264success: true,
265data: images
266});
267} catch (error) {
268console.error("[SERVER] Error listing images:", error);
269return c.json({
270success: false,
271message: "An error occurred listing images"
272}, 500);
273}
274});
275276// Delete an image
277app.delete("/image", async (c) => {
278try {
279const body = await c.req.json();
280const { imageId, submissionId } = body;
281console.log(`[SERVER] Delete image request: imageId=${imageId}, submissionId=${submissionId}`);
282
283if (!imageId || !submissionId) {
284console.log("[SERVER] Missing required parameters for image deletion");
285return c.json({
286success: false,
287message: "Image ID and Submission ID are required"
288}, 400);
289}
290
291// Get image metadata to verify it belongs to this submission
292const metadata = await getImageMetadata(imageId);
293if (!metadata) {
294console.log(`[SERVER] Metadata not found for image: ${imageId}`);
295return c.json({
296success: false,
297message: "Image not found"
298}, 404);
299}
300
301if (metadata.submissionId !== submissionId) {
302console.log(`[SERVER] Image ${imageId} does not belong to submission ${submissionId}`);
303console.log(`[SERVER] Image belongs to submission: ${metadata.submissionId}`);
304return c.json({
305success: false,
306message: "Image does not belong to this submission"
307}, 403);
308}
309
310console.log(`[SERVER] Verified image ${imageId} belongs to submission ${submissionId}`);
311
312// Delete the image
313const success = await deleteImage(imageId);
314
315if (!success) {
316console.log(`[SERVER] Failed to delete image: ${imageId}`);
317return c.json({ success: false, message: "Failed to delete image" }, 500);
318}
319
320console.log(`[SERVER] Successfully deleted image: ${imageId}`);
321
322return c.json({
323success: true,
324message: "Image deleted successfully"
325});
326} catch (error) {
327console.error("[SERVER] Error deleting image:", error);
328return c.json({
329success: false,
330message: "An error occurred deleting the image"
331}, 500);
332}
adportalimage-storage.ts75 matches
1import { blob } from "https://esm.town/v/std/blob";
23// Prefix for image keys in blob storage
4const IMAGE_PREFIX = "laura_mepson_image_";
56// Interface for image metadata
7export interface ImageMetadata {
8id: string;
9originalName: string;
14}
1516// Generate a unique ID for an image
17export function generateImageId(submissionId: string): string {
18const timestamp = Date.now().toString(36);
19const randomPart = Math.random().toString(36).substring(2, 8);
20return `${IMAGE_PREFIX}${submissionId}_${timestamp}${randomPart}`;
21}
2223// Store an image in blob storage
24export async function storeImage(
25imageData: Uint8Array, // Raw image data
26fileName: string,
27contentType: string,
28submissionId: string
29): Promise<ImageMetadata> {
30try {
31console.log(`[BLOB STORAGE] Storing image: ${fileName} (${contentType}) for submission: ${submissionId}`);
32console.log(`[BLOB STORAGE] Image size: ${imageData.length} bytes`);
33
34// Generate a unique ID for the image
35const imageId = generateImageId(submissionId);
36console.log(`[BLOB STORAGE] Generated image ID: ${imageId}`);
37
38// Store the image data directly
39await blob.set(imageId, imageData, { contentType });
40console.log(`[BLOB STORAGE] Image data stored successfully with ID: ${imageId}`);
41
42// Verify the image was stored
43const storedData = await blob.get(imageId);
44if (storedData) {
45console.log(`[BLOB STORAGE] Verified image data is stored (${storedData.length} bytes)`);
46} else {
47console.log(`[BLOB STORAGE] WARNING: Could not verify image data storage`);
48}
49
50// Create metadata
51const metadata: ImageMetadata = {
52id: imageId,
53originalName: fileName,
54contentType,
55size: imageData.length,
56uploadedAt: new Date().toISOString(),
57submissionId
60console.log(`[BLOB STORAGE] Storing metadata: ${JSON.stringify(metadata)}`);
61
62// Store metadata alongside the image
63await blob.setJSON(`${imageId}_metadata`, metadata);
64console.log(`[BLOB STORAGE] Image metadata stored successfully`);
65
66// Verify metadata was stored
67const storedMetadata = await blob.getJSON(`${imageId}_metadata`);
68if (storedMetadata) {
69console.log(`[BLOB STORAGE] Verified metadata is stored: ${JSON.stringify(storedMetadata)}`);
79return metadata;
80} catch (error) {
81console.error("[BLOB STORAGE] Error storing image:", error);
82throw error;
83}
84}
8586// Get an image from blob storage
87export async function getImage(imageId: string): Promise<Uint8Array | null> {
88try {
89console.log(`[BLOB STORAGE] Retrieving image with ID: ${imageId}`);
90const imageData = await blob.get(imageId);
91
92if (imageData) {
93console.log(`[BLOB STORAGE] Image retrieved successfully, size: ${imageData.length} bytes`);
94} else {
95console.log(`[BLOB STORAGE] Image not found with ID: ${imageId}`);
96}
97
98return imageData;
99} catch (error) {
100console.error(`[BLOB STORAGE] Error getting image ${imageId}:`, error);
101return null;
102}
103}
104105// Get image metadata
106export async function getImageMetadata(imageId: string): Promise<ImageMetadata | null> {
107try {
108console.log(`[BLOB STORAGE] Retrieving metadata for image: ${imageId}`);
109const metadata = await blob.getJSON(`${imageId}_metadata`);
110
111if (metadata) {
112console.log(`[BLOB STORAGE] Metadata retrieved successfully for image: ${imageId}`);
113} else {
114console.log(`[BLOB STORAGE] Metadata not found for image: ${imageId}`);
115}
116
117return metadata as ImageMetadata;
118} catch (error) {
119console.error(`[BLOB STORAGE] Error getting image metadata for ${imageId}:`, error);
120return null;
121}
122}
123124// List all images for a submission
125export async function listSubmissionImages(submissionId: string): Promise<ImageMetadata[]> {
126try {
127console.log(`[BLOB STORAGE] Listing images for submission: ${submissionId}`);
128console.log(`[BLOB STORAGE] Using prefix: ${IMAGE_PREFIX}${submissionId}_`);
129
130// First, list all keys to see what's in the blob storage
133allKeys.forEach(key => console.log(`[BLOB STORAGE] - ${key}`));
134
135// List all blobs with the image prefix for this submission
136const keys = await blob.list(`${IMAGE_PREFIX}${submissionId}_`);
137console.log(`[BLOB STORAGE] Found ${keys.length} keys for submission: ${submissionId}`);
138keys.forEach(key => console.log(`[BLOB STORAGE] - ${key}`));
139
140// Filter out metadata keys and get only image keys
141const imageKeys = keys.filter(key => !key.endsWith('_metadata'));
142console.log(`[BLOB STORAGE] Found ${imageKeys.length} image keys after filtering`);
143imageKeys.forEach(key => console.log(`[BLOB STORAGE] - Image key: ${key}`));
144
145// Get metadata for each image
146const metadataPromises = imageKeys.map(key => getImageMetadata(key));
147const metadataResults = await Promise.all(metadataPromises);
148
149// Filter out any null results
150const validMetadata = metadataResults.filter(metadata => metadata !== null) as ImageMetadata[];
151console.log(`[BLOB STORAGE] Retrieved ${validMetadata.length} valid metadata objects`);
152validMetadata.forEach(meta => console.log(`[BLOB STORAGE] - Metadata: ${meta.id}, ${meta.originalName}`));
154return validMetadata;
155} catch (error) {
156console.error(`[BLOB STORAGE] Error listing submission images for ${submissionId}:`, error);
157return [];
158}
159}
160161// Delete an image and its metadata
162export async function deleteImage(imageId: string): Promise<boolean> {
163try {
164console.log(`[BLOB STORAGE] Deleting image with ID: ${imageId}`);
165
166await blob.delete(imageId);
167console.log(`[BLOB STORAGE] Image deleted: ${imageId}`);
168
169await blob.delete(`${imageId}_metadata`);
170console.log(`[BLOB STORAGE] Image metadata deleted: ${imageId}_metadata`);
171
172return true;
173} catch (error) {
174console.error(`[BLOB STORAGE] Error deleting image ${imageId}:`, error);
175return false;
176}
218await blob.delete(testKey);
219
220// 6. List image keys for this submission
221console.log(`[BLOB STORAGE TEST] Listing image keys for submission: ${submissionId}`);
222const imageKeys = await blob.list(`${IMAGE_PREFIX}${submissionId}_`);
223console.log(`[BLOB STORAGE TEST] Found ${imageKeys.length} image keys`);
224
225return {
229totalKeys: allKeys.length,
230prefixedKeys: prefixedKeys.length,
231imageKeys: imageKeys,
232testKeyWorked: true
233}
blob_adminREADME.md1 match
3This is a lightweight Blob Admin interface to view and debug your Blob data.
45
67To use this, fork it to your account.
blob_adminmain.tsx2 matches
60const { ValTown } = await import("npm:@valtown/sdk");
61const vt = new ValTown();
62const { email: authorEmail, profileImageUrl, username } = await vt.me.profile.retrieve();
63// const authorEmail = me.email;
64128129c.set("email", email);
130c.set("profile", { profileImageUrl, username });
131await next();
132};
blob_adminapp.tsx3 matches
437{profile && (
438<div className="flex items-center space-x-4">
439<img src={profile.profileImageUrl} alt="Profile" className="w-8 h-8 rounded-full" />
440<span>{profile.username}</span>
441<a href="/auth/logout" className="text-blue-400 hover:text-blue-300">Logout</a>
580alt="Blob content"
581className="max-w-full h-auto"
582onError={() => console.error("Error loading image")}
583/>
584</div>
630<li>Create public shareable links for blobs</li>
631<li>View and manage public folder</li>
632<li>Preview images directly in the interface</li>
633</ul>
634</div>
10- Email functionality to send users a unique link
11- Continuation page for adding more information
12- Image upload and storage functionality
1314## Project Structure
20- `landing.html` - Main landing page
21- `continue.html` - Page for adding additional information
22- `image-storage.ts` - Image storage and retrieval functionality
2324## Database Schema
445. User can click the link to add more information
456. Additional information is saved to the database
467. User can upload and manage images related to their submission
4748## URL Structure
51- Continuation page: `https://[username].web.val.run?action=continue&id=[unique_id]`
5253## Image Storage
5455Images are stored using Val Town's blob storage:
56- Images are stored with a unique ID
57- Metadata is stored alongside each image
58- Users can upload, view, and delete images
59- Images are associated with a specific submission
6061## Development Notes