diff options
author | polwex <polwex@sortug.com> | 2025-05-21 14:00:28 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-21 14:00:28 +0700 |
commit | e839a5f61f0faa21ca8b4bd5767f7575d5e576ee (patch) | |
tree | 53e5bcc3977b6ebef687521a7ac387a89aeb21c8 /src/components/ParseForm.tsx | |
parent | 4f2bd597beaa778476b84c10b571db1b13524301 (diff) |
the card flip animation is legit
Diffstat (limited to 'src/components/ParseForm.tsx')
-rw-r--r-- | src/components/ParseForm.tsx | 222 |
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; |