diff --git a/apps/sovoli.com/src/app/(dashboard)/new/actions/newHighlightAction.ts b/apps/sovoli.com/src/app/(dashboard)/new/actions/newHighlightAction.ts deleted file mode 100644 index 90dcaee6..00000000 --- a/apps/sovoli.com/src/app/(dashboard)/new/actions/newHighlightAction.ts +++ /dev/null @@ -1,93 +0,0 @@ -"use server"; - -import { unauthorized } from "next/navigation"; -import { google } from "@ai-sdk/google"; -import { withZod } from "@rvf/zod"; -import { auth } from "@sovoli/auth"; -import { generateObject, generateText } from "ai"; -import { z } from "zod"; - -import { formNewHighlightSchema } from "./schemas"; - -const model = google("gemini-1.5-flash"); - -export type State = { - status: "error"; - message: string; - errors?: Record; -} | null; - -const validator = withZod(formNewHighlightSchema); - -export async function newHighlightAction( - _prevState: State, - formData: FormData, -): Promise { - const session = await auth(); - if (!session) { - unauthorized(); - } - - const result = await validator.validate(formData); - - if (result.error) { - return { - status: "error", - message: "Failed to create note", - errors: result.error.fieldErrors, - }; - } - - const image = result.data.image; - - const fileBuffer = await image.arrayBuffer(); - // const aiResponse = await generateText({ - // model: model, - // messages: [ - // { - // role: "user", - // content: [ - // { - // type: "text", - // text: "What are the highlighted words from this image?", - // }, - // { - // type: "image", - // image: fileBuffer, - // }, - // ], - // }, - // ], - // }); - - const { object } = await generateObject({ - model: model, - schema: z.object({ - page: z.number(), - chapter: z.string(), - highlights: z.string().array(), - }), - messages: [ - { - role: "user", - content: [ - { - type: "text", - text: `Look at this image of a page from a book and tell me what the highlighted words are. - They will be highlighted with a marker or a pen. The sentences should make sense and follow line breaks properly.`, - }, - { - type: "image", - image: fileBuffer, - }, - ], - }, - ], - }); - console.log(object); - - return { - status: "error", - message: object.highlights.join(", "), - }; -} diff --git a/apps/sovoli.com/src/app/(dashboard)/new/components/HighlightForm.tsx b/apps/sovoli.com/src/app/(dashboard)/new/components/HighlightForm.tsx deleted file mode 100644 index 84e6f70a..00000000 --- a/apps/sovoli.com/src/app/(dashboard)/new/components/HighlightForm.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { useActionState } from "react"; -import { Alert } from "@sovoli/ui/components/alert"; -import { Button } from "@sovoli/ui/components/button"; -import { Form } from "@sovoli/ui/components/form"; - -import type { State } from "../actions/newNoteAction"; -import { newHighlightAction } from "../actions/newHighlightAction"; - -export const HighlightForm = () => { - const [state, formAction, pending] = useActionState( - newHighlightAction, - null, - ); - - return ( -
-
-
- - {state?.status === "error" && ( - - {Object.entries(state.errors ?? {}).map(([key, value]) => ( -
  • - {key}: {value} -
  • - ))} - - } - /> - )} -
    -
    - -
    -
    -
    - ); -}; diff --git a/apps/sovoli.com/src/app/(dashboard)/new/components/NoteForm.tsx b/apps/sovoli.com/src/app/(dashboard)/new/components/NoteForm.tsx index 36f9bc4f..22178caa 100644 --- a/apps/sovoli.com/src/app/(dashboard)/new/components/NoteForm.tsx +++ b/apps/sovoli.com/src/app/(dashboard)/new/components/NoteForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useActionState } from "react"; +import { useActionState, useState } from "react"; import { Alert } from "@sovoli/ui/components/alert"; import { Button } from "@sovoli/ui/components/button"; import { Form } from "@sovoli/ui/components/form"; @@ -22,8 +22,49 @@ export const NoteForm = ({ title, description, content }: NoteFormProps) => { null, ); + const [fileUploadStatus, setFileUploadStatus] = useState< + "idle" | "uploading" | "success" | "error" + >("idle"); + + const handleFileUpload = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + setFileUploadStatus("uploading"); + + try { + const formData = new FormData(); + formData.append("image", file); + + const response = await fetch("/api/ai/images/analyze", { + method: "POST", + body: formData, + }); + + if (response.ok) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const data = await response.json(); + console.log("File uploaded successfully:", data); + setFileUploadStatus("success"); + } else { + console.error("File upload failed:", response.statusText); + setFileUploadStatus("error"); + } + } catch (error) { + console.error("Error uploading file:", error); + setFileUploadStatus("error"); + } + }; + return (
    + {" "} + + {fileUploadStatus === "uploading" &&

    Uploading...

    } + {fileUploadStatus === "success" &&

    File uploaded successfully!

    } + {fileUploadStatus === "error" &&

    Failed to upload file.

    } { }} defaultValue={title} /> - - { + return file.type === "image/png" || file.type === "image/jpeg"; + }, + { + message: "File must be an image", + }, +); + +export const formRequestBodySchema = z.object({ + image: imageFileSchema, +}); + +const validator = withZod(formRequestBodySchema); + +export async function POST(req: NextRequest): Promise { + const formData = await req.formData(); + + const result = await validator.validate(formData); + + if (result.error) { + return new Response(JSON.stringify(result.error), { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }); + } + + const image = result.data.image; + + const fileBuffer = await image.arrayBuffer(); + + const { object } = await generateObject({ + model: model, + schema: z.object({ + page: z.number().optional(), + chapter: z.string().optional(), + highlights: z.string().array(), + }), + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: ` + This is an image of a page from a book. + The chapter is displayed at the top of the page. If you cannot determine page or chapter, leave it. + It should contain highlighted text, which may include a word, a phrase, a sentence, multiple sentences, or a paragraph. + Separate each highlight into distinct entries if there is a natural or logical boundary, such as the end of a sentence or a clear change in context. + If a highlight spans multiple sentences, group them together into one entry only if they are part of the same continuous thought or context, and there is no visible separation. + `, + }, + { + type: "image", + image: fileBuffer, + }, + ], + }, + ], + }); + console.log(object); + + return new Response(JSON.stringify(object), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); +}