17<meta charSet="UTF-8" />
18<meta name="viewport" content="width=device-width, initial-scale=1.0" />
19<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
2021<title>{title}</title>
28<meta property="og:description" content={description} />
29{/*
30<meta property="og:image" content={socialImage} />
31*/}
3233{/* Twitter */}
34<meta property="twitter:card" content="summary_large_image" />
35<meta property="twitter:url" content={BLOG_URL} />
36<meta property="twitter:title" content={title} />
37<meta property="twitter:description" content={description} />
38{/*
39<meta property="twitter:image" content={socialImage} />
40*/}
41
blogfavicon.svg.ts1 match
13{
14headers: {
15"Content-Type": "image/svg+xml",
16},
17},
1Eventually we should host all our images properly, but for now, drag and drop them here 👇
23* https://imagedelivery.net/iHX6Ovru0O7AjmyT5yZRoA/4d90a6f7-247c-4df4-3de6-928364e10000/public
4* https://imagedelivery.net/iHX6Ovru0O7AjmyT5yZRoA/f175100b-a190-4772-7056-04c09f273a00/public
spagindex.html30 matches
17<br />
1819Replace <code>name</code> with the name you gave the image.
20</p>
2199<audio controls style="display: none" id="preview-audio"></audio>
100</div>
101<input type="file" id="file" accept="audio/*, image/*" required />
102</label>
103<label for="singing">
138const nameInput = document.getElementById("name");
139const fileInput = document.getElementById("file");
140const previewImage = document.getElementById("preview");
141const previewAudio = document.getElementById("preview-audio");
142const uploadButton = document.getElementById("upload-button");
153reader.onload = () => {
154const dataUrl = reader.result;
155previewImage.src = dataUrl; // the form just grabs whatever is here and uploads this, which may even be audio
156previewAudio.src = dataUrl;
157};
166});
167168previewImage.addEventListener("load", () => {
169previewImage.style.display = "block";
170});
171174});
175176previewImage.addEventListener("error", () => {
177previewImage.style.display = "none";
178});
179226event.preventDefault();
227const name = encodeURIComponent(nameInput.value);
228const dataUrl = previewImage.src;
229const body = JSON.stringify({ name, dataUrl });
230uploadButton.disabled = true;
238nameInput.value = "";
239fileInput.value = "";
240previewImage.src = "";
241previewAudio.src = "";
242} else {
320321const contentType = response.headers.get("content-type");
322if (contentType.startsWith("image")) {
323const imageElement = document.createElement("img");
324imageElement.loading = "lazy";
325imageElement.style.width = "100%";
326imageElement.style.objectFit = "contain";
327imageElement.style.height = "300px";
328// src the response
329const blob = await response.blob();
330imageElement.src = URL.createObjectURL(blob);
331imageElement.onerror = () => {
332imageElement.remove();
333};
334previewContainer.appendChild(imageElement);
335} else if (contentType.startsWith("audio")) {
336const audioElement = document.createElement("audio");
368});
369370// const imageElement = document.createElement("img");
371// imageElement.loading = "lazy";
372// imageElement.src = `https://tode.party?${upload.name}`;
373// imageElement.style.width = "100%";
374// imageElement.style.objectFit = "contain";
375// imageElement.style.border = "1px solid rgb(159, 174, 238)";
376// imageElement.style.backgroundColor = "rgb(55, 67, 98)";
377// imageElement.style.height = "300px";
378// imageElement.onerror = () => {
379// imageElement.remove();
380// };
381
Open-ToownieuseChatLogic.ts4 matches
9bearerToken: string;
10selectedFiles: string[];
11images: (string | null)[];
12soundEnabled: boolean;
13}
19bearerToken,
20selectedFiles,
21images,
22soundEnabled,
23}: UseChatLogicProps) {
41anthropicApiKey,
42selectedFiles,
43images: images
44.filter((img): img is string => {
45const isValid = typeof img === "string" && img.startsWith("data:");
46if (!isValid && img !== null) {
47console.warn("Invalid image format:", img?.substring(0, 50) + "...");
48}
49return isValid;
Open-ToownieTODOs.md1 match
30- [x] File write as a code embed
31- [x] str_replace as a diff view
32- [x] make image drop area invisible and bigger
33- [x] Give it all the code (except maybe .txt files) as initial context (like cursor sonnet max)
34- [x] I seem to have lost the delete file tool and instructions, try to find them back in history or re-create?
Open-Toowniesystem_prompt.txt10 matches
12- If a section of code that you're working on is getting too complex, consider refactoring it into subcomponents
1314## Image Handling
1516- When users upload images, carefully analyze them to understand their content
17- Reference specific details from the images in your responses
18- If multiple images are uploaded, consider their relationship to each other
19- For code-related images (screenshots, diagrams), extract relevant information and incorporate it into your solutions
20- For UI mockups or design images, use them as reference for layout and design, use the colors from the image
21- When images contain text or code, transcribe relevant portions as needed
22- If image content is unclear, ask clarifying questions about what the user wants you to focus on
2324## Technical Requirements
95## Val Town Platform Specifics
96- **Redirects:** Use `return new Response(null, { status: 302, headers: { Location: "/place/to/redirect" }})` instead of `Response.redirect` which is broken
97- **Images:** Avoid external images or base64 images. Use emojis, unicode symbols, or icon fonts/libraries instead
98- For AI-generated images, use: `https://maxm-imggenurl.web.val.run/the-description-of-your-image`
99- **Storage:** DO NOT use the Deno KV module for storage
100- **Browser APIs:** DO NOT use the `alert()`, `prompt()`, or `confirm()` methods
Open-Toowniesend-message.ts11 matches
18}
1920const { messages, project, branchId, anthropicApiKey, selectedFiles, images } = await c.req.json();
21console.log("Original messages:", JSON.stringify(messages, null, 2));
22console.log("Images received:", JSON.stringify(images, null, 2));
2324// Check if API key is available
46let coreMessages = convertToCoreMessages(messages);
4748// If there are images, we need to add them to the last user message
49if (images && Array.isArray(images) && images.length > 0) {
50// Find the last user message
51const lastUserMessageIndex = coreMessages.findIndex(
69};
7071// Add each image to the content array using the correct ImagePart format
72for (const image of images) {
73if (image && image.url) {
74// Extract mime type from data URL if available
75let mimeType = undefined;
76if (image.url.startsWith("data:")) {
77const matches = image.url.match(/^data:([^;]+);/);
78if (matches && matches.length > 1) {
79mimeType = matches[1];
8283newUserMessage.content.push({
84type: "image",
85image: image.url,
86mimeType,
87});
Open-ToownieMessagePart.tsx11 matches
2import { type Message } from "https://esm.sh/@ai-sdk/react?dev&deps=react@18.2.0&react-dom@18.2.0";
3import ReactMarkdown from "https://esm.sh/react-markdown?dev&deps=react@18.2.0&react-dom@18.2.0";
4import { ImagePreview } from "./ImageUpload.tsx";
56// Helper function to detect language from file path
280);
281}
282if (part.type === "image") {
283// Handle both formats: {image: {url: string}} and {image: string}
284const imageUrl = typeof part.image === "string"
285? part.image
286: part.image.url || (part.image as any).source?.url;
287288return (
289<div className="mt-2">
290<img
291src={imageUrl}
292alt="Uploaded image"
293className="max-h-64 max-w-full object-contain rounded"
294/>
296);
297}
298// Handle multiple images in a single part
299if (part.type === "images") {
300return <ImagePreview images={part.images.map(img => img.url)} />;
301}
302}
Open-ToownieImageUpload.tsx47 matches
2import React, { useRef, useState } from "https://esm.sh/react@18.2.0?dev";
34// Maximum number of images that can be uploaded
5export const PROMPT_IMAGE_LIMIT = 5;
67interface ImageUploadProps {
8images: (string | null)[];
9setImages: (images: (string | null)[]) => void;
10processFiles: (files: File[]) => void;
11}
1213export function ImageUpload({ images, setImages, processFiles }: ImageUploadProps) {
14const fileInputRef = useRef<HTMLInputElement>(null);
1521};
2223// Handle removing an image
24const removeImage = (index: number) => {
25const newImages = [...images];
26newImages.splice(index, 1);
27setImages(newImages);
28};
2930// Check if we've reached the image limit
31const isAtLimit = images.filter(Boolean).length >= PROMPT_IMAGE_LIMIT;
3233return (
34<div className="w-full">
35{/* Image previews */}
36{images.length > 0 && (
37<div className="flex flex-wrap gap-2 mb-2">
38{images.map((image, index) => (
39<div key={index} className="relative">
40{image ? (
41<div className="relative group">
42<img
43src={image}
44alt={`Uploaded ${index + 1}`}
45className="h-16 w-16 object-cover rounded border border-gray-300"
47<button
48className="absolute top-0 right-0 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
49onClick={() => removeImage(index)}
50title="Remove image"
51>
52×
68ref={fileInputRef}
69onChange={handleFileChange}
70accept="image/*"
71multiple
72className="hidden"
8081// Process files utility function - moved from the component to be reusable
82export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
83// Filter for image files only
84const imageFiles = files.filter(file => file.type.startsWith('image/'));
85
86// Limit the number of images
87const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
88
89if (filesToProcess.length === 0) return;
9091// Add null placeholders for loading state
92const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
93setImages(newImages);
9495// Process each file
96const processedImages = await Promise.all(
97filesToProcess.map(async (file) => {
98return await readFileAsDataURL(file);
100);
101102// Replace null placeholders with actual images
103const updatedImages = [...images];
104processedImages.forEach((dataUrl, index) => {
105updatedImages[images.length + index] = dataUrl;
106});
107
108setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
109};
110115reader.onload = () => {
116const result = reader.result as string;
117console.log("Image loaded, size:", result.length, "bytes");
118resolve(result);
119};
123};
124125// Component to display images in messages
126export function ImagePreview({ images }: { images: string[] }) {
127const [expandedImage, setExpandedImage] = useState<string | null>(null);
128129if (!images || images.length === 0) return null;
130131return (
132<div className="mt-2">
133<div className="flex flex-wrap gap-2">
134{images.map((image, index) => (
135<div key={index} className="relative">
136<img
137src={image}
138alt={`Image ${index + 1}`}
139className="max-h-32 max-w-32 object-contain rounded cursor-pointer"
140onClick={() => setExpandedImage(image)}
141/>
142</div>
144</div>
145146{/* Modal for expanded image */}
147{expandedImage && (
148<div
149className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
150onClick={() => setExpandedImage(null)}
151>
152<div className="max-w-[90%] max-h-[90%]">
153<img
154src={expandedImage}
155alt="Expanded view"
156className="max-w-full max-h-full object-contain"