diff options
author | polwex <polwex@sortug.com> | 2025-05-15 15:50:51 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-15 15:50:51 +0700 |
commit | 05d13b6f166eae5c2de8fe6f6038819b1b6ba1a0 (patch) | |
tree | 795ca33a3319d11bd9daa0366d4d15f31eabf024 /src | |
parent | 92139f14a92a535f123ad49a60498138dd2ec6cf (diff) |
m
Diffstat (limited to 'src')
-rw-r--r-- | src/actions/lang.ts | 23 | ||||
-rw-r--r-- | src/components/Main.tsx | 128 | ||||
-rw-r--r-- | src/components/zoom/Entry.tsx | 14 | ||||
-rw-r--r-- | src/pages.gen.ts | 3 | ||||
-rw-r--r-- | src/pages/api/formdata/[slug].ts | 50 | ||||
-rw-r--r-- | src/pages/api/nlp.ts | 32 | ||||
-rw-r--r-- | src/pages/api/proxy.ts | 23 | ||||
-rw-r--r-- | src/pages/form.tsx | 27 |
8 files changed, 221 insertions, 79 deletions
diff --git a/src/actions/lang.ts b/src/actions/lang.ts index d54a220..5242c7a 100644 --- a/src/actions/lang.ts +++ b/src/actions/lang.ts @@ -1,12 +1,17 @@ "use server"; import { AsyncRes } from "@/lib/types"; -import db from "../lib/db"; +import { NLP } from "sortug-ai"; +// import db from "../lib/db"; -export async function spacy(text: string, lang: string): AsyncRes<any> { - const res = await call(formData, true); - console.log("reg res", res); - if ("error" in res) return { error: "Something went wrong" }; - else { - return { success: true }; - } -} +// export async function textAction( +// text: string, +// lang: string, +// ): AsyncRes<NLP.Spacy.SpacyRes> { +// const res = await NLP.Spacy.run(text, lang); +// return res; +// } + +// export async function ocrAction(file: File): AsyncRes<string[]> { +// const res = await NLP.ocr(file); +// return res; +// } diff --git a/src/components/Main.tsx b/src/components/Main.tsx index fc587e2..2157a91 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -24,10 +24,8 @@ 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 [extractedTextResult, setExtractedTextResult] = useState<string | null>( - null, - ); const textareaRef = useRef<HTMLTextAreaElement>(null); @@ -41,15 +39,13 @@ const SorlangPage: React.FC = () => { }, [pastedImageUrl]); const handlePaste = useCallback( - (event: React.ClipboardEvent<HTMLTextAreaElement>) => { + (event: ClipboardEvent) => { const items = event.clipboardData?.items; console.log({ items }); if (!items) return; let imageFound = false; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (!item) return; + 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(); @@ -60,90 +56,85 @@ const SorlangPage: React.FC = () => { const newImageUrl = URL.createObjectURL(file); setPastedImageUrl(newImageUrl); setPastedImageFile(file); - setTextValue(""); // Clear textarea when image is pasted, or decide on desired behavior - setExtractedTextResult(null); // Clear previous extraction results 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); - } + // // 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 handleProcessText = () => { - if (!textValue.trim()) { + const handleProcessText = async () => { + setIsAnalyzing(true); + const text = textValue.trim(); + if (!text) { alert("Text area is empty!"); return; } - console.log("Processing text:", textValue); - // Add your text processing logic here - alert( - `Text submitted: "${textValue.substring(0, 50)}${textValue.length > 50 ? "..." : ""}"`, - ); + const opts = { + method: "POST", + headers: { "Content-type": "application/json" }, + body: JSON.stringify({ text, app: "spacy" }), + }; + const res = await fetch("/api/nlp", opts); + const j = await res.json(); + console.log("j", j); + if ("ok" in j) { + console.log("good"); + } + setIsAnalyzing(false); }; - const [isPending, startTransition] = useTransition(); - const onClick = () => { - startTransition(async () => { - const lol = "lmao"; - }); - }; + // const [isPending, startTransition] = useTransition(); const handleExtractTextFromImage = async () => { if (!pastedImageFile) { alert("No image to extract text from!"); return; } setIsExtracting(true); - setExtractedTextResult(null); + const formData = new FormData(); + formData.append("file", pastedImageFile, pastedImageFile.name); console.log("Extracting text from image:", pastedImageFile.name); - - // --- SIMULATE OCR API CALL --- - // In a real app, you would send `pastedImageFile` to a backend - // or use a client-side OCR library like Tesseract.js - await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate network delay - - // Example: Simulate successful extraction - const mockExtractedText = `This is simulated extracted text from "${pastedImageFile.name}".\nIt could be multiple lines.`; - - // Example: Simulate an error - // const mockExtractedText = null; - // alert("Failed to extract text (simulated)."); - - if (mockExtractedText) { - setTextValue(mockExtractedText); // Put extracted text into the textarea - setExtractedTextResult( - `Successfully extracted text and placed it in the textarea.`, - ); - } else { - setExtractedTextResult("Failed to extract text (simulated)."); + 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")); } - // --- END SIMULATION --- - setIsExtracting(false); - // Optionally clear the image after attempting extraction - // setPastedImageUrl(null); - // setPastedImageFile(null); + // handleClearImage(); }; + // setPastedImageUrl(null); + // setPastedImageFile(null); + const handleClearImage = () => { if (pastedImageUrl) { URL.revokeObjectURL(pastedImageUrl); } setPastedImageUrl(null); setPastedImageFile(null); - setExtractedTextResult(null); }; return ( @@ -159,9 +150,8 @@ const SorlangPage: React.FC = () => { ref={textareaRef} value={textValue} onChange={(e) => setTextValue(e.target.value)} - onPaste={handlePaste} placeholder="Paste text here, or paste an image..." - className="min-h-[200px] text-base" + className="min-h-[200px] w-full text-base" aria-label="Input text area" /> @@ -185,14 +175,6 @@ const SorlangPage: React.FC = () => { /> </div> )} - - {extractedTextResult && ( - <p - className={`mt-2 text-sm ${extractedTextResult.startsWith("Failed") ? "text-destructive" : "text-green-600"}`} - > - {extractedTextResult} - </p> - )} </CardContent> <CardFooter className="flex flex-col sm:flex-row justify-center gap-4"> {pastedImageUrl ? ( @@ -212,7 +194,13 @@ const SorlangPage: React.FC = () => { </Button> ) : ( <Button onClick={handleProcessText} className="w-full sm:w-auto"> - Process Text + {isAnalyzing ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Analyzing... + </> + ) : ( + "Process Text" + )} </Button> )} </CardFooter> diff --git a/src/components/zoom/Entry.tsx b/src/components/zoom/Entry.tsx new file mode 100644 index 0000000..a60c75c --- /dev/null +++ b/src/components/zoom/Entry.tsx @@ -0,0 +1,14 @@ +"use client"; +import { Zoom } from "prosody-ui"; +import { NLP } from "sortug-ai"; + +type Props = { text: string; doc: NLP.Spacy.SpacyRes }; +function ZoomText(props: Props) { + return ( + <div> + <Zoom.FullText {...props} /> + </div> + ); +} + +export default ZoomText; diff --git a/src/pages.gen.ts b/src/pages.gen.ts index 94e0a4a..9e2fcfb 100644 --- a/src/pages.gen.ts +++ b/src/pages.gen.ts @@ -8,6 +8,8 @@ import type { getConfig as Login_getConfig } from './pages/login'; // prettier-ignore import type { getConfig as Db_getConfig } from './pages/db'; // prettier-ignore +import type { getConfig as Form_getConfig } from './pages/form'; +// prettier-ignore import type { getConfig as About_getConfig } from './pages/about'; // prettier-ignore import type { getConfig as Index_getConfig } from './pages/index'; @@ -16,6 +18,7 @@ import type { getConfig as Index_getConfig } from './pages/index'; type Page = | ({ path: '/login' } & GetConfigResponse<typeof Login_getConfig>) | ({ path: '/db' } & GetConfigResponse<typeof Db_getConfig>) +| ({ path: '/form' } & GetConfigResponse<typeof Form_getConfig>) | ({ path: '/about' } & GetConfigResponse<typeof About_getConfig>) | ({ path: '/' } & GetConfigResponse<typeof Index_getConfig>); diff --git a/src/pages/api/formdata/[slug].ts b/src/pages/api/formdata/[slug].ts new file mode 100644 index 0000000..3580e6a --- /dev/null +++ b/src/pages/api/formdata/[slug].ts @@ -0,0 +1,50 @@ +// import db from "../../lib/db"; +import { NLP } from "sortug-ai"; + +type Req = { endpoint: "nlp" | "llm"; app: string; body: any }; +export const POST = async (request: Request): Promise<Response> => { + const url = URL.parse(request.url); + const path = url!.pathname.replace("/api/formdata", ""); + const body = await request.formData(); + if (path === "/ocr") return postOCR(body); + // TODO audio etc. a lot of stuff goes through here + + // if (!body.name || !body.creds) { + return Response.json({ message: "Invalid" }, { status: 400 }); + // } + + // try { + // const res = db.loginUser(body.name, body.creds); + // console.log({ res }); + + // return Response.json(res, { status: 200 }); + // } catch (error) { + // return Response.json({ message: "Failure" }, { status: 500 }); + // } +}; +export const GET = async (request: Request): Promise<Response> => { + console.log({ request }); + + // if (!body.name || !body.creds) { + return Response.json({ message: "Invalid" }, { status: 400 }); + // } + + // try { + // const res = db.loginUser(body.name, body.creds); + // console.log({ res }); + + // return Response.json(res, { status: 200 }); + // } catch (error) { + // return Response.json({ message: "Failure" }, { status: 500 }); + // } +}; + +async function postOCR(formData: FormData) { + try { + const res = await NLP.ocr(formData); + console.log({ res }); + return Response.json(res, { status: 200 }); + } catch (error) { + return Response.json({ message: "Failure" }, { status: 500 }); + } +} diff --git a/src/pages/api/nlp.ts b/src/pages/api/nlp.ts new file mode 100644 index 0000000..0e5eacb --- /dev/null +++ b/src/pages/api/nlp.ts @@ -0,0 +1,32 @@ +// import db from "../../lib/db"; +import { z } from "zod"; +import { NLP } from "sortug-ai"; + +const schema = z.object({ + app: z.enum(["stanza", "spacy"]), + text: z.string().min(3, "minimum 3 characters"), + lang: z + .custom<string>((val) => { + const check = NLP.ISO.BCP47.parse(val); + if (!check.language) return false; + const twochars = Object.values(NLP.ISO.iso6393To1); + return twochars.includes(check.language); + }) + .optional(), +}); + +export const POST = async (request: Request): Promise<Response> => { + const bod = await request.json(); + const { app, text, lang } = await schema.parseAsync(bod); + + try { + const res = + app === "stanza" + ? NLP.Stanza.segmenter(text, lang) + : NLP.Spacy.run(text, lang); + const r = await res; + return Response.json(r, { status: 200 }); + } catch (error) { + return Response.json({ message: "Failure" }, { status: 500 }); + } +}; diff --git a/src/pages/api/proxy.ts b/src/pages/api/proxy.ts new file mode 100644 index 0000000..3114f6b --- /dev/null +++ b/src/pages/api/proxy.ts @@ -0,0 +1,23 @@ +// import db from "../../lib/db"; +import { z } from "zod"; + +export const proxySchema = z.object({ + path: z.string().startsWith("/").optional(), + url: z.string().url("Invalid urladdress"), + body: z.any().optional(), + headers: z.record(z.string(), z.string()).optional(), +}); + +export const POST = async (request: Request): Promise<Response> => { + const bod = await request.json(); + const parsedBody = await proxySchema.parseAsync(bod); + + try { + const res = await fetch(parsedBody.url, parsedBody as any); + console.log({ res }); + + return Response.json(res, { status: 200 }); + } catch (error) { + return Response.json({ message: "Failure" }, { status: 500 }); + } +}; diff --git a/src/pages/form.tsx b/src/pages/form.tsx new file mode 100644 index 0000000..82ffd99 --- /dev/null +++ b/src/pages/form.tsx @@ -0,0 +1,27 @@ +import { Link } from "waku"; + +import { Counter } from "../components/counter"; +import { getContextData } from "waku/middleware/context"; +import Main from "../components/Main"; + +export default async function HomePage() { + const { user } = getContextData(); + + return <Main />; +} + +const getData = async () => { + const data = { + title: "Waku", + headline: "Waku", + body: "Hello world!", + }; + + return data; +}; + +export const getConfig = async () => { + return { + render: "dynamic", + } as const; +}; |