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

verschiebung des upload buttons

parent 9f1fcd5a
......@@ -11,6 +11,7 @@ type ChatWindowProps = {
onSend: () => void;
onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void;
onUploadSolution?: (file: File) => void | Promise<void>;
onInspectDoc?: (doc: RetrievedDoc) => void;
docIndex?: Record<string, RetrievedDoc>;
docSlugIndex?: Record<string, RetrievedDoc>;
......@@ -23,6 +24,7 @@ export default function ChatWindow({
onSend,
onHistoryNavigate,
onToggleCanvas,
onUploadSolution,
onInspectDoc,
docIndex,
docSlugIndex,
......@@ -42,6 +44,7 @@ export default function ChatWindow({
onSend={onSend}
onHistoryNavigate={onHistoryNavigate}
onToggleCanvas={onToggleCanvas}
onUploadSolution={onUploadSolution}
/>
</div>
);
......
import { EditPencil, Send } from "iconoir-react";
import { useRef } from "react";
import { EditPencil, Send, Upload } from "iconoir-react";
import { t } from "../../i18n";
type MessageInputProps = {
......@@ -7,6 +8,7 @@ type MessageInputProps = {
onSend: () => void;
onHistoryNavigate?: (direction: "older" | "newer") => boolean;
onToggleCanvas?: () => void;
onUploadSolution?: (file: File) => void | Promise<void>;
};
export default function MessageInput({
......@@ -15,8 +17,25 @@ export default function MessageInput({
onSend,
onHistoryNavigate,
onToggleCanvas,
onUploadSolution,
}: MessageInputProps) {
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 (
<div className="composer">
......@@ -30,6 +49,22 @@ export default function MessageInput({
>
<EditPencil width={18} height={18} aria-hidden="true" />
</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
className="btn primary"
type="button"
......
......@@ -12,6 +12,7 @@
pen: "Pen",
clear: "Clear",
saveAndConvert: "Save + Convert",
uploadSolution: "Upload solution image",
hide: "Hide",
show: "Show",
newChat: "New Chat",
......@@ -55,6 +56,10 @@
failedLoadSavedChats: "Could not load saved chats.",
failedLoadSelectedChat: "Could not load the selected chat.",
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}).",
canvasSaveFailed: "Saving the canvas failed.",
canvasSaved: "Canvas saved.",
......@@ -91,6 +96,7 @@
pen: "Stift",
clear: "Leeren",
saveAndConvert: "Speichern + Konvertieren",
uploadSolution: "Lösungsbild hochladen",
hide: "Verstecken",
show: "Anzeigen",
newChat: "neuer Chat",
......@@ -138,6 +144,11 @@
failedLoadSelectedChat: "Der ausgewählte Chat konnte nicht geladen werden.",
savingDrawingConverting:
"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.",
canvasSaveFailed: "Speichern des Canvas fehlgeschlagen. Probiere es nochmal oder lade die Seite neu.",
canvasSaved: "Canvas gespeichert.",
......
......@@ -737,10 +737,13 @@ export default function ChatPage() {
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat");
};
const handleCanvasSave = async (dataUrl: string) => {
const handleCanvasSave = async (
dataUrl: string,
statusMessage = t("savingDrawingConverting")
) => {
setCanvasStatus({
kind: "info",
message: t("savingDrawingConverting"),
message: statusMessage,
});
try {
......@@ -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) => {
setDraft((prev) => (prev ? `${prev} [${docId}]` : `[${docId}]`));
};
......@@ -891,6 +949,7 @@ export default function ChatPage() {
onSend={handleSend}
onHistoryNavigate={handleHistoryNavigate}
onToggleCanvas={handleToggleCanvas}
onUploadSolution={handleCanvasUpload}
onInspectDoc={handleInspectDoc}
docIndex={docIndexes.bySourceKey}
docSlugIndex={docIndexes.bySlug}
......
......@@ -614,6 +614,18 @@ body {
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 {
padding: 12px;
border-radius: 12px;
......@@ -776,6 +788,10 @@ body {
.canvas-actions-header {
margin-left: 0;
}
.canvas-actions-header .btn {
flex: 1 1 120px;
}
}
.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