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";
1213export function Chat({
25const [soundEnabled, setSoundEnabled] = useLocalStorage<boolean>("soundEnabled", true);
26const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
27const [images, setImages] = useState<(string | null)[]>([]);
28const [isDragging, setIsDragging] = useState(false);
2959bearerToken,
60selectedFiles,
61images,
62soundEnabled,
63});
94
95if (e.dataTransfer.files && !running) {
96processFiles(Array.from(e.dataTransfer.files), images, setImages);
97}
98};
124
125if (e.dataTransfer?.files && !running) {
126processFiles(Array.from(e.dataTransfer.files), images, setImages);
127}
128};
141document.removeEventListener('drop', handleDocumentDrop);
142};
143}, [images, running, setImages]);
144145return (
184handleSubmit={handleSubmit}
185running={running}
186images={images}
187setImages={setImages}
188isDragging={isDragging}
189/>
OpenTownieChatInput.tsx19 matches
1/** @jsxImportSource https://esm.sh/react@18.2.0?dev */
2import React, { useState, useEffect, useRef } 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}}
105title="Attach images"
106>
107Attach images 📎
108</button>
109)}
118}
119}}
120accept="image/*"
121multiple
122className="hidden"
157<div className="bg-white p-6 rounded-lg shadow-lg text-center">
158<div className="text-2xl mb-2">📁</div>
159<div className="text-xl font-semibold">Drop images here</div>
160</div>
161</div>
MiniAppStarterindex.ts2 matches
2import { serveFile } from "https://esm.town/v/std/utils@64-main/index.ts";
3import { Hono } from "npm:hono";
4import { embedMetadata, handleFarcasterEndpoints, iconUrl, name, ogImageUrl } from "./farcaster.ts";
56const app = new Hono();
16<meta name="fc:frame" content={JSON.stringify(embedMetadata(baseUrl))} />
17<link rel="icon" href={iconUrl} />
18<meta property="og:image" content={ogImageUrl} />
19</head>
20<body class="bg-white text-black dark:bg-black dark:text-white">
MiniAppStarterfarcaster.ts4 matches
3export const name = "Mini App Starter";
4export const iconUrl = "https://imgur.com/TrJLlwp.png";
5export const ogImageUrl = "https://imgur.com/xKVOVUE.png";
67export function embedMetadata(baseUrl: string) {
8return {
9version: "next",
10imageUrl: ogImageUrl,
11button: {
12title: name,
15name: name,
16url: baseUrl,
17splashImageUrl: iconUrl,
18splashBackgroundColor: "#111111",
19},
44"iconUrl": iconUrl,
45"homeUrl": baseUrl,
46"splashImageUrl": iconUrl,
47"splashBackgroundColor": "#111111",
48"webhookUrl": baseUrl + "/webhook",
hn_notifierindex.html1 match
16href="/public/favicon.svg"
17sizes="any"
18type="image/svg+xml"
19/>
20<style>
1import { createCanvas, loadImage } from "https://deno.land/x/canvas/mod.ts";
2import { Color, getPixels, Image, quantizeByMedianCut } from "https://esm.town/v/maxm/monke/mod.ts";
34const methods = [
10];
1112async function getImage(url: string) {
13const data = await fetch(url).then((e) => e.arrayBuffer()).then((e) => new Uint8Array(e));
14const image = await loadImage(data);
1516const canvas = createCanvas(image.width(), image.height());
1718const ctx = canvas.getContext("2d");
1920ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
21const d = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
22return new Image(d, canvas.width, canvas.height);
23}
2438// ctx.fillStyle = "red";
39// ctx.fillRect(10, 10, 200 - 20, 200 - 20);
40// const d = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
41const image = await getImage(
42imgUrl,
43);
44image.dither(["#000000", "#ffffff"].map((e) => new Color(e)), {
45method: methods.find((e) => e === url.searchParams.get("method")) || methods[0],
46});
47const i = createCanvas(image.width, image.height);
4849const ctx2 = i.getContext("2d");
5051const data = image.toImageData();
5253ctx2.putImageData(data, 0, 0);
54return new Response(i.toBuffer("image/png"), {
55headers: {
56"Content-Type": "image/png",
57},
58});
monkehistogram.ts1 match
3/**
4* Histogram of colors with reduced space
5* Effectively quantizes the image into 32768 colors
6*/
7export class ColorHistogram {
23/** Box blur with r = 3 */
4export function boxBlur(image: { pixels: Color[]; width: number }) {
5let i = 0;
6// pixels is an array of pixels with r, g, b values
7// width is the width of the image in pixels
8while (i < (image.pixels.length)) {
9const sumNeighbours = [0, 0, 0];
10let numAdded = 0;
1112if (i > image.width + 1) {
13// 1
14sumNeighbours[0] += image.pixels[i - image.width - 1].r;
15sumNeighbours[1] += image.pixels[i - image.width - 1].g;
16sumNeighbours[2] += image.pixels[i - image.width - 1].b;
17numAdded += 1;
18}
19if (i > image.width) {
20// 2
21sumNeighbours[0] += image.pixels[i - image.width].r;
22sumNeighbours[1] += image.pixels[i - image.width].g;
23sumNeighbours[2] += image.pixels[i - image.width].b;
2425// 3
26sumNeighbours[0] += image.pixels[i - image.width + 1].r;
27sumNeighbours[1] += image.pixels[i - image.width + 1].g;
28sumNeighbours[2] += image.pixels[i - image.width + 1].b;
29numAdded += 2;
30}
3132if (i % image.width > 0) {
33// 4
34sumNeighbours[0] += image.pixels[i - 1].r;
35sumNeighbours[1] += image.pixels[i - 1].g;
36sumNeighbours[2] += image.pixels[i - 1].b;
37numAdded += 1;
38}
39// 5
40sumNeighbours[0] += image.pixels[i].r;
41sumNeighbours[1] += image.pixels[i].g;
42sumNeighbours[2] += image.pixels[i].b;
43numAdded += 1;
4445if ((i % image.width) - image.width < -1) {
46// 6
47sumNeighbours[0] += image.pixels[i + 1].r;
48sumNeighbours[1] += image.pixels[i + 1].g;
49sumNeighbours[2] += image.pixels[i + 1].b;
50numAdded += 1;
51}
5253if (image.pixels.length - i > image.width) {
54// 7
55sumNeighbours[0] += image.pixels[i + image.width].r;
56sumNeighbours[1] += image.pixels[i + image.width].g;
57sumNeighbours[2] += image.pixels[i + image.width].b;
5859// 8
60sumNeighbours[0] += image.pixels[i + image.width - 1].r;
61sumNeighbours[1] += image.pixels[i + image.width - 1].g;
62sumNeighbours[2] += image.pixels[i + image.width - 1].b;
63numAdded += 2;
6465if (image.pixels.length - i > image.width + 1) {
66// 9
67sumNeighbours[0] += image.pixels[i + image.width + 1].r;
68sumNeighbours[1] += image.pixels[i + image.width + 1].g;
69sumNeighbours[2] += image.pixels[i + image.width + 1].b;
70numAdded += 1;
71}
72}
7374image.pixels[i].r = Math.trunc(sumNeighbours[0] / numAdded);
75image.pixels[i].g = Math.trunc(sumNeighbours[1] / numAdded);
76image.pixels[i].b = Math.trunc(sumNeighbours[2] / numAdded);
77i += 1;
78}
34/**
5* Dither the image into a smaller palette
6* Uses two-row Sierra matrix
7*/
13let i = 0;
14// pixels is an array of pixels with r, g, b values
15// width is the width of the image in pixels
16while (i < (pixels.length)) {
17const newC = findClosestColor(pixels[i], palette);
6465/**
66* Dither the image into a smaller palette
67* Uses Sierra lite matrix
68* Use twoRowSierraDither for more accuracy
75let i = pixels.length - 1;
76// pixels is an array of pixels with r, g, b values
77// width is the width of the image in pixels
78while (i >= 0) {
79const newC = findClosestColor(pixels[i], palette);