From e839a5f61f0faa21ca8b4bd5767f7575d5e576ee Mon Sep 17 00:00:00 2001
From: polwex <polwex@sortug.com>
Date: Wed, 21 May 2025 14:00:28 +0700
Subject: the card flip animation is legit

---
 src/components/ParseForm.tsx | 222 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 222 insertions(+)
 create mode 100644 src/components/ParseForm.tsx

(limited to 'src/components/ParseForm.tsx')

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;
-- 
cgit v1.2.3

