diff --git a/CHANGELOG.md b/CHANGELOG.md index 0212ab5..09f526e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * User view text to be more consistent +* User meal entry form to allow input of additional metadata and context clues ### Removed diff --git a/common/components/GeminiApiComponent.php b/common/components/GeminiApiComponent.php index 6725102..ede0646 100644 --- a/common/components/GeminiApiComponent.php +++ b/common/components/GeminiApiComponent.php @@ -3,6 +3,7 @@ namespace common\components; use common\models\Meal; +use common\models\MealForm; use Exception; use Yii; use yii\helpers\FileHelper; @@ -27,28 +28,19 @@ class GeminiApiComponent extends \yii\base\Component 'format' => Client::FORMAT_JSON ], ]); - } - public function mealInquiry($filePath) + public function mealInquiry(MealForm $model) { $data = [ "contents" => [ - [ - "role" => "user", - "parts" => [ - [ - "text" => "INSERT_INPUT_HERE" - ] - ] - ], [ "role" => "user", "parts" => [ [ "inline_data" => [ - "data" => base64_encode(file_get_contents($filePath)), - "mimeType" => FileHelper::getMimeType($filePath) + "data" => base64_encode(file_get_contents($model->filepath)), + "mimeType" => FileHelper::getMimeType($model->filepath) ] ] ] @@ -58,7 +50,10 @@ class GeminiApiComponent extends \yii\base\Component "role" => "user", "parts" => [ [ - "text" => "Provide a caloric and macro estimate for pictures I provide to you. Try to be as accurate as possible and always calculate the everything you see in the picture. Proivde a 3 or 4 word `food_name`" + "text" => "Provide a caloric and macro estimate for pictures I provide to you. Try to be as + accurate as possible and always calculate the everything you see in the picture. Provide a + 3 or 7 word `food_name` with no special characters. If the user provides context pay attention + as it may contain details about the calories or specifics details about the picture." ] ] ], @@ -102,6 +97,10 @@ class GeminiApiComponent extends \yii\base\Component ] ]; + $data['contents'][0]['parts'][] = [ + 'text' => !empty($model->context) ? 'Context: ' . $model->context : 'INSERT_TEXT_HERE' + ]; + $response = $this->client ->post([$this->model, 'key' => $this->apiKey]) ->setData($data) @@ -114,6 +113,9 @@ class GeminiApiComponent extends \yii\base\Component $meal = new Meal(); $gemini = json_decode($response->getContent(), true); $geminiMeal = json_decode($gemini['candidates'][0]['content']['parts'][0]['text'], true); + $meal->context = $model->context; + $meal->date = $model->date->format('Y-m-d H:i:s'); + $meal->type = $model->type; $meal->protein = $geminiMeal['protein']; $meal->calories = $geminiMeal['calories']; $meal->carbohydrates = $geminiMeal['carbohydrates']; @@ -122,8 +124,10 @@ class GeminiApiComponent extends \yii\base\Component $meal->food_name = $geminiMeal['food_name']; // @TODO if moved a job queue then this must be an object otherwise the queue is NOT aware of the user $meal->user_id = Yii::$app->user->id; - $meal->file_name = $filePath; + $meal->file_name = $model->filepath; Yii::debug($meal); + $meal->validate(); + $errors = $meal->getErrors(); $meal->save(); // @TODO catch unidentified pictures? diff --git a/common/config/main.php b/common/config/main.php index c66d6ca..2d15aa5 100644 --- a/common/config/main.php +++ b/common/config/main.php @@ -10,7 +10,7 @@ $params = array_merge( ); return [ - 'name' => $params['company_name'] . ' - ' . $params['product_name'], + 'name' => $params['product_name'], 'aliases' => [ '@bower' => '@vendor/bower-asset', '@npm' => '@vendor/npm-asset', diff --git a/common/models/Meal.php b/common/models/Meal.php index 149a64d..fb9bf5b 100644 --- a/common/models/Meal.php +++ b/common/models/Meal.php @@ -2,6 +2,7 @@ namespace common\models; +use DateTime; use Yii; use yii\behaviors\TimestampBehavior; use yii\db\ActiveRecord; @@ -13,6 +14,7 @@ use yii\web\UnauthorizedHttpException; * @property int $id * @property string $file_name * @property string $context + * @property DateTime $date * @property string $food_name * @property string $type * @property int $calories @@ -63,7 +65,6 @@ class Meal extends ActiveRecord return [ [['date', 'food_name', 'calories', 'protein', 'fat', 'carbohydrates', 'fiber', 'type'], 'required'], [['user_id', 'calories', 'protein', 'fat', 'carbohydrates', 'fiber', 'created_at', 'updated_at'], 'integer'], - [['date'], 'date'], [['type', 'context'], 'string'], [['type'], 'in', 'range' => [self::BREAKFAST, self::LUNCH, self::DINNER, self::OTHER]], [['file_name'], 'string', 'max' => 255], diff --git a/common/models/MealForm.php b/common/models/MealForm.php index 90a2c78..ed20164 100644 --- a/common/models/MealForm.php +++ b/common/models/MealForm.php @@ -2,6 +2,8 @@ namespace common\models; +use DateInterval; +use DateTime; use Ramsey\Uuid\Uuid; use Yii; use yii\base\Model; @@ -10,31 +12,85 @@ use yii\web\UploadedFile; class MealForm extends Model { + public $context; /** * @var UploadedFile */ public $picture; public string $filepath; + public $date; + public int $day = 0; + public $type = Meal::OTHER; // type of meal - default to other - public function rules() { + public function init() + { + // @todo get user timezone to determine - should depend on their location not their settings + $hour = (int) (new DateTime())->format('H'); + if ($hour >= 6 && $hour < 11) { // Breakfast time + $this->type = Meal::BREAKFAST; + } elseif ($hour >= 11 && $hour < 15) { // Lunch time + $this->type = Meal::LUNCH; + } elseif ($hour >= 15 && $hour < 21) { // Dinner time + $this->type = Meal::DINNER; + } + } + + + public function rules() + { return [ - [['picture'], 'file', 'skipOnEmpty' => false], - [['picture'], 'required'], + [['picture'], 'image', 'skipOnEmpty' => false], + [['picture', 'day', 'type'], 'required'], + [['type'], 'string'], + [['day'], 'integer'], + [['context'], 'string', 'length' => [0, 100]], + [['day'], 'in', 'range' => [0, -1, -2, -3, -4]], + [['day'], 'validateCreationDate'], + [['type'], 'in', 'range' => [Meal::BREAKFAST, Meal::LUNCH, Meal::DINNER, Meal::OTHER]], ]; } - public function newFileName() + /** + * How many days to subtract depending on the user selection + * + * @param $attribute + * @param $params + * @param $validator + * @param $current + * @return void + * @throws \DateInvalidOperationException + * @throws \DateMalformedIntervalStringException + */ + public function validateCreationDate($attribute, $params, $validator, $current) + { + $this->date = new DateTime(); + $this->date = $this->date->sub(new DateInterval('P' . abs($current) . 'D')); + } + + + public function newFileName(): void { $this->filepath = (string)'uploads/' . Yii::$app->user->id . '-' . Uuid::uuid4() . '.' . $this->picture->extension; } - public function upload() + public function getTypeList(): array + { + return [ + Meal::BREAKFAST => 'Breakfast', + Meal::LUNCH => 'Lunch', + Meal::DINNER => 'Dinner', + Meal::OTHER => '🤷' + ]; + } + + public function upload(): bool { if ($this->validate()) { $this->newFileName(); - $this->picture->saveAs('@frontend/web/'.$this->filepath); + $this->picture->saveAs('@frontend/web/' . $this->filepath); return true; } else { + $errors = $this->getErrors(); return false; } } diff --git a/frontend/views/meal/index.php b/frontend/views/meal/index.php index 7f10a42..b104967 100644 --- a/frontend/views/meal/index.php +++ b/frontend/views/meal/index.php @@ -24,14 +24,14 @@ $this->params['breadcrumbs'][] = $this->title; $dataProvider, 'columns' => [ - ['class' => 'yii\grid\SerialColumn'], 'food_name', + 'type', 'calories', 'protein', 'fat', 'carbohydrates', 'fiber', - 'created_at:datetime', + 'date:date', [ 'class' => ActionColumn::class, 'urlCreator' => function ($action, Meal $model, $key, $index, $column) { diff --git a/frontend/views/meal/upload.php b/frontend/views/meal/upload.php index 146924a..f9019c9 100644 --- a/frontend/views/meal/upload.php +++ b/frontend/views/meal/upload.php @@ -1,10 +1,11 @@ registerJS( }); " ); + +$this->registerCssFile('@web/css/upload.css'); ?>
@@ -83,40 +86,43 @@ $this->registerJS( ['enctype' => 'multipart/form-data']]); ?> -
-
- -
-
+ + field($model, 'context')->textInput([ + 'class' => 'form-control mb-3', + 'placeholder' => 'Add context (optional)', + 'autofocus', + ])->label(false) ?> +
Tap to take a picture or Long Press to upload a file
'display: none;', 'id' => 'file-input']) ?> + field($model, 'day')->textInput()->label(false); ?> + -
- - - + + +
-
- - - - - - - - - - -
+ field($model, 'type') + ->radioList($model->getTypeList(), [ + 'class' => 'btn-group d-flex justify-content-center', + 'item' => function ($index, $label, $name, $checked, $value) { + $return = ''; + $return .= ''; + return $return; + }, + ]) + ->label(false) + ; ?>
@@ -128,33 +134,9 @@ $this->registerJS(
- +
- - \ No newline at end of file diff --git a/frontend/web/css/site.css b/frontend/web/css/site.css index 24ca0a0..69ccff6 100644 --- a/frontend/web/css/site.css +++ b/frontend/web/css/site.css @@ -216,12 +216,12 @@ a.desc:after { padding: 0; margin: 20px 0; } - + /* Pagination item styles */ .pagination li { margin: 0 5px; } - + /* Pagination link styles */ .pagination li a, .pagination li span { display: flex; @@ -238,7 +238,7 @@ a.desc:after { font-weight: bold; transition: background-color 0.3s, color 0.3s; } - + .pagination li.active.disabled a, .pagination li.disabled.active a { background-color: black; diff --git a/frontend/web/css/upload.css b/frontend/web/css/upload.css new file mode 100644 index 0000000..0eea796 --- /dev/null +++ b/frontend/web/css/upload.css @@ -0,0 +1,22 @@ +#upload-title { + font-size: 2rem; + font-weight: bold; + min-height: 50px; + display: inline-block; + transition: color 0.2s ease-in-out; +} + +#gesture-area { + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10+ and Edge */ + user-select: none; /* Standard syntax */ + -webkit-touch-callout: none; /* Prevents default callout on hold in iOS */ +} + + +.gesture-area { + border: 2px dashed #ccc; + padding: 20px; + margin-bottom: 10px; + text-align: center; +} \ No newline at end of file