diff --git a/Cargo.lock b/Cargo.lock index 081c2ec..32b230a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,44 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-multipart" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee489e3c01eae4d1c35b03c4493f71cb40d93f66b14558feb1b1a807671cc4e" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7" +dependencies = [ + "darling 0.14.4", + "parse-size", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "actix-router" version = "0.5.1" @@ -278,6 +316,7 @@ dependencies = [ "actix-cors", "actix-files", "actix-identity", + "actix-multipart", "actix-session", "actix-web", "adf_bdd", @@ -918,8 +957,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", ] [[package]] @@ -936,13 +985,38 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", "quote", "syn 1.0.109", ] @@ -1881,6 +1955,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + [[package]] name = "password-hash" version = "0.5.0" @@ -2339,6 +2419,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2367,7 +2456,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "syn 1.0.109", diff --git a/frontend/package.json b/frontend/package.json index c64943f..5914cb1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@fontsource/roboto": "^4.5.8", "@mui/material": "^5.11.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.10.0" } } diff --git a/frontend/src/components/adf-details.tsx b/frontend/src/components/adf-details.tsx new file mode 100644 index 0000000..ba5a990 --- /dev/null +++ b/frontend/src/components/adf-details.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import {AlertColor} from '@mui/material'; + +import { GraphProps } from './graph-g6'; + +export type Parsing = 'Naive' | 'Hybrid'; + +export type StrategySnakeCase = 'parse_only' | 'ground' | 'complete' | 'stable' | 'stable_counting_a' | 'stable_counting_b' | 'stable_nogood'; + +export type StrategyCamelCase = 'ParseOnly' | 'Ground' | 'Complete' | 'Stable' | 'StableCountingA' | 'StableCountingB' | 'StableNogood'; + +export interface AcAndGraph { + ac: string[], + graph: GraphProps, +} + +export type AcsWithGraphsOpt = { + type: 'None', +} | { + type: 'Error', + content: string +} | { + type: 'Some', + content: AcAndGraph[] +}; + +export type Task = { + type: 'Parse', +} | { + type: 'Solve', + content: StrategyCamelCase, +}; + +export interface AdfProblemInfo { + name: string, + code: string, + parsing_used: Parsing, + acs_per_strategy: { [key in StrategySnakeCase]: AcsWithGraphsOpt }, // NOTE: the keys are really only strategies + running_tasks: Task[], +} + +export function acsWithGraphOptToColor(status: AcsWithGraphsOpt, running: boolean): AlertColor { + if (running) { + return 'warning'; + } + + switch (status.type) { + case 'None': return 'info'; + case 'Error': return 'error'; + case 'Some': return 'success'; + } +} + +export function acsWithGraphOptToText(status: AcsWithGraphsOpt, running: boolean): string { + if (running) { + return 'Running'; + } + + switch (status.type) { + case 'None': return 'Not attempted'; + case 'Error': return 'Failed'; + case 'Some': return 'Done'; + } +} + +function AdfDetails() { + return ( +
Details
+ ); +} + +export default AdfDetails; diff --git a/frontend/src/components/adf-new-form.tsx b/frontend/src/components/adf-new-form.tsx new file mode 100644 index 0000000..7033666 --- /dev/null +++ b/frontend/src/components/adf-new-form.tsx @@ -0,0 +1,169 @@ +import React, { + useState, useContext, useCallback, useRef, +} from 'react'; + +import { + Backdrop, + Button, + CircularProgress, + CssBaseline, + Container, + FormControl, + FormControlLabel, + FormLabel, + Link, + Pagination, + Paper, + Radio, + RadioGroup, + Stack, + Typography, + TextField, + ToggleButtonGroup, + ToggleButton, +} from '@mui/material'; + +import LoadingContext from './loading-context'; +import SnackbarContext from './snackbar-context'; + +import { Parsing } from './adf-details'; + +const PLACEHOLDER = `s(a). +s(b). +s(c). +s(d). +ac(a,c(v)). +ac(b,b). +ac(c,and(a,b)). +ac(d,neg(b)).`; + +function AdfNewForm({fetchProblems}: { fetchProblems: () => void; }) { + const { setLoading } = useContext(LoadingContext); + const { setStatus: setSnackbarInfo } = useContext(SnackbarContext); + const [isFileUpload, setFileUpload] = useState(false); + const [code, setCode] = useState(PLACEHOLDER); + const [filename, setFilename] = useState(''); + const [parsing, setParsing] = useState('Naive'); + const [name, setName] = useState(''); + const fileRef = useRef(null); + + const addAdf = useCallback( + () => { + setLoading(true); + + const formData = new FormData(); + + if (isFileUpload && fileRef.current) { + formData.append('file', fileRef.current.files![0]); + } else { + formData.append('code', code); + } + + formData.append('parsing', parsing); + formData.append('name', name); + + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/add`, { + method: 'POST', + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + body: formData, + }) + .then((res) => { + switch (res.status) { + case 200: + setSnackbarInfo({ message: 'Successfully added ADF problem!', severity: 'success', potentialUserChange: true }); + fetchProblems(); + break; + default: + setSnackbarInfo({ message: 'An error occured while adding the ADF problem.', severity: 'error', potentialUserChange: true }); + break; + } + }) + .finally(() => setLoading(false)); + }, + [isFileUpload, code, filename, parsing, name, fileRef.current], + ); + + return ( + + + + Add a new Problem + + + + { setFileUpload(newValue); setFilename(''); }} + > + + Write by Hand + + + Upload File + + + + + + + {isFileUpload ? ( + + + + ) : ( + + For more info on the syntax, have a + look + {' '} + here + . + + )} + multiline + fullWidth + variant="filled" + value={code} + onChange={(event) => { setCode(event.target.value); }} + /> + )} + + + + + + Parsing Strategy + setParsing(((e.target as HTMLInputElement).value) as Parsing)} + > + } label="Naive" /> + } label="Hybrid" /> + + + { setName(event.target.value); }} + /> + + + + + + ); +} + +export default AdfNewForm; diff --git a/frontend/src/components/adf-overview.tsx b/frontend/src/components/adf-overview.tsx new file mode 100644 index 0000000..4ceb4c6 --- /dev/null +++ b/frontend/src/components/adf-overview.tsx @@ -0,0 +1,138 @@ +import React, { useState, useCallback, useEffect } from 'react'; + +import { + Backdrop, + Button, + Chip, + CircularProgress, + CssBaseline, + Container, + FormControl, + FormControlLabel, + FormLabel, + Link, + Pagination, + Paper, + Radio, + RadioGroup, + Stack, + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Typography, + TextField, + ToggleButtonGroup, + ToggleButton, +} from '@mui/material'; + +import AdfNewForm from './adf-new-form'; + +import {AdfProblemInfo, StrategySnakeCase, StrategyCamelCase, Task, acsWithGraphOptToColor, acsWithGraphOptToText} from './adf-details'; + +function AdfOverview() { + const [problems, setProblems] = useState([]); + + const fetchProblems = useCallback( + () => { + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/`, { + method: 'GET', + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => { + switch (res.status) { + case 200: + res.json().then((problems) => { + setProblems(problems); + }); + break; + default: + break; + } + }); + }, + [setProblems], + ); + + useEffect( + () => { fetchProblems(); }, + [], + ); + + // TODO set timeout for refetching if there are running problems + + return ( + <> + + ADF-BDD.DEV + + {problems.length > 0 && + + + + Existing Problems + + + + + + ADF Problem Name + Parse Status + Grounded Solution + Complete Solution + Stable Solution + Stable Solution (Counting Method A) + Stable Solution (Counting Method B) + Stable Solution (Nogood-Based) + + + + {problems.map((problem) => ( + + + {problem.name} + + { + (() => { + const status = problem.acs_per_strategy.parse_only; + const running = problem.running_tasks.some((t: Task) => t.type === 'Parse'); + + const color = acsWithGraphOptToColor(status, running); + const text = acsWithGraphOptToText(status, running); + + return ; + })() + } + { + (['Ground', 'Complete', 'Stable', 'StableCountingA', 'StableCountingB', 'StableNogood'] as StrategyCamelCase[]).map((strategy) => { + const status = problem.acs_per_strategy[strategy.replace(/^([A-Z])/, (_, p1) => p1.toLowerCase()).replace(/([A-Z])/g, (_, p1) => `_${p1.toLowerCase()}`) as StrategySnakeCase]; + const running = problem.running_tasks.some((t: Task) => t.type === 'Solve' && t.content === strategy); + + const color = acsWithGraphOptToColor(status, running); + const text = acsWithGraphOptToText(status, running); + + return ; + }) + } + + ))} + +
+
+
+
+ } + + + ); +} + +export default AdfOverview; diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index b312717..e464913 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + import { ThemeProvider, createTheme } from '@mui/material/styles'; import { + Alert, + AlertColor, Backdrop, Button, CircularProgress, @@ -15,169 +19,190 @@ import { Paper, Radio, RadioGroup, + Snackbar, Typography, TextField, + useMediaQuery, } from '@mui/material'; import LoadingContext from './loading-context'; +import SnackbarContext from './snackbar-context'; import GraphG6, { GraphProps } from './graph-g6'; import Footer from './footer'; +import AdfOverview from './adf-overview'; +import AdfDetails from './adf-details'; const { useState, useCallback, useMemo } = React; -const darkTheme = createTheme({ - palette: { - mode: 'dark', +const browserRouter = createBrowserRouter([ + { + path: '/', + element: , }, -}); - -const placeholder = `s(a). -s(b). -s(c). -s(d). -ac(a,c(v)). -ac(b,b). -ac(c,and(a,b)). -ac(d,neg(b)).`; - -enum Parsing { - Naive = 'Naive', - Hybrid = 'Hybrid', -} - -enum Strategy { - ParseOnly = 'ParseOnly', - Ground = 'Ground', - Complete = 'Complete', - Stable = 'Stable', - StableCountingA = 'StableCountingA', - StableCountingB = 'StableCountingB', - StableNogood = 'StableNogood', -} + { + path: '/:adfName', + element: , + }, +]); function App() { - const [loading, setLoading] = useState(false); - const [code, setCode] = useState(placeholder); - const [parsing, setParsing] = useState(Parsing.Naive); - const [graphs, setGraphs] = useState(); - const [graphIndex, setGraphIndex] = useState(0); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const submitHandler = useCallback( - (strategy: Strategy) => { - setLoading(true); - - fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/solve`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const theme = useMemo( + () => + createTheme({ + palette: { + mode: prefersDarkMode ? 'dark' : 'light', }, - body: JSON.stringify({ code, strategy, parsing }), - }) - .then((res) => res.json()) - .then((data) => { - setGraphs(data); - setGraphIndex(0); - }) - .finally(() => setLoading(false)); - // TODO: error handling - }, - [code, parsing], + }), + [prefersDarkMode], ); + const [loading, setLoading] = useState(false); const loadingContext = useMemo(() => ({ loading, setLoading }), [loading, setLoading]); + const [snackbarInfo, setSnackbarInfo] = useState<{ + message: string, + severity: AlertColor, + potentialUserChange: boolean, + } | undefined>(); + const snackbarContext = useMemo(() => ({ status: snackbarInfo, setStatus: setSnackbarInfo }), [snackbarInfo, setSnackbarInfo]); + return ( - + - -
- - Solve your ADF Problem with OBDDs! - + + +
+ +
+
- - - For more info on the syntax, have a - look - {' '} - here - . - - )} - multiline - fullWidth - variant="filled" - value={code} - onChange={(event) => { setCode(event.target.value); }} - /> - - - - Parsing Strategy - setParsing(((e.target as HTMLInputElement).value) as Parsing)} - > - } label="Naive" /> - } label="Hybrid" /> - - -
-
- - {' '} - - {' '} - - {' '} - - {' '} - - {' '} - - {' '} - -
- - {graphs - && ( - - {graphs.length > 1 - && ( - <> - Models: -
- setGraphIndex(value - 1)} /> - - )} - {graphs.length > 0 - && ( - - - - )} - {graphs.length === 0 - && <>No models!} -
- )} -
-