1
2export const PROMPT_IMAGE_LIMIT = 5;
3
4export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
5 const imageFiles = files.filter(file => file.type.startsWith('image/'));
6 const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
7
8 if (filesToProcess.length === 0) return;
9
10 const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
11 setImages(newImages);
12
13 const processedImages = await Promise.all(
14 filesToProcess.map(async (file) => {
15 return await readFileAsDataURL(file);
17 );
18
19 const updatedImages = [...images];
20 processedImages.forEach((dataUrl, index) => {
21 updatedImages[images.length + index] = dataUrl;
22 });
23
24 setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
25};
26
30 reader.onload = () => {
31 const result = reader.result as string;
32 console.log("Image loaded, size:", result.length, "bytes");
33 resolve(result);
34 };
33 <button className="h6">Log out</button>
34 </form>
35 {user?.profileImageUrl && (
36 <img
37 src={user.profileImageUrl}
38 alt={user.username}
39 width="32"
10 return new Response(svg, {
11 headers: {
12 "Content-Type": "image/svg+xml",
13 },
14 });
11 total_cache_write_tokens: number;
12 total_price: number;
13 total_images: number;
14 used_inference_data?: boolean;
15}
40 <th>Cache Write</th>
41 <th>Total Price</th>
42 <th>Images</th>
43 </tr>
44 </thead>
54 <td>${formatNumber(row.total_cache_write_tokens)} ${row.used_inference_data ? '<span class="badge badge-info" title="Using inference data">I</span>' : ''}</td>
55 <td class="price">${formatPrice(row.total_price)} ${row.used_inference_data ? '<span class="badge badge-info" title="Using inference data">I</span>' : ''}</td>
56 <td>${formatNumber(row.total_images)}</td>
57 </tr>
58 `).join("")}
178
179- **Redirects:** Use `return new Response(null, { status: 302, headers: { Location: "/place/to/redirect" }})` instead of `Response.redirect` which is broken
180- **Images:** Avoid external images or base64 images. Use emojis, unicode symbols, or icon fonts/libraries instead
181- **AI Image:** To inline generate an AI image use: `<img src="https://maxm-imggenurl.web.val.run/the-description-of-your-image" />`
182- **Storage:** DO NOT use the Deno KV module for storage
183- **Browser APIs:** DO NOT use the `alert()`, `prompt()`, or `confirm()` methods
9import { useUsageStats } from "../hooks/useUsageStats.ts";
10import { Messages } from "./Messages.tsx";
11import { InputBox, ImageDropContainer } from "./InputBox.tsx";
12import { PreviewFrame } from "./PreviewFrame.tsx";
13import { BranchSelect } from "./BranchSelect.tsx";
66 refetch: () => void;
67}) {
68 const [images, setImages] = useState<(string|null)[]>([]);
69 const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
70 const { audio, user } = useContext(AppContext);
85 branchId,
86 selectedFiles,
87 images,
88 soundEnabled: audio,
89 });
109
110 return (
111 <ImageDropContainer
112 running={running}
113 images={images}
114 setImages={setImages}>
115 <div className="single-column-container">
116 <div className="single-sticky-header">
120 rel="norefferer"
121 className="block-link text-link lockup">
122 {project.imageUrl ? (
123 <img src={project.imageUrl} className="image-thumbnail" />
124 ) : user?.profileImageUrl ? (
125 <img
126 src={user.profileImageUrl}
127 className="avatar"
128 alt={user.username}
131 />
132 ) : (
133 <div className="image-placeholder" />
134 )}
135 <div>{project.name}</div>
178 onSubmit={e => {
179 handleSubmit(e);
180 setImages([]);
181 }}
182 onCancel={handleStop}
183 running={running}
184 error={error}
185 images={images}
186 setImages={setImages}
187 />
188 )}
190 </div>
191 </div>
192 </ImageDropContainer>
193 );
194}
3It's common to have code and types that are needed on both the frontend and the backend. It's important that you write this code in a particularly defensive way because it's limited by what both environments support:
4
5
6
7For example, you *cannot* use the `Deno` keyword. For imports, you can't use `npm:` specifiers, so we reccomend `https://esm.sh` because it works on the server & client. You *can* use TypeScript because that is transpiled in `/backend/index.ts` for the frontend. Most code that works on the frontend tends to work in Deno, because Deno is designed to support "web-standards", but there are definitely edge cases to look out for.
21## `favicon.svg`
22
23As of this writing Val Town only supports text files, which is why the favicon is an SVG and not an .ico or any other binary image format. If you need binary file storage, check out [Blob Storage](https://docs.val.town/std/blob/).
24
25## `components/`
73});
74
75// --- Blob Image Serving Routes ---
76
77// GET /api/images/:filename - Serve images from blob storage
78app.get("/api/images/:filename", async (c) => {
79 const filename = c.req.param("filename");
80
81 try {
82 // Get image data from blob storage
83 const imageData = await blob.get(filename);
84
85 if (!imageData) {
86 return c.json({ error: "Image not found" }, 404);
87 }
88
90 let contentType = "application/octet-stream"; // Default
91 if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
92 contentType = "image/jpeg";
93 } else if (filename.endsWith(".png")) {
94 contentType = "image/png";
95 } else if (filename.endsWith(".gif")) {
96 contentType = "image/gif";
97 } else if (filename.endsWith(".svg")) {
98 contentType = "image/svg+xml";
99 }
100
101 // Return the image with appropriate headers
102 return new Response(imageData, {
103 headers: {
104 "Content-Type": contentType,
107 });
108 } catch (error) {
109 console.error(`Error serving image ${filename}:`, error);
110 return c.json(
111 { error: "Failed to load image", details: error.message },
112 500,
113 );
10 href="/public/favicon.svg"
11 sizes="any"
12 type="image/svg+xml"
13 />
14 <link rel="preconnect" href="https://fonts.googleapis.com" />
36 height: 100%;
37 font-family: "Pixelify Sans", sans-serif;
38 image-rendering: pixelated;
39 }
40 body::before {
50 /* For pixel art aesthetic */
51 * {
52 image-rendering: pixelated;
53 }
54 </style>