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";
56export function MessagePart({ part }: { part: NonNullable<Message["parts"]>[number] }) {
61);
62}
63if (part.type === "image") {
64// Handle both formats: {image: {url: string}} and {image: string}
65const imageUrl = typeof part.image === 'string'
66? part.image
67: part.image.url || (part.image as any).source?.url;
68
69return (
70<div className="mt-2">
71<img
72src={imageUrl}
73alt="Uploaded image"
74className="max-h-64 max-w-full object-contain rounded"
75/>
77);
78}
79// Handle multiple images in a single part
80if (part.type === "images") {
81return <ImagePreview images={part.images.map(img => img.url)} />;
82}
83}
OpenTownieindex.ts11 matches
51}
5253const { messages, project, branchId, anthropicApiKey, selectedFiles, images } = await c.req.json();
54console.log("Original messages:", JSON.stringify(messages, null, 2));
55console.log("Images received:", JSON.stringify(images, null, 2));
5657// Check if API key is available
79let coreMessages = convertToCoreMessages(messages);
8081// If there are images, we need to add them to the last user message
82if (images && Array.isArray(images) && images.length > 0) {
83// Find the last user message
84const lastUserMessageIndex = coreMessages.findIndex(
103});
104105// Add each image to the content array using the correct ImagePart format
106for (const image of images) {
107if (image && image.url) {
108// Extract mime type from data URL if available
109let mimeType = undefined;
110if (image.url.startsWith("data:")) {
111const matches = image.url.match(/^data:([^;]+);/);
112if (matches && matches.length > 1) {
113mimeType = matches[1];
116117newUserMessage.content.push({
118type: "image",
119image: image.url,
120mimeType,
121});
OpenTownieImageUpload.tsx49 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;
10}
1112export function ImageUpload({ images, setImages }: ImageUploadProps) {
13const fileInputRef = useRef<HTMLInputElement>(null);
14const [isDragging, setIsDragging] = useState(false);
23// Process the selected files
24const processFiles = async (files: File[]) => {
25// Filter for image files only
26const imageFiles = files.filter(file => file.type.startsWith('image/'));
27
28// Limit the number of images
29const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
30
31if (filesToProcess.length === 0) return;
3233// Add null placeholders for loading state
34const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
35setImages(newImages);
3637// Process each file
38const processedImages = await Promise.all(
39filesToProcess.map(async (file) => {
40return await readFileAsDataURL(file);
42);
4344// Replace null placeholders with actual images
45const updatedImages = [...images];
46processedImages.forEach((dataUrl, index) => {
47updatedImages[images.length + index] = dataUrl;
48});
49
50setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
51};
5257reader.onload = () => {
58const result = reader.result as string;
59console.log("Image loaded, size:", result.length, "bytes");
60resolve(result);
61};
93};
9495// Handle removing an image
96const removeImage = (index: number) => {
97const newImages = [...images];
98newImages.splice(index, 1);
99setImages(newImages);
100};
101102// Check if we've reached the image limit
103const isAtLimit = images.filter(Boolean).length >= PROMPT_IMAGE_LIMIT;
104105return (
106<div className="w-full">
107{/* Image previews */}
108{images.length > 0 && (
109<div className="flex flex-wrap gap-2 mb-2">
110{images.map((image, index) => (
111<div key={index} className="relative">
112{image ? (
113<div className="relative group">
114<img
115src={image}
116alt={`Uploaded ${index + 1}`}
117className="h-16 w-16 object-cover rounded border border-gray-300"
119<button
120className="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"
121onClick={() => removeImage(index)}
122title="Remove image"
123>
124×
151ref={fileInputRef}
152onChange={handleFileChange}
153accept="image/*"
154multiple
155className="hidden"
157<div className="text-sm text-gray-500">
158{isDragging ? (
159"Drop images here"
160) : (
161<>
162<span className="text-blue-500">Upload images</span> or drag and drop
163<div className="text-xs mt-1">
164{images.filter(Boolean).length}/{PROMPT_IMAGE_LIMIT} images
165</div>
166</>
173}
174175// Component to display images in messages
176export function ImagePreview({ images }: { images: string[] }) {
177const [expandedImage, setExpandedImage] = useState<string | null>(null);
178179if (!images || images.length === 0) return null;
180181return (
182<div className="mt-2">
183<div className="flex flex-wrap gap-2">
184{images.map((image, index) => (
185<div key={index} className="relative">
186<img
187src={image}
188alt={`Image ${index + 1}`}
189className="max-h-32 max-w-32 object-contain rounded cursor-pointer"
190onClick={() => setExpandedImage(image)}
191/>
192</div>
194</div>
195196{/* Modal for expanded image */}
197{expandedImage && (
198<div
199className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
200onClick={() => setExpandedImage(null)}
201>
202<div className="max-w-[90%] max-h-[90%]">
203<img
204src={expandedImage}
205alt="Expanded view"
206className="max-w-full max-h-full object-contain"
OpenTownieChat.tsx4 matches
24const [soundEnabled, setSoundEnabled] = useLocalStorage<boolean>("soundEnabled", true);
25const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
26const [images, setImages] = useState<(string | null)[]>([]);
2728// Use custom hook to fetch project files
57bearerToken,
58selectedFiles,
59images,
60soundEnabled,
61});
96handleSubmit={handleSubmit}
97running={running}
98images={images}
99setImages={setImages}
100/>
101</div>
OpenTownieChatInput.tsx13 matches
1/** @jsxImportSource https://esm.sh/react@18.2.0?dev */
2import React, { useState, useEffect } from "https://esm.sh/react@18.2.0?dev";
3import { ImageUpload } from "./ImageUpload.tsx";
45interface ChatInputProps {
8handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
9running: boolean;
10images: (string | null)[];
11setImages: React.Dispatch<React.SetStateAction<(string | null)[]>>;
12}
1317handleSubmit,
18running,
19images,
20setImages
21}: ChatInputProps) {
22// State to detect if the user is on a mobile device
40const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
41e.preventDefault();
42const validImages = images.filter((img): img is string => typeof img === "string");
43console.log("Submitting with images:", validImages);
44if (input.trim() || validImages.length > 0) {
45handleSubmit(e);
46setImages([]);
47}
48};
54disabled={running}
55>
56{(images.length > 0 || !running) && <ImageUpload images={images} setImages={setImages} />}
5758<div className="flex gap-2">
69if (e.key === "Enter" && !e.shiftKey && !isMobile) {
70e.preventDefault();
71if (input.trim() || images.filter(Boolean).length > 0) {
72handleFormSubmit(e as any);
73}
91}
92}}
93title="Attach images"
94>
95Attach images 📎
96</button>
97)}
sqliteExplorerAppREADME.md1 match
3View and interact with your Val Town SQLite data. It's based off Steve's excellent [SQLite Admin](https://www.val.town/v/stevekrouse/sqlite_admin?v=46) val, adding the ability to run SQLite queries directly in the interface. This new version has a revised UI and that's heavily inspired by [LibSQL Studio](https://github.com/invisal/libsql-studio) by [invisal](https://github.com/invisal). This is now more an SPA, with tables, queries and results showing up on the same page.
45
67## Install
tolerantGreenLadybugmain.tsx42 matches
5455function Kix() {
56const [image, setImage] = useState<string | null>(null);
57const [filter, setFilter] = useState<string>("none");
58const [error, setError] = useState<string | null>(null);
67const [cropArea, setCropArea] = useState({ x: 0, y: 0, width: 0, height: 0 });
68const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
69const imageRef = useRef<HTMLImageElement>(null);
70const canvasRef = useRef<HTMLCanvasElement>(null);
7172// Add missing methods
73const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
74const file = e.target.files?.[0];
75setError(null);
8182// Validate file type and size
83const validTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
84const maxSize = 10 * 1024 * 1024; // 10MB
8586if (!validTypes.includes(file.type)) {
87setError("Invalid file type. Please upload an image (JPEG, PNG, GIF, WebP)");
88return;
89}
99if (result && typeof result === "string") {
100// Validate the data URL
101if (result.startsWith("data:image/")) {
102setImage(result);
103} else {
104setError("Invalid image file");
105}
106}
133};
134135const rotateImage = () => {
136const canvas = canvasRef.current;
137const ctx = canvas?.getContext("2d");
138if (canvas && ctx && image) {
139const img = new Image();
140img.onload = () => {
141canvas.width = img.height;
143ctx.translate(canvas.width / 2, canvas.height / 2);
144ctx.rotate(Math.PI / 2);
145ctx.drawImage(img, -img.width / 2, -img.height / 2);
146setImage(canvas.toDataURL());
147};
148img.src = image;
149}
150};
155156const handleMouseDown = (e: React.MouseEvent) => {
157if (!isCropping || !imageRef.current) return;
158const rect = imageRef.current.getBoundingClientRect();
159const x = e.clientX - rect.left;
160const y = e.clientY - rect.top;
164165const handleMouseMove = (e: React.MouseEvent) => {
166if (!isCropping || !imageRef.current || !startPoint) return;
167const rect = imageRef.current.getBoundingClientRect();
168const currentX = e.clientX - rect.left;
169const currentY = e.clientY - rect.top;
177};
178179const cropImage = () => {
180const canvas = canvasRef.current;
181const ctx = canvas?.getContext("2d");
182const img = new Image();
183184if (canvas && ctx && image) {
185img.onload = () => {
186const scaleX = img.naturalWidth / imageRef.current!.width;
187const scaleY = img.naturalHeight / imageRef.current!.height;
188189canvas.width = cropArea.width * scaleX;
190canvas.height = cropArea.height * scaleY;
191192ctx.drawImage(
193img,
194cropArea.x * scaleX,
202);
203204setImage(canvas.toDataURL());
205setIsCropping(false);
206setCropArea({ x: 0, y: 0, width: 0, height: 0 });
207};
208img.src = image;
209}
210};
211212const downloadImage = () => {
213if (image) {
214const link = document.createElement("a");
215link.href = image;
216link.download = "edited_image.png";
217link.click();
218}
220221const removeBackground = async () => {
222if (!image) return;
223224setIsLoading(true);
231"Content-Type": "application/json",
232},
233body: JSON.stringify({ image }),
234});
235239240const result = await response.json();
241setImage(result.image);
242} catch (err) {
243setError(err instanceof Error ? err.message : "Unknown error occurred");
247};
248249const enhanceImage = async (type: string) => {
250if (!image) return;
251252setIsLoading(true);
261},
262body: JSON.stringify({
263image,
264enhanceType: type,
265}),
267268if (!response.ok) {
269throw new Error("Image enhancement failed");
270}
271272const result = await response.json();
273setImage(result.image);
274} catch (err) {
275setError(err instanceof Error ? err.message : "Unknown error occurred");
312<div className="header">
313<KixLogo />
314<h1>Kix Image Editor</h1>
315</div>
316<input
317type="file"
318accept="image/*"
319onChange={handleImageUpload}
320/>
321{error && (
344<html>
345<head>
346<title>Kix Image Editor</title>
347<meta name="viewport" content="width=device-width, initial-scale=1">
348<script src="https://esm.town/v/std/catch"></script>
OutfitPlannermain.tsx4 matches
55const newClothes = await Promise.all(
56Array.from(files).map(async (file) => {
57const imageUrl = URL.createObjectURL(file);
58return {
59id: Date.now(),
60name: file.name,
61url: imageUrl,
62type: file.type.startsWith('image/') ? 'image' : 'other',
63metadata: {
64color: '',
163type="file"
164multiple
165accept="image/*"
166onChange={handleFileUpload}
167style={{
48return new Response(svg, {
49headers: {
50"Content-Type": "image/svg+xml",
51"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
52"Pragma": "no-cache",
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;