Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dFDA request helpers, measurement and variable UI improvements, and image2measurements stuff #188

Merged
merged 2 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added apps/nextjs/.eslintrc.json
Empty file.
53 changes: 15 additions & 38 deletions apps/nextjs/app/api/dfda/[dfdaPath]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,33 @@ import { z } from 'zod';

import { authOptions } from '@/lib/auth';
import { handleError } from '@/lib/errorHandler';
import { getOrCreateDfdaAccessToken } from '@/lib/dfda';
import {dfdaGET, dfdaPOST} from '@/lib/dfda';

const routeContextSchema = z.object({
params: z.object({
dfdaPath: z.string(),
}),
})

// Utility function to reduce duplication
async function fetchDfdaApi(req: Request, method: 'GET' | 'POST', context: z.infer<typeof routeContextSchema>) {
export async function GET(req: Request, context: z.infer<typeof routeContextSchema>) {
const { params } = routeContextSchema.parse(context);
const urlParams = Object.fromEntries(new URL(req.url).searchParams);
const session = await getServerSession(authOptions);
try {
const { params } = routeContextSchema.parse(context);
const session = await getServerSession(authOptions);

if (!session?.user) {
return new Response(null, { status: 403 });
}

const url = new URL(req.url, `http://${req.headers.get("host")}`);
const dfdaParams = url.searchParams;
let dfdaUrl = `https://safe.dfda.earth/api/v3/${params.dfdaPath}?${dfdaParams}`;

const init: RequestInit = {
method: method,
headers: {
'accept': 'application/json',
'Authorization': `Bearer ${await getOrCreateDfdaAccessToken(session?.user.id)}`,
'Content-Type': method === 'POST' ? 'application/json' : undefined,
} as HeadersInit,
credentials: 'include',
};

if (method === 'POST') {
const requestBody = await req.json();
init.body = JSON.stringify(requestBody);
}
console.log(`Making ${method} request to ${dfdaUrl}`); // with init:`, init);
const response = await fetch(dfdaUrl, init);
const data = await response.json();
//console.log("Response data:", data);
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
return dfdaGET(params.dfdaPath, urlParams, session?.user.id);
} catch (error) {
return handleError(error);
}
}

export async function GET(req: Request, context: z.infer<typeof routeContextSchema>) {
return fetchDfdaApi(req, 'GET', context);
}

export async function POST(req: Request, context: z.infer<typeof routeContextSchema>) {
return fetchDfdaApi(req, 'POST', context);
const { params } = routeContextSchema.parse(context);
const urlParams = Object.fromEntries(new URL(req.url).searchParams);
const body = await req.json();
const session = await getServerSession(authOptions);
try {
return dfdaPOST(params.dfdaPath, body, session?.user.id, urlParams);
} catch (error) {
return handleError(error);
}
}
Binary file added apps/nextjs/app/api/image2measurements/meal.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/nextjs/app/api/image2measurements/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export async function POST(request: NextRequest) {

// Extracting the file (in base64 format) and an optional custom prompt
// from the request body. This is essential for processing the image using OpenAI's API.
const { file: base64Image, prompt: customPrompt, detail, max_tokens } = await request.json();
let { file: base64Image, prompt: customPrompt, detail, max_tokens, image } = await request.json();

base64Image = base64Image || image;
// Check if the image file is included in the request. If not, return an error response.
if (!base64Image) {
console.error('No file found in the request');
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/nextjs/app/api/text2measurements/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export async function POST(request: NextRequest) {
// Log the receipt of the image in base64 format
try {
// Process the statement to extract measurements
const measurements = await processStatement(prompt);
const measurements = await processStatement(prompt);
// If you want to save them, uncomment await dfdaPOST('/v3/measurements', measurements, session?.user.id);

// Return the analysis in the response
return NextResponse.json({ success: true, measurements: measurements });
Expand Down
10 changes: 9 additions & 1 deletion apps/nextjs/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ const App: React.FC = () => {
setBase64Image(base64);
}, []);

// Function to handle Blob from CameraButton and convert it to File
const handleCapture = useCallback((blob: Blob | null) => {
if (blob) {
const file = new File([blob], "captured_image.png", { type: blob.type });
handleFileChange(file);
}
}, [handleFileChange]);

// Function to handle submission for image analysis
const handleSubmit = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
Expand Down Expand Up @@ -87,7 +95,7 @@ const App: React.FC = () => {
<div className="flex">
<div className="text-center mx-auto my-5 p-5 border border-gray-300 rounded-lg max-w-md">
<div className="p-4">
<CameraButton onCapture={handleFileChange}/>
<CameraButton onCapture={handleCapture}/>
</div>
<div>
OR
Expand Down
160 changes: 160 additions & 0 deletions apps/nextjs/app/dashboard/image2measurements/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"use client"
import { ChangeEvent, useState, FormEvent } from "react"
import NutritionFactsLabel from '@/components/NutritionFactsLabel'; // Adjust the import path as necessary

function getStringBetween(prefix: string, suffix: string, str: string): string {
const start = str.indexOf(prefix);
if (start === -1) return str; // prefix not found

const end = str.indexOf(suffix, start + prefix.length);
if (end === -1) return str; // suffix not found

return str.substring(start + prefix.length, end);
}
export default function Home() {
const [image, setImage] = useState<string>("");
const [openAIResponse, setOpenAIResponse] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [nutritionData, setNutritionData] = useState<any[]>([]);
const [completeResponse, setCompleteResponse] = useState<string>("");

function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
if(event.target.files === null) {
window.alert("No file selected. Choose a file.")
return;
}
const file = event.target.files[0];

const reader = new FileReader();
reader.readAsDataURL(file);

reader.onload = () => {
if(typeof reader.result === "string") {
console.log(reader.result);
setImage(reader.result);
}
}

reader.onerror = (error) => {
console.log("error: " + error);
}

}

async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();

if(image === "") {
alert("Upload an image.")
return;
}

setIsLoading(true); // Start loading
setOpenAIResponse(""); // Reset previous responses
setNutritionData([]); // Clear previous data

await fetch("/api/image2measurements", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
image: image // base64 image
})
})
.then(async (response: any) => {
// Because we are getting a streaming text response
// we have to make some logic to handle the streaming text
const reader = response.body?.getReader();
setOpenAIResponse("");
// reader allows us to read a new piece of info on each "read"
// "Hello" + "I am" + "Cooper Codes" reader.read();
while (true) {
const { done, value } = await reader?.read();
// done is true once the response is done
if(done) {


const prefix = "```json";
const suffix = "```";
const jsonData = getStringBetween(prefix, suffix, openAIResponse);
//const parsedData = JSON.parse(jsonData); // Assuming the response is JSON formatted
debugger
//setNutritionData(parsedData); // Set the parsed data for rendering
break;
}

// value : uint8array -> a string.
const currentChunk = new TextDecoder().decode(value);
setOpenAIResponse((prev) => prev + currentChunk);
}


})
.catch(error => {
console.error('Error fetching data:', error);
alert('Failed to fetch data.');
})
.finally(() => {
setIsLoading(false); // End loading
});
}

return (
<div className="min-h-screen flex items-center justify-center text-md">
<div className='bg-slate-800 w-full max-w-2xl rounded-lg shadow-md p-8'>
<h2 className='text-xl font-bold mb-4'>Uploaded Image</h2>
{ image !== "" ?
<div className="mb-4 overflow-hidden">
<img
src={image}
className="w-full object-contain max-h-72"
/>
</div>
:
<div className="mb-4 p-8 text-center">
<p>Once you upload an image, you will see it here.</p>
</div>
}


<form onSubmit={(e) => handleSubmit(e)}>
<div className='flex flex-col mb-6'>
<label className='mb-2 text-sm font-medium'>Upload Image</label>
<input
type="file"
className="text-sm border rounded-lg cursor-pointer"
onChange={(e) => handleFileChange(e)}
/>
</div>

<div className='flex justify-center'>
<button type="submit" className='p-2 bg-sky-600 rounded-md mb-4'>
Ask ChatGPT To Analyze Your Image
</button>
</div>

</form>

{nutritionData.length ? (
<div>Loading...</div>
) : (
nutritionData.map((item, index) => (
<NutritionFactsLabel key={index} data={item} />
))
)}

{openAIResponse !== "" ?
<div className="border-t border-gray-300 pt-4">
<h2 className="text-xl font-bold mb-2">AI Response</h2>
<p>{openAIResponse}</p>
</div>
:
null
}


</div>
</div>
)
}
16 changes: 10 additions & 6 deletions apps/nextjs/components/CameraButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Button } from "@/components/ui/button";
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';

// CameraButton component to capture images from webcam or mobile camera
export const CameraButton = ({ onCapture }) => {
export const CameraButton = ({ onCapture }: { onCapture: (blob: Blob | null) => void }) => {
const [capturing, setCapturing] = useState(false);
const [showModal, setShowModal] = useState(false);
const [videoStream, setVideoStream] = useState(null);
const videoRef = useRef(null);
const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);

useEffect(() => {
if (videoStream && videoRef.current) {
Expand Down Expand Up @@ -37,9 +37,13 @@ export const CameraButton = ({ onCapture }) => {
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
canvas.toBlob(onCapture);
videoStream.getTracks().forEach((track: { stop: () => any; }) => track.stop());
if (ctx) {
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => onCapture(blob));
}
if (videoStream) {
videoStream.getTracks().forEach((track: { stop: () => any; }) => track.stop());
}
setShowModal(false);
setCapturing(false);
}
Expand Down
62 changes: 62 additions & 0 deletions apps/nextjs/components/NutritionFactsLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';

interface Macronutrient {
value: string | number;
unit: string;
}

interface Micronutrient {
name: string;
value: string | number;
unit: string;
}

interface NutritionDataItem {
food_item: string;
serving_size: string | number;
calories: string | number;
macronutrients: {
protein: Macronutrient;
carbohydrates: Macronutrient;
fat: Macronutrient;
};
micronutrients: Micronutrient[];
}

interface NutritionFactsLabelProps {
data: NutritionDataItem[];
}

const NutritionFactsLabel: React.FC<NutritionFactsLabelProps> = ({ data }) => {
return (
<div className="nutrition-facts-label">
{data.map((item, index) => (
<div key={index} className="food-item">
<h3>{item.food_item}</h3>
<p>Serving Size: {item.serving_size}</p>
<p>Calories: {item.calories}</p>

<div className="macronutrients">
<h4>Macronutrients</h4>
<p>Protein: {item.macronutrients.protein.value} {item.macronutrients.protein.unit}</p>
<p>Carbohydrates: {item.macronutrients.carbohydrates.value} {item.macronutrients.carbohydrates.unit}</p>
<p>Fat: {item.macronutrients.fat.value} {item.macronutrients.fat.unit}</p>
</div>

<div className="micronutrients">
<h4>Micronutrients</h4>
<ul>
{item.micronutrients.map((nutrient, i) => (
<li key={i}>
{nutrient.name}: {nutrient.value} {nutrient.unit}
</li>
))}
</ul>
</div>
</div>
))}
</div>
);
};

export default NutritionFactsLabel;
Loading