From 922ddcc828786b77aab4ee7ff7adea0fd02740eb Mon Sep 17 00:00:00 2001 From: Rama Priyanto Date: Fri, 15 Nov 2024 17:53:04 +0700 Subject: [PATCH] feat:generate article form --- app/(admin)/admin/article/generate/page.tsx | 10 + app/(admin)/admin/article/page.tsx | 8 +- .../form/article/generate-article-form.tsx | 605 ++++++++++++++++++ .../article/generate-bulk-article-form.tsx | 506 +++++++++++++++ .../form/article/generate-rewrite-form.tsx | 273 ++++++++ .../article/generate-single-article-form.tsx | 334 ++++++++++ .../form/article/speech-to-text-form.tsx | 288 +++++++++ components/form/form-article.tsx | 1 + components/layout/admin-layout.tsx | 4 +- components/layout/sidebar/sidebar.tsx | 219 +++---- .../table/disestages/article-draft-table.tsx | 189 ++++++ .../disestages/transcript-draft-table.tsx | 192 ++++++ components/ui/breadcrumb.tsx | 2 +- package-lock.json | 31 +- package.json | 3 +- service/generate-article.ts | 190 ++++++ services/http-config/disestages-instance.ts | 12 + services/http-config/disestages-services.ts | 47 ++ store/generated-article-store.tsx | 32 + utils/global.tsx | 22 + 20 files changed, 2839 insertions(+), 129 deletions(-) create mode 100644 app/(admin)/admin/article/generate/page.tsx create mode 100644 components/form/article/generate-article-form.tsx create mode 100644 components/form/article/generate-bulk-article-form.tsx create mode 100644 components/form/article/generate-rewrite-form.tsx create mode 100644 components/form/article/generate-single-article-form.tsx create mode 100644 components/form/article/speech-to-text-form.tsx create mode 100644 components/table/disestages/article-draft-table.tsx create mode 100644 components/table/disestages/transcript-draft-table.tsx create mode 100644 service/generate-article.ts create mode 100644 services/http-config/disestages-instance.ts create mode 100644 services/http-config/disestages-services.ts create mode 100644 store/generated-article-store.tsx diff --git a/app/(admin)/admin/article/generate/page.tsx b/app/(admin)/admin/article/generate/page.tsx new file mode 100644 index 0000000..9ffa6d2 --- /dev/null +++ b/app/(admin)/admin/article/generate/page.tsx @@ -0,0 +1,10 @@ +import GenerateArticleForm from "@/components/form/article/generate-article-form"; +import { Card } from "@nextui-org/react"; + +export default function GenerateArticle() { + return ( + + + + ); +} diff --git a/app/(admin)/admin/article/page.tsx b/app/(admin)/admin/article/page.tsx index 60b34ad..eb6aa4a 100644 --- a/app/(admin)/admin/article/page.tsx +++ b/app/(admin)/admin/article/page.tsx @@ -8,13 +8,19 @@ export default function BasicPage() { return (
-
+
+ + +
diff --git a/components/form/article/generate-article-form.tsx b/components/form/article/generate-article-form.tsx new file mode 100644 index 0000000..e74a741 --- /dev/null +++ b/components/form/article/generate-article-form.tsx @@ -0,0 +1,605 @@ +"use client"; +import { error } from "@/config/swal"; +import { createArticle } from "@/service/article"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Button, + Card, + Chip, + Input, + Select, + SelectItem, + SelectSection, + Selection, + Spinner, +} from "@nextui-org/react"; +import JoditEditor from "jodit-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import React, { ChangeEvent, useEffect, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import Swal from "sweetalert2"; +import withReactContent from "sweetalert2-react-content"; +import * as z from "zod"; +import ReactSelect from "react-select"; +import makeAnimated from "react-select/animated"; +import GenerateSingleArticle from "./generate-single-article-form"; +import { getDetailArticle } from "@/service/generate-article"; +import { delay } from "@/utils/global"; +import GenerateBulkArticle from "./generate-bulk-article-form"; +import generatedArticleIds from "@/store/generated-article-store"; +import SpeechToTextOperator from "./speech-to-text-form"; +import GenerateRewriteArticle from "./generate-rewrite-form"; +import SpeechToText from "./speech-to-text-form"; + +const articleSchema = z.object({ + title: z.string().min(1, { message: "Required" }), + article: z.string().min(1, { message: "Required" }), + slug: z.string().min(1, { message: "Required" }), + tags: z.string().min(0, { message: "Required" }).optional(), + description: z.string().min(0, { message: "Required" }).optional(), +}); + +const dummyCategory = [ + { + id: 1, + label: "Category 1", + value: "category-1", + }, + { + id: 2, + label: "Category 2", + value: "category-2", + }, + { + id: 3, + label: "Category 3", + value: "category-3", + }, + { + id: 4, + label: "Category 4", + value: "category-4", + }, + { + id: 5, + label: "Category 5", + value: "category-5", + }, +]; + +export default function GenerateArticleForm() { + const animatedComponents = makeAnimated(); + + const router = useRouter(); + const [title, setTitle] = useState(""); + const [article, setArticle] = React.useState(new Set([])); + const [slug, setSlug] = useState(""); + const [tags, setTags] = useState([]); + const [newTags, setNewTags] = useState(""); + const editor = useRef(null); + const [content, setContent] = useState(""); + const MySwal = withReactContent(Swal); + const [selectedImages, setSelectedImages] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(); + const [selectedContentType, setSelectedContentType] = + useState("single-article"); + + const [collectSingleArticleId, setCollectSingleArticleId] = useState< + number[] + >([]); + const [collectBulkArticleId, setCollectBulkArticleId] = useState( + [] + ); + const [collectRewriteArticleId, setCollectRewriteArticleId] = useState< + number[] + >([]); + + const [generatedTranscriptId, setGeneratedTranscriptId] = useState(); + + const generatedArticleIdStore = generatedArticleIds( + (state) => state.articleIds + ); + const setGeneratedArticleIdStore = generatedArticleIds( + (state) => state.setArticleIds + ); + + const [selectedGeneratedArticleId, setSelectedGeneratedArticleId] = + useState(); + + const formOptions = { resolver: zodResolver(articleSchema) }; + type MicroIssueSchema = z.infer; + const { + register, + control, + handleSubmit, + setValue, + formState: { errors }, + } = useForm(formOptions); + + const TypeId = [ + { + key: 1, + label: "Article", + }, + { + key: 2, + label: "Magazine", + }, + ]; + + const handleImageChange = (event: ChangeEvent) => { + if (event.target.files) { + const files = Array.from(event.target.files); + setSelectedImages((prevImages) => [...prevImages, ...files]); + } + }; + + const handleRemoveImage = (index: number) => { + setSelectedImages((prevImages) => prevImages.filter((_, i) => i !== index)); + }; + + const handleClose = (tagsToRemove: string) => { + setTags(tags.filter((tag) => tag !== tagsToRemove)); + if (tags.length === 1) { + setTags([]); + } + }; + + const handleAddTags = (e: any) => { + if (newTags.trim() !== "") { + setTags([...tags, newTags.trim()]); + setNewTags(""); + e.preventDefault(); + } + }; + + const handleKeyDown = (event: any) => { + if (event.key === "Enter") { + handleAddTags(event); + } + }; + + async function save(data: any) { + const formData = { + title: title, + typeId: parseInt(String(Array.from(article)[0])), + slug: slug, + tags: tags.join(","), + description: content, + htmlDescription: content, + }; + + console.log("Form Data:", formData); + const response = await createArticle(formData); + + if (response?.error) { + error(response.message); + return false; + } + + successSubmit("/admin/article"); + } + + function successSubmit(redirect: any) { + MySwal.fire({ + title: "Sukses", + icon: "success", + confirmButtonColor: "#3085d6", + confirmButtonText: "OK", + }).then((result) => { + if (result.isConfirmed) { + router.push(redirect); + } + }); + } + + async function onSubmit(data: any) { + MySwal.fire({ + title: "Simpan Data", + text: "", + icon: "warning", + showCancelButton: true, + cancelButtonColor: "#d33", + confirmButtonColor: "#3085d6", + confirmButtonText: "Simpan", + }).then((result) => { + if (result.isConfirmed) { + save(data); + } + }); + } + + const singleArticleId = (id: number) => { + let temp = [...collectSingleArticleId, id]; + setCollectSingleArticleId(temp); + }; + const bulkArticleId = (id: number[]) => { + let temp: number[] = [...collectBulkArticleId, ...id]; + setCollectBulkArticleId(temp); + }; + const rewriteArticleId = (id: number) => { + let temp = [...collectRewriteArticleId, id]; + setCollectRewriteArticleId(temp); + }; + + useEffect(() => { + getArticleDetail(); + }, [selectedGeneratedArticleId]); + + const checkArticleStatus = async (data: string | null) => { + if (data === null) { + delay(10000).then(() => { + getArticleDetail(); + }); + } + }; + + const getArticleDetail = async () => { + if (selectedGeneratedArticleId) { + const res = await getDetailArticle(selectedGeneratedArticleId); + const data = res?.data?.data?.articleBody; + checkArticleStatus(data); + if (data !== null) { + setContent(data); + } + } + }; + + return ( +
+
+ +
+ setTitle(e.target.value)} + label="Judul" + variant="bordered" + placeholder="Enter Text" + labelPlacement="outside" + /> +
+ {title.length === 0 && errors.title && errors.title.message} +
+
+
+ +
+ {errors.article?.message} +
+ {/*

{article}

*/} +
+
+

Category

+ + "!rounded-xl bg-white !border-1 !border-gray-200", + }} + classNamePrefix="select" + onChange={setSelectedCategory} + closeMenuOnSelect={false} + components={animatedComponents} + isClearable={true} + isSearchable={true} + isMulti={true} + placeholder="Category ..." + name="sub-module" + options={dummyCategory} + /> +
+ {(!selectedCategory || selectedCategory?.length < 1) && ( +

Required

+ )} +
+
+
+ setSlug(e.target.value)} + label="Slug" + variant="bordered" + placeholder="Enter Text" + labelPlacement="outside" + /> +
+ {slug.length === 0 && errors.slug && errors.slug.message} +
+
+
+ setNewTags(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Tambahkan tag baru dan tekan Enter" + /> +
+ {tags.length === 0 && errors.tags && errors.tags.message} +
+
+ {tags.map((tag, index) => ( + handleClose(tag)} + > + {tag} + + ))} +
+
+ + {selectedContentType === "single-article" ? ( +
+ singleArticleId(data)} + /> +
+ {collectSingleArticleId.length > 0 && + collectSingleArticleId.map((data) => ( + + ))} +
+ {selectedGeneratedArticleId && + collectSingleArticleId.includes(selectedGeneratedArticleId) && + (selectedGeneratedArticleId && content !== "" ? ( +
+

Description

+ setContent(newContent)} + className="dark:text-black" + /> +
+ {content?.length === 0 && + errors?.description && + errors?.description?.message} +
+
+ ) : selectedGeneratedArticleId && content == "" ? ( + + ) : collectSingleArticleId.length > 0 ? ( +

Select Article ID

+ ) : ( + "" + ))} +
+ ) : selectedContentType === "bulk-article" ? ( +
+ bulkArticleId(data)} /> +
+ {collectBulkArticleId.length > 0 && + collectBulkArticleId.map((data) => ( + + ))} +
+ {selectedGeneratedArticleId && + collectBulkArticleId.includes(selectedGeneratedArticleId) && + (selectedGeneratedArticleId && content !== "" ? ( +
+

Description

+ setContent(newContent)} + className="dark:text-black" + /> +
+ {content?.length === 0 && + errors?.description && + errors?.description?.message} +
+
+ ) : selectedGeneratedArticleId && content == "" ? ( + + ) : collectSingleArticleId.length > 0 ? ( +

Select Article ID

+ ) : ( + "" + ))} +
+ ) : selectedContentType === "rewrite" ? ( +
+ rewriteArticleId(data)} + initTranscript={generatedTranscriptId} + /> +
+ {collectRewriteArticleId.length > 0 && + collectRewriteArticleId.map((data) => ( + + ))} +
+ {selectedGeneratedArticleId && + collectRewriteArticleId.includes(selectedGeneratedArticleId) && + (selectedGeneratedArticleId && content !== "" ? ( +
+

Description

+ setContent(newContent)} + className="dark:text-black" + /> +
+ {content?.length === 0 && + errors?.description && + errors?.description?.message} +
+
+ ) : selectedGeneratedArticleId && content == "" ? ( + + ) : collectRewriteArticleId.length > 0 ? ( +

Select Article ID

+ ) : ( + "" + ))} +
+ ) : selectedContentType === "speech-to-text" ? ( + { + setGeneratedTranscriptId(transcriptId); + setSelectedContentType("rewrite"); + }} + /> + ) : ( + "" + )} + +
+

Attachment (Opsional)

+
+ +
+ {selectedImages?.length > 0 ? ( +
+

Pratinjau:

+
+ {selectedImages.map((image, index) => ( +
+ handleRemoveImage(index)} + > + X + + Pratinjau Gambar +
+ ))} +
+
+ ) : ( + "" + )} +
+
+ + + + +
+
+
+
+ ); +} diff --git a/components/form/article/generate-bulk-article-form.tsx b/components/form/article/generate-bulk-article-form.tsx new file mode 100644 index 0000000..d13d131 --- /dev/null +++ b/components/form/article/generate-bulk-article-form.tsx @@ -0,0 +1,506 @@ +"use client"; +import { + Button, + Input, + Select, + SelectItem, + SelectSection, +} from "@nextui-org/react"; +import { FormEvent, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { close, error, loading } from "@/config/swal"; +import { + generateDataArticle, + getGenerateKeywords, + getGenerateTitle, + getGenerateTopicKeywords, + saveBulkArticle, +} from "@/service/generate-article"; + +const writingStyle = [ + { + id: 1, + name: "Friendly", + }, + { + id: 1, + name: "Professional", + }, + { + id: 3, + name: "Informational", + }, + { + id: 4, + name: "Neutral", + }, + { + id: 5, + name: "Witty", + }, +]; + +const articleSize = [ + { + id: 1, + name: "News (300 - 900 words)", + value: "News", + }, + { + id: 2, + name: "Info (900 - 2000 words)", + value: "Info", + }, + { + id: 3, + name: "Detail (2000 - 5000 words)", + value: "Detail", + }, +]; + +const formSchema = z.object({ + rows: z.array( + z.object({ + mainKeyword: z.string().min(1, { + message: "Main Keyword must be at least 2 characters.", + }), + title: z.string().min(1, { + message: "Title must be at least 2 characters.", + }), + additionalKeyword: z.string().min(1, { + message: "Additional Keyword must be at least 2 characters.", + }), + }) + ), +}); + +export default function GenerateBulkArticle(props: { + articleId: (data: number[]) => void; +}) { + const [selectedWritingSyle, setSelectedWritingStyle] = + useState("Informational"); + const [selectedArticleSize, setSelectedArticleSize] = useState("News"); + const [selectedLanguage, setSelectedLanguage] = useState("id"); + + const formOptions = { + resolver: zodResolver(formSchema), + defaultValues: { + rows: [{ mainKeyword: "", title: "", additionalKeyword: "" }], + }, + }; + type UserSettingSchema = z.infer; + const { + register, + control, + handleSubmit, + formState: { errors }, + setValue, + getValues, + trigger, + } = useForm(formOptions); + + const { fields, append, remove } = useFieldArray({ + control, + name: "rows", + }); + + const onSubmit = async () => { + loading(); + const listData = []; + for (let i = 0; i < fields.length; i++) { + listData.push({ + title: getValues(`rows.${i}.title`), + mainKeyword: getValues(`rows.${i}.mainKeyword`), + additionalKeywords: getValues(`rows.${i}.additionalKeyword`), + }); + } + const request = { + style: "Friendly", + website: "None", + connectToWeb: true, + lang: selectedLanguage, + pointOfView: "0", + imageSource: "Web", + targetCountry: null, + articleSize: selectedArticleSize, + projectId: 2, + data: listData, + createdBy: "123123", + clientId: "humasClientIdtest", + }; + + const res = await saveBulkArticle(request); + if (res.error) { + error(res.message); + return false; + } + + console.log("res?s", res?.data?.data); + const temp: number[] = []; + res?.data?.data.map((data: any) => temp.push(data.id)); + props.articleId(temp); + close(); + }; + + const generateTitle = async (keyword: string | undefined, index: number) => { + if (keyword) { + const req = { + keyword: keyword, + style: selectedWritingSyle, + website: "None", + connectToWeb: true, + lang: selectedLanguage, + pointOfView: "None", + clientId: "", + }; + setValue(`rows.${index}.title`, "process..."); + + const res = await getGenerateTitle(req); + const data = res?.data?.data; + setValue(`rows.${index}.title`, data); + } + }; + + const generateKeywords = async ( + keyword: string | undefined, + index: number + ) => { + if (keyword) { + const req = { + keyword: keyword, + style: selectedWritingSyle, + website: "None", + connectToWeb: true, + lang: selectedLanguage, + pointOfView: "0", + clientId: "", + }; + setValue(`rows.${index}.additionalKeyword`, "process..."); + const res = await getGenerateKeywords(req); + const data = res?.data?.data; + setValue(`rows.${index}.additionalKeyword`, data); + } + }; + + const processAll = async () => { + let emptyMainKeyword = 0; + const mainKeyword = getValues(`rows.0.mainKeyword`); + for (let i = 0; i < fields.length; i++) { + const mainKeyNow = getValues(`rows.${i}.mainKeyword`); + if (mainKeyNow === "") { + emptyMainKeyword++; + } + } + if (mainKeyword !== "") { + if (emptyMainKeyword > 0) { + loading(); + const res = await getGenerateTopicKeywords({ + keyword: mainKeyword, + count: emptyMainKeyword, + }); + const data = res?.data?.data; + let j = 0; + for (let i = 0; i < fields.length; i++) { + const mainKeyNow = getValues(`rows.${i}.mainKeyword`); + if (mainKeyNow === "") { + setValue(`rows.${i}.mainKeyword`, data[j]); + + j++; + } + } + for (let i = 0; i < fields.length; i++) { + const mainKeyNow = getValues(`rows.${i}.mainKeyword`); + if (getValues(`rows.${i}.title`) == "") { + generateTitle(mainKeyNow, i); + } + if (getValues(`rows.${i}.additionalKeyword`) == "") { + generateKeywords(mainKeyNow, i); + } + } + + close(); + } else { + loading(); + for (let i = 0; i < fields.length; i++) { + const mainKeyNow = getValues(`rows.${i}.mainKeyword`); + if (getValues(`rows.${i}.title`) == "") { + generateTitle(mainKeyNow, i); + } + if (getValues(`rows.${i}.additionalKeyword`) == "") { + generateKeywords(mainKeyNow, i); + } + } + close(); + } + } + }; + + const processRow = (index: number) => { + const addKeyword = getValues(`rows.${index}.mainKeyword`); + console.log("index", index, addKeyword); + generateTitle(addKeyword, index); + + generateKeywords(addKeyword, index); + }; + + const validateFields = async () => { + const validation = await trigger(); + if (validation) { + onSubmit(); + } + }; + + return ( +
+
+
+ + + +
+
+ {fields.map((field, index) => ( +
+
+
+
+

Main Keyword

+ +
+
+ {index === fields.length - 1 && ( + + )} + {index !== 0 && ( + remove(index)} + className="cursor-pointer mb-2 w-[20px] h-[20px]text-center rounded-full flex justify-center items-center" + > + - + + )} +
+
+ ( + + )} + /> +
+
+
+

Title

+ +
+ ( + + )} + /> +
+ +
+
+
+
+

SEO

+ +
+ ( + + )} + /> +
+
+ +
+ {index === fields.length - 1 && ( + + )} + {index !== 0 && ( + remove(index)} + className="cursor-pointer mb-2 w-[20px] h-[20px]text-center rounded-full flex justify-center items-center" + > + - + + )} +
+
+
+ ))} +
+ + +
+
+ ); +} diff --git a/components/form/article/generate-rewrite-form.tsx b/components/form/article/generate-rewrite-form.tsx new file mode 100644 index 0000000..8466e6e --- /dev/null +++ b/components/form/article/generate-rewrite-form.tsx @@ -0,0 +1,273 @@ +"use client"; +import { + Button, + Input, + Select, + SelectItem, + SelectSection, + Textarea, +} from "@nextui-org/react"; +import { FormEvent, useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { close, error, loading } from "@/config/swal"; +import { getGenerateRewriter } from "@/service/generate-article"; +import TranscriptDraftTable from "@/components/table/disestages/transcript-draft-table"; +import ArticleDraftTable from "@/components/table/disestages/article-draft-table"; + +const writingStyle = [ + { + id: 1, + name: "Friendly", + }, + { + id: 1, + name: "Professional", + }, + { + id: 3, + name: "Informational", + }, + { + id: 4, + name: "Neutral", + }, + { + id: 5, + name: "Witty", + }, +]; + +const articleSize = [ + { + id: 1, + name: "News (300 - 900 words)", + value: "News", + }, + { + id: 2, + name: "Info (900 - 2000 words)", + value: "Info", + }, + { + id: 3, + name: "Detail (2000 - 5000 words)", + value: "Detail", + }, +]; + +const formSchema = z.object({ + field1: z.string().min(2, { + message: "Required", + }), + advancedConfiguration: z.string().optional(), +}); + +export default function GenerateRewriteArticle(props: { + articleId: (data: number) => void; + initTranscript?: number; +}) { + const [selectedWritingSyle, setSelectedWritingStyle] = + useState("Informational"); + const [selectedArticleSize, setSelectedArticleSize] = useState("News"); + const [selectedLanguage, setSelectedLanguage] = useState("id"); + const [contextType, setContextType] = useState("text"); + + useEffect(() => { + if (props.initTranscript) { + setContextType("transcript"); + } + }, [props.initTranscript]); + + const formOptions = { resolver: zodResolver(formSchema) }; + type UserSettingSchema = z.infer; + const { + control, + handleSubmit, + formState: { errors }, + setValue, + } = useForm(formOptions); + + const onSubmit = async (values: z.infer) => { + loading(); + const request = { + advConfig: values.advancedConfiguration || "", + context: contextType == "url" ? null : values.field1, + style: selectedWritingSyle, + sentiment: "Humorous", + clientId: "humasClientIdtest", + createdBy: "123123", + contextType: contextType, + urlContext: contextType === "url" ? values.field1 : null, + lang: selectedLanguage, + }; + const res = await getGenerateRewriter(request); + close(); + if (res?.error) { + error("Error"); + } + props.articleId(res?.data?.data?.id); + }; + + useEffect(() => { + setValue("field1", ""); + }, [contextType]); + return ( +
+
+
+ + + + +
+
+ {(contextType === "text" || contextType === "url") && ( +

+ {contextType === "text" ? "Enter your text here" : "Insert URL"} +

+ )} + {(contextType === "text" || contextType === "url") && ( + + contextType === "text" ? ( +