From 2a83b1e7946f168be19ec5544ac25c2db14d3cae Mon Sep 17 00:00:00 2001 From: Andreas Lilleby Hjulstad Date: Tue, 5 Nov 2024 20:48:25 +0100 Subject: [PATCH 1/5] build: fix webapp port CORS error CADAiD only allows requests from 3000 and 5000 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 25c4c0e..8d11a9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: NEXT_PUBLIC_CLIENTVAR: "clientvar" working_dir: /app ports: - - "80:3000" + - "3000:3000" image: t3-app environment: - DATABASE_URL=${DATABASE_URL} From 0a84c133b75957966ded8ae8f1b506a682f61d42 Mon Sep 17 00:00:00 2001 From: Andreas Lilleby Hjulstad Date: Tue, 5 Nov 2024 20:49:00 +0100 Subject: [PATCH 2/5] build: fix database not running in docker since docker is a debian image, we need debian openssl to run the database --- webapp/prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/prisma/schema.prisma b/webapp/prisma/schema.prisma index 859b51e..b0e4b94 100644 --- a/webapp/prisma/schema.prisma +++ b/webapp/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-3.0.x"] } datasource db { From c916b27a73b0365174e528c301a039eb4c7ec6aa Mon Sep 17 00:00:00 2001 From: Andreas Lilleby Hjulstad Date: Tue, 5 Nov 2024 20:56:04 +0100 Subject: [PATCH 3/5] feat: integrate CADAiD page with backend Now stores responses and creates applications if they don't already exist --- webapp/src/components/CADAiD.tsx | 143 ++++++++++++++++++++++++----- webapp/src/components/ui/input.tsx | 25 +++++ webapp/src/server/api/root.ts | 5 +- 3 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 webapp/src/components/ui/input.tsx diff --git a/webapp/src/components/CADAiD.tsx b/webapp/src/components/CADAiD.tsx index fae2833..2deab79 100644 --- a/webapp/src/components/CADAiD.tsx +++ b/webapp/src/components/CADAiD.tsx @@ -1,19 +1,21 @@ "use client"; -import React, { useState } from 'react'; -import FileList from './FileList'; -import Results from './Results'; -import FilePreview from './FilePreview'; -import type { Detection } from '~/types/detection'; +import React, { useState } from "react"; +import FileList from "./FileList"; +import Results from "./Results"; +import FilePreview from "./FilePreview"; +import type { Detection } from "~/types/detection"; +import { api } from "~/trpc/react"; +import { Input } from "./ui/input"; async function fetchDetection(formData: FormData): Promise { - const response = await fetch('http://localhost:5001/detect/', { - method: 'POST', + const response = await fetch("http://localhost:5001/detect/", { + method: "POST", body: formData, - }) + }); if (!response.ok) { - throw new Error('Failed to upload files'); + throw new Error("Failed to upload files"); } return response.json() as Promise; @@ -24,23 +26,33 @@ const CadaidPage: React.FC = () => { const [files, setFiles] = useState([]); const [results, setResults] = useState([]); const [errorMessage, setErrorMessage] = useState(null); + const [applicationId, setApplicationId] = useState(null); + const createApplicationMutation = + api.application.createApplication.useMutation(); + const createResponseMutation = api.response.createResponse.useMutation(); + const createModelMutation = api.model.createModel.useMutation(); + const utils = api.useUtils(); /** * Handles file upload by sending selected files to the backend. * @param event - The file input change event. */ - const handleFileUpload = async (event: React.ChangeEvent) => { + const handleFileUpload = async ( + event: React.ChangeEvent, + ) => { if (event.target.files) { const uploadedFiles = Array.from(event.target.files); if (uploadedFiles.length === 0) { - setErrorMessage('No files selected'); + setErrorMessage("No files selected"); return; } - const ALLOWED_FILE_TYPES = ['application/pdf', 'image/jpeg', 'image/png']; - const invalidFiles = uploadedFiles.filter(file => !ALLOWED_FILE_TYPES.includes(file.type)); + const ALLOWED_FILE_TYPES = ["application/pdf", "image/jpeg", "image/png"]; + const invalidFiles = uploadedFiles.filter( + (file) => !ALLOWED_FILE_TYPES.includes(file.type), + ); if (invalidFiles.length > 0) { - setErrorMessage('Invalid file type.'); + setErrorMessage("Invalid file type."); return; } @@ -49,15 +61,65 @@ const CadaidPage: React.FC = () => { const formData = new FormData(); uploadedFiles.forEach((file) => { - formData.append('uploaded_files', file); + formData.append("uploaded_files", file); }); try { const detections = await fetchDetection(formData); setResults((prevResults) => [...prevResults, ...detections]); + + // Add detections to the database + const modelData = await utils.model.getModelsByName.ensureData({ + modelName: "CADAiD", + }); + + // Kind of awful, maybe change the getModelsByName to return a single model, since the names are unique, but have to change schema for that + let model = modelData.find((model) => model.modelName === "CADAiD"); + + if (!model) { + console.log("CREATING MODEL"); + const newModel = await createModelMutation.mutateAsync({ + modelName: "CADAiD", + }); + model = newModel; + } + + if (!modelData) { + // Model doesn't exist, create it + createModelMutation.mutate({ modelName: model.modelName }); + } + + // Use a local variable to hold the application ID from state + let applicationIdToUse = applicationId; + + if (!applicationIdToUse) { + // User hasn't entered an application ID, create a new application + const application = await createApplicationMutation.mutateAsync({ + status: "NEW", + address: "Adresseveien 123", + municipality: "Oslo", + submissionDate: new Date(), + }); + setApplicationId(application.applicationID); + applicationIdToUse = application.applicationID; + } + + detections.forEach((detection) => { + createResponseMutation + .mutateAsync({ + response: JSON.stringify(detection), + modelID: model.modelID, + applicationID: applicationIdToUse, + }) + .catch((error) => { + console.error(error); + setErrorMessage("En feil oppsto under lagring av responsen."); + }); + // } + }); } catch (error) { console.error(error); - setErrorMessage('An error occurred while uploading files.'); + setErrorMessage("En feil oppsto under opplasting av filer."); } finally { setIsLoading(false); // Ensure loading state is reset } @@ -69,25 +131,58 @@ const CadaidPage: React.FC = () => { * @param fileName - The name of the file to remove. */ const handleFileRemove = (fileName: string) => { - setFiles((prevFiles) => prevFiles.filter(file => file.name !== fileName)); - setResults((prevResults) => prevResults.filter(result => result.file_name !== fileName)); + setFiles((prevFiles) => prevFiles.filter((file) => file.name !== fileName)); + setResults((prevResults) => + prevResults.filter((result) => result.file_name !== fileName), + ); + }; + + const handleApplicationIdChanged = ( + event: React.ChangeEvent, + ) => { + try { + setApplicationId(parseInt(event.target.value)); + } catch (error) { + setErrorMessage("Ugyldig byggesaks-id"); + console.error(error); + setApplicationId(null); + } }; return ( -
+
{/* Left Column */}
- - +
+ + +
+ + {isLoading && ( -
-
+
+
)} {errorMessage && (
{} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/webapp/src/server/api/root.ts b/webapp/src/server/api/root.ts index e594164..7dd7069 100644 --- a/webapp/src/server/api/root.ts +++ b/webapp/src/server/api/root.ts @@ -4,6 +4,7 @@ import { applicationRouter } from "./routers/application"; import { documentRouter } from "./routers/document"; import { modelErrorRouter } from "./routers/model-error"; import { arkivGptRouter } from "./routers/arkivgpt"; +import { responseRouter } from "./routers/response"; /** * This is the primary router for your server. @@ -14,8 +15,8 @@ export const appRouter = createTRPCRouter({ model: modelRouter, application: applicationRouter, document: documentRouter, - response: modelErrorRouter, - modelErrors: modelErrorRouter, + response: responseRouter, + modelError: modelErrorRouter, arkivgpt: arkivGptRouter, }); From 8c4cf89ce91936f5c2f97c1bb25074926ce0dc9b Mon Sep 17 00:00:00 2001 From: Andreas Lilleby Hjulstad Date: Tue, 5 Nov 2024 20:59:29 +0100 Subject: [PATCH 4/5] refactor: remove commented-out code --- webapp/src/components/CADAiD.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/components/CADAiD.tsx b/webapp/src/components/CADAiD.tsx index 2deab79..9f3c384 100644 --- a/webapp/src/components/CADAiD.tsx +++ b/webapp/src/components/CADAiD.tsx @@ -115,7 +115,6 @@ const CadaidPage: React.FC = () => { console.error(error); setErrorMessage("En feil oppsto under lagring av responsen."); }); - // } }); } catch (error) { console.error(error); From 62c0d938fb098d30bc4597b3a6335e7e12f36a45 Mon Sep 17 00:00:00 2001 From: Andreas Lilleby Hjulstad Date: Tue, 5 Nov 2024 21:05:10 +0100 Subject: [PATCH 5/5] fix: ci crashing because of eslint error --- webapp/src/components/ui/input.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/ui/input.tsx b/webapp/src/components/ui/input.tsx index f0847d6..5a1e6b6 100644 --- a/webapp/src/components/ui/input.tsx +++ b/webapp/src/components/ui/input.tsx @@ -1,9 +1,8 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; -export interface InputProps - extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( ({ className, type, ...props }, ref) => { @@ -12,14 +11,14 @@ const Input = React.forwardRef( type={type} className={cn( "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} ref={ref} {...props} /> - ) - } -) -Input.displayName = "Input" + ); + }, +); +Input.displayName = "Input"; -export { Input } +export { Input };