diff --git a/apps/nextjs/.eslintrc.json b/apps/nextjs/.eslintrc.json new file mode 100644 index 000000000..e69de29bb diff --git a/apps/nextjs/app/api/dfda/[dfdaPath]/route.ts b/apps/nextjs/app/api/dfda/[dfdaPath]/route.ts index bf21eea40..61b871a8e 100644 --- a/apps/nextjs/app/api/dfda/[dfdaPath]/route.ts +++ b/apps/nextjs/app/api/dfda/[dfdaPath]/route.ts @@ -3,7 +3,7 @@ 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({ @@ -11,48 +11,25 @@ const routeContextSchema = z.object({ }), }) -// Utility function to reduce duplication -async function fetchDfdaApi(req: Request, method: 'GET' | 'POST', context: z.infer) { +export async function GET(req: Request, context: z.infer) { + 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) { - return fetchDfdaApi(req, 'GET', context); -} - export async function POST(req: Request, context: z.infer) { - 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); + } } diff --git a/apps/nextjs/app/api/image2measurements/meal.jpeg b/apps/nextjs/app/api/image2measurements/meal.jpeg new file mode 100644 index 000000000..e35842db0 Binary files /dev/null and b/apps/nextjs/app/api/image2measurements/meal.jpeg differ diff --git a/apps/nextjs/app/api/image2measurements/route.ts b/apps/nextjs/app/api/image2measurements/route.ts index 01ada3365..427e70ff1 100644 --- a/apps/nextjs/app/api/image2measurements/route.ts +++ b/apps/nextjs/app/api/image2measurements/route.ts @@ -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'); diff --git a/apps/nextjs/app/api/image2measurements/supplement-label.jpeg b/apps/nextjs/app/api/image2measurements/supplement-label.jpeg new file mode 100644 index 000000000..e767caec7 Binary files /dev/null and b/apps/nextjs/app/api/image2measurements/supplement-label.jpeg differ diff --git a/apps/nextjs/app/api/text2measurements/route.ts b/apps/nextjs/app/api/text2measurements/route.ts index 10cf2adc5..eb94b1df3 100644 --- a/apps/nextjs/app/api/text2measurements/route.ts +++ b/apps/nextjs/app/api/text2measurements/route.ts @@ -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 }); diff --git a/apps/nextjs/app/chat/page.tsx b/apps/nextjs/app/chat/page.tsx index 61844779a..9b9703e4d 100644 --- a/apps/nextjs/app/chat/page.tsx +++ b/apps/nextjs/app/chat/page.tsx @@ -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) => { event.preventDefault(); @@ -87,7 +95,7 @@ const App: React.FC = () => {
- +
OR diff --git a/apps/nextjs/app/dashboard/image2measurements/page.tsx b/apps/nextjs/app/dashboard/image2measurements/page.tsx new file mode 100644 index 000000000..89c191b06 --- /dev/null +++ b/apps/nextjs/app/dashboard/image2measurements/page.tsx @@ -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(""); + const [openAIResponse, setOpenAIResponse] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [nutritionData, setNutritionData] = useState([]); + const [completeResponse, setCompleteResponse] = useState(""); + + function handleFileChange(event: ChangeEvent) { + 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) { + 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 ( +
+
+

Uploaded Image

+ { image !== "" ? +
+ +
+ : +
+

Once you upload an image, you will see it here.

+
+ } + + +
handleSubmit(e)}> +
+ + handleFileChange(e)} + /> +
+ +
+ +
+ +
+ + {nutritionData.length ? ( +
Loading...
+ ) : ( + nutritionData.map((item, index) => ( + + )) + )} + + {openAIResponse !== "" ? +
+

AI Response

+

{openAIResponse}

+
+ : + null + } + + +
+
+ ) +} diff --git a/apps/nextjs/components/CameraButton.tsx b/apps/nextjs/components/CameraButton.tsx index 711577901..91180a2b3 100644 --- a/apps/nextjs/components/CameraButton.tsx +++ b/apps/nextjs/components/CameraButton.tsx @@ -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(null); + const videoRef = useRef(null); useEffect(() => { if (videoStream && videoRef.current) { @@ -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); } diff --git a/apps/nextjs/components/NutritionFactsLabel.tsx b/apps/nextjs/components/NutritionFactsLabel.tsx new file mode 100644 index 000000000..be89a160e --- /dev/null +++ b/apps/nextjs/components/NutritionFactsLabel.tsx @@ -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 = ({ data }) => { + return ( +
+ {data.map((item, index) => ( +
+

{item.food_item}

+

Serving Size: {item.serving_size}

+

Calories: {item.calories}

+ +
+

Macronutrients

+

Protein: {item.macronutrients.protein.value} {item.macronutrients.protein.unit}

+

Carbohydrates: {item.macronutrients.carbohydrates.value} {item.macronutrients.carbohydrates.unit}

+

Fat: {item.macronutrients.fat.value} {item.macronutrients.fat.unit}

+
+ +
+

Micronutrients

+
    + {item.micronutrients.map((nutrient, i) => ( +
  • + {nutrient.name}: {nutrient.value} {nutrient.unit} +
  • + ))} +
+
+
+ ))} +
+ ); +}; + +export default NutritionFactsLabel; diff --git a/apps/nextjs/components/survey/SurveyComponent.tsx b/apps/nextjs/components/survey/SurveyComponent.tsx deleted file mode 100644 index 73af1fc22..000000000 --- a/apps/nextjs/components/survey/SurveyComponent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// components/survey/index.tsx -import React from "react" -import * as Survey from "survey-react" // import surveyjs -import { questions } from "./content" // these are the survey questions -import { Container } from "./styles" // your styles here - -// Modern theme -import "survey-react/modern.min.css" -// Default theme -// import 'survey-react/survey.min.css'; - -const SurveyComponent = () => { - // Apply theme - Survey.StylesManager.applyTheme("modern") - - // Create a modal - const survey = new Survey.Model(questions) - - // Render the survey - return ( - - - - ) -} - -export default SurveyComponent \ No newline at end of file diff --git a/apps/nextjs/components/userVariable/measurement-button.tsx b/apps/nextjs/components/userVariable/measurement-button.tsx index 845d5a6c8..941d7a9c9 100644 --- a/apps/nextjs/components/userVariable/measurement-button.tsx +++ b/apps/nextjs/components/userVariable/measurement-button.tsx @@ -18,7 +18,7 @@ interface MeasurementButtonProps extends ButtonProps { UserVariable, "id" | "name" | "description" | "createdAt" | "imageUrl" | "combinationOperation" | "unitAbbreviatedName" | "variableCategoryName" | - "lastValue" | "unitName" + "lastValue" | "unitName" | "userId" | "variableId" > className: string; size: string; @@ -35,12 +35,13 @@ export function MeasurementButton({ userVariable, ...props }: MeasurementButtonP //router.refresh(); } - // Destructure `ref` out of props to avoid passing it to the Button component - const { ref, ...buttonProps } = props; + // Destructure `ref` and `size` out of props to avoid passing it to the Button component if not valid + const { ref, size, ...buttonProps } = props; + return ( <> - {isFormOpen && ( diff --git a/apps/nextjs/components/userVariable/user-variable-add-button.tsx b/apps/nextjs/components/userVariable/user-variable-add-button.tsx index 72f458d47..55a944d6f 100644 --- a/apps/nextjs/components/userVariable/user-variable-add-button.tsx +++ b/apps/nextjs/components/userVariable/user-variable-add-button.tsx @@ -33,7 +33,6 @@ export function UserVariableAddButton({ ...props }: UserVariableAddButtonProps) }, body: JSON.stringify({ name: "New Variable", - colorCode: "#ffffff", }), }) diff --git a/apps/nextjs/components/userVariable/user-variable-edit-form.tsx b/apps/nextjs/components/userVariable/user-variable-edit-form.tsx index 3c581347f..471317913 100644 --- a/apps/nextjs/components/userVariable/user-variable-edit-form.tsx +++ b/apps/nextjs/components/userVariable/user-variable-edit-form.tsx @@ -3,7 +3,6 @@ import * as React from "react" import { useRouter } from "next/navigation" import { zodResolver } from "@hookform/resolvers/zod" -import { HexColorPicker } from "react-colorful" import { useForm } from "react-hook-form" import * as z from "zod" @@ -45,11 +44,8 @@ export function UserVariableEditForm({ resolver: zodResolver(userVariablePatchSchema), defaultValues: { name: userVariable?.name || "", - description: userVariable?.description || "", - colorCode: "", }, }) - const [color, setColor] = React.useState(userVariable.colorCode || "#ffffff") async function onSubmit(data: FormData) { const response = await fetch(`/api/userVariables/${userVariable.id}`, { @@ -59,8 +55,6 @@ export function UserVariableEditForm({ }, body: JSON.stringify({ name: data.name, - description: data.description, - colorCode: color, }), }) @@ -111,20 +105,6 @@ export function UserVariableEditForm({ Description{" "} (optional) -