diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..88b4bcd --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +API_KEY=Put Open AI key here +GPT_MODEL=gpt-4o-mini +#GPT_MODEL=gpt-4-turbo # More expensive model, use sparingly \ No newline at end of file diff --git a/.gitignore b/.gitignore index c9d575d..3387e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ yarn-error.* *.tsbuildinfo app-example +android +.env \ No newline at end of file diff --git a/README.md b/README.md index 4ca5146..6042755 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,23 @@ 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: + +cp .env.example .env + +2. Open the .env file and add your OpenAI API key: + +API_KEY=your_openai_api_key_here +MODEL_NAME=your_model_name + +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..c270037 100644 --- a/app.json +++ b/app.json @@ -15,7 +15,8 @@ "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "package": "com.anonymous.pokerchipshelper" }, "web": { "bundler": "metro", diff --git a/app/index.tsx b/app/index.tsx index eb5f1b3..f1bfc4a 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -3,12 +3,15 @@ import { ScrollView, Text, Alert, Button } from "react-native"; import PlayerSelector from "@/components/PlayerSelector"; import BuyInSelector from "@/components/BuyInSelector"; import ChipsSelector from "@/components/ChipsSelector"; -import ChipDistributionSummary from "@/components/ChipDistributionSummary"; +import ChipDistributionSummary from "@/components/ChipDistributionSummary"; +import ChipDetection from "@/components/ChipDetection"; + const IndexScreen = () => { const [playerCount, setPlayerCount] = useState(2); const [buyInAmount, setBuyInAmount] = useState(null); const [numberOfChips, setNumberOfChips] = useState(5); const [totalChipsCount, setTotalChipsCount] = useState([]); + const handleSave = () => { if (buyInAmount === null) { Alert.alert("Error", "Please select a valid buy-in amount"); @@ -19,6 +22,16 @@ 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 ( @@ -29,6 +42,7 @@ const IndexScreen = () => { setPlayerCount={setPlayerCount} /> + { ); }; + export default IndexScreen; diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..c0fa271 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,15 @@ +module.exports = { + presets: ["babel-preset-expo", "@babel/preset-typescript"], + plugins: [ + [ + "module:react-native-dotenv", + { + moduleName: "@env", + path: ".env", + safe: true, + allowUndefined: false, + }, + ], + "react-native-reanimated/plugin", + ], +}; diff --git a/components/ChipDetection.tsx b/components/ChipDetection.tsx new file mode 100644 index 0000000..78815ec --- /dev/null +++ b/components/ChipDetection.tsx @@ -0,0 +1,136 @@ +import React, { useState } from "react"; +import { + View, + Button, + Image, + ActivityIndicator, + Text, + ScrollView, +} from "react-native"; +import * as ImagePicker from "expo-image-picker"; +import { API_KEY, MODEL_NAME } from "@env"; + +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( + "https://api.openai.com/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: 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); + + // Filter out colors with a count of 0 + const filteredData = Object.fromEntries( + Object.entries(parsedData).filter(([_, count]) => count > 0) + ); + + setLastDetectedChips(filteredData); // Store detected chip counts + updateChipCount(filteredData); + } catch (error) { + setError("Failed to analyze the image."); + } finally { + setLoading(false); + } + }; + + return ( + +