4// DOM Elements
5const startCameraBtn = document.getElementById('start-camera');
6const uploadImageBtn = document.getElementById('upload-image');
7const fileInput = document.getElementById('file-input');
8const cameraView = document.getElementById('camera-view');
10const captureBtn = document.getElementById('capture-btn');
11const cameraOverlay = document.querySelector('.camera-overlay');
12const previewImage = document.getElementById('preview-image');
13const analysisForm = document.getElementById('analysis-form');
14const imageData = document.getElementById('image-data');
15
16// Camera stream reference
28cameraView.classList.remove('hidden');
29cameraOverlay.classList.remove('hidden');
30previewImage.classList.add('hidden');
31analysisForm.classList.add('hidden');
32
35} catch (error) {
36console.error('Error accessing camera:', error);
37alert('Could not access camera. Please check permissions or try uploading an image instead.');
38}
39});
47// Draw video frame to canvas
48const context = cameraCanvas.getContext('2d');
49context.drawImage(cameraView, 0, 0, cameraCanvas.width, cameraCanvas.height);
50
51// Convert to data URL
52const dataUrl = cameraCanvas.toDataURL('image/jpeg');
53
54// Stop camera stream
61cameraView.classList.add('hidden');
62cameraOverlay.classList.add('hidden');
63previewImage.src = dataUrl;
64previewImage.classList.remove('hidden');
65
66// Prepare form data - extract base64 data without the prefix
67const base64Data = dataUrl.split(',')[1];
68prepareImageForUpload(base64Data);
69});
70
71// Handle image upload
72uploadImageBtn.addEventListener('click', () => {
73fileInput.click();
74});
81reader.onload = (event) => {
82const dataUrl = event.target.result;
83previewImage.src = dataUrl;
84previewImage.classList.remove('hidden');
85cameraView.classList.add('hidden');
86cameraOverlay.classList.add('hidden');
88// Extract base64 data without the prefix
89const base64Data = dataUrl.toString().split(',')[1];
90prepareImageForUpload(base64Data);
91};
92
95});
96
97// Prepare image for upload
98function prepareImageForUpload(base64Data) {
99// Show the analysis form
100analysisForm.classList.remove('hidden');
102// Set the base64 data as a value in a hidden input
103// This is a workaround since we can't directly set files programmatically
104const dataInput = document.getElementById('image-data');
105dataInput.value = base64Data;
106}
1// Vision service for analyzing food images using Llama 4 Maverick via Groq API
23interface FoodAnalysis {
13}
1415// Analyze a food image using Groq API with Llama 4 Maverick
16export async function analyzeImage(base64Image: string): Promise<FoodAnalysis> {
17const apiKey = Deno.env.get("GROQ_API_KEY");
18
23try {
24// Ensure the base64 string is properly formatted for the API
25const imageUrl = base64Image.startsWith('data:')
26? base64Image
27: `data:image/jpeg;base64,${base64Image}`;
28
29const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
41{
42type: "text",
43text: "Analyze this food image. Identify what food items are present, estimate the calories, and provide descriptive tags. Return the result as a JSON object with the following fields: calories (number), description (string), tags (array of strings), and nutritionalInfo (object with protein, carbs, fat, fiber in grams). Be as accurate as possible with the calorie estimation based on portion size."
44},
45{
46type: "image_url",
47image_url: {
48url: imageUrl
49}
50}
77};
78} catch (error) {
79console.error("Error analyzing image:", error);
80
81// Return a fallback response
82return {
83calories: 0,
84description: "Could not analyze the image",
85tags: ["error"],
86nutritionalInfo: {}
calpicsindex.html7 matches
19</script>
20<!-- Favicon link -->
21<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>"
22<style>
23.camera-container {
79<video id="camera-view" autoplay playsinline class="hidden"></video>
80<canvas id="camera-canvas" class="hidden"></canvas>
81<img id="preview-image" class="w-full h-auto rounded-lg mb-4 hidden" alt="Food preview">
82
83<div class="camera-controls flex justify-center gap-4 mb-4">
88Take Photo
89</button>
90<input type="file" id="file-input" accept="image/*" class="hidden">
91<button id="upload-image" class="bg-gray-200 text-gray-800 px-4 py-2 rounded-md hover:bg-gray-300 transition">
92<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-1" viewBox="0 0 20 20" fill="currentColor">
93<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM6.293 6.707a1 1 0 010-1.414l3-3a1 1 0 011.414 0l3 3a1 1 0 01-1.414 1.414L11 5.414V13a1 1 0 11-2 0V5.414L7.707 6.707a1 1 0 01-1.414 0z" clip-rule="evenodd" />
94</svg>
95Upload Image
96</button>
97</div>
105<form id="food-form" hx-post="/api/analyze" hx-encoding="multipart/form-data" hx-target="#analysis-result" hx-swap="innerHTML" hx-indicator="#loading-indicator">
106<input type="hidden" name="userId" value="anonymous">
107<input type="hidden" name="image" id="image-data">
108<button type="submit" class="w-full bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition">
109Analyze Food
202{{^length}}
203<div class="text-center py-4 text-gray-500">
204No food entries yet. Start by analyzing a food image!
205</div>
206{{/length}}
9## Types
1011- `FoodAnalysisResult`: The result of analyzing a food image
12- `FoodEntryData`: Data structure for a food entry in the database
13- `StatsData`: Data structure for calorie statistics
1213- Camera integration for taking food photos
14- File upload for existing food images
15- Real-time food analysis with AI
16- Food entry history display
26## How It Works
27281. The user can either take a photo using their device camera or upload an existing image
292. The image is sent to the backend for analysis
303. The AI analyzes the image and returns information about the food
314. The results are displayed to the user
325. The entry is saved to the database for future reference
14## API Endpoints
1516- `POST /api/analyze`: Analyze a food image
17- Accepts: `multipart/form-data` with `image` file and optional `userId`
18- Returns: Analysis results including calories, description, and tags
19
calpicsqueries.ts3 matches
33const result = await sqlite.execute(
34`INSERT INTO ${TABLES.FOOD_ENTRIES}
35(user_id, image_data, calories, description, tags)
36VALUES (?, ?, ?, ?, ?)`,
37[
38input.userId,
39input.imageData,
40input.calories,
41input.description,
52limit: number = 10,
53offset: number = 0
54): Promise<Omit<FoodEntry, "image_data">[]> {
55const entries = await sqlite.execute(
56`SELECT id, user_id, timestamp, calories, description, tags
22user_id TEXT NOT NULL,
23timestamp INTEGER DEFAULT (unixepoch()),
24image_data TEXT NOT NULL,
25calories INTEGER,
26description TEXT,
42user_id: string;
43timestamp: number;
44image_data: string;
45calories: number;
46description: string;
50export interface FoodEntryInput {
51userId: string;
52imageData: string;
53calories: number;
54description: string;
stevensDemoREADME.md1 match
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.
stevensDemoREADME.md1 match
21## `favicon.svg`
2223As of this writing Val Town only supports text files, which is why the favicon is an SVG and not an .ico or any other binary image format. If you need binary file storage, check out [Blob Storage](https://docs.val.town/std/blob/).
2425## `components/`