diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d881b2b --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +EXPO_PUBLIC_API_URL=https://api.openai.com/v1/chat/completions +EXPO_PUBLIC_API_KEY=put-open-ai-key-here +EXPO_PUBLIC_MODEL_NAME=gpt-4o-mini +#EXPO_PUBLIC_MODEL_NAME=gpt-4-turbo # More expensive model, use sparingly \ No newline at end of file diff --git a/.gitignore b/.gitignore index c9d575d..55c8749 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ yarn-error.* *.tsbuildinfo app-example +android +.env +coverage \ No newline at end of file diff --git a/README.md b/README.md index 4ca5146..eff77e9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,24 @@ This applications uses the React Native + Expo framework and by extension is pri This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). +### Setting Up Environment Variables + +To set up your environment variables: + +1. Copy the example environment variable file to create your own `.env` file: + +```bash +cp .env.example .env +``` + +2. Open the `.env` file and add your OpenAI API key: + +`EXPO_PUBLIC_API_KEY=put-open-ai-key-here` + +3. Save the .env file. + +This setup allows you to run the application with your own API credentials, and you can switch models if needed. + ### VSCode plugins - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) diff --git a/app.json b/app.json index a619d4b..501d4f3 100644 --- a/app.json +++ b/app.json @@ -1,10 +1,10 @@ { "expo": { - "name": "poker-chips-helper", + "name": "Poker Chips Helper", "slug": "poker-chips-helper", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/icon.png", + "icon": "./assets/images/icon1.png", "scheme": "myapp", "userInterfaceStyle": "automatic", "newArchEnabled": true, @@ -15,7 +15,8 @@ "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "package": "com.anonymous.pokerchipshelper" }, "web": { "bundler": "metro", diff --git a/app/_layout.tsx b/app/_layout.tsx index d2a8b0b..c3648ac 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,5 +1,7 @@ import { Stack } from "expo-router"; +import React from "react"; -export default function RootLayout() { - return ; -} +const RootLayout: React.FC = () => ( + +); +export default RootLayout; diff --git a/app/index.tsx b/app/index.tsx index faf3557..e80c05c 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -4,6 +4,7 @@ import PlayerSelector from "@/components/PlayerSelector"; import BuyInSelector from "@/components/BuyInSelector"; import ChipsSelector from "@/components/ChipsSelector"; import ChipDistributionSummary from "@/components/ChipDistributionSummary"; +import ChipDetection from "@/components/ChipDetection"; export enum COLORS { "white", @@ -13,11 +14,12 @@ export enum COLORS { "black", } -const IndexScreen = () => { +const IndexScreen: React.FC = () => { const [playerCount, setPlayerCount] = useState(2); const [buyInAmount, setBuyInAmount] = useState(20); const [numberOfChips, setNumberOfChips] = useState(5); const [totalChipsCount, setTotalChipsCount] = useState([]); + const handleSave = () => { if (buyInAmount === null) { Alert.alert("Error", "Please select a valid buy-in amount"); @@ -28,16 +30,24 @@ const IndexScreen = () => { ); } }; + + // Update chip count based on detection or manual edit + const updateChipCount = (chipData: { [color: string]: number }) => { + // Convert the chip data from the API response or manual edit to a count array + const chipCountArray = Object.entries(chipData).map( + ([color, count]) => count + ); + setTotalChipsCount(chipCountArray); // Update the parent component's state + }; + return ( - - Poker Chip Helper - + { ); }; + export default IndexScreen; diff --git a/assets/images/icon1.png b/assets/images/icon1.png new file mode 100644 index 0000000..b01e897 Binary files /dev/null and b/assets/images/icon1.png differ diff --git a/components/ChipDetection.tsx b/components/ChipDetection.tsx new file mode 100644 index 0000000..0a51c46 --- /dev/null +++ b/components/ChipDetection.tsx @@ -0,0 +1,131 @@ +import React, { useState } from "react"; +import { + View, + Button, + Image, + ActivityIndicator, + Text, + ScrollView, +} from "react-native"; +import * as ImagePicker from "expo-image-picker"; + +const ChipDetection = ({ updateChipCount }) => { + const [imageUri, setImageUri] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastDetectedChips, setLastDetectedChips] = useState({}); + + const requestCameraPermissions = async () => { + const cameraPermission = await ImagePicker.requestCameraPermissionsAsync(); + return cameraPermission.granted; + }; + + const pickImage = async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + base64: true, + quality: 1, + }); + + if (!result.canceled) { + setImageUri(result.assets[0].uri); + await processImage(result.assets[0].base64); + } + }; + + const takePhoto = async () => { + const hasPermission = await requestCameraPermissions(); + if (!hasPermission) { + setError("Camera permission is required to take a photo."); + return; + } + + const result = await ImagePicker.launchCameraAsync({ + base64: true, + quality: 1, + }); + + if (!result.canceled) { + setImageUri(result.assets[0].uri); + await processImage(result.assets[0].base64); + } + }; + + const processImage = async (base64Image) => { + setLoading(true); + setError(null); + + try { + const response = await fetch(process.env.EXPO_PUBLIC_API_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.EXPO_PUBLIC_API_KEY}`, // Use environment variable for API key + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: process.env.EXPO_PUBLIC_MODEL_NAME, // Use environment variable for model name + messages: [ + { + role: "system", + content: + "Identify and count poker chips by color. Return only the count for each color in JSON format.", + }, + { + role: "user", + content: [ + { + type: "text", + text: "How many poker chips are there for each color? Return structured JSON.", + }, + { + type: "image_url", + image_url: { url: `data:image/png;base64,${base64Image}` }, + }, + ], + }, + ], + max_tokens: 1000, + }), + }); + + const result = await response.json(); + + if (!response.ok || !result.choices || !result.choices[0].message) { + throw new Error("Invalid response from API."); + } + + const rawContent = result.choices[0].message.content.trim(); + const cleanJSON = rawContent.replace(/```json|```/g, "").trim(); + + const parsedData = JSON.parse(cleanJSON); + + const filteredData = Object.fromEntries( + Object.entries(parsedData).filter(([_, count]) => count > 0) + ); + + setLastDetectedChips(filteredData); + updateChipCount(filteredData); + } catch (error) { + setError("Failed to analyze the image."); + } finally { + setLoading(false); + } + }; + + return ( + +