Commit 104161ac authored by Kantz's avatar Kantz
Browse files

added Canvas and API

parent 9e376369
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,
)
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={onSave}> <button className="btn primary" type="button" onClick={handleSave}>
Save + Convert Save + Convert
</button> </button>
</div> </div>
......
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[];
......
...@@ -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>
......
...@@ -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 {
......
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