Commit 9c04fca1 authored by Kantz's avatar Kantz
Browse files

apit aufgeteilt

parent 3d5974ad
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),
)
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"])
from __future__ import annotations
from fastapi import APIRouter
router = APIRouter()
@router.get("/api/retrieval/health")
def retrieval_health() -> dict:
return {"status": "ok"}
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),
)
...@@ -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>
......
...@@ -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>
......
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>
......
...@@ -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"
......
...@@ -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}
/> />
<CanvasDrawer {isCanvasVisible ? (
isVisible={isCanvasVisible} <CanvasDrawer
onToggle={handleToggleCanvas} isVisible={isCanvasVisible}
onSave={handleCanvasSave} onToggle={handleToggleCanvas}
/> onSave={handleCanvasSave}
/>
) : null}
</section> </section>
<aside className="retrieval-column"> <aside className="retrieval-column">
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment