summaryrefslogtreecommitdiff
path: root/src/components/ParseForm.tsx
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-21 14:00:28 +0700
committerpolwex <polwex@sortug.com>2025-05-21 14:00:28 +0700
commite839a5f61f0faa21ca8b4bd5767f7575d5e576ee (patch)
tree53e5bcc3977b6ebef687521a7ac387a89aeb21c8 /src/components/ParseForm.tsx
parent4f2bd597beaa778476b84c10b571db1b13524301 (diff)
the card flip animation is legit
Diffstat (limited to 'src/components/ParseForm.tsx')
-rw-r--r--src/components/ParseForm.tsx222
1 files changed, 222 insertions, 0 deletions
diff --git a/src/components/ParseForm.tsx b/src/components/ParseForm.tsx
new file mode 100644
index 0000000..3e6f3e7
--- /dev/null
+++ b/src/components/ParseForm.tsx
@@ -0,0 +1,222 @@
+// src/components/SorlangPage.tsx
+"use client"; // For Next.js App Router, if applicable
+
+import React, {
+ useState,
+ useRef,
+ useTransition,
+ useEffect,
+ useCallback,
+ startTransition,
+} from "react";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Loader2 } from "lucide-react"; // Loading spinner
+import { useRouter } from "waku";
+
+const SorlangPage: React.FC = () => {
+ const [textValue, setTextValue] = useState<string>("");
+ const [pastedImageUrl, setPastedImageUrl] = useState<string | null>(null);
+ const [pastedImageFile, setPastedImageFile] = useState<File | null>(null); // Store the file for extraction
+ const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
+ const [isExtracting, setIsExtracting] = useState<boolean>(false);
+
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+ // Cleanup object URL when component unmounts or image changes
+ useEffect(() => {
+ return () => {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl);
+ }
+ };
+ }, [pastedImageUrl]);
+
+ const handlePaste = useCallback(
+ (event: ClipboardEvent) => {
+ const items = event.clipboardData?.items;
+ console.log({ items });
+ if (!items) return;
+
+ let imageFound = false;
+ for (const item of items) {
+ if (item.kind === "file" && item.type.startsWith("image/")) {
+ event.preventDefault(); // Prevent pasting image data as text
+ const file = item.getAsFile();
+ if (file) {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl); // Revoke previous if any
+ }
+ const newImageUrl = URL.createObjectURL(file);
+ setPastedImageUrl(newImageUrl);
+ setPastedImageFile(file);
+ imageFound = true;
+ }
+ break; // Handle first image found
+ }
+ }
+
+ // // If no image was found, let the default text paste happen
+ // // Or, if you want to explicitly handle text paste:
+ // if (!imageFound) {
+ // // Let the default textarea paste handle it, or:
+ // // event.preventDefault();
+ // // const text = event.clipboardData.getData('text/plain');
+ // // setTextValue(prev => prev + text); // Or replace, depending on desired behavior
+ // // setPastedImageUrl(null); // Clear image if text is pasted
+ // // setPastedImageFile(null);
+ // }
+ },
+ [pastedImageUrl],
+ );
+ useEffect(() => {
+ window.addEventListener("paste", handlePaste);
+ return () => {
+ window.removeEventListener("paste", handlePaste);
+ };
+ }, [handlePaste]);
+
+ const router = useRouter();
+ async function fetchNLP(text: string, app: "spacy" | "stanza") {
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body: JSON.stringify({ text, app }),
+ };
+ const res = await fetch("/api/nlp", opts);
+ const j = await res.json();
+ console.log("j", j);
+ if ("ok" in j) {
+ sessionStorage.setItem(`${app}res`, JSON.stringify(j.ok));
+ }
+ }
+
+ const handleProcessText = async () => {
+ setIsAnalyzing(true);
+ const text = textValue.trim();
+ if (!text) {
+ alert("Text area is empty!");
+ return;
+ }
+ await Promise.all([fetchNLP(text, "spacy"), fetchNLP(text, "stanza")]);
+ router.push("/zoom");
+ setIsAnalyzing(false);
+ };
+
+ // const [isPending, startTransition] = useTransition();
+ const handleExtractTextFromImage = async () => {
+ if (!pastedImageFile) {
+ alert("No image to extract text from!");
+ return;
+ }
+ setIsExtracting(true);
+ const formData = new FormData();
+ formData.append("file", pastedImageFile, pastedImageFile.name);
+ console.log("Extracting text from image:", pastedImageFile.name);
+ const res = await fetch("/api/formdata/ocr", {
+ method: "POST",
+ body: formData,
+ });
+ const j = await res.json();
+ console.log("ocr res", j);
+ if ("ok" in j) {
+ setTextValue((t) => t + j.ok.join("\n"));
+ }
+ setIsExtracting(false);
+ // handleClearImage();
+ };
+
+ // setPastedImageUrl(null);
+ // setPastedImageFile(null);
+
+ const handleClearImage = () => {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl);
+ }
+ setPastedImageUrl(null);
+ setPastedImageFile(null);
+ };
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center bg-background p-4">
+ <Card className="w-full max-w-2xl">
+ <CardHeader>
+ <CardTitle className="text-center text-3xl font-bold">
+ Sorlang
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-6">
+ <Textarea
+ ref={textareaRef}
+ value={textValue}
+ onChange={(e) => setTextValue(e.target.value)}
+ placeholder="Paste text here, or paste an image..."
+ className="min-h-[200px] w-full text-base"
+ aria-label="Input text area"
+ />
+
+ {pastedImageUrl && (
+ <div className="mt-4 p-4 border rounded-md bg-muted/40">
+ <div className="flex justify-between items-start mb-2">
+ <h3 className="text-lg font-semibold">Pasted Image:</h3>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={handleClearImage}
+ className="text-xs"
+ >
+ Clear Image
+ </Button>
+ </div>
+ <img
+ src={pastedImageUrl}
+ alt="Pasted content"
+ className="max-w-full max-h-60 mx-auto rounded-md border"
+ />
+ </div>
+ )}
+ </CardContent>
+ <CardFooter className="flex flex-col sm:flex-row justify-center gap-4">
+ {pastedImageUrl ? (
+ <Button
+ onClick={handleExtractTextFromImage}
+ disabled={isExtracting}
+ className="w-full sm:w-auto"
+ >
+ {isExtracting ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Extracting...
+ </>
+ ) : (
+ "Extract Text from Image"
+ )}
+ </Button>
+ ) : (
+ <Button onClick={handleProcessText} className="w-full sm:w-auto">
+ {isAnalyzing ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Analyzing...
+ </>
+ ) : (
+ "Process Text"
+ )}
+ </Button>
+ )}
+ </CardFooter>
+ </Card>
+ <footer className="mt-8 text-center text-sm text-muted-foreground">
+ <p>© {new Date().getFullYear()} Sorlang App. All rights reserved.</p>
+ </footer>
+ </div>
+ );
+};
+
+export default SorlangPage;