From 73ca0fc9496f95408baa93c085cc2666155b7507 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 4 Aug 2026 17:47:51 +0200 Subject: [PATCH 1/2] impr: support more filetypes for text import (@fehmer) --- frontend/__tests__/utils/strings.spec.ts | 1 - frontend/package.json | 1 + .../ts/components/modals/CustomTextModal.tsx | 45 ++- frontend/src/ts/utils/fileparser.ts | 67 ++++ frontend/vite.config.ts | 10 +- pnpm-lock.yaml | 368 +++++++++++++----- 6 files changed, 377 insertions(+), 115 deletions(-) create mode 100644 frontend/src/ts/utils/fileparser.ts diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 01bcd6f441b1..f83e1d6b2797 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -590,7 +590,6 @@ describe("string utils", () => { }); }); }); - describe("countChars", () => { describe("it should count characters correctly", () => { const testCases = [ diff --git a/frontend/package.json b/frontend/package.json index 9234f3fdf969..aff11e2b19dd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,6 +64,7 @@ "lz-ts": "1.1.2", "modern-screenshot": "4.6.8", "object-hash": "3.0.0", + "officeparser": "7.5.1", "slim-select": "2.9.2", "stemmer": "2.0.1", "tailwind-merge": "3.6.0", diff --git a/frontend/src/ts/components/modals/CustomTextModal.tsx b/frontend/src/ts/components/modals/CustomTextModal.tsx index 602cda856cab..aaa8aa58cd21 100644 --- a/frontend/src/ts/components/modals/CustomTextModal.tsx +++ b/frontend/src/ts/components/modals/CustomTextModal.tsx @@ -21,6 +21,7 @@ import { getLoadedChallenge, setLoadedChallenge } from "../../states/test"; import * as CustomText from "../../test/custom-text"; import * as PractiseWords from "../../test/practise-words"; import { cn } from "../../utils/cn"; +import { convertToText } from "../../utils/fileparser"; import * as Strings from "../../utils/strings"; import { AnimatedModal } from "../common/AnimatedModal"; import { Button } from "../common/Button"; @@ -320,25 +321,39 @@ export function CustomTextModal(): JSXElement { }); }; - const handleFileOpen = () => { + const handleFileOpen = async () => { const file = fileInputRef?.files?.[0]; if (!file) return; - if (file.type !== "text/plain") { - showErrorNotification("File is not a text file", { durationMs: 5000 }); + const fileExtension = file.name.split(".").pop()?.toLowerCase() ?? ""; + const isTextFile = file.type === "text/plain" || fileExtension === "txt"; + + if (isTextFile) { + const reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + reader.onload = (e) => { + const content = e.target?.result as string; + const text = content; + form.setFieldValue("text", text); + fileInputRef.value = ""; + }; + reader.onerror = () => { + showErrorNotification("Failed to read file", { durationMs: 5000 }); + }; + } else { + try { + const text = await convertToText(file, file.name, file.type); + form.setFieldValue("text", text); + fileInputRef.value = ""; + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to parse file"; + showErrorNotification(`Failed to parse file: ${message}`, { + durationMs: 5000, + }); + } return; } - - const reader = new FileReader(); - reader.readAsText(file, "UTF-8"); - reader.onload = (e) => { - const content = e.target?.result as string; - form.setFieldValue("text", content); - fileInputRef.value = ""; - }; - reader.onerror = () => { - showErrorNotification("Failed to read file", { durationMs: 5000 }); - }; }; const handleTextareaKeydown = (e: KeyboardEvent) => { @@ -618,7 +633,7 @@ export function CustomTextModal(): JSXElement { ref={fileInputRef} type="file" class="hidden" - accept=".txt" + accept=".txt,.md,.docx,.doc,.odt,.odp,.ods,.pdf,.rtf,.epub" onChange={handleFileOpen} />