townie-126styles.css11 matches
688background-color: var(--highlight);
689}
690.card-image {
691display: flex;
692align-items: center;
720}
721722.image-placeholder,
723.image-thumbnail {
724flex-shrink: 0;
725width: 40px;
728object-fit: cover;
729}
730.image-placeholder {
731background-color: var(--muted);
732}
739}
740741.image-row {
742display: flex;
743gap: var(--space-1);
744}
745.input-image {
746position: relative;
747border: 1px solid var(--muted);
748border-radius: 6px;
749}
750.remove-image-button {
751position: absolute;
752top: 0;
761opacity: 0;
762}
763.input-image:hover .remove-image-button {
764opacity: 1;
765}
766767.image-drop-overlay {
768position: fixed;
769top: 0;
778justify-content: center;
779}
780.image-drop-inner {
781padding: var(--space-2);
782background-color: var(--background);
858}
859860.transition, .input-box, .icon-button, .button, .remove-image-button {
861transition-property: color, background-color, border-color, opacity;
862transition-duration: 200ms;
townie-126send-message.ts12 matches
29}
3031const { messages, project, branchId, anthropicApiKey, selectedFiles, images } = await c.req.json();
3233// do we want to allow user-provided tokens still
55branch_id: branchId,
56val_id: project.id,
57num_images: images?.length || 0,
58model,
59});
87townie_usage_id: rowid,
88townie_our_api_token: our_api_token,
89townie_num_images: images?.length || 0,
90townie_selected_files_count: selectedFiles?.length || 0,
91},
105let coreMessages = convertToCoreMessages(messages);
106107// If there are images, we need to add them to the last user message
108if (images && Array.isArray(images) && images.length > 0) {
109// Find the last user message
110const lastUserMessageIndex = coreMessages.findIndex(
128};
129130// Add each image to the content array using the correct ImagePart format
131for (const image of images) {
132if (image && image.url) {
133// Extract mime type from data URL if available
134let mimeType = undefined;
135if (image.url.startsWith("data:")) {
136const matches = image.url.match(/^data:([^;]+);/);
137if (matches && matches.length > 1) {
138mimeType = matches[1];
141142newUserMessage.content.push({
143type: "image",
144image: image.url,
145mimeType,
146});
townie-126schema.tsx2 matches
19price?: number;
20finish_reason?: string;
21num_images?: number;
22our_api_token: boolean;
23}
44price REAL,
45finish_reason TEXT,
46num_images INTEGER,
47our_api_token INTEGER NOT NULL,
48finish_timestamp INTEGER
townie-126requests.ts3 matches
16price: number | null;
17finish_reason: string | null;
18num_images: number | null;
19our_api_token: number;
20}
68<th>Price</th>
69<th>Finish</th>
70<th>Images</th>
71<th>Our API</th>
72</tr>
87<td class="price">${formatPrice(row.price)}</td>
88<td>${row.finish_reason || '-'}</td>
89<td>${formatNumber(row.num_images)}</td>
90<td>${formatBoolean(row.our_api_token)}</td>
91</tr>
townie-126queries.tsx4 matches
141model,
142our_api_token,
143num_images,
144tablePrefix = "",
145}: {
149model: string;
150our_api_token: boolean;
151num_images: number;
152tablePrefix: string;
153}) {
162model,
163our_api_token,
164num_images
165) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
166`,
173model,
174our_api_token ? 1 : 0,
175num_images,
176],
177);
townie-126queries_test.tsx1 match
29model,
30our_api_token,
31num_images
32) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
33`,
townie-126ProjectsRoute.tsx7 matches
44user: {
45username: string;
46profileImageUrl: string | null;
47};
48project: any;
50return (
51<div className="card">
52{project.imageUrl ? (
53<img src={project.imageUrl} className="card-image" />
54) : user.profileImageUrl ? (
55<div className="card-image">
56<img
57src={user.profileImageUrl}
58width="48"
59height="48"
62</div>
63) : (
64<div className="card-image placeholder" />
65)}
66<div className="card-body">
townie-126InputBox.tsx47 matches
3import { Link } from "react-router";
4import { PlusIcon, ArrowUpIcon, Square, XIcon } from "./icons.tsx";
5import { processFiles } from "../utils/images.ts";
67export function InputBox ({
12running,
13error,
14images,
15setImages,
16} : {
17value: string;
21running: boolean;
22error: any;
23images: (string|null)[];
24setImages: (images: (string|null)[]) => void;
25}) {
26const form = useRef(null);
62/>
63</div>
64<ImageRow images={images} setImages={setImages} />
65<div className="toolbar">
66<UploadButton
67disabled={running}
68images={images}
69setImages={setImages}
70/>
71<div className="spacer" />
94}
9596export function ImageDropContainer ({
97images,
98setImages,
99running,
100children,
101}: {
102images: (string|null)[];
103setImages: (images: (string|null)[]) => void;
104running: boolean;
105children: React.ReactNode;
106}) {
107const dragging = useImageDrop({ images, setImages, running });
108109return (
111{children}
112{dragging && (
113<div className="image-drop-overlay">
114<div className="image-drop-inner">
115Drop images here to upload
116</div>
117</div>
121}
122123export function useImageDrop ({ images, setImages, running }: {
124images: (string|null)[];
125setImages(images: (string|null)[]) => void;
126running: boolean;
127}) {
149setDragging(false);
150if (e.dataTransfer?.files && !running) {
151processFiles(Array.from(e.dataTransfer.files), images, setImages);
152}
153}
165document.removeEventListener("drop", onDrop);
166}
167}, [images, setImages, running]);
168169return dragging;
170}
171172function ImageRow ({ images, setImages }: {
173images: (string|null)[];
174setImages: (images: (string|null)[]) => void;
175}) {
176return (
177<div className="image-row">
178{images.map((image, i) => (
179<Thumbnail
180key={i}
181image={image}
182onRemove={() => {
183setImages([
184...images.slice(0, i),
185...images.slice(i + 1),
186]);
187}}
192}
193194function Thumbnail ({ image, onRemove }: {
195image: string|null;
196onRemove: () => void;
197}) {
198if (!image) return null;
199200return (
201<div className="input-image">
202<img
203src={image}
204alt="User uploaded image"
205className="image-thumbnail"
206/>
207<button
208type="button"
209title="Remove image"
210className="remove-image-button"
211onClick={onRemove}
212>
218219function UploadButton ({
220images,
221setImages,
222disabled,
223}: {
224images: (string|null)[];
225setImages: (images: (string|null)[]) => void;
226disabled: boolean;
227}) {
232<button
233type="button"
234title="Upload image"
235disabled={disabled}
236onClick={e => {
240<PlusIcon />
241<div className="sr-only">
242Upload image
243</div>
244</button>
249onChange={e => {
250if (e.target.files) {
251processFiles(Array.from(e.target.files), images, setImages);
252}
253}}
townie-126images.ts12 matches
12export const PROMPT_IMAGE_LIMIT = 5;
34export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
5const imageFiles = files.filter(file => file.type.startsWith('image/'));
6const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
78if (filesToProcess.length === 0) return;
910const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
11setImages(newImages);
1213const processedImages = await Promise.all(
14filesToProcess.map(async (file) => {
15return await readFileAsDataURL(file);
17);
1819const updatedImages = [...images];
20processedImages.forEach((dataUrl, index) => {
21updatedImages[images.length + index] = dataUrl;
22});
2324setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
25};
2630reader.onload = () => {
31const result = reader.result as string;
32console.log("Image loaded, size:", result.length, "bytes");
33resolve(result);
34};
townie-126Header.tsx2 matches
7374function Avatar ({ user }) {
75if (!user?.profileImageUrl) {
76return (
77<div className="avatar" />
81return (
82<img
83src={user.profileImageUrl}
84alt={user.username}
85width="32"