Commit 0893fd21 authored by Kantz's avatar Kantz
Browse files

Canvas größe verringert und deutsche Übersetztung hinzugefügt

parent 6ab334f1
import { useEffect, useRef, useState } from "react";
import CanvasToggle from "./CanvasToggle";
import { t } from "../../i18n";
type CanvasDrawerProps = {
isVisible: boolean;
......@@ -37,8 +38,8 @@ export default function CanvasDrawer({
}
const ratio = window.devicePixelRatio || 1;
const width = 720;
const height = 320;
const width = Math.min(560, window.innerWidth - 80);
const height = 150;
canvas.width = width * ratio;
canvas.height = height * ratio;
......@@ -134,7 +135,20 @@ export default function CanvasDrawer({
return (
<div className="canvas-zone">
<div className="canvas-header">
<div className="canvas-title">Canvas</div>
<div className="canvas-title">{t("canvas")}</div>
{isVisible ? (
<div className="canvas-actions canvas-actions-header">
<button className="btn" type="button" onClick={handleToggleEraser}>
{isErasing ? t("pen") : t("eraser")}
</button>
<button className="btn" type="button" onClick={handleClear}>
{t("clear")}
</button>
<button className="btn primary" type="button" onClick={handleSave}>
{t("saveAndConvert")}
</button>
</div>
) : null}
<CanvasToggle isVisible={isVisible} onToggle={onToggle} />
</div>
{isVisible ? (
......@@ -156,20 +170,9 @@ export default function CanvasDrawer({
onPointerLeave={handlePointerUp}
/>
</div>
<div className="canvas-actions">
<button className="btn" type="button" onClick={handleToggleEraser}>
{isErasing ? "Pen" : "Eraser"}
</button>
<button className="btn" type="button" onClick={handleClear}>
Clear
</button>
<button className="btn primary" type="button" onClick={handleSave}>
Save + Convert
</button>
</div>
</div>
) : (
<div className="canvas-hidden">Canvas hidden</div>
<div className="canvas-hidden">{t("canvasHidden")}</div>
)}
</div>
);
......
import { t } from "../../i18n";
type CanvasToggleProps = {
isVisible: boolean;
onToggle: () => void;
......@@ -9,7 +11,7 @@ export default function CanvasToggle({
}: CanvasToggleProps) {
return (
<button className="btn" type="button" onClick={onToggle}>
{isVisible ? "Hide" : "Show"}
{isVisible ? t("hide") : t("show")}
</button>
);
}
......@@ -2,6 +2,7 @@ import MessageInput from "./MessageInput";
import MessageList from "./MessageList";
import type { ChatMessage } from "./MessageList";
import type { RetrievedDoc } from "../Retrieval/DocPanel";
import { t } from "../../i18n";
type ChatWindowProps = {
messages: ChatMessage[];
......@@ -26,7 +27,7 @@ export default function ChatWindow({
}: ChatWindowProps) {
return (
<div className="chat-window">
<div className="chat-title">Chat</div>
<div className="chat-title">{t("chat")}</div>
<MessageList
messages={messages}
onInspectDoc={onInspectDoc}
......
import { useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
import type { RetrievedDoc } from "../Retrieval/DocPanel";
import { t } from "../../i18n";
type MessageBubbleProps = {
role: "user" | "assistant";
......@@ -48,7 +49,9 @@ export default function MessageBubble({
return (
<div className={`message-bubble ${role}`} ref={bubbleRef}>
<div className="message-role">{role}</div>
<div className="message-role">
{role === "user" ? t("roleUser") : t("roleAssistant")}
</div>
<div className="message-text">
<ReactMarkdown
components={{
......
import { EditPencil, Send } from "iconoir-react";
import { t } from "../../i18n";
type MessageInputProps = {
value: string;
......@@ -22,8 +23,8 @@ export default function MessageInput({
className="btn"
type="button"
onClick={onToggleCanvas}
aria-label="Draw"
title="Draw"
aria-label={t("draw")}
title={t("draw")}
>
<EditPencil width={18} height={18} aria-hidden="true" />
</button>
......@@ -32,15 +33,15 @@ export default function MessageInput({
type="button"
onClick={onSend}
disabled={!canSend}
aria-label="Send"
title="Send"
aria-label={t("send")}
title={t("send")}
>
<Send width={18} height={18} aria-hidden="true" />
</button>
</div>
<textarea
className="composer-input"
placeholder="Type your question or paste LaTeX..."
placeholder={t("typeQuestionOrLatex")}
rows={3}
value={value}
onChange={(event) => onChange(event.target.value)}
......
import { useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
import { t } from "../../i18n";
declare global {
interface Window {
......@@ -63,10 +64,10 @@ export default function DocCard({
</div>
<div className="doc-actions">
<button className="btn small" type="button" onClick={onInspect}>
Inspect
{t("inspect")}
</button>
<button className="btn small" type="button" onClick={onCite}>
Cite
{t("cite")}
</button>
</div>
</details>
......
import DocCard from "./DocCard";
import { t } from "../../i18n";
export type RetrievedDoc = {
uid: string;
......@@ -37,7 +38,7 @@ const buildTitle = (doc: RetrievedDoc) => {
meta.subsection_title ||
meta.section_title ||
meta.path ||
"Untitled"
t("untitled")
);
};
......@@ -113,16 +114,16 @@ export default function DocPanel({
return (
<div className="retrieval-panel">
<div className="retrieval-title">Retrieved Notes</div>
{isLoading ? <div className="retrieval-state">Loading...</div> : null}
<div className="retrieval-title">{t("retrievedNotes")}</div>
{isLoading ? <div className="retrieval-state">{t("loading")}</div> : null}
{error ? <div className="retrieval-state error">{error}</div> : null}
{!isLoading && !error && total === 0 ? (
<div className="retrieval-state">No results yet.</div>
<div className="retrieval-state">{t("noResultsYet")}</div>
) : null}
{renderGroup("Direct children", directChildren, onCiteDoc, onInspectDoc)}
{renderGroup("Indirect children", indirectChildren, onCiteDoc, onInspectDoc)}
{renderGroup("Subsection summary", subsections, onCiteDoc, onInspectDoc)}
{renderGroup("Section summary", sections, onCiteDoc, onInspectDoc)}
{renderGroup(t("directChildren"), directChildren, onCiteDoc, onInspectDoc)}
{renderGroup(t("indirectChildren"), indirectChildren, onCiteDoc, onInspectDoc)}
{renderGroup(t("subsectionSummary"), subsections, onCiteDoc, onInspectDoc)}
{renderGroup(t("sectionSummary"), sections, onCiteDoc, onInspectDoc)}
</div>
);
}
const translations = {
en: {
chats: "Chats",
prototypeWorkspace: "Prototype Workspace",
task: "Task",
chat: "Chat",
canvas: "Canvas",
retrievedNotes: "Retrieved Notes",
typeQuestionOrLatex: "Type your question or paste LaTeX...",
noResultsYet: "No results yet.",
eraser: "Eraser",
pen: "Pen",
clear: "Clear",
saveAndConvert: "Save + Convert",
hide: "Hide",
show: "Show",
newChat: "New Chat",
loading: "Loading...",
directChildren: "Direct children",
indirectChildren: "Indirect children",
subsectionSummary: "Subsection summary",
sectionSummary: "Section summary",
untitled: "Untitled",
inspect: "Inspect",
cite: "Cite",
draw: "Draw",
send: "Send",
roleUser: "user",
roleAssistant: "assistant",
noTasksAvailable: "No tasks available",
noTaskSelected: "No task selected.",
savedChats: "Saved Chats",
saving: "Saving...",
noSavedChatsYet: "No saved chats yet.",
canvasHidden: "Canvas hidden",
failedLoadTasks: "Could not load tasks.",
chatRequestFailed: "The chat request failed. Please check backend logs.",
retrievalFailed: "Source retrieval failed. Please check backend logs.",
failedLoadSavedChats: "Could not load saved chats.",
failedLoadSelectedChat: "Could not load the selected chat.",
savingDrawingConverting: "Saving drawing and converting to LaTeX...",
saveFailedStatus: "Saving failed (status: {status}).",
canvasSaveFailed: "Saving the canvas failed.",
canvasSaved: "Canvas saved.",
canvasSaveFailedCheckLogs:
"Saving the canvas failed. Please check backend logs.",
},
de: {
chats: "Chats",
prototypeWorkspace: "Prototyp-Arbeitsbereich",
task: "Aufgabe",
chat: "Chat",
canvas: "Canvas",
retrievedNotes: "Quellen",
typeQuestionOrLatex:
"Gebe hier deine Frage ein. Nutze $ $ für mathematische Eingaben...",
noResultsYet: "bisher keine Quellen",
eraser: "Radierer",
pen: "Stift",
clear: "Leeren",
saveAndConvert: "Speichern + Konvertieren",
hide: "verstecken",
show: "anzeigen",
newChat: "neuer Chat",
loading: "Lade...",
directChildren: "Direkte Quellen",
indirectChildren: "Indirekte Quellen",
subsectionSummary: "Unterabschnitt-Zusammenfassung",
sectionSummary: "Abschnitt-Zusammenfassung",
untitled: "Ohne Titel",
inspect: "Ansehen",
cite: "Zitieren",
draw: "Zeichnen",
send: "Senden",
roleUser: "nutzer",
roleAssistant: "assistent",
noTasksAvailable: "Keine Aufgaben verfuegbar",
noTaskSelected: "Keine Aufgabe ausgewaehlt.",
savedChats: "Gespeicherte Chats",
saving: "Speichere...",
noSavedChatsYet: "Noch keine gespeicherten Chats.",
canvasHidden: "Canvas ausgeblendet",
failedLoadTasks: "Aufgaben konnten nicht geladen werden.",
chatRequestFailed:
"Chat-Anfrage ist fehlgeschlagen. Bitte pruefe die Backend-Logs.",
retrievalFailed:
"Quellenabruf ist fehlgeschlagen. Bitte pruefe die Backend-Logs.",
failedLoadSavedChats: "Gespeicherte Chats konnten nicht geladen werden.",
failedLoadSelectedChat: "Der ausgewaehlte Chat konnte nicht geladen werden.",
savingDrawingConverting:
"Zeichnung wird gespeichert und in LaTeX konvertiert...",
saveFailedStatus: "Speichern fehlgeschlagen (Status: {status}).",
canvasSaveFailed: "Speichern des Canvas fehlgeschlagen.",
canvasSaved: "Canvas gespeichert.",
canvasSaveFailedCheckLogs:
"Speichern des Canvas fehlgeschlagen. Bitte pruefe die Backend-Logs.",
},
} as const;
type Language = keyof typeof translations;
type TranslationKey = keyof (typeof translations)["en"];
const envLanguage = String(import.meta.env.VITE_FRONTEND_LANG || "en")
.trim()
.toLowerCase();
export const language: Language = envLanguage.startsWith("de") ? "de" : "en";
export const t = (
key: TranslationKey,
vars?: Record<string, string | number>
): string => {
let value: string = translations[language][key] || translations.en[key];
if (!vars) {
return value;
}
Object.entries(vars).forEach(([name, raw]) => {
value = value.replaceAll(`{${name}}`, String(raw));
});
return value;
};
......@@ -5,6 +5,7 @@ import CanvasDrawer from "../components/Canvas/CanvasDrawer";
import DocPanel from "../components/Retrieval/DocPanel";
import type { ChatMessage } from "../components/Chat/MessageList";
import type { RetrievedDoc } from "../components/Retrieval/DocPanel";
import { t } from "../i18n";
const backendUrl = import.meta.env.VITE_BACKEND_URL || "http://localhost:8000";
......@@ -252,7 +253,7 @@ export default function App() {
if (!isActive) {
return;
}
setTasksError("Failed to load tasks.");
setTasksError(t("failedLoadTasks"));
setIsTaskModeEnabled(false);
setTaskFiles([]);
setSelectedTaskRef(null);
......@@ -349,13 +350,14 @@ export default function App() {
}
const payload: { reply?: string } = await response.json();
if (payload.reply) {
const reply = payload.reply;
if (typeof reply === "string" && reply.length > 0) {
setMessages((prev) => [
...prev,
{
id: `m-${Date.now()}-assistant`,
role: "assistant",
text: payload.reply,
text: reply,
},
]);
}
......@@ -365,10 +367,10 @@ export default function App() {
{
id: `m-${Date.now()}-assistant`,
role: "assistant",
text: "Chat request failed. Check the API logs.",
text: t("chatRequestFailed"),
},
]);
setRetrievalError("Retrieval failed. Check the API logs.");
setRetrievalError(t("retrievalFailed"));
setRetrievalLoading(false);
void error;
return;
......@@ -425,7 +427,7 @@ export default function App() {
setSubsections(nextSubsections);
setSections(nextSections);
} catch (error) {
setRetrievalError("Retrieval failed. Check the API logs.");
setRetrievalError(t("retrievalFailed"));
void error;
} finally {
setRetrievalLoading(false);
......@@ -472,7 +474,7 @@ export default function App() {
setSelectedArchiveId(payload[0].chat_id);
}
} catch (error) {
setArchiveError("Failed to load saved chats.");
setArchiveError(t("failedLoadSavedChats"));
void error;
}
};
......@@ -495,7 +497,7 @@ export default function App() {
setMessages(payload.history || []);
setChatSessionId(payload.chat_id);
} catch (error) {
setArchiveError("Failed to load selected chat.");
setArchiveError(t("failedLoadSelectedChat"));
void error;
}
};
......@@ -535,7 +537,7 @@ export default function App() {
const handleCanvasSave = async (dataUrl: string) => {
setCanvasStatus({
kind: "info",
message: "Saving drawing and converting to LaTeX...",
message: t("savingDrawingConverting"),
});
try {
......@@ -546,7 +548,7 @@ export default function App() {
});
if (!response.ok) {
let message = `Save failed: ${response.status}`;
let message = t("saveFailedStatus", { status: response.status });
try {
const errorPayload: { detail?: string } = await response.json();
if (errorPayload.detail) {
......@@ -563,21 +565,20 @@ export default function App() {
if (payload.status && payload.status !== "ok") {
setCanvasStatus({
kind: "error",
message: payload.latex || "Canvas save failed.",
message: payload.latex || t("canvasSaveFailed"),
});
return;
}
if (payload.latex) {
setDraft((prev) =>
prev ? `${prev} ${payload.latex}` : payload.latex
);
setCanvasStatus({ kind: "ok", message: "Canvas saved." });
const latex = payload.latex;
if (typeof latex === "string" && latex.length > 0) {
setDraft((prev) => (prev ? `${prev} ${latex}` : latex));
setCanvasStatus({ kind: "ok", message: t("canvasSaved") });
setIsCanvasVisible(false);
}
} catch (error) {
setCanvasStatus({
kind: "error",
message: "Canvas save failed. Check the API logs.",
message: t("canvasSaveFailedCheckLogs"),
});
void error;
}
......@@ -593,7 +594,7 @@ export default function App() {
doc.metadata.subsection_title ||
doc.metadata.section_title ||
doc.metadata.path ||
"Document";
t("untitled");
const html = `<!doctype html>
<html lang="en">
......@@ -655,13 +656,13 @@ export default function App() {
className="btn sidebar-toggle"
onClick={handleOpenSidebar}
>
Chats
{t("chats")}
</button>
<div className="brand">
<span className="brand-mark">SUM</span>
<div className="brand-text">
<div className="brand-title">Math Tutor</div>
<div className="brand-subtitle">Prototype Workspace</div>
<div className="brand-subtitle">{t("prototypeWorkspace")}</div>
</div>
</div>
<div className="header-meta">
......@@ -676,7 +677,7 @@ export default function App() {
{isTaskModeEnabled ? (
<section className="task-panel">
<div className="task-panel-header">
<div className="task-panel-title">Task</div>
<div className="task-panel-title">{t("task")}</div>
<select
className="task-select"
value={
......@@ -694,12 +695,12 @@ export default function App() {
</option>
))
) : (
<option value="">No tasks available</option>
<option value="">{t("noTasksAvailable")}</option>
)}
</select>
</div>
<div className="task-panel-content" ref={taskDisplayRef}>
{selectedTask?.fullText || "No task selected."}
{selectedTask?.fullText || t("noTaskSelected")}
</div>
{tasksError ? <div className="task-panel-error">{tasksError}</div> : null}
</section>
......@@ -742,7 +743,7 @@ export default function App() {
</main>
<div className={`sidebar ${isSidebarOpen ? "active" : ""}`}>
<div className="sd-header">
<h4 className="sd-title">Saved Chats</h4>
<h4 className="sd-title">{t("savedChats")}</h4>
<button
type="button"
className="sidebar-button"
......@@ -760,7 +761,7 @@ export default function App() {
onClick={handleNewChat}
disabled={isArchiving}
>
{isArchiving ? "Saving..." : "New Chat"}
{isArchiving ? t("saving") : t("newChat")}
</button>
</li>
<li>
......@@ -785,7 +786,7 @@ export default function App() {
))
) : (
<li>
<div className="sd-empty">No saved chats yet.</div>
<div className="sd-empty">{t("noSavedChatsYet")}</div>
</li>
)}
</ul>
......
......@@ -366,13 +366,14 @@ body {
display: flex;
flex-direction: column;
gap: 12px;
flex: 0 0 320px;
flex: 0 0 auto;
}
.canvas-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.canvas-body {
......@@ -411,8 +412,8 @@ body {
.canvas-board {
width: 100%;
height: 100%;
min-height: 200px;
height: 60%;
min-height: 100px;
display: block;
cursor: crosshair;
}
......@@ -420,6 +421,11 @@ body {
.canvas-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.canvas-actions-header {
margin-left: auto;
}
.canvas-hidden {
......@@ -549,7 +555,7 @@ body {
flex: 1 1 120px;
}
.canvas-actions {
flex-direction: column;
.canvas-actions-header {
margin-left: 0;
}
}
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