Skip to content

Commit

Permalink
feat: overhaul the frontend
Browse files Browse the repository at this point in the history
- Added better mobile support
- Added additional information about the project
- Changed the layout
  • Loading branch information
filipepcampos committed Sep 26, 2024
1 parent 663ddf1 commit 7f37685
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 150 deletions.
38 changes: 38 additions & 0 deletions frontend/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export default function About() {
return (
<main
className="grid grid-rows-[20px_1fr_20px] justify-items-center p-8 gap-16 sm:p-0 min-h-screen font-[family-name:var(--font-geist-sans)
mx-auto w-full max-w-screen-xl"
>
<div className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div className="flex-grow grid h-screen place-content-center bg-white px-4">
<div className="text-center">
<p className="text-2xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Uh-oh! Under construction.
</p>

<p className="mt-4 text-gray-700">
We are glad for your interest in the application. Unfortunately at
this time the About page is still under construction. Meanwhile if
you are still craving for more information do feel free to check
out our{" "}
<a
href="https://github.com/filipepcampos/case-based-explanations-demo"
className="text-blue-500 hover:text-blue-700"
>
github repository.
</a>
</p>

<a
href="/"
className="mt-6 inline-block rounded bg-blue-500 px-5 py-3 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring"
>
Go Back Home
</a>
</div>
</div>
</div>
</main>
);
}
2 changes: 1 addition & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { PublicEnvScript } from 'next-runtime-env';
import { PublicEnvScript } from "next-runtime-env";
import localFont from "next/font/local";
import "./globals.css";

Expand Down
37 changes: 12 additions & 25 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
import { env } from "next-runtime-env";

import Image from "next/image";
import InteractiveSection from "../components/InteractiveSection";
import Footer from "@/components/Footer";

export default function Home() {
// Send a heartbeat to the API to wake up the API container
const host = env("NEXT_PUBLIC_API_HOST");
fetch(`${host}/`, { method: "GET"});
fetch(`${host}/`, { method: "GET" });

return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<h1 className="flex mb-4 text-4xl font-extrabold leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">
Case-based explanations demo
<main
className="grid grid-rows-[20px_1fr_20px] justify-items-center p-8 gap-16 sm:p-0 min-h-screen font-[family-name:var(--font-geist-sans)
mx-auto w-full max-w-screen-xl"
>
<div className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<h1 className="mb-4 text-4xl font-extrabold leading-none text-gray-800 md:text-5xl lg:text-6xl dark:text-white">
Case-based explanations - A small demo
</h1>
<InteractiveSection />
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://github.com/filipepcampos/case-based-explanations-demo"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Github
</a>
</footer>
</div>
<Footer />
</div>
</main>
);
}
4 changes: 4 additions & 0 deletions frontend/components/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}
90 changes: 30 additions & 60 deletions frontend/components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
"use client";

import React, { useRef, useState } from "react";
import { useMediaQuery } from 'react-responsive';
import { env } from "next-runtime-env";
import Image from "next/image";
import { useMediaQuery } from "react-responsive";

type PredictionType = {
prediction: number | null;
confidence: number | null;
explanations: string[];
};

export type { PredictionType};
export type { PredictionType };

export function Canvas({ setResult }: { setResult: (result: PredictionType) => void }) {
export function Canvas({
onSubmit,
onClear,
}: {
onSubmit: (blob: Blob) => void;
onClear: () => void;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [isDrawing, setIsDrawing] = useState(false);


// Prevent scrolling on touch devices
const isTouchDevice = useMediaQuery({ query: '(pointer: coarse)' });
const isTouchDevice = useMediaQuery({ query: "(pointer: coarse)" });
if (isTouchDevice) {
document.body.style.overflow = isDrawing ? "hidden" : "auto";
}
Expand All @@ -32,7 +35,6 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
return;
}


const thickness = 2;

ctx.fillStyle = "black";
Expand All @@ -41,8 +43,7 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
ctx.fillRect(x + i * 10, y + j * 10, 10, 10);
}
}

}
};

const startDrawing = (event: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
Expand Down Expand Up @@ -90,7 +91,7 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
paintAt(ctx, offsetX, offsetY);
}
}
}
};

const drawTouch = (event: React.TouchEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
Expand All @@ -106,7 +107,7 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
paintAt(ctx, offsetX, offsetY);
}
}
}
};

const stopDrawing = () => {
setIsDrawing(false);
Expand All @@ -119,11 +120,7 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
setResult({
prediction: null,
confidence: null,
explanations: [],
});
onClear();
}
};

Expand All @@ -132,39 +129,13 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
if (canvas) {
canvas.toBlob((blob) => {
if (!blob) return;

const file = new File([blob], "canvas-image.png", { type: blob.type });

const formData = new FormData();
formData.append("file", file);

// Send the file to the server
const host = env('NEXT_PUBLIC_API_HOST');
fetch(host + "/predict", {
method: "POST",
body: formData,
})
.then((response) => {
if (response.ok) {
response.json().then((data) => {
setResult({
prediction: data.prediction,
confidence: data.confidence,
explanations: data.explanations,
});
});
} else {
console.error("Failed to upload image.");
}
})
.catch((error) => console.error("Error:", error));
onSubmit(blob);
}, "image/png"); // You can specify the image type and quality here
}
};


return (
<div className="justify-center">
<div className="grid grid-cols-2 md:grid-cols-4 place-items-center">
<canvas
ref={canvasRef}
onMouseDown={startDrawing}
Expand All @@ -177,22 +148,21 @@ export function Canvas({ setResult }: { setResult: (result: PredictionType) => v
onTouchCancel={stopDrawing}
width={280}
height={280}
className="border border-solid border-black dark:invert"
className="border border-solid border-black dark:invert col-span-2 md:col-span-4"
/>
<div className="flex gap-4 items-center flex-col sm:flex-row mt-4">
<button
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-30"
onClick={submitCanvas}
>
Submit{" "}
</button>
<button
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-30"
onClick={clearCanvas}
>
Clear
</button>
</div>

<button
className="mt-4 mr-6 md:col-start-2 rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-30"
onClick={submitCanvas}
>
Submit{" "}
</button>
<button
className="mt-4 ml-6 md:col-start-3 rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-30"
onClick={clearCanvas}
>
Clear
</button>
</div>
);
}
42 changes: 42 additions & 0 deletions frontend/components/Confidence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PredictionType } from "./Canvas";

export default function Confidence({ result }: { result: PredictionType }) {
return (
<div>
{(result.prediction || result.prediction === 0) && result.confidence && (
<div>
<div className="mt-4 text-lg font-bold mb-2">Prediction:</div>

<div className="flex flex-col md:flex-row md:gap-0 gap-4">
<div className="text-base">
You drew the number{" "}
<span className="text-blue-700 dark:text-white">
{result.prediction}
</span>
.
</div>
<div className="ml-6 flex-1">
<div className="w-1/2 bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: result.confidence * 100 + "%" }}
></div>
<div className="flex justify-between mt-1">
<span className="text-xs text-blue-700 dark:text-white">
Confidence
</span>
<span className="text-xs text-blue-700 dark:text-white">
{Math.round(result.confidence * 100)}%
</span>
</div>
</div>
</div>
</div>
<div className="mt-8 md:mt-4 text-lg font-bold mb-2">
Explanations:
</div>
</div>
)}
</div>
);
}
56 changes: 56 additions & 0 deletions frontend/components/ExplanationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { PredictionType } from "./Canvas";
import Image from "next/image";

export function ExplanationList({ result }: { result: PredictionType }) {
return (
<div className="flex gap-2 justify-center flex-wrap">
{result.explanations.map((explanation, index) => (
<div key={index}>
<span className="mb-1 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:text-gray-300">
{" "}
{"explanation " + (index + 1)} -{" "}
<span className="">{explanation.split("/").at(-2)}</span>
</span>

<Image
className="dark:invert-0 invert flex-shrink-0 rounded-lg border border-solid border-gray-700 dark:border-white"
src={explanation}
alt="Case-based explanation"
width={150}
height={150}
/>
</div>
))}
</div>
);
}

export function MockExplanationList() {
return (
<div className="flex gap-2 justify-center flex-wrap">
{[...Array(6)].map((explanation, index) => (
<div key={index}>
<span className="mb-1 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:text-gray-300">
{" "}
{"explanation " + (index + 1)}
</span>

<div
className="flex items-center justify-center flex-shrink-0 rounded-lg border border-solid border-gray-200 dark:border-white dark:bg-gray-700 animate-pulse"
style={{ width: "150px", height: "150px" }}
>
<svg
className="w-10 h-10 text-gray-200 dark:text-gray-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 18"
>
<path d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z" />
</svg>
</div>
</div>
))}
</div>
);
}
Loading

0 comments on commit 7f37685

Please sign in to comment.