diff --git a/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts b/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts index d4d2506..0382853 100644 --- a/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts +++ b/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts @@ -1,8 +1,7 @@ import each from "jest-each"; -import { regexBlank } from "../../../src/converter/dimacs/Syntax/CommonSyntax"; import { DimacsParser } from "../../../src/converter/dimacs/DimacsParser"; +import { regexBlank } from "../../../src/converter/dimacs/Syntax/CommonSyntax"; import { regexComment } from "../../../src/converter/dimacs/Syntax/DimacsSyntax"; -import { LogicalExpressionParser } from "../../../src/converter/dimacs/LogicalExpressionParser"; import { regexNOT } from "../../../src/converter/dimacs/Syntax/LogicalExpressionSyntax"; function isEquivalentLogicalExpression(f1: string, f2: string) { @@ -19,36 +18,6 @@ function isEquivalentDimacs(f1: string, f2: string) { describe("Parsing", () => { let dimacsParser = new DimacsParser(); - let logicalExpressionParser = new LogicalExpressionParser(); - - each([ - [ - "((1 or 2 or not 3) and (!4 and (not 5 and 6)) and 3 and (7 or 2))", - "c First comment\nc Some Comment\nc 1 => 1\np sat 7\n*(+( 1 2 -3 )*( -4 *( -5 6 )) 3 +( 7 2 ))", - ], - [ - "(1 or (2 and 3) or (((1 and 4) or 5) and 6))", - "p sat 6\n+(1 *(2 3)*(+(*(1 4) 5) 6))", - ], - [ - "(((1 and not 2 and 3 and 4) or 3) and 5)", - "c Sample DIMACS .sat file\np sat 5\n*(+(*(1 -2 3 4) 3) 5)", - ], - ["((1 and not 2) or 3)", "p sat 3\n+(*(1 -2) 3)"], - ["((1 and 2) or not 3)", "p sat 3\n+(*(1 2) -3)"], - ]).test( - "parsing bi-directional", - (logicalExpression: string, dimacs: string) => { - isEquivalentDimacs( - logicalExpressionParser.parseDimacs(logicalExpression), - dimacs - ); - isEquivalentLogicalExpression( - dimacsParser.parseLogicalExpression(dimacs), - logicalExpression - ); - } - ); each([ [ diff --git a/src/api/ToolboxAPI.ts b/src/api/ToolboxAPI.ts index d0703f0..4b86d3c 100644 --- a/src/api/ToolboxAPI.ts +++ b/src/api/ToolboxAPI.ts @@ -10,6 +10,19 @@ import { SolveRequest } from "./data-model/SolveRequest"; */ export const baseUrl = () => process.env.NEXT_PUBLIC_API_BASE_URL; +function invalidSolution(reason: string): Solution { + return { + id: -1, + status: SolutionStatus.INVALID, + solverName: "", + executionMilliseconds: 0, + solutionData: "", + debugData: "", + metaData: "", + error: `${reason}`, + }; +} + export async function postProblem( problemType: string, solveRequest: SolveRequest @@ -23,18 +36,19 @@ export async function postProblem( }) .then((response) => response.json()) .then((json) => json as Solution) - .catch((reason) => { - return { - id: -1, - status: SolutionStatus.INVALID, - solverName: "", - executionMilliseconds: 0, - solutionData: "", - debugData: "", - metaData: "", - error: `${reason}`, - }; - }); + .catch(invalidSolution); +} + +export async function fetchSolution(problemType: string, solutionId: number) { + return fetch(`${baseUrl()}/solution/${problemType}?id=${solutionId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((json) => json as Solution) + .catch(invalidSolution); } export async function fetchSolvers( diff --git a/src/components/solvers/History.tsx b/src/components/solvers/History.tsx new file mode 100644 index 0000000..6e8ea84 --- /dev/null +++ b/src/components/solvers/History.tsx @@ -0,0 +1,52 @@ +import { + Button, + Divider, + Heading, + List, + ListItem, + Stack, + Tooltip, +} from "@chakra-ui/react"; +import { ProblemState } from "./HistoryStorage"; + +interface HistoryProps { + problemStates: ProblemState[]; + + onRequestRollback: (problemState: ProblemState) => void; +} + +export const History = (props: HistoryProps) => { + if (props.problemStates.length == 0) return null; + + function reloadState(state: ProblemState) { + props.onRequestRollback(state); + } + + return ( + + + History + + + {props.problemStates.map((x) => { + let problemString = x.problemInput.toString(); + return ( + + + + + + + ); + })} + + + ); +}; diff --git a/src/components/solvers/HistoryStorage.ts b/src/components/solvers/HistoryStorage.ts new file mode 100644 index 0000000..5fc8265 --- /dev/null +++ b/src/components/solvers/HistoryStorage.ts @@ -0,0 +1,30 @@ +import { SolutionIds } from "./ProgressHandler"; + +export interface ProblemState { + /** + * Problem input + */ + problemInput: T; + /** + * Ids of the solutions of the problem input per problem type + */ + solutionIds: SolutionIds; +} + +export function getHistory(problemTypes: string[]): ProblemState[] { + let historyItem = localStorage.getItem(getStoreId(problemTypes)); + if (historyItem === null) return []; + + return JSON.parse(historyItem); +} + +export function storeHistory( + problemTypes: string[], + history: ProblemState[] +) { + localStorage.setItem(getStoreId(problemTypes), JSON.stringify(history)); +} + +export function getStoreId(problemTypes: string[]) { + return `problemStates-${problemTypes}`; +} diff --git a/src/components/solvers/ProgressHandler.tsx b/src/components/solvers/ProgressHandler.tsx index de12e0b..2ade649 100644 --- a/src/components/solvers/ProgressHandler.tsx +++ b/src/components/solvers/ProgressHandler.tsx @@ -1,19 +1,26 @@ -import { Box, Center, HStack, VStack } from "@chakra-ui/react"; -import React, { useState } from "react"; -import { GoButton } from "./buttons/GoButton"; -import { postProblem } from "../../api/ToolboxAPI"; -import { SolutionView } from "./SolutionView"; -import { Container } from "../Container"; +import { Box, Center, HStack } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; import { Solution } from "../../api/data-model/Solution"; import { SolveRequest } from "../../api/data-model/SolveRequest"; +import { fetchSolution, postProblem } from "../../api/ToolboxAPI"; +import { Container } from "../Container"; +import { GoButton } from "./buttons/GoButton"; +import { History } from "./History"; +import { getHistory, ProblemState, storeHistory } from "./HistoryStorage"; +import { SolutionView } from "./SolutionView"; import { SolverPicker } from "./SolverPicker"; +export type SolutionIds = { + [problemTypeId: string]: number; +}; + export interface ProgressHandlerProps { /** * List of problem types that should be solved with the given input. */ problemTypes: string[]; problemInput: T; + setProblemInput: (t: T) => void; } export const ProgressHandler = ( @@ -22,11 +29,26 @@ export const ProgressHandler = ( const [wasClicked, setClicked] = useState(false); const [finished, setFinished] = useState(false); const [solutions, setSolutions] = useState(); + const [problemStates, setProblemStates] = useState[]>([]); const [solveRequest, setSolveRequest] = useState>({ requestContent: props.problemInput, requestedSubSolveRequests: {}, }); + useEffect(() => { + // Handle problem states from local storage + if (problemStates.length == 0) { + let history = getHistory(props.problemTypes); + if (history.length > 0) { + setProblemStates(history); + } + } + }, [problemStates, props.problemTypes]); + + useEffect(() => { + storeHistory(props.problemTypes, problemStates); + }, [problemStates, props.problemTypes]); + async function startSolving() { setClicked(true); setFinished(false); @@ -39,7 +61,35 @@ export const ProgressHandler = ( setSolveRequest(newSolveRequest); Promise.all( props.problemTypes.map((problemType) => - postProblem(problemType, newSolveRequest) + postProblem(problemType, newSolveRequest).then((s) => ({ + problemType: problemType, + solution: s, + })) + ) + ).then((result) => { + let solutionIdMap = result.reduce((acc, item) => { + acc[item.problemType] = item.solution.id; + return acc; + }, {} as SolutionIds); + + let newProblemState: ProblemState = { + problemInput: props.problemInput, + solutionIds: solutionIdMap, + }; + + setSolutions(result.map((x) => x.solution)); + setProblemStates([...problemStates, newProblemState]); + setFinished(true); + }); + } + + async function loadSolution(ids: SolutionIds) { + setClicked(true); + setFinished(false); + + Promise.all( + props.problemTypes.map((problemType) => + fetchSolution(problemType, ids[problemType]) ) ).then((solutions) => { setSolutions(solutions); @@ -86,6 +136,14 @@ export const ProgressHandler = ( )) : null} + + { + props.setProblemInput(problemState.problemInput); + loadSolution(problemState.solutionIds); + }} + /> ); }; diff --git a/src/components/solvers/SAT/TextArea.tsx b/src/components/solvers/SAT/TextArea.tsx index b18ca71..43db5dd 100644 --- a/src/components/solvers/SAT/TextArea.tsx +++ b/src/components/solvers/SAT/TextArea.tsx @@ -1,13 +1,12 @@ +import { Box } from "@chakra-ui/react"; +import { highlight } from "prismjs"; import "prismjs/themes/prism-solarizedlight.css"; //TODO: use custom styling -import React from "react"; import Editor from "react-simple-code-editor"; -import { highlight } from "prismjs"; -import { Box, Container } from "@chakra-ui/react"; import { SAT_language } from "./prism-SAT"; interface TextAreaProps { problemString: string; - setProblemString: React.Dispatch>; + setProblemString: (problemString: string) => void; } export const TextArea = (props: TextAreaProps) => { diff --git a/src/components/solvers/SAT/prism-SAT.ts b/src/components/solvers/SAT/prism-SAT.ts index ba230f3..0cf8dc9 100644 --- a/src/components/solvers/SAT/prism-SAT.ts +++ b/src/components/solvers/SAT/prism-SAT.ts @@ -1,7 +1,7 @@ import { regexVariable } from "../../../converter/dimacs/Syntax/CommonSyntax"; import { regexAND, - regexNOTVariable, + regexNOT, regexOR, } from "../../../converter/dimacs/Syntax/LogicalExpressionSyntax"; @@ -16,7 +16,7 @@ export const SAT_language = { }, "SAT-punctuation": /[()]/, negation: { - pattern: regexNOTVariable, + pattern: regexNOT, alias: "string", }, "SAT-variable": { diff --git a/src/components/solvers/TextInputMask.tsx b/src/components/solvers/TextInputMask.tsx index ec44db7..8d83f93 100644 --- a/src/components/solvers/TextInputMask.tsx +++ b/src/components/solvers/TextInputMask.tsx @@ -1,24 +1,21 @@ -import { Divider, Text, Textarea } from "@chakra-ui/react"; +import { Divider, Textarea } from "@chakra-ui/react"; import Head from "next/head"; -import React, { ReactElement, useState } from "react"; +import { ReactElement, useState } from "react"; import { Container } from "../Container"; -import { Main } from "../Main"; -import { Help } from "./SAT/Help"; import { EditorControls } from "./EditorControls"; export interface TextInputMaskProperties { textPlaceholder: string; - onTextChanged: (text: string) => void; + text: string; + onTextChanged: (value: string) => void; body?: ReactElement; } export const TextInputMask = (props: TextInputMaskProperties) => { - const [text, setText] = useState(""); const [errorString, setErrorString] = useState(""); function onTextChanged(text: string): void { try { - setText(text); props.onTextChanged(text); setErrorString(""); @@ -40,11 +37,11 @@ export const TextInputMask = (props: TextInputMaskProperties) => { errorText={errorString} idleText={props.textPlaceholder + " 👇"} onUpload={onTextChanged} - editorContent={text} + editorContent={props.text} />