OpenTownieuseChatLogic.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;
OpenTownieTODOs.md1 match
29- [x] File write as a code embed
30- [x] str_replace as a diff view
31- [x] make image drop area invisible and bigger
32- [x] Give it all the code (except maybe .txt files) as initial context (like cursor sonnet max)
33- [x] I seem to have lost the delete file tool and instructions, try to find them back in history or re-create?
OpenTowniesystem_prompt.txt2 matches
172173- **Redirects:** Use `return new Response(null, { status: 302, headers: { Location: "/place/to/redirect" }})` instead of `Response.redirect` which is broken
174- **Images:** Avoid external images or base64 images. Use emojis, unicode symbols, or icon fonts/libraries instead
175- **AI Image:** To inline generate an AI image use: `<img src="https://maxm-imggenurl.web.val.run/the-description-of-your-image" />`
176- **Storage:** DO NOT use the Deno KV module for storage
177- **Browser APIs:** DO NOT use the `alert()`, `prompt()`, or `confirm()` methods
OpenTowniesend-message.ts11 matches
19}
2021const { messages, project, branchId, anthropicApiKey, selectedFiles, images } = await c.req.json();
22console.log("Original messages:", JSON.stringify(messages, null, 2));
23console.log("Images received:", JSON.stringify(images, null, 2));
2425// Check if API key is available
47let coreMessages = convertToCoreMessages(messages);
4849// If there are images, we need to add them to the last user message
50if (images && Array.isArray(images) && images.length > 0) {
51// Find the last user message
52const lastUserMessageIndex = coreMessages.findIndex(
70};
7172// Add each image to the content array using the correct ImagePart format
73for (const image of images) {
74if (image && image.url) {
75// Extract mime type from data URL if available
76let mimeType = undefined;
77if (image.url.startsWith("data:")) {
78const matches = image.url.match(/^data:([^;]+);/);
79if (matches && matches.length > 1) {
80mimeType = matches[1];
8384newUserMessage.content.push({
85type: "image",
86image: image.url,
87mimeType,
88});
OpenTownieMessagePart.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
322);
323}
324if (part.type === "image") {
325// Handle both formats: {image: {url: string}} and {image: string}
326const imageUrl = typeof part.image === "string"
327? part.image
328: part.image.url || (part.image as any).source?.url;
329330return (
331<div className="mt-2">
332<img
333src={imageUrl}
334alt="Uploaded image"
335className="max-h-64 max-w-full object-contain rounded"
336/>
338);
339}
340// Handle multiple images in a single part
341if (part.type === "images") {
342return <ImagePreview images={part.images.map(img => img.url)} />;
343}
344}
OpenTownieImageUpload.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"
OpenTownieChat.tsx8 matches
9import { ChatInput } from "./ChatInput.tsx";
10import { ApiKeyWarning } from "./ApiKeyWarning.tsx";
11import { processFiles } from "./ImageUpload.tsx";
12import { Preview } from "./Preview.tsx";
1326const [soundEnabled, setSoundEnabled] = useLocalStorage<boolean>("soundEnabled", true);
27const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
28const [images, setImages] = useState<(string | null)[]>([]);
29const [isDragging, setIsDragging] = useState(false);
3060bearerToken,
61selectedFiles,
62images,
63soundEnabled,
64});
95
96if (e.dataTransfer.files && !running) {
97processFiles(Array.from(e.dataTransfer.files), images, setImages);
98}
99};
125
126if (e.dataTransfer?.files && !running) {
127processFiles(Array.from(e.dataTransfer.files), images, setImages);
128}
129};
142document.removeEventListener('drop', handleDocumentDrop);
143};
144}, [images, running, setImages]);
145146return (
197handleSubmit={handleSubmit}
198running={running}
199images={images}
200setImages={setImages}
201isDragging={isDragging}
202/>
OpenTownieChatInput.tsx20 matches
1/** @jsxImportSource https://esm.sh/react@18.2.0?dev */
2import React, { useEffect, useRef, useState } from "https://esm.sh/react@18.2.0?dev";
3import { ImageUpload, processFiles } from "./ImageUpload.tsx";
45interface ChatInputProps {
8handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
9running: boolean;
10images: (string | null)[];
11setImages: React.Dispatch<React.SetStateAction<(string | null)[]>>;
12isDragging: boolean;
13}
18handleSubmit,
19running,
20images,
21setImages,
22isDragging,
23}: ChatInputProps) {
43const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
44e.preventDefault();
45const validImages = images.filter((img): img is string => typeof img === "string");
46console.log("Submitting with images:", validImages);
47if (input.trim() || validImages.length > 0) {
48handleSubmit(e);
49setImages([]);
50}
51};
5253const handleProcessFiles = (files: File[]) => {
54processFiles(files, images, setImages);
55};
5661disabled={running}
62>
63{images.length > 0 && (
64<ImageUpload
65images={images}
66setImages={setImages}
67processFiles={handleProcessFiles}
68/>
82if (e.key === "Enter" && !e.shiftKey && !isMobile) {
83e.preventDefault();
84if (input.trim() || images.filter(Boolean).length > 0) {
85handleFormSubmit(e as any);
86}
103}
104}}
105accept="image/*"
106multiple
107className="hidden"
139</div>
140
141{/* Attach images button below textarea */}
142{!running && (
143<div className="flex justify-start mt-1 mb-2">
150}
151}}
152title="Attach images"
153>
154<span>📎</span> Attach images
155</button>
156</div>
162<div className="bg-white p-6 rounded-lg shadow-lg text-center">
163<div className="text-2xl mb-2">📁</div>
164<div className="text-xl font-semibold">Drop images here</div>
165</div>
166</div>
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:
45
67For 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.