Commit 01c1d3c3 authored by Kantz's avatar Kantz
Browse files

verschiebung des upload buttons

parent 9f1fcd5a
...@@ -11,6 +11,7 @@ type ChatWindowProps = { ...@@ -11,6 +11,7 @@ type ChatWindowProps = {
onSend: () => void; onSend: () => void;
onHistoryNavigate?: (direction: "older" | "newer") => boolean; onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void; onToggleCanvas?: () => void;
onUploadSolution?: (file: File) => void | Promise<void>;
onInspectDoc?: (doc: RetrievedDoc) => void; onInspectDoc?: (doc: RetrievedDoc) => void;
docIndex?: Record<string, RetrievedDoc>; docIndex?: Record<string, RetrievedDoc>;
docSlugIndex?: Record<string, RetrievedDoc>; docSlugIndex?: Record<string, RetrievedDoc>;
...@@ -23,6 +24,7 @@ export default function ChatWindow({ ...@@ -23,6 +24,7 @@ export default function ChatWindow({
onSend, onSend,
onHistoryNavigate, onHistoryNavigate,
onToggleCanvas, onToggleCanvas,
onUploadSolution,
onInspectDoc, onInspectDoc,
docIndex, docIndex,
docSlugIndex, docSlugIndex,
...@@ -42,6 +44,7 @@ export default function ChatWindow({ ...@@ -42,6 +44,7 @@ export default function ChatWindow({
onSend={onSend} onSend={onSend}
onHistoryNavigate={onHistoryNavigate} onHistoryNavigate={onHistoryNavigate}
onToggleCanvas={onToggleCanvas} onToggleCanvas={onToggleCanvas}
onUploadSolution={onUploadSolution}
/> />
</div> </div>
); );
......
import { EditPencil, Send } from "iconoir-react"; import { useRef } from "react";
import { EditPencil, Send, Upload } from "iconoir-react";
import { t } from "../../i18n"; import { t } from "../../i18n";
type MessageInputProps = { type MessageInputProps = {
...@@ -7,6 +8,7 @@ type MessageInputProps = { ...@@ -7,6 +8,7 @@ type MessageInputProps = {
onSend: () => void; onSend: () => void;
onHistoryNavigate?: (direction: "older" | "newer") => boolean; onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void; onToggleCanvas?: () => void;
onUploadSolution?: (file: File) => void | Promise<void>;
}; };
export default function MessageInput({ export default function MessageInput({
...@@ -15,8 +17,25 @@ export default function MessageInput({ ...@@ -15,8 +17,25 @@ export default function MessageInput({
onSend, onSend,
onHistoryNavigate, onHistoryNavigate,
onToggleCanvas, onToggleCanvas,
onUploadSolution,
}: MessageInputProps) { }: MessageInputProps) {
const canSend = value.trim().length > 0; const canSend = value.trim().length > 0;
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const handleUploadClick = () => {
uploadInputRef.current?.click();
};
const handleUploadChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
event.target.value = "";
if (!file) {
return;
}
await onUploadSolution?.(file);
};
return ( return (
<div className="composer"> <div className="composer">
...@@ -30,6 +49,22 @@ export default function MessageInput({ ...@@ -30,6 +49,22 @@ export default function MessageInput({
> >
<EditPencil width={18} height={18} aria-hidden="true" /> <EditPencil width={18} height={18} aria-hidden="true" />
</button> </button>
<button
className="btn"
type="button"
onClick={handleUploadClick}
aria-label={t("uploadSolution")}
title={t("uploadSolution")}
>
<Upload width={18} height={18} aria-hidden="true" />
</button>
<input
ref={uploadInputRef}
type="file"
accept="image/*"
className="canvas-upload-input"
onChange={handleUploadChange}
/>
<button <button
className="btn primary" className="btn primary"
type="button" type="button"
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
pen: "Pen", pen: "Pen",
clear: "Clear", clear: "Clear",
saveAndConvert: "Save + Convert", saveAndConvert: "Save + Convert",
uploadSolution: "Upload solution image",
hide: "Hide", hide: "Hide",
show: "Show", show: "Show",
newChat: "New Chat", newChat: "New Chat",
...@@ -55,6 +56,10 @@ ...@@ -55,6 +56,10 @@
failedLoadSavedChats: "Could not load saved chats.", failedLoadSavedChats: "Could not load saved chats.",
failedLoadSelectedChat: "Could not load the selected chat.", failedLoadSelectedChat: "Could not load the selected chat.",
savingDrawingConverting: "Saving drawing and converting to LaTeX...", savingDrawingConverting: "Saving drawing and converting to LaTeX...",
uploadingSolutionConverting: "Uploading image and converting to LaTeX...",
uploadFailed: "Uploading the image failed.",
uploadReadFailed: "Could not read the selected image.",
uploadInvalidFileType: "Please select an image file.",
saveFailedStatus: "Saving failed (status: {status}).", saveFailedStatus: "Saving failed (status: {status}).",
canvasSaveFailed: "Saving the canvas failed.", canvasSaveFailed: "Saving the canvas failed.",
canvasSaved: "Canvas saved.", canvasSaved: "Canvas saved.",
...@@ -91,6 +96,7 @@ ...@@ -91,6 +96,7 @@
pen: "Stift", pen: "Stift",
clear: "Leeren", clear: "Leeren",
saveAndConvert: "Speichern + Konvertieren", saveAndConvert: "Speichern + Konvertieren",
uploadSolution: "Lösungsbild hochladen",
hide: "Verstecken", hide: "Verstecken",
show: "Anzeigen", show: "Anzeigen",
newChat: "neuer Chat", newChat: "neuer Chat",
...@@ -138,6 +144,11 @@ ...@@ -138,6 +144,11 @@
failedLoadSelectedChat: "Der ausgewählte Chat konnte nicht geladen werden.", failedLoadSelectedChat: "Der ausgewählte Chat konnte nicht geladen werden.",
savingDrawingConverting: savingDrawingConverting:
"Zeichnung wird gespeichert und in LaTeX konvertiert...", "Zeichnung wird gespeichert und in LaTeX konvertiert...",
uploadingSolutionConverting:
"Bild wird hochgeladen und in LaTeX konvertiert...",
uploadFailed: "Das Hochladen des Bildes ist fehlgeschlagen.",
uploadReadFailed: "Das ausgewählte Bild konnte nicht gelesen werden.",
uploadInvalidFileType: "Bitte wähle eine Bilddatei aus.",
saveFailedStatus: "Speichern fehlgeschlagen (Status: {status}). Probiere es nochmal oder lade die Seite neu.", saveFailedStatus: "Speichern fehlgeschlagen (Status: {status}). Probiere es nochmal oder lade die Seite neu.",
canvasSaveFailed: "Speichern des Canvas fehlgeschlagen. Probiere es nochmal oder lade die Seite neu.", canvasSaveFailed: "Speichern des Canvas fehlgeschlagen. Probiere es nochmal oder lade die Seite neu.",
canvasSaved: "Canvas gespeichert.", canvasSaved: "Canvas gespeichert.",
......
...@@ -737,10 +737,13 @@ export default function ChatPage() { ...@@ -737,10 +737,13 @@ export default function ChatPage() {
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat"); navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat");
}; };
const handleCanvasSave = async (dataUrl: string) => { const handleCanvasSave = async (
dataUrl: string,
statusMessage = t("savingDrawingConverting")
) => {
setCanvasStatus({ setCanvasStatus({
kind: "info", kind: "info",
message: t("savingDrawingConverting"), message: statusMessage,
}); });
try { try {
...@@ -787,6 +790,61 @@ export default function ChatPage() { ...@@ -787,6 +790,61 @@ export default function ChatPage() {
} }
}; };
const fileToPngDataUrl = async (file: File): Promise<string> => {
if (!file.type.startsWith("image/")) {
throw new Error(t("uploadInvalidFileType"));
}
const fileDataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
return;
}
reject(new Error(t("uploadReadFailed")));
};
reader.onerror = () => reject(new Error(t("uploadReadFailed")));
reader.readAsDataURL(file);
});
if (file.type === "image/png") {
return fileDataUrl;
}
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(t("uploadReadFailed")));
img.src = fileDataUrl;
});
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth || image.width;
canvas.height = image.naturalHeight || image.height;
const context = canvas.getContext("2d");
if (!context) {
throw new Error(t("uploadReadFailed"));
}
context.drawImage(image, 0, 0);
return canvas.toDataURL("image/png");
};
const handleCanvasUpload = async (file: File) => {
if (!file) {
return;
}
try {
const dataUrl = await fileToPngDataUrl(file);
await handleCanvasSave(dataUrl, t("uploadingSolutionConverting"));
} catch (error) {
const message = error instanceof Error ? error.message : t("uploadFailed");
setCanvasStatus({ kind: "error", message });
void error;
}
};
const handleCiteDoc = (docId: string) => { const handleCiteDoc = (docId: string) => {
setDraft((prev) => (prev ? `${prev} [${docId}]` : `[${docId}]`)); setDraft((prev) => (prev ? `${prev} [${docId}]` : `[${docId}]`));
}; };
...@@ -891,6 +949,7 @@ export default function ChatPage() { ...@@ -891,6 +949,7 @@ export default function ChatPage() {
onSend={handleSend} onSend={handleSend}
onHistoryNavigate={handleHistoryNavigate} onHistoryNavigate={handleHistoryNavigate}
onToggleCanvas={handleToggleCanvas} onToggleCanvas={handleToggleCanvas}
onUploadSolution={handleCanvasUpload}
onInspectDoc={handleInspectDoc} onInspectDoc={handleInspectDoc}
docIndex={docIndexes.bySourceKey} docIndex={docIndexes.bySourceKey}
docSlugIndex={docIndexes.bySlug} docSlugIndex={docIndexes.bySlug}
......
...@@ -614,6 +614,18 @@ body { ...@@ -614,6 +614,18 @@ body {
margin-left: auto; margin-left: auto;
} }
.canvas-upload-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.canvas-hidden { .canvas-hidden {
padding: 12px; padding: 12px;
border-radius: 12px; border-radius: 12px;
...@@ -776,6 +788,10 @@ body { ...@@ -776,6 +788,10 @@ body {
.canvas-actions-header { .canvas-actions-header {
margin-left: 0; margin-left: 0;
} }
.canvas-actions-header .btn {
flex: 1 1 120px;
}
} }
.app-loading { .app-loading {
......
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