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 typing import List, Optional
import base64
import os
import time
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
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
from app.api import canvas, chat, retrieval
app = FastAPI(title="Math Tutor API", version="0.1.0")
load_dotenv()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
......@@ -28,100 +12,11 @@ app.add_middleware(
allow_headers=["*"],
)
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 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.include_router(chat.router)
app.include_router(canvas.router)
app.include_router(retrieval.router)
@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")
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 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<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>
<body>
<div id="root"></div>
......
......@@ -7,7 +7,6 @@ type ChatWindowProps = {
draft: string;
onDraftChange: (value: string) => void;
onSend: () => void;
onInsertLatex?: () => void;
onToggleCanvas?: () => void;
};
......@@ -16,7 +15,6 @@ export default function ChatWindow({
draft,
onDraftChange,
onSend,
onInsertLatex,
onToggleCanvas,
}: ChatWindowProps) {
return (
......@@ -27,7 +25,6 @@ export default function ChatWindow({
value={draft}
onChange={onDraftChange}
onSend={onSend}
onInsertLatex={onInsertLatex}
onToggleCanvas={onToggleCanvas}
/>
</div>
......
import { useEffect, useRef } from "react";
type MessageBubbleProps = {
role: "user" | "assistant";
text: string;
};
declare global {
interface Window {
MathJax?: {
typesetPromise?: (elements?: Element[]) => Promise<void>;
};
}
}
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 (
<div className={`message-bubble ${role}`}>
<div className={`message-bubble ${role}`} ref={bubbleRef}>
<div className="message-role">{role}</div>
<div className="message-text">{text}</div>
</div>
......
......@@ -2,7 +2,6 @@ type MessageInputProps = {
value: string;
onChange: (value: string) => void;
onSend: () => void;
onInsertLatex?: () => void;
onToggleCanvas?: () => void;
};
......@@ -10,7 +9,6 @@ export default function MessageInput({
value,
onChange,
onSend,
onInsertLatex,
onToggleCanvas,
}: MessageInputProps) {
const canSend = value.trim().length > 0;
......@@ -21,9 +19,6 @@ export default function MessageInput({
<button className="btn" type="button" onClick={onToggleCanvas}>
Draw
</button>
<button className="btn" type="button" onClick={onInsertLatex}>
Insert LaTeX
</button>
<button
className="btn primary"
type="button"
......
......@@ -7,7 +7,7 @@ import type { ChatMessage } from "../components/Chat/MessageList";
import type { RetrievedDoc } from "../components/Retrieval/DocPanel";
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",
role: "assistant",
......@@ -55,10 +55,6 @@ export default function App() {
setDraft("");
};
const handleInsertLatex = () => {
setDraft((prev) => (prev ? `${prev} \\frac{a}{b}` : "\\frac{a}{b}"));
};
const handleToggleCanvas = () => {
setIsCanvasVisible((prev) => !prev);
};
......@@ -72,6 +68,7 @@ export default function App() {
text: "Saving drawing and converting to LaTeX...",
},
]);
setIsCanvasVisible(false);
try {
const response = await fetch("http://localhost:8000/api/canvas/save", {
......@@ -131,15 +128,16 @@ export default function App() {
draft={draft}
onDraftChange={setDraft}
onSend={handleSend}
onInsertLatex={handleInsertLatex}
onToggleCanvas={handleToggleCanvas}
/>
<CanvasDrawer
isVisible={isCanvasVisible}
onToggle={handleToggleCanvas}
onSave={handleCanvasSave}
/>
{isCanvasVisible ? (
<CanvasDrawer
isVisible={isCanvasVisible}
onToggle={handleToggleCanvas}
onSave={handleCanvasSave}
/>
) : null}
</section>
<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