2023-08-07 13:02:34 +02:00
|
|
|
import type { ActionArgs, LoaderArgs } from "@remix-run/node";
|
2023-08-07 10:52:44 +02:00
|
|
|
import { json, redirect } from "@remix-run/node";
|
2023-08-22 14:58:46 +02:00
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
Link,
|
|
|
|
|
useActionData,
|
|
|
|
|
useLoaderData,
|
|
|
|
|
useNavigation,
|
|
|
|
|
} from "@remix-run/react";
|
2023-08-07 10:52:44 +02:00
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
|
|
2023-08-07 11:21:43 +02:00
|
|
|
import { createTranslation } from "~/models/translation.server";
|
2023-08-07 13:02:34 +02:00
|
|
|
import { requireUser } from "~/session.server";
|
|
|
|
|
|
|
|
|
|
import { Configuration, OpenAIApi } from "openai";
|
2023-08-07 10:52:44 +02:00
|
|
|
|
|
|
|
|
export const action = async ({ request }: ActionArgs) => {
|
2023-08-07 13:02:34 +02:00
|
|
|
const user = await requireUser(request);
|
2023-08-07 10:52:44 +02:00
|
|
|
|
|
|
|
|
const formData = await request.formData();
|
2023-08-07 11:21:43 +02:00
|
|
|
const lang = formData.get("lang");
|
|
|
|
|
const text = formData.get("text");
|
2023-08-07 10:52:44 +02:00
|
|
|
|
2023-08-07 11:21:43 +02:00
|
|
|
if (typeof lang !== "string" || lang.length === 0) {
|
2023-08-07 10:52:44 +02:00
|
|
|
return json(
|
2023-08-07 13:02:34 +02:00
|
|
|
{ errors: { text: null, lang: "Lang is required", result: null } },
|
2023-08-07 10:52:44 +02:00
|
|
|
{ status: 400 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 11:21:43 +02:00
|
|
|
if (typeof text !== "string" || text.length === 0) {
|
2023-08-07 10:52:44 +02:00
|
|
|
return json(
|
2023-08-07 13:02:34 +02:00
|
|
|
{ errors: { text: "Text is required", lang: null, result: null } },
|
2023-08-07 10:52:44 +02:00
|
|
|
{ status: 400 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 13:02:34 +02:00
|
|
|
if (!user.openAIKey?.length) {
|
|
|
|
|
return redirect("/account");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const configuration = new Configuration({
|
|
|
|
|
apiKey: user.openAIKey,
|
|
|
|
|
});
|
|
|
|
|
const openai = new OpenAIApi(configuration);
|
|
|
|
|
|
|
|
|
|
const completion = await openai.createChatCompletion({
|
|
|
|
|
model: "gpt-3.5-turbo",
|
|
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
role: "system",
|
|
|
|
|
content: `You will be provided with a sentence, your task is to translate it into ${lang}.`,
|
|
|
|
|
},
|
|
|
|
|
{ role: "user", content: text },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
const result = completion.data.choices[0].message?.content;
|
|
|
|
|
|
|
|
|
|
if (typeof result !== "string" || result.length === 0) {
|
|
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
text: null,
|
|
|
|
|
lang: null,
|
|
|
|
|
result: "Error while retrieving translation result",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const t = await createTranslation({ lang, text, result, userId: user.id });
|
|
|
|
|
|
|
|
|
|
return redirect(`/t/${t.id}`);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
let error = e as any;
|
|
|
|
|
if (error.response) {
|
|
|
|
|
console.error(error.response.status);
|
|
|
|
|
console.error(error.response.data);
|
|
|
|
|
|
|
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
text: null,
|
|
|
|
|
lang: null,
|
|
|
|
|
result: `[${error.response.status}] ${
|
|
|
|
|
error.response.data || error.message
|
|
|
|
|
}`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(error.message);
|
|
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
text: null,
|
|
|
|
|
lang: null,
|
|
|
|
|
result: `[${error.name}] ${error.message}`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const loader = async ({ params, request }: LoaderArgs) => {
|
|
|
|
|
const user = await requireUser(request);
|
2023-08-07 10:52:44 +02:00
|
|
|
|
2023-08-07 13:02:34 +02:00
|
|
|
if (!user.openAIKey?.length) {
|
|
|
|
|
return redirect("/account");
|
|
|
|
|
}
|
2023-08-07 11:21:43 +02:00
|
|
|
|
2023-08-07 13:02:34 +02:00
|
|
|
return json({ userHasOpenAIKey: true });
|
2023-08-07 10:52:44 +02:00
|
|
|
};
|
|
|
|
|
|
2023-08-07 11:21:43 +02:00
|
|
|
export default function NewTranslationPage() {
|
2023-08-07 10:52:44 +02:00
|
|
|
const actionData = useActionData<typeof action>();
|
2023-08-07 13:02:34 +02:00
|
|
|
const loaderData = useLoaderData<typeof loader>();
|
2023-08-07 11:21:43 +02:00
|
|
|
const langRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const textRef = useRef<HTMLTextAreaElement>(null);
|
2023-08-22 14:58:46 +02:00
|
|
|
const navigation = useNavigation();
|
2023-08-07 10:52:44 +02:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-08-07 11:21:43 +02:00
|
|
|
if (actionData?.errors?.lang) {
|
|
|
|
|
langRef.current?.focus();
|
|
|
|
|
} else if (actionData?.errors?.text) {
|
|
|
|
|
textRef.current?.focus();
|
2023-08-07 10:52:44 +02:00
|
|
|
}
|
|
|
|
|
}, [actionData]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Form
|
|
|
|
|
method="post"
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
gap: 8,
|
|
|
|
|
width: "100%",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="flex w-full flex-col gap-1">
|
2023-08-07 11:21:43 +02:00
|
|
|
<span>Translate to: </span>
|
2023-08-07 10:52:44 +02:00
|
|
|
<input
|
2023-08-07 11:21:43 +02:00
|
|
|
ref={langRef}
|
|
|
|
|
name="lang"
|
2023-08-22 14:58:46 +02:00
|
|
|
required
|
|
|
|
|
disabled={
|
|
|
|
|
loaderData?.userHasOpenAIKey === false ||
|
|
|
|
|
navigation.state === "submitting"
|
|
|
|
|
}
|
2023-08-07 10:52:44 +02:00
|
|
|
className="flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose"
|
2023-08-07 11:21:43 +02:00
|
|
|
aria-invalid={actionData?.errors?.lang ? true : undefined}
|
2023-08-07 10:52:44 +02:00
|
|
|
aria-errormessage={
|
2023-08-07 11:21:43 +02:00
|
|
|
actionData?.errors?.lang ? "lang-error" : undefined
|
2023-08-07 10:52:44 +02:00
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
2023-08-07 11:21:43 +02:00
|
|
|
{actionData?.errors?.lang ? (
|
|
|
|
|
<div className="pt-1 text-red-700" id="lang-error">
|
|
|
|
|
{actionData.errors.lang}
|
2023-08-07 10:52:44 +02:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="flex w-full flex-col gap-1">
|
2023-08-07 11:21:43 +02:00
|
|
|
<span>Text: </span>
|
2023-08-07 10:52:44 +02:00
|
|
|
<textarea
|
2023-08-07 11:21:43 +02:00
|
|
|
ref={textRef}
|
|
|
|
|
name="text"
|
2023-08-22 14:58:46 +02:00
|
|
|
required
|
2023-08-07 10:52:44 +02:00
|
|
|
rows={8}
|
2023-08-22 14:58:46 +02:00
|
|
|
disabled={
|
|
|
|
|
loaderData?.userHasOpenAIKey === false ||
|
|
|
|
|
navigation.state === "submitting"
|
|
|
|
|
}
|
2023-08-07 10:52:44 +02:00
|
|
|
className="w-full flex-1 rounded-md border-2 border-blue-500 px-3 py-2 text-lg leading-6"
|
2023-08-07 11:21:43 +02:00
|
|
|
aria-invalid={actionData?.errors?.text ? true : undefined}
|
2023-08-07 10:52:44 +02:00
|
|
|
aria-errormessage={
|
2023-08-07 11:21:43 +02:00
|
|
|
actionData?.errors?.text ? "text-error" : undefined
|
2023-08-07 10:52:44 +02:00
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
2023-08-07 11:21:43 +02:00
|
|
|
{actionData?.errors?.text ? (
|
|
|
|
|
<div className="pt-1 text-red-700" id="text-error">
|
|
|
|
|
{actionData.errors.text}
|
2023-08-07 10:52:44 +02:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
2023-08-22 14:58:46 +02:00
|
|
|
disabled={
|
|
|
|
|
loaderData?.userHasOpenAIKey === false ||
|
|
|
|
|
navigation.state === "submitting"
|
|
|
|
|
}
|
2023-08-07 10:52:44 +02:00
|
|
|
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
|
|
|
|
|
>
|
2023-08-22 14:58:46 +02:00
|
|
|
{navigation.state === "submitting" && (
|
|
|
|
|
<svg
|
|
|
|
|
className="mr-2 inline-block h-4 w-4 animate-spin text-white"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<circle
|
|
|
|
|
className="opacity-25"
|
|
|
|
|
cx="12"
|
|
|
|
|
cy="12"
|
|
|
|
|
r="10"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="4"
|
|
|
|
|
/>
|
|
|
|
|
<path
|
|
|
|
|
className="opacity-75"
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2
|
|
|
|
|
5.291A7.962 7.962 0 014 12H0c0 3.042
|
|
|
|
|
1.135 5.824 3 7.938l3-2.647z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
2023-08-07 11:21:43 +02:00
|
|
|
Translate
|
2023-08-07 10:52:44 +02:00
|
|
|
</button>
|
|
|
|
|
</div>
|
2023-08-07 13:02:34 +02:00
|
|
|
|
|
|
|
|
{loaderData?.userHasOpenAIKey === false && (
|
|
|
|
|
<div className="rounded border border-red-200 bg-red-100 p-4 text-red-700">
|
|
|
|
|
<p>
|
|
|
|
|
You need to add your OpenAI API key to your account before you can
|
|
|
|
|
translate text.
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
Go to <Link to="/account">your account</Link> to add your key.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{actionData?.errors?.result && (
|
|
|
|
|
<div className="rounded border border-red-200 bg-red-100 p-4 text-red-700">
|
|
|
|
|
<p>{actionData.errors.result}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2023-08-07 10:52:44 +02:00
|
|
|
</Form>
|
|
|
|
|
);
|
|
|
|
|
}
|