Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8d92173
implemented textarea with special symbols buttons
ashwin6-dev Jan 20, 2026
3f1863b
fixed ResponseArea Loading Error by changing responseType to SANDBOX
HarrySu123 Jan 20, 2026
386f6c7
implemented drawing and photo mode, but preview not available yet (im…
HarrySu123 Jan 26, 2026
c886eda
implemented truth table ui
ashwin6-dev Feb 3, 2026
3294edd
fixed answer updating issues and changed truth table to use dropdowns
ashwin6-dev Feb 4, 2026
1fcfc9c
implemented wizard component with truth table
ashwin6-dev Feb 4, 2026
9ada330
fixed truth table not showing issue by renaming index to SANDBOX
HarrySu123 Feb 4, 2026
784ceae
let user configure the table themselves
ashwin6-dev Feb 10, 2026
613bdef
Merge branch 'main' of https://github.com/ashwin6-dev/prop-logic-resp…
ashwin6-dev Feb 10, 2026
1a814df
removed omni input temporarily
ashwin6-dev Feb 17, 2026
6cc6370
feat: done the wiring withthe frontend, certain schema might need cha…
HongleiGu Feb 17, 2026
c0ad390
feat: wizard contains marking parameters
ashwin6-dev Feb 19, 2026
9bf9191
special characters keyboard fixed. now adds to cursor position
ashwin6-dev Mar 2, 2026
e20e41a
factored special characters keyboard out into its own component
ashwin6-dev Mar 3, 2026
4bf2ae0
added ability to remove rows and cols from truth table
ashwin6-dev Mar 3, 2026
33fffb0
added XOR to special characters
ashwin6-dev Mar 10, 2026
1e3260a
removed package-lock.json
ashwin6-dev Mar 10, 2026
a2492e6
added yarn.lock to .gitignore
ashwin6-dev Mar 10, 2026
ff5ac18
only show truth table button if answer expects it
ashwin6-dev Mar 10, 2026
99ca3e1
restored original yarn.lock
ashwin6-dev Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ yarn-error.log*
tsconfig.tsbuildinfo

# env files (copy .env.example)
.env
.env
2 changes: 1 addition & 1 deletion externals/api/fetcher.ts
Copy link
Member

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?

Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export const fetchData = <TData, TVariables>(
_options?: unknown,
): (() => Promise<TData>) => {
return null as any
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this file be commited?

Empty file.
120 changes: 44 additions & 76 deletions externals/styles/fonts.tsx
Copy link
Member

Choose a reason for hiding this comment

The 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)
22 changes: 20 additions & 2 deletions src/sandbox-component.tsx
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({
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -31,4 +49,4 @@ class WrappedSandboxResponseAreaTub extends SandboxResponseAreaTub {
}
}

export default WrappedSandboxResponseAreaTub
export default WrappedSandboxResponseAreaTub
204 changes: 204 additions & 0 deletions src/types/PropositionalLogic/PropositionalLogic.component.tsx
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
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
Loading