From e2bf3a7636274c56b3998f3ad5fdcac575dbab54 Mon Sep 17 00:00:00 2001 From: Davide Carpini Date: Sat, 23 Mar 2024 00:52:22 +0100 Subject: [PATCH 1/3] feat(project): add chat gpt translation generation --- app/api/chatGpt/route.ts | 106 ++++++++++++++++++ components/app/project/index.tsx | 1 + .../app/project/table/detail-slide-over.tsx | 43 +++++-- components/app/project/table/table.tsx | 4 + lib/i18n/languages/en.json | 8 +- store/useI18nState.ts | 2 +- utils/OpenAiUtils.ts | 4 +- 7 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 app/api/chatGpt/route.ts diff --git a/app/api/chatGpt/route.ts b/app/api/chatGpt/route.ts new file mode 100644 index 00000000..6e8e25f2 --- /dev/null +++ b/app/api/chatGpt/route.ts @@ -0,0 +1,106 @@ +import { NextRequest } from "next/server" +import { I18n, I18nInfo, I18nLang, ProjectSettings } from "@/store/useI18nState" +import { OpenAIHelper } from "@/utils/OpenAiUtils" +import { getServerSession } from "next-auth/next" +import * as z from "zod" + +import { authOptions } from "@/lib/auth" +import { db } from "@/lib/db" +import { handleCatchApi } from "@/lib/exceptions" +import i18n from "@/lib/i18n" +import { ErrorResponse } from "@/lib/response" + +const generateTranslationSchema = z.object({ + projectId: z.string(), + keyword: z.string(), +}) + +export async function GET(req: NextRequest) { + try { + const session = await getServerSession(authOptions) + + if (!session) { + return new Response("Unauthorized", { status: 403 }) + } + + const searchParams = req.nextUrl.searchParams + + const { keyword, projectId } = generateTranslationSchema.parse({ + projectId: searchParams.get("projectId"), + keyword: searchParams.get("keyword"), + }) + + const project = await db.project.findFirst({ + where: { + id: projectId, + }, + }) + + if (!project) { + return ErrorResponse({ + error: i18n.t("The project does not exist or is not published"), + }) + } + + const languages = project.languages as I18nLang[] + + if (!languages) { + return ErrorResponse({ + error: i18n.t("The project does not have any languages"), + }) + } + + const languagesPropt = languages + .map((language) => language.short) + .join(", ") + + const { context } = (project.info as I18nInfo[])?.find( + (ele) => ele.key === keyword + ) || { + key: keyword, + context: "", + } + + const settings = project.settings as ProjectSettings + + try { + const prompt = ` +I'm working on internationalizing my application. I'd like to translate the text "${keyword}" ${ + context ? `, used in this context: "${context}"` : "" + }. Could you write the translations in [${languagesPropt}]? +[[Translations should be informal]] <-- ${settings.formality} +[[in the tone of a tech website]] <-- ${settings.description} +[[The target audience is both male and female]] <-- ${settings.audience} +with an age range between ${settings.ageStart} and ${settings.ageEnd} years old. + +respond using an unique JSON object without any comments or any other descriptions, like so: +{ + "en": "", + "it": "", + "es": "" +} + +where: +language-id for english = en +language-id for italian = it +language-id for spanish = es + +` + + const openaiHelper = new OpenAIHelper() + const response = await openaiHelper.askChatGPT({ + prompt, + }) + const jsonString = openaiHelper.getResponseJSONString(response) + const result = openaiHelper.parseChatGPTJSONString(jsonString) + if (result) { + return new Response(JSON.stringify(result)) + } + throw new Error("Failed to get response from Chat GPT.") + } catch (e) { + throw new Error(e) + } + } catch (error) { + return handleCatchApi(error) + } +} diff --git a/components/app/project/index.tsx b/components/app/project/index.tsx index 17ad4c46..9f9accf0 100644 --- a/components/app/project/index.tsx +++ b/components/app/project/index.tsx @@ -117,6 +117,7 @@ export function Editor(props: EditorProps) { editKey={editKey} checkIfKeyAlreadyExists={checkIfKeyAlreadyExists} isSaving={isSaving} + project={project} /> {isProjectSettingsOpened && ( diff --git a/components/app/project/table/detail-slide-over.tsx b/components/app/project/table/detail-slide-over.tsx index d1783cc5..e9aca585 100644 --- a/components/app/project/table/detail-slide-over.tsx +++ b/components/app/project/table/detail-slide-over.tsx @@ -1,5 +1,7 @@ import { ChangeEvent, useCallback, useState } from "react" +import { Project } from "@prisma/client" +import i18n from "@/lib/i18n" import { Button } from "@/components/ui/button" import SlideOver, { SlideOverRow } from "@/components/slide-over" @@ -13,6 +15,7 @@ type Props = { editContext: (key: string, context: string) => void editKey: (key: string, newKey: string) => void checkIfKeyAlreadyExists: (key: string) => boolean + project: Project } const DetailSlideOver = (props: Props) => { @@ -24,6 +27,7 @@ const DetailSlideOver = (props: Props) => { editContext, editKey, checkIfKeyAlreadyExists, + project, } = props const [key, setKey] = useState(keyword.key) @@ -59,37 +63,52 @@ const DetailSlideOver = (props: Props) => { editKey(keyword.key, key) }, [editKey, key, keyword.key]) + const askAI = useCallback(async () => { + const response = await fetch( + `/api/chatGpt?projectId=${project?.id}&keyword=${keyword.key}` + ).then((res) => res.json()) + Object.keys(response).forEach((language) => { + editTranslation(language, keyword.key, response[language]) + }) + }, [editTranslation, keyword.key, project?.id]) + return (
- +
+ + +
- +
diff --git a/components/app/project/table/table.tsx b/components/app/project/table/table.tsx index e96040b7..aa4e2a85 100644 --- a/components/app/project/table/table.tsx +++ b/components/app/project/table/table.tsx @@ -1,6 +1,7 @@ "use client" import { useCallback, useMemo, useState } from "react" +import { Project } from "@prisma/client" import i18n from "@/lib/i18n" @@ -18,6 +19,7 @@ type Props = { editContext: (key: string, context: string) => void editKey: (key: string, newKey: string) => void checkIfKeyAlreadyExists: (key: string) => boolean + project: Project } const Table = (props: Props) => { @@ -30,6 +32,7 @@ const Table = (props: Props) => { editContext, editKey, checkIfKeyAlreadyExists, + project, } = props const [keySelected, selectKey] = useState(undefined) @@ -126,6 +129,7 @@ const Table = (props: Props) => {
{keywordSelected && ( ()( i18n: { ...state.i18n, languages: state.i18n.languages.map((_language) => { - if (_language.lang !== language) { + if (_language.lang !== language && _language.short !== language) { return _language } diff --git a/utils/OpenAiUtils.ts b/utils/OpenAiUtils.ts index 1fc0d1fa..59c0312d 100644 --- a/utils/OpenAiUtils.ts +++ b/utils/OpenAiUtils.ts @@ -30,7 +30,7 @@ export class OpenAIHelper { maxTokens?: number }) => this.openai.chat.completions.create({ - model: "gpt-3.5-turbo", + model: "gpt-4-vision-preview", max_tokens: maxTokens, messages: [ { @@ -59,7 +59,7 @@ export class OpenAIHelper { maxTokens?: number }) => this.openai.chat.completions.create({ - model: "gpt-4-vision-preview", + model: "gpt-3.5-turbo", max_tokens: maxTokens, messages: [ { From dde077cc722613a6e39d6baa8c4a72881b82bf34 Mon Sep 17 00:00:00 2001 From: Davide Carpini Date: Sat, 23 Mar 2024 00:56:18 +0100 Subject: [PATCH 2/3] fix(general): fix project types --- components/app/project/index.tsx | 6 ++---- components/app/project/table/detail-slide-over.tsx | 4 ++-- components/app/project/table/table.tsx | 4 ++-- components/app/project/types.ts | 6 ++++++ 4 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 components/app/project/types.ts diff --git a/components/app/project/index.tsx b/components/app/project/index.tsx index 9f9accf0..b45b26a9 100644 --- a/components/app/project/index.tsx +++ b/components/app/project/index.tsx @@ -16,13 +16,11 @@ import { DownloadKeywordsDropdownMenu } from "./dialogs/download" import ImportKeywordsModal from "./dialogs/import-keywords" import ProjectSettingsSlideOver from "./settings-slide-over" import Table from "./table/table" +import { ProjectData } from "./types" import useTranslation from "./useTranslation" export interface EditorProps { - project: Pick< - Project, - "id" | "title" | "languages" | "published" | "info" | "settings" - > + project: ProjectData } export function Editor(props: EditorProps) { diff --git a/components/app/project/table/detail-slide-over.tsx b/components/app/project/table/detail-slide-over.tsx index e9aca585..db76a611 100644 --- a/components/app/project/table/detail-slide-over.tsx +++ b/components/app/project/table/detail-slide-over.tsx @@ -1,10 +1,10 @@ import { ChangeEvent, useCallback, useState } from "react" -import { Project } from "@prisma/client" import i18n from "@/lib/i18n" import { Button } from "@/components/ui/button" import SlideOver, { SlideOverRow } from "@/components/slide-over" +import { ProjectData } from "../types" import { Keyword } from "../useTranslation" type Props = { @@ -15,7 +15,7 @@ type Props = { editContext: (key: string, context: string) => void editKey: (key: string, newKey: string) => void checkIfKeyAlreadyExists: (key: string) => boolean - project: Project + project: ProjectData } const DetailSlideOver = (props: Props) => { diff --git a/components/app/project/table/table.tsx b/components/app/project/table/table.tsx index aa4e2a85..24f76581 100644 --- a/components/app/project/table/table.tsx +++ b/components/app/project/table/table.tsx @@ -1,11 +1,11 @@ "use client" import { useCallback, useMemo, useState } from "react" -import { Project } from "@prisma/client" import i18n from "@/lib/i18n" import AddNewKeyword, { NewKeyword } from "../dialogs/add-new-keyword" +import { ProjectData } from "../types" import { Keyword } from "../useTranslation" import DetailSlideOver from "./detail-slide-over" import Row from "./row" @@ -19,7 +19,7 @@ type Props = { editContext: (key: string, context: string) => void editKey: (key: string, newKey: string) => void checkIfKeyAlreadyExists: (key: string) => boolean - project: Project + project: ProjectData } const Table = (props: Props) => { diff --git a/components/app/project/types.ts b/components/app/project/types.ts new file mode 100644 index 00000000..ab9c7f22 --- /dev/null +++ b/components/app/project/types.ts @@ -0,0 +1,6 @@ +import { Project } from "@prisma/client" + +export type ProjectData = Pick< + Project, + "id" | "title" | "languages" | "published" | "info" | "settings" +> From 5dab7caf6c2baef49efc2bcad8adfb5c61caae97 Mon Sep 17 00:00:00 2001 From: Davide Carpini Date: Sat, 23 Mar 2024 01:00:57 +0100 Subject: [PATCH 3/3] fix(general): remove unused variables, mate sta ci ha rotto il cazzo mettiamo nel commit lint --- components/app/project/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/app/project/index.tsx b/components/app/project/index.tsx index b45b26a9..f707ae37 100644 --- a/components/app/project/index.tsx +++ b/components/app/project/index.tsx @@ -2,7 +2,6 @@ import * as React from "react" import Link from "next/link" -import { Project } from "@prisma/client" import "@/styles/editor.css" import { useState } from "react"