diff --git a/cake-game/.eslintrc.cjs b/cake-game/.eslintrc.cjs
index 3e212e1..ff4d29a 100644
--- a/cake-game/.eslintrc.cjs
+++ b/cake-game/.eslintrc.cjs
@@ -13,6 +13,7 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
+ 'react/prop-types': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
diff --git a/cake-game/package-lock.json b/cake-game/package-lock.json
index a6eb898..e334078 100644
--- a/cake-game/package-lock.json
+++ b/cake-game/package-lock.json
@@ -14,7 +14,8 @@
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.22.3"
+ "react-router-dom": "^6.22.3",
+ "uuid": "^9.0.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
@@ -8227,6 +8228,18 @@
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz",
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ=="
},
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/v8-to-istanbul": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
@@ -14325,6 +14338,11 @@
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz",
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ=="
},
+ "uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
+ },
"v8-to-istanbul": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
diff --git a/cake-game/package.json b/cake-game/package.json
index 08c443f..38620bb 100644
--- a/cake-game/package.json
+++ b/cake-game/package.json
@@ -17,7 +17,8 @@
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.22.3"
+ "react-router-dom": "^6.22.3",
+ "uuid": "^9.0.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
diff --git a/cake-game/src/components/classifier-table-row/ClassifierTableRow.css b/cake-game/src/components/classifier-table-row/ClassifierTableRow.css
new file mode 100644
index 0000000..90bcb95
--- /dev/null
+++ b/cake-game/src/components/classifier-table-row/ClassifierTableRow.css
@@ -0,0 +1,20 @@
+.image {
+ width: 50px;
+ height: auto;
+}
+
+.classification {
+ font-weight: normal;
+}
+
+tr {
+ padding: 0.5rem 3rem;
+}
+
+.row {
+ background-color: var(--qwik-dark-background);
+}
+
+.alternative-row {
+ background-color: var(--qwik-alt-background);
+}
\ No newline at end of file
diff --git a/cake-game/src/components/classifier-table-row/ClassifierTableRow.jsx b/cake-game/src/components/classifier-table-row/ClassifierTableRow.jsx
new file mode 100644
index 0000000..e9606b7
--- /dev/null
+++ b/cake-game/src/components/classifier-table-row/ClassifierTableRow.jsx
@@ -0,0 +1,37 @@
+import "./ClassifierTableRow.css";
+
+function ClassifierTableRow(props) {
+
+ function formatClassificationCollections(predictions, attributeName) {
+ if (!predictions || predictions.length === 0) {
+ return 'N/A';
+ }
+
+ return predictions[0][attributeName];
+ }
+
+ function formatClassificationString(classifier) {
+ if (!classifier) {
+ return 'N/A';
+ }
+
+ return classifier;
+ }
+
+ return (
+ <>
+
+
+
+ |
+ { props.result.user_category } |
+ { formatClassificationCollections(props.result.models.mobilenet_classifier, 'className') } |
+ { formatClassificationCollections(props.result.models?.coco_ssd_predictions, 'class') } |
+ { formatClassificationString(props.result.models?.my_transfer_model_classifier?.category) } |
+ { formatClassificationString(props.result.models?.my_model_classifier.category) } |
+
+ >
+ );
+}
+
+export default ClassifierTableRow;
diff --git a/cake-game/src/components/classifier-table-row/ClassifierTableRow.test.js b/cake-game/src/components/classifier-table-row/ClassifierTableRow.test.js
new file mode 100644
index 0000000..d8b1a6f
--- /dev/null
+++ b/cake-game/src/components/classifier-table-row/ClassifierTableRow.test.js
@@ -0,0 +1,9 @@
+import { test, expect } from '@playwright/experimental-ct-react';
+import ClassifierTableRow from './ClassifierTableRow';
+
+test.use({ viewport: { width: 500, height: 500 } });
+
+test('should render', async ({ mount }) => {
+ const component = await mount();
+ expect(component).toBeDefined();
+});
\ No newline at end of file
diff --git a/cake-game/src/functions/game_results.js b/cake-game/src/functions/game_results.js
new file mode 100644
index 0000000..01af1ef
--- /dev/null
+++ b/cake-game/src/functions/game_results.js
@@ -0,0 +1,26 @@
+import { getGameResults } from "../util/elasticsearch";
+import { convertRequest, generateResponse } from "../util/helper";
+
+/**
+ * Get a random image
+ * Note: Netlify deploys this function at the endpoint /.netlify/functions/image
+ * @param {*} event
+ * @param {*} context
+ * @returns
+ */
+export async function handler(event, context) {
+ const gameMetadata = convertRequest(event.body);
+
+ try {
+ const response = await getGameResults(gameMetadata);
+ const results = response.hits.hits.flatMap((document) => {
+ return document._source;
+ });
+
+ return generateResponse(200, results);
+ } catch (e) {
+ console.log(e);
+
+ return generateResponse(500, e);
+ }
+}
diff --git a/cake-game/src/index.css b/cake-game/src/index.css
index c91f1c5..8a87e16 100644
--- a/cake-game/src/index.css
+++ b/cake-game/src/index.css
@@ -6,6 +6,7 @@
--qwik-dark-purple: #713fc2;
--qwik-dirty-black: #1d2033;
--qwik-dark-background: #151934;
+ --qwik-alt-background: #332b6a;
--qwik-dark-text: #ffffff;
}
diff --git a/cake-game/src/main.jsx b/cake-game/src/main.jsx
index d695785..e7325d4 100644
--- a/cake-game/src/main.jsx
+++ b/cake-game/src/main.jsx
@@ -12,6 +12,7 @@ import Error from './Error.jsx';
import Start from './routes/start/Start.jsx';
import Play from './routes/play/Play.jsx';
import Home from './routes/home/Home.jsx';
+import End from './routes/end/End.jsx';
const router = createBrowserRouter([
{
@@ -29,7 +30,11 @@ const router = createBrowserRouter([
},
{
path: '/play',
- element:
+ element: ,
+ },
+ {
+ path: '/end',
+ element:
}
]
},
diff --git a/cake-game/src/routes/end/End.css b/cake-game/src/routes/end/End.css
new file mode 100644
index 0000000..b5e2ea3
--- /dev/null
+++ b/cake-game/src/routes/end/End.css
@@ -0,0 +1,29 @@
+section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2rem;
+
+ margin: auto;
+}
+
+table {
+ width: 95%;
+
+ /*padding: 0.5rem 2rem;*/
+
+ border: 1px solid;
+ border-color: var(--qwik-dark-purple);
+}
+
+.header-row {
+ background-color: var(--qwik-dark-background);
+ color: var(--qwik-dark-text);
+ font-size: large;
+
+ height: 5rem;
+}
+
+th {
+ text-wrap: wrap;
+}
\ No newline at end of file
diff --git a/cake-game/src/routes/end/End.jsx b/cake-game/src/routes/end/End.jsx
new file mode 100644
index 0000000..a1afdb3
--- /dev/null
+++ b/cake-game/src/routes/end/End.jsx
@@ -0,0 +1,92 @@
+import { useEffect, useState } from "react"; //TO DO should I use useMemo to get results and classifications
+import { useNavigate, useSearchParams } from "react-router-dom";
+
+import axios from "axios";
+
+import "./End.css";
+import ClassifierTableRow from "../../components/classifier-table-row/ClassifierTableRow";
+import StartButton from "../../components/start-button/StartButton";
+
+function End() {
+ const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const [username, setUsername] = useState();
+ const [gameId, setGameId] = useState();
+
+ const [results, setResults] = useState();
+
+ useEffect(() => {
+ if (searchParams && !gameId) {
+ const user = searchParams.get("username");
+ const game = searchParams.get("game_id");
+
+ setUsername(user);
+ setGameId(game);
+
+ getGameResults(user, game);
+ }
+ }, [username, gameId]);
+
+ async function getGameResults(user, game) {
+ const gameMetadata = { username: user, game_id: game };
+
+ try {
+ const response = await axios.post(
+ ".netlify/functions/game_results",
+ gameMetadata
+ );
+
+ if (response.status !== 200) {
+ throw new Error("Unable to get game results");
+ }
+
+ const gameResults = response.data;
+ setResults(gameResults);
+ } catch (error) {
+ console.log("Unable to get game results");
+ setResults([]);
+ }
+ }
+
+ return (
+ <>
+
+
+ Were they (F)ake?!
+
+
+
+ Did you find the cake? Check out how you fared against our models
+ below.
+
+
+ {results && results.length > 0 ? (
+
+
+
+ Image |
+ You |
+ MobileNet |
+ COCO-SSD |
+ MobileNet Transfer Classifier |
+ Carly Model |
+
+
+
+ {results.map((result, index) => {
+ return ;
+ })}
+
+
+ ) : (
+ No results available!
+ )}
+ Want to try again?
+
+
+ >
+ );
+}
+
+export default End;
diff --git a/cake-game/src/routes/end/End.test.js b/cake-game/src/routes/end/End.test.js
new file mode 100644
index 0000000..42b1644
--- /dev/null
+++ b/cake-game/src/routes/end/End.test.js
@@ -0,0 +1,10 @@
+import { test, expect } from '@playwright/experimental-ct-react';
+import End from './End';
+
+test.use({ viewport: { width: 500, height: 500 } });
+
+test('should work', async ({ mount }) => {
+ const component = await mount();
+ expect(component).toBeDefined();
+ //await expect(component).toContainText('Is it (F)ake?!');
+});
\ No newline at end of file
diff --git a/cake-game/src/routes/play/Play.jsx b/cake-game/src/routes/play/Play.jsx
index 410fd8a..77efca4 100644
--- a/cake-game/src/routes/play/Play.jsx
+++ b/cake-game/src/routes/play/Play.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from 'react-router-dom';
import axios from "axios";
+import { v4 as uuidv4 } from 'uuid';
import "./Play.css";
@@ -9,7 +10,11 @@ function Play() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
+ // UUID is
+ const gameId = useState(uuidv4());
+
const [username, setUsername] = useState();
+ const [imageCount, setImageCount] = useState(0);
const [imageUrl, setImageUrl] = useState();
const [expectedCategory, setExpectedCategory] = useState('cake');
@@ -45,12 +50,14 @@ function Play() {
async function castVote(event) {
const classification = {
+ game_id: gameId[0],
username: username,
timestamp: new Date().toISOString(),
image_url: imageUrl,
expected_category: expectedCategory,
user_category: event.target.value
- }
+ };
+
try {
const response = await axios.post('.netlify/functions/result', classification);
@@ -58,7 +65,12 @@ function Play() {
throw new Error('Unable to get next image');
}
- await getNextRandomImage();
+ setImageCount(imageCount + 1);
+ if (imageCount < 9) {
+ await getNextRandomImage();
+ } else {
+ navigate(`/end?username=${username}&game_id=${classification.game_id}`);
+ }
}
catch(error) {
console.log('Unable to get next image');
diff --git a/cake-game/src/util/elasticsearch.js b/cake-game/src/util/elasticsearch.js
index 77faaba..771637f 100644
--- a/cake-game/src/util/elasticsearch.js
+++ b/cake-game/src/util/elasticsearch.js
@@ -1,7 +1,9 @@
import { Client } from '@elastic/elasticsearch-serverless';
const classificationsIndex = 'classifications';
-const userClassificationsIndex = 'user-classifications'
+const userClassificationsIndex = 'user-classifications';
+const pipeline = 'add-classifications-to-gameplay';
+
const endpoint = process.env.ELASTIC_URL || '';
const apiKey = process.env.ELASTIC_API_KEY || '';
@@ -33,12 +35,30 @@ export async function getRandomImage() {
/**
* Adds manual classification from user gameplay to the index
- * @param { username, timestamp, imageUrl, expectedCategory, userCategory } userClassification
+ * @param { game_id, username, timestamp, image_url, expected_category, user_category } userClassification
* @returns
*/
export async function saveUserClassification(userClassification) {
return await client.index({
index: userClassificationsIndex,
- document: userClassification
+ document: userClassification,
+ pipeline: pipeline
+ });
+}
+
+/**
+ *
+ * @param { username, game_ud} gameMetadata
+ * @returns
+ */
+export async function getGameResults(gameMetadata) {
+ return await client.search({
+ index: userClassificationsIndex,
+ _source: ["image_url", "user_category", "models"],
+ query: {
+ match: {
+ game_id: gameMetadata.game_id
+ }
+ }
});
}
\ No newline at end of file