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
9c04fca1
Commit
9c04fca1
authored
Jan 19, 2026
by
Kantz
Browse files
apit aufgeteilt
parent
3d5974ad
Changes
9
Show whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/canvas.py
View file @
9c04fca1
from
__future__
import
annotations
import
base64
import
os
import
time
from
pathlib
import
Path
from
typing
import
Optional
from
fastapi
import
APIRouter
,
HTTPException
from
pydantic
import
BaseModel
,
Field
from
dotenv
import
load_dotenv
try
:
from
mpxpy.mathpix_client
import
MathpixClient
except
ImportError
:
# pragma: no cover - optional dependency
MathpixClient
=
None
router
=
APIRouter
()
load_dotenv
()
MATHPIX_APP_ID
=
os
.
getenv
(
"MATHPIX_APP_ID"
)
MATHPIX_APP_KEY
=
os
.
getenv
(
"MATHPIX_APP_KEY"
)
mathpix_client
=
None
if
MathpixClient
and
MATHPIX_APP_ID
and
MATHPIX_APP_KEY
:
mathpix_client
=
MathpixClient
(
app_id
=
MATHPIX_APP_ID
,
app_key
=
MATHPIX_APP_KEY
)
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
@
router
.
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"
)
header
,
encoded
=
request
.
data_url
.
split
(
","
,
1
)
if
"image/png"
not
in
header
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"only image/png supported"
)
try
:
raw
=
base64
.
b64decode
(
encoded
)
except
base64
.
binascii
.
Error
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"invalid base64 payload"
)
from
exc
drawings_dir
=
Path
(
__file__
).
resolve
().
parents
[
1
]
/
"storage"
/
"drawings"
drawings_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
timestamp
=
int
(
time
.
time
())
safe_hint
=
request
.
filename_hint
or
"drawing"
safe_hint
=
""
.
join
(
ch
for
ch
in
safe_hint
if
ch
.
isalnum
()
or
ch
in
(
"-"
,
"_"
))
if
not
safe_hint
:
safe_hint
=
"drawing"
filename
=
f
"
{
safe_hint
}
-
{
timestamp
}
.png"
file_path
=
drawings_dir
/
filename
with
file_path
.
open
(
"wb"
)
as
handle
:
handle
.
write
(
raw
)
latex
=
"
\\
frac{a}{b}"
if
mathpix_client
:
try
:
image
=
mathpix_client
.
image_new
(
str
(
file_path
))
mmd
=
image
.
mmd
()
conversion
=
mathpix_client
.
conversion_new
(
mmd
=
mmd
,
convert_to_md
=
True
,
)
conversion
.
wait_until_complete
()
latex
=
conversion
.
to_md_text
()
except
Exception
as
exc
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"mathpix failed"
)
from
exc
return
CanvasSaveResponse
(
status
=
"ok"
,
latex
=
latex
,
saved_as
=
str
(
file_path
),
)
math-tutor/backend/app/api/chat.py
View file @
9c04fca1
from
__future__
import
annotations
from
typing
import
List
,
Optional
from
fastapi
import
APIRouter
,
HTTPException
from
pydantic
import
BaseModel
,
Field
router
=
APIRouter
()
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
]
=
[]
@
router
.
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"
])
math-tutor/backend/app/api/retrieval.py
View file @
9c04fca1
from
__future__
import
annotations
from
fastapi
import
APIRouter
router
=
APIRouter
()
@
router
.
get
(
"/api/retrieval/health"
)
def
retrieval_health
()
->
dict
:
return
{
"status"
:
"ok"
}
math-tutor/backend/app/main.py
View file @
9c04fca1
from
__future__
import
annotations
from
fastapi
import
FastAPI
from
typing
import
List
,
Optional
import
base64
import
os
import
time
from
pathlib
import
Path
from
fastapi
import
FastAPI
,
HTTPException
from
fastapi.middleware.cors
import
CORSMiddleware
from
fastapi.middleware.cors
import
CORSMiddleware
from
pydantic
import
BaseModel
,
Field
from
app.api
import
canvas
,
chat
,
retrieval
from
dotenv
import
load_dotenv
try
:
from
mpxpy.mathpix_client
import
MathpixClient
except
ImportError
:
# pragma: no cover - optional dependency
MathpixClient
=
None
app
=
FastAPI
(
title
=
"Math Tutor API"
,
version
=
"0.1.0"
)
app
=
FastAPI
(
title
=
"Math Tutor API"
,
version
=
"0.1.0"
)
load_dotenv
()
app
.
add_middleware
(
app
.
add_middleware
(
CORSMiddleware
,
CORSMiddleware
,
allow_origins
=
[
"http://localhost:5173"
],
allow_origins
=
[
"http://localhost:5173"
],
...
@@ -28,100 +12,11 @@ app.add_middleware(
...
@@ -28,100 +12,11 @@ app.add_middleware(
allow_headers
=
[
"*"
],
allow_headers
=
[
"*"
],
)
)
MATHPIX_APP_ID
=
os
.
getenv
(
"MATHPIX_APP_ID"
)
app
.
include_router
(
chat
.
router
)
MATHPIX_APP_KEY
=
os
.
getenv
(
"MATHPIX_APP_KEY"
)
app
.
include_router
(
canvas
.
router
)
app
.
include_router
(
retrieval
.
router
)
mathpix_client
=
None
if
MathpixClient
and
MATHPIX_APP_ID
and
MATHPIX_APP_KEY
:
mathpix_client
=
MathpixClient
(
app_id
=
MATHPIX_APP_ID
,
app_key
=
MATHPIX_APP_KEY
)
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"
)
@
app
.
get
(
"/api/health"
)
def
health
()
->
dict
:
def
health
()
->
dict
:
return
{
"status"
:
"ok"
}
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"
)
header
,
encoded
=
request
.
data_url
.
split
(
","
,
1
)
if
"image/png"
not
in
header
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"only image/png supported"
)
try
:
raw
=
base64
.
b64decode
(
encoded
)
except
base64
.
binascii
.
Error
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
"invalid base64 payload"
)
from
exc
drawings_dir
=
Path
(
__file__
).
resolve
().
parent
/
"storage"
/
"drawings"
drawings_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
timestamp
=
int
(
time
.
time
())
safe_hint
=
request
.
filename_hint
or
"drawing"
safe_hint
=
""
.
join
(
ch
for
ch
in
safe_hint
if
ch
.
isalnum
()
or
ch
in
(
"-"
,
"_"
))
if
not
safe_hint
:
safe_hint
=
"drawing"
filename
=
f
"
{
safe_hint
}
-
{
timestamp
}
.png"
file_path
=
drawings_dir
/
filename
with
file_path
.
open
(
"wb"
)
as
handle
:
handle
.
write
(
raw
)
latex
=
"
\\
frac{a}{b}"
if
mathpix_client
:
try
:
image
=
mathpix_client
.
image_new
(
str
(
file_path
))
mmd
=
image
.
mmd
()
conversion
=
mathpix_client
.
conversion_new
(
mmd
=
mmd
,
convert_to_md
=
True
,
)
conversion
.
wait_until_complete
()
latex
=
conversion
.
to_md_text
()
except
Exception
as
exc
:
raise
HTTPException
(
status_code
=
500
,
detail
=
"mathpix failed"
)
from
exc
return
CanvasSaveResponse
(
status
=
"ok"
,
latex
=
latex
,
saved_as
=
str
(
file_path
),
)
math-tutor/frontend/index.html
View file @
9c04fca1
...
@@ -4,7 +4,23 @@
...
@@ -4,7 +4,23 @@
<meta
charset=
"UTF-8"
/>
<meta
charset=
"UTF-8"
/>
<link
rel=
"icon"
type=
"image/svg+xml"
href=
"/vite.svg"
/>
<link
rel=
"icon"
type=
"image/svg+xml"
href=
"/vite.svg"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<title>
frontend
</title>
<title>
Math Tutor
</title>
<script>
window
.
MathJax
=
{
tex
:
{
inlineMath
:
[[
"
$
"
,
"
$
"
],
[
"
\\
(
"
,
"
\\
)
"
]],
displayMath
:
[[
"
$$
"
,
"
$$
"
],
[
"
\\
[
"
,
"
\\
]
"
]],
},
options
:
{
skipHtmlTags
:
[
"
script
"
,
"
noscript
"
,
"
style
"
,
"
textarea
"
,
"
pre
"
,
"
code
"
],
},
};
</script>
<script
id=
"mathjax-script"
async
src=
"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
></script>
</head>
</head>
<body>
<body>
<div
id=
"root"
></div>
<div
id=
"root"
></div>
...
...
math-tutor/frontend/src/components/Chat/ChatWindow.tsx
View file @
9c04fca1
...
@@ -7,7 +7,6 @@ type ChatWindowProps = {
...
@@ -7,7 +7,6 @@ type ChatWindowProps = {
draft
:
string
;
draft
:
string
;
onDraftChange
:
(
value
:
string
)
=>
void
;
onDraftChange
:
(
value
:
string
)
=>
void
;
onSend
:
()
=>
void
;
onSend
:
()
=>
void
;
onInsertLatex
?:
()
=>
void
;
onToggleCanvas
?:
()
=>
void
;
onToggleCanvas
?:
()
=>
void
;
};
};
...
@@ -16,7 +15,6 @@ export default function ChatWindow({
...
@@ -16,7 +15,6 @@ export default function ChatWindow({
draft
,
draft
,
onDraftChange
,
onDraftChange
,
onSend
,
onSend
,
onInsertLatex
,
onToggleCanvas
,
onToggleCanvas
,
}:
ChatWindowProps
)
{
}:
ChatWindowProps
)
{
return
(
return
(
...
@@ -27,7 +25,6 @@ export default function ChatWindow({
...
@@ -27,7 +25,6 @@ export default function ChatWindow({
value
=
{
draft
}
value
=
{
draft
}
onChange
=
{
onDraftChange
}
onChange
=
{
onDraftChange
}
onSend
=
{
onSend
}
onSend
=
{
onSend
}
onInsertLatex
=
{
onInsertLatex
}
onToggleCanvas
=
{
onToggleCanvas
}
onToggleCanvas
=
{
onToggleCanvas
}
/>
/>
</
div
>
</
div
>
...
...
math-tutor/frontend/src/components/Chat/MessageBubble.tsx
View file @
9c04fca1
import
{
useEffect
,
useRef
}
from
"
react
"
;
type
MessageBubbleProps
=
{
type
MessageBubbleProps
=
{
role
:
"
user
"
|
"
assistant
"
;
role
:
"
user
"
|
"
assistant
"
;
text
:
string
;
text
:
string
;
};
};
declare
global
{
interface
Window
{
MathJax
?:
{
typesetPromise
?:
(
elements
?:
Element
[])
=>
Promise
<
void
>
;
};
}
}
export
default
function
MessageBubble
({
role
,
text
}:
MessageBubbleProps
)
{
export
default
function
MessageBubble
({
role
,
text
}:
MessageBubbleProps
)
{
const
bubbleRef
=
useRef
<
HTMLDivElement
|
null
>
(
null
);
useEffect
(()
=>
{
if
(
window
.
MathJax
?.
typesetPromise
&&
bubbleRef
.
current
)
{
window
.
MathJax
.
typesetPromise
([
bubbleRef
.
current
]).
catch
(()
=>
undefined
);
}
},
[
text
]);
return
(
return
(
<
div
className
=
{
`message-bubble
${
role
}
`
}
>
<
div
className
=
{
`message-bubble
${
role
}
`
}
ref
=
{
bubbleRef
}
>
<
div
className
=
"message-role"
>
{
role
}
</
div
>
<
div
className
=
"message-role"
>
{
role
}
</
div
>
<
div
className
=
"message-text"
>
{
text
}
</
div
>
<
div
className
=
"message-text"
>
{
text
}
</
div
>
</
div
>
</
div
>
...
...
math-tutor/frontend/src/components/Chat/MessageInput.tsx
View file @
9c04fca1
...
@@ -2,7 +2,6 @@ type MessageInputProps = {
...
@@ -2,7 +2,6 @@ type MessageInputProps = {
value
:
string
;
value
:
string
;
onChange
:
(
value
:
string
)
=>
void
;
onChange
:
(
value
:
string
)
=>
void
;
onSend
:
()
=>
void
;
onSend
:
()
=>
void
;
onInsertLatex
?:
()
=>
void
;
onToggleCanvas
?:
()
=>
void
;
onToggleCanvas
?:
()
=>
void
;
};
};
...
@@ -10,7 +9,6 @@ export default function MessageInput({
...
@@ -10,7 +9,6 @@ export default function MessageInput({
value
,
value
,
onChange
,
onChange
,
onSend
,
onSend
,
onInsertLatex
,
onToggleCanvas
,
onToggleCanvas
,
}:
MessageInputProps
)
{
}:
MessageInputProps
)
{
const
canSend
=
value
.
trim
().
length
>
0
;
const
canSend
=
value
.
trim
().
length
>
0
;
...
@@ -21,9 +19,6 @@ export default function MessageInput({
...
@@ -21,9 +19,6 @@ export default function MessageInput({
<
button
className
=
"btn"
type
=
"button"
onClick
=
{
onToggleCanvas
}
>
<
button
className
=
"btn"
type
=
"button"
onClick
=
{
onToggleCanvas
}
>
Draw
Draw
</
button
>
</
button
>
<
button
className
=
"btn"
type
=
"button"
onClick
=
{
onInsertLatex
}
>
Insert LaTeX
</
button
>
<
button
<
button
className
=
"btn primary"
className
=
"btn primary"
type
=
"button"
type
=
"button"
...
...
math-tutor/frontend/src/pages/App.tsx
View file @
9c04fca1
...
@@ -7,7 +7,7 @@ import type { ChatMessage } from "../components/Chat/MessageList";
...
@@ -7,7 +7,7 @@ import type { ChatMessage } from "../components/Chat/MessageList";
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
const
initialMessages
:
ChatMessage
[]
=
[
const
initialMessages
:
ChatMessage
[]
=
[
{
id
:
"
m1
"
,
role
:
"
user
"
,
text
:
"
How do I factor x^2 - 5x + 6?
"
},
{
id
:
"
m1
"
,
role
:
"
user
"
,
text
:
"
How do I factor
$
x^2 - 5x + 6?
$
"
},
{
{
id
:
"
m2
"
,
id
:
"
m2
"
,
role
:
"
assistant
"
,
role
:
"
assistant
"
,
...
@@ -55,10 +55,6 @@ export default function App() {
...
@@ -55,10 +55,6 @@ export default function App() {
setDraft
(
""
);
setDraft
(
""
);
};
};
const
handleInsertLatex
=
()
=>
{
setDraft
((
prev
)
=>
(
prev
?
`
${
prev
}
\\frac{a}{b}`
:
"
\\
frac{a}{b}
"
));
};
const
handleToggleCanvas
=
()
=>
{
const
handleToggleCanvas
=
()
=>
{
setIsCanvasVisible
((
prev
)
=>
!
prev
);
setIsCanvasVisible
((
prev
)
=>
!
prev
);
};
};
...
@@ -72,6 +68,7 @@ export default function App() {
...
@@ -72,6 +68,7 @@ export default function App() {
text
:
"
Saving drawing and converting to LaTeX...
"
,
text
:
"
Saving drawing and converting to LaTeX...
"
,
},
},
]);
]);
setIsCanvasVisible
(
false
);
try
{
try
{
const
response
=
await
fetch
(
"
http://localhost:8000/api/canvas/save
"
,
{
const
response
=
await
fetch
(
"
http://localhost:8000/api/canvas/save
"
,
{
...
@@ -131,15 +128,16 @@ export default function App() {
...
@@ -131,15 +128,16 @@ export default function App() {
draft
=
{
draft
}
draft
=
{
draft
}
onDraftChange
=
{
setDraft
}
onDraftChange
=
{
setDraft
}
onSend
=
{
handleSend
}
onSend
=
{
handleSend
}
onInsertLatex
=
{
handleInsertLatex
}
onToggleCanvas
=
{
handleToggleCanvas
}
onToggleCanvas
=
{
handleToggleCanvas
}
/>
/>
{
isCanvasVisible
?
(
<
CanvasDrawer
<
CanvasDrawer
isVisible
=
{
isCanvasVisible
}
isVisible
=
{
isCanvasVisible
}
onToggle
=
{
handleToggleCanvas
}
onToggle
=
{
handleToggleCanvas
}
onSave
=
{
handleCanvasSave
}
onSave
=
{
handleCanvasSave
}
/>
/>
)
:
null
}
</
section
>
</
section
>
<
aside
className
=
"retrieval-column"
>
<
aside
className
=
"retrieval-column"
>
...
...
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