Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
math_tutor_dev
public_math_tutor
Commits
104161ac
Commit
104161ac
authored
Jan 19, 2026
by
Kantz
Browse files
added Canvas and API
parent
9e376369
Changes
5
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/main.py
View file @
104161ac
from
__future__
import
annotations
from
typing
import
List
,
Optional
from
fastapi
import
FastAPI
,
HTTPException
from
pydantic
import
BaseModel
,
Field
app
=
FastAPI
(
title
=
"Math Tutor API"
,
version
=
"0.1.0"
)
class
ChatMessage
(
BaseModel
):
role
:
str
=
Field
(...,
pattern
=
"^(user|assistant)$"
)
text
:
str
=
Field
(...,
min_length
=
1
)
class
ChatRequest
(
BaseModel
):
messages
:
List
[
ChatMessage
]
draft
:
Optional
[
str
]
=
None
class
ChatResponse
(
BaseModel
):
reply
:
str
sources
:
List
[
str
]
=
[]
class
CanvasSaveRequest
(
BaseModel
):
data_url
:
str
=
Field
(...,
min_length
=
1
)
filename_hint
:
Optional
[
str
]
=
None
class
CanvasSaveResponse
(
BaseModel
):
status
:
str
latex
:
str
saved_as
:
Optional
[
str
]
=
None
@
app
.
get
(
"/api/health"
)
def
health
()
->
dict
:
return
{
"status"
:
"ok"
}
@
app
.
post
(
"/api/chat"
,
response_model
=
ChatResponse
)
def
chat
(
request
:
ChatRequest
)
->
ChatResponse
:
if
not
request
.
messages
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"messages required"
)
last
=
request
.
messages
[
-
1
]
reply
=
f
"Mock reply to:
{
last
.
text
}
"
return
ChatResponse
(
reply
=
reply
,
sources
=
[
"doc:example"
])
@
app
.
post
(
"/api/canvas/save"
,
response_model
=
CanvasSaveResponse
)
def
save_canvas
(
request
:
CanvasSaveRequest
)
->
CanvasSaveResponse
:
if
not
request
.
data_url
.
startswith
(
"data:image"
):
raise
HTTPException
(
status_code
=
400
,
detail
=
"data_url must be an image"
)
return
CanvasSaveResponse
(
status
=
"ok"
,
latex
=
"
\\
frac{a}{b}"
,
saved_as
=
request
.
filename_hint
,
)
math-tutor/frontend/src/components/Canvas/CanvasDrawer.tsx
View file @
104161ac
import
{
useEffect
,
useRef
,
useState
}
from
"
react
"
;
import
CanvasToggle
from
"
./CanvasToggle
"
;
import
CanvasToggle
from
"
./CanvasToggle
"
;
type
CanvasDrawerProps
=
{
type
CanvasDrawerProps
=
{
isVisible
:
boolean
;
isVisible
:
boolean
;
onToggle
:
()
=>
void
;
onToggle
:
()
=>
void
;
onClear
?:
()
=>
void
;
onClear
?:
()
=>
void
;
onSave
?:
()
=>
void
;
onSave
?:
(
dataUrl
:
string
)
=>
void
;
};
};
export
default
function
CanvasDrawer
({
export
default
function
CanvasDrawer
({
...
@@ -13,6 +14,119 @@ export default function CanvasDrawer({
...
@@ -13,6 +14,119 @@ export default function CanvasDrawer({
onClear
,
onClear
,
onSave
,
onSave
,
}:
CanvasDrawerProps
)
{
}:
CanvasDrawerProps
)
{
const
canvasRef
=
useRef
<
HTMLCanvasElement
|
null
>
(
null
);
const
[
isDrawing
,
setIsDrawing
]
=
useState
(
false
);
const
[
isErasing
,
setIsErasing
]
=
useState
(
false
);
const
[
lastPoint
,
setLastPoint
]
=
useState
<
{
x
:
number
;
y
:
number
}
|
null
>
(
null
);
useEffect
(()
=>
{
const
canvas
=
canvasRef
.
current
;
if
(
!
canvas
)
{
return
;
}
const
context
=
canvas
.
getContext
(
"
2d
"
);
if
(
!
context
)
{
return
;
}
const
ratio
=
window
.
devicePixelRatio
||
1
;
const
width
=
720
;
const
height
=
320
;
canvas
.
width
=
width
*
ratio
;
canvas
.
height
=
height
*
ratio
;
canvas
.
style
.
width
=
`
${
width
}
px`
;
canvas
.
style
.
height
=
`
${
height
}
px`
;
context
.
scale
(
ratio
,
ratio
);
context
.
lineWidth
=
2
;
context
.
lineCap
=
"
round
"
;
context
.
strokeStyle
=
"
#1d1b16
"
;
},
[]);
const
getPoint
=
(
event
:
React
.
PointerEvent
<
HTMLCanvasElement
>
)
=>
{
const
canvas
=
canvasRef
.
current
;
if
(
!
canvas
)
{
return
null
;
}
const
rect
=
canvas
.
getBoundingClientRect
();
return
{
x
:
event
.
clientX
-
rect
.
left
,
y
:
event
.
clientY
-
rect
.
top
,
};
};
const
handlePointerDown
=
(
event
:
React
.
PointerEvent
<
HTMLCanvasElement
>
)
=>
{
const
point
=
getPoint
(
event
);
if
(
!
point
)
{
return
;
}
setIsDrawing
(
true
);
setLastPoint
(
point
);
};
const
handlePointerMove
=
(
event
:
React
.
PointerEvent
<
HTMLCanvasElement
>
)
=>
{
if
(
!
isDrawing
||
!
lastPoint
)
{
return
;
}
const
canvas
=
canvasRef
.
current
;
const
context
=
canvas
?.
getContext
(
"
2d
"
);
const
point
=
getPoint
(
event
);
if
(
!
context
||
!
point
)
{
return
;
}
context
.
globalCompositeOperation
=
isErasing
?
"
destination-out
"
:
"
source-over
"
;
context
.
beginPath
();
context
.
moveTo
(
lastPoint
.
x
,
lastPoint
.
y
);
context
.
lineTo
(
point
.
x
,
point
.
y
);
context
.
stroke
();
context
.
globalCompositeOperation
=
"
source-over
"
;
setLastPoint
(
point
);
};
const
handlePointerUp
=
()
=>
{
setIsDrawing
(
false
);
setLastPoint
(
null
);
};
const
handleClear
=
()
=>
{
const
canvas
=
canvasRef
.
current
;
const
context
=
canvas
?.
getContext
(
"
2d
"
);
if
(
!
canvas
||
!
context
)
{
return
;
}
context
.
clearRect
(
0
,
0
,
canvas
.
width
,
canvas
.
height
);
onClear
?.();
};
const
handleSave
=
()
=>
{
const
canvas
=
canvasRef
.
current
;
if
(
!
canvas
)
{
return
;
}
const
dataUrl
=
canvas
.
toDataURL
(
"
image/png
"
);
onSave
?.(
dataUrl
);
};
const
handleToggleEraser
=
()
=>
{
setIsErasing
((
prev
)
=>
!
prev
);
};
return
(
return
(
<
div
className
=
"canvas-zone"
>
<
div
className
=
"canvas-zone"
>
<
div
className
=
"canvas-header"
>
<
div
className
=
"canvas-header"
>
...
@@ -21,12 +135,24 @@ export default function CanvasDrawer({
...
@@ -21,12 +135,24 @@ export default function CanvasDrawer({
</
div
>
</
div
>
{
isVisible
?
(
{
isVisible
?
(
<
div
className
=
"canvas-body"
>
<
div
className
=
"canvas-body"
>
<
div
className
=
"canvas-placeholder"
>
Drawing surface goes here
</
div
>
<
div
className
=
"canvas-surface"
>
<
canvas
ref
=
{
canvasRef
}
className
=
"canvas-board"
onPointerDown
=
{
handlePointerDown
}
onPointerMove
=
{
handlePointerMove
}
onPointerUp
=
{
handlePointerUp
}
onPointerLeave
=
{
handlePointerUp
}
/>
</
div
>
<
div
className
=
"canvas-actions"
>
<
div
className
=
"canvas-actions"
>
<
button
className
=
"btn"
type
=
"button"
onClick
=
{
onClear
}
>
<
button
className
=
"btn"
type
=
"button"
onClick
=
{
handleToggleEraser
}
>
{
isErasing
?
"
Pen
"
:
"
Eraser
"
}
</
button
>
<
button
className
=
"btn"
type
=
"button"
onClick
=
{
handleClear
}
>
Clear
Clear
</
button
>
</
button
>
<
button
className
=
"btn primary"
type
=
"button"
onClick
=
{
on
Save
}
>
<
button
className
=
"btn primary"
type
=
"button"
onClick
=
{
handle
Save
}
>
Save + Convert
Save + Convert
</
button
>
</
button
>
</
div
>
</
div
>
...
...
math-tutor/frontend/src/components/Chat/ChatWindow.tsx
View file @
104161ac
import
MessageInput
from
"
./MessageInput
"
;
import
MessageInput
from
"
./MessageInput
"
;
import
MessageList
,
{
ChatMessage
}
from
"
./MessageList
"
;
import
MessageList
from
"
./MessageList
"
;
import
type
{
ChatMessage
}
from
"
./MessageList
"
;
type
ChatWindowProps
=
{
type
ChatWindowProps
=
{
messages
:
ChatMessage
[];
messages
:
ChatMessage
[];
...
...
math-tutor/frontend/src/pages/App.tsx
View file @
104161ac
...
@@ -63,6 +63,21 @@ export default function App() {
...
@@ -63,6 +63,21 @@ export default function App() {
setIsCanvasVisible
((
prev
)
=>
!
prev
);
setIsCanvasVisible
((
prev
)
=>
!
prev
);
};
};
const
handleCanvasSave
=
(
dataUrl
:
string
)
=>
{
setDraft
((
prev
)
=>
prev
?
`
${
prev
}
[drawing saved]`
:
"
[drawing saved]
"
);
setMessages
((
prev
)
=>
[
...
prev
,
{
id
:
`m-
${
Date
.
now
()}
-assistant`
,
role
:
"
assistant
"
,
text
:
"
Drawing saved (mock). Ready to send to Mathpix.
"
,
},
]);
void
dataUrl
;
};
const
handleCiteDoc
=
(
docId
:
string
)
=>
{
const
handleCiteDoc
=
(
docId
:
string
)
=>
{
setDraft
((
prev
)
=>
(
prev
?
`
${
prev
}
[
${
docId
}
]`
:
`[
${
docId
}
]`
));
setDraft
((
prev
)
=>
(
prev
?
`
${
prev
}
[
${
docId
}
]`
:
`[
${
docId
}
]`
));
};
};
...
@@ -98,6 +113,7 @@ export default function App() {
...
@@ -98,6 +113,7 @@ export default function App() {
<
CanvasDrawer
<
CanvasDrawer
isVisible
=
{
isCanvasVisible
}
isVisible
=
{
isCanvasVisible
}
onToggle
=
{
handleToggleCanvas
}
onToggle
=
{
handleToggleCanvas
}
onSave
=
{
handleCanvasSave
}
/>
/>
</
section
>
</
section
>
...
...
math-tutor/frontend/src/styles/theme.css
View file @
104161ac
...
@@ -191,13 +191,19 @@ body {
...
@@ -191,13 +191,19 @@ body {
gap
:
12px
;
gap
:
12px
;
}
}
.canvas-
placeholder
{
.canvas-
surface
{
border
:
1px
dashed
#b9b2a4
;
border
:
1px
dashed
#b9b2a4
;
border-radius
:
12px
;
border-radius
:
12px
;
min-height
:
160px
;
background
:
#fffdf8
;
display
:
grid
;
padding
:
12px
;
place-items
:
center
;
}
color
:
#6f675d
;
.canvas-board
{
width
:
100%
;
height
:
100%
;
min-height
:
200px
;
display
:
block
;
cursor
:
crosshair
;
}
}
.canvas-actions
{
.canvas-actions
{
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment