-
Notifications
You must be signed in to change notification settings - Fork 6
Propositional Logic Response Area #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8d92173
3f1863b
386f6c7
c886eda
3294edd
1fcfc9c
9ada330
784ceae
613bdef
1a814df
6cc6370
c0ad390
9bf9191
e20e41a
4bf2ae0
33fffb0
1e3260a
a2492e6
ff5ac18
99ca3e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,4 +15,4 @@ yarn-error.log* | |
| tsconfig.tsbuildinfo | ||
|
|
||
| # env files (copy .env.example) | ||
| .env | ||
| .env | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this file be commited? |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was this workaround necessary? This would be good to feedback. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,80 +1,48 @@ | ||
| import { Fira_Sans, Fira_Mono, Lato, Roboto } from 'next/font/google' | ||
| // Vite-compatible font loader (replaces next/font/google) | ||
| // Since this is a Vite build, we can't use next/font/google which requires Next.js compiler | ||
| // Instead, we create compatible objects and inject Google Fonts via CSS | ||
|
|
||
| export const roboto = Roboto({ | ||
| subsets: ['latin'], | ||
| weight: ['400', '700'], | ||
| style: ['normal', 'italic'], | ||
| fallback: [ | ||
| '-apple-system', | ||
| 'BlinkMacSystemFont', | ||
| 'Segoe UI', | ||
| 'Roboto', | ||
| 'Helvetica Neue', | ||
| 'Arial', | ||
| 'sans-serif', | ||
| 'Apple Color Emoji', | ||
| 'Segoe UI Emoji', | ||
| 'Segoe UI Symbol', | ||
| ], | ||
| variable: '--font-roboto', | ||
| preload: false, | ||
| }) | ||
| // Helper function to create a font object compatible with next/font/google structure | ||
| function createFontObject(fontFamily: string, fallback: string[] = []) { | ||
| const fallbackStr = fallback.length > 0 ? `, ${fallback.join(', ')}` : '' | ||
| return { | ||
| style: { | ||
| fontFamily: `"${fontFamily}"${fallbackStr}`, | ||
| }, | ||
| variable: `--font-${fontFamily.toLowerCase().replace(/\s+/g, '-')}`, | ||
| } | ||
| } | ||
|
|
||
| export const firaSans = Fira_Sans({ | ||
| subsets: ['latin'], | ||
| weight: ['300', '400', '500', '600', '700'], | ||
| style: ['normal', 'italic'], | ||
| fallback: [ | ||
| '-apple-system', | ||
| 'BlinkMacSystemFont', | ||
| 'Segoe UI', | ||
| 'Roboto', | ||
| 'Helvetica Neue', | ||
| 'Arial', | ||
| 'sans-serif', | ||
| 'Apple Color Emoji', | ||
| 'Segoe UI Emoji', | ||
| 'Segoe UI Symbol', | ||
| ], | ||
| variable: '--font-fira-sans', | ||
| preload: false, | ||
| }) | ||
| // Inject Google Fonts CSS if not already injected | ||
| if (typeof document !== 'undefined') { | ||
| const fontLinkId = 'google-fonts-vite-loader' | ||
| if (!document.getElementById(fontLinkId)) { | ||
| const link = document.createElement('link') | ||
| link.id = fontLinkId | ||
| link.rel = 'stylesheet' | ||
| link.href = | ||
| 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&family=Fira+Sans:wght@300;400;500;600;700&family=Fira+Mono:wght@400&family=Lato:wght@400&display=swap' | ||
| document.head.appendChild(link) | ||
| } | ||
| } | ||
|
|
||
| export const firaMono = Fira_Mono({ | ||
| subsets: ['latin'], | ||
| weight: ['400'], | ||
| fallback: [ | ||
| '-apple-system', | ||
| 'BlinkMacSystemFont', | ||
| 'Segoe UI', | ||
| 'Roboto', | ||
| 'Helvetica Neue', | ||
| 'Arial', | ||
| 'sans-serif', | ||
| 'Apple Color Emoji', | ||
| 'Segoe UI Emoji', | ||
| 'Segoe UI Symbol', | ||
| ], | ||
| variable: '--font-fira-mono', | ||
| preload: false, | ||
| }) | ||
| const robotoFallback = [ | ||
| '-apple-system', | ||
| 'BlinkMacSystemFont', | ||
| 'Segoe UI', | ||
| 'Roboto', | ||
| 'Helvetica Neue', | ||
| 'Arial', | ||
| 'sans-serif', | ||
| 'Apple Color Emoji', | ||
| 'Segoe UI Emoji', | ||
| 'Segoe UI Symbol', | ||
| ] | ||
|
|
||
| export const lato = Lato({ | ||
| subsets: ['latin'], | ||
| weight: ['400'], | ||
| style: ['normal', 'italic'], | ||
| fallback: [ | ||
| '-apple-system', | ||
| 'BlinkMacSystemFont', | ||
| 'Segoe UI', | ||
| 'Roboto', | ||
| 'Helvetica Neue', | ||
| 'Arial', | ||
| 'sans-serif', | ||
| 'Apple Color Emoji', | ||
| 'Segoe UI Emoji', | ||
| 'Segoe UI Symbol', | ||
| ], | ||
| variable: '--font-lato', | ||
| preload: false, | ||
| }) | ||
| export const roboto = createFontObject('Roboto', robotoFallback) | ||
|
|
||
| export const firaSans = createFontObject('Fira Sans', robotoFallback) | ||
|
|
||
| export const firaMono = createFontObject('Fira Mono', robotoFallback) | ||
|
|
||
| export const lato = createFontObject('Lato', robotoFallback) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,26 @@ | ||
| import { QueryClient, QueryClientProvider } from 'react-query' | ||
| import { SnackbarProvider } from 'notistack' | ||
| import { ThemeProvider } from '@styles/minimal/theme-provider' | ||
| import { SandboxResponseAreaTub } from './types/Sandbox/index' | ||
|
|
||
| // Create a QueryClient instance | ||
| const queryClient = new QueryClient({ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the query client used for? |
||
| defaultOptions: { | ||
| queries: { | ||
| refetchOnWindowFocus: false, | ||
| retry: false, | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| function ResponseAreaInputWrapper({ children }: { children: React.ReactNode }) { | ||
| return <ThemeProvider>{children}</ThemeProvider> | ||
| return ( | ||
| <QueryClientProvider client={queryClient}> | ||
| <SnackbarProvider maxSnack={3}> | ||
| <ThemeProvider>{children}</ThemeProvider> | ||
| </SnackbarProvider> | ||
| </QueryClientProvider> | ||
| ) | ||
| } | ||
|
|
||
| // wrap the components with the necessary providers; only in the sandbox | ||
|
|
@@ -31,4 +49,4 @@ class WrappedSandboxResponseAreaTub extends SandboxResponseAreaTub { | |
| } | ||
| } | ||
|
|
||
| export default WrappedSandboxResponseAreaTub | ||
| export default WrappedSandboxResponseAreaTub | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import Stack from '@mui/material/Stack' | ||
| import Box from '@mui/system/Box' | ||
| import React, { useCallback, useEffect, useRef, useState } from 'react' | ||
|
|
||
| import { | ||
| OmniInputResponsArea, | ||
| OmniInputResponsAreaProps, | ||
| } from '@components/OmniInput/OmniInputResponseArea.component' | ||
| import { ResponseAreaOmniInputContainer } from '@modules/shared/components/ResponseArea/ResponseAreaOmniInputContainer.component' | ||
| import { BaseResponseAreaProps } from '../base-props.type' | ||
| import { PropositionalLogicAnswerSchema } from './PropositionalLogic.schema' | ||
| import { PropositionalLogicSymbolKeyboard } from './PropositionalLogicSymbolKeyboard.component' | ||
| import { TruthTableSection } from './TruthTableSection.component' | ||
|
|
||
| type PropositionalLogicProps = Omit< | ||
| BaseResponseAreaProps, | ||
| 'handleChange' | 'answer' | ||
| > & { | ||
| handleChange: (answer: PropositionalLogicAnswerSchema) => void | ||
| answer: PropositionalLogicAnswerSchema | undefined | ||
| allowDraw: boolean | ||
| allowScan: boolean | ||
| enableRefinement: boolean | ||
| allowTruthTable?: boolean | ||
| } | ||
|
|
||
| export const PropositionalLogic: React.FC<PropositionalLogicProps> = ({ | ||
| handleChange, | ||
| handleSubmit, | ||
| answer, | ||
| allowDraw, | ||
| allowScan, | ||
| allowTruthTable = false, | ||
| hasPreview, | ||
| enableRefinement, | ||
| feedback, | ||
| typesafeErrorMessage, | ||
| checkIsLoading, | ||
| preResponseText, | ||
| postResponseText, | ||
| responsePreviewParams, | ||
| displayMode, | ||
| }) => { | ||
| // Normalize answer to object shape { formula, truthTable } | ||
| const answerObject = answer ?? { formula: '', truthTable: undefined } | ||
| const currentFormula = answerObject.formula ?? '' | ||
|
|
||
| // Remount OmniInput when symbol button is clicked so it shows updated value (it only reads defaultValue on mount) | ||
| const [formulaKey, setFormulaKey] = useState(0) | ||
| const [displayAnswer, setDisplayAnswer] = useState(currentFormula) | ||
|
|
||
| // Sync displayAnswer with answer prop when it changes | ||
| useEffect(() => { | ||
| setDisplayAnswer(currentFormula) | ||
| }, [currentFormula]) | ||
|
|
||
| const omniInputContainerRef = useRef<HTMLDivElement | null>(null) | ||
| const cursorRef = useRef({ start: 0, end: 0 }) | ||
| const pendingCursorRef = useRef<number | null>(null) | ||
|
|
||
| const submitAnswer = useCallback( | ||
| (formula: string, truthTable: PropositionalLogicAnswerSchema['truthTable']) => { | ||
| handleChange({ | ||
| formula, | ||
| truthTable: allowTruthTable ? truthTable : undefined, | ||
| }) | ||
| }, | ||
| [handleChange, allowTruthTable], | ||
| ) | ||
|
|
||
| const onFormulaChange = useCallback<OmniInputResponsAreaProps['handleChange']>( | ||
| (newFormula) => { | ||
| setDisplayAnswer(newFormula) | ||
| submitAnswer(newFormula, answerObject.truthTable) | ||
| }, | ||
| [answerObject.truthTable, submitAnswer], | ||
| ) | ||
|
|
||
| const insertSymbol = useCallback( | ||
| (symbol: string) => { | ||
| const { start, end } = cursorRef.current | ||
| const newValue = | ||
| displayAnswer.slice(0, start) + symbol + displayAnswer.slice(end) | ||
| setDisplayAnswer(newValue) | ||
| submitAnswer(newValue, answerObject.truthTable) | ||
| pendingCursorRef.current = start + symbol.length | ||
| setFormulaKey(k => k + 1) | ||
| }, | ||
| [displayAnswer, answerObject.truthTable, submitAnswer], | ||
| ) | ||
|
|
||
| // Attach cursor-tracking listeners to OmniInput's textarea (found via DOM) | ||
| useEffect(() => { | ||
| const container = omniInputContainerRef.current | ||
| if (!container) return | ||
|
|
||
| let timeoutId: ReturnType<typeof setTimeout> | ||
| let cleanup: (() => void) | undefined | ||
|
|
||
| const tryAttach = () => { | ||
| const textarea = container.querySelector('textarea') | ||
| if (textarea) { | ||
| const updateCursor = () => { | ||
| cursorRef.current = { | ||
| start: textarea.selectionStart ?? 0, | ||
| end: textarea.selectionEnd ?? 0, | ||
| } | ||
| } | ||
| const events = ['select', 'keyup', 'mouseup', 'blur', 'focus', 'input'] as const | ||
| events.forEach(ev => textarea.addEventListener(ev, updateCursor)) | ||
| cleanup = () => { | ||
| events.forEach(ev => textarea.removeEventListener(ev, updateCursor)) | ||
| } | ||
| return | ||
| } | ||
| timeoutId = setTimeout(tryAttach, 50) | ||
| } | ||
|
|
||
| tryAttach() | ||
|
|
||
| return () => { | ||
| clearTimeout(timeoutId) | ||
| cleanup?.() | ||
| } | ||
| }, [formulaKey]) | ||
|
|
||
| // Restore cursor position after symbol insert (OmniInput remounts) | ||
| useEffect(() => { | ||
| const pos = pendingCursorRef.current | ||
| if (pos === null) return | ||
|
|
||
| const container = omniInputContainerRef.current | ||
| if (!container) return | ||
|
|
||
| const tryRestore = () => { | ||
| const textarea = container.querySelector('textarea') | ||
| if (textarea) { | ||
| pendingCursorRef.current = null | ||
| textarea.focus() | ||
| textarea.setSelectionRange(pos, pos) | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| if (!tryRestore()) { | ||
| const id = setTimeout(() => tryRestore(), 0) | ||
| return () => clearTimeout(id) | ||
| } | ||
| }, [displayAnswer, formulaKey]) | ||
|
|
||
| const onTruthTableChange = useCallback( | ||
| (truthTable: PropositionalLogicAnswerSchema['truthTable']) => { | ||
| if (!truthTable) return | ||
| submitAnswer(displayAnswer, truthTable) | ||
| }, | ||
| [displayAnswer, submitAnswer], | ||
| ) | ||
|
|
||
| const onRemoveTruthTable = useCallback(() => { | ||
| submitAnswer(displayAnswer, undefined) | ||
| }, [displayAnswer, submitAnswer]) | ||
|
|
||
| return ( | ||
| <ResponseAreaOmniInputContainer | ||
ashwin6-dev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| preResponseText={preResponseText} | ||
| postResponseText={postResponseText}> | ||
| <Stack spacing={1}> | ||
| <PropositionalLogicSymbolKeyboard onInsert={insertSymbol} /> | ||
| <Box ref={omniInputContainerRef}> | ||
| <OmniInputResponsArea | ||
| key={formulaKey} | ||
| handleChange={onFormulaChange} | ||
| handleSubmit={handleSubmit} | ||
| answer={displayAnswer} | ||
| processingMode="markdown" | ||
| allowDraw={allowDraw} | ||
| allowScan={allowScan} | ||
| hasPreview={hasPreview} | ||
| enableRefinement={false} | ||
| feedback={feedback} | ||
| typesafeErrorMessage={typesafeErrorMessage} | ||
| checkIsLoading={checkIsLoading} | ||
| responsePreviewParams={responsePreviewParams} | ||
| displayMode={displayMode} | ||
| /> | ||
| </Box> | ||
| {allowTruthTable && ( | ||
| <TruthTableSection | ||
| formula={displayAnswer} | ||
| truthTable={answerObject.truthTable ?? undefined} | ||
| onTruthTableChange={onTruthTableChange} | ||
| onRemoveTruthTable={onRemoveTruthTable} | ||
| allowDraw={allowDraw} | ||
| allowScan={allowScan} | ||
| processingMode="markdown" | ||
| /> | ||
| )} | ||
| </Stack> | ||
| </ResponseAreaOmniInputContainer> | ||
| ) | ||
| } | ||
|
|
||
| export const HMR = true | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be commited as a change?