feat:generate article form
This commit is contained in:
parent
6931edf52d
commit
922ddcc828
|
|
@ -0,0 +1,10 @@
|
|||
import GenerateArticleForm from "@/components/form/article/generate-article-form";
|
||||
import { Card } from "@nextui-org/react";
|
||||
|
||||
export default function GenerateArticle() {
|
||||
return (
|
||||
<Card className="rounded-md border bg-transparent">
|
||||
<GenerateArticleForm />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,13 +8,19 @@ export default function BasicPage() {
|
|||
return (
|
||||
<div className="overflow-x-hidden overflow-y-scroll rounded-lg border-2">
|
||||
<div className="px-2 md:px-4 w-full">
|
||||
<div className="rounded-md my-5 px-5 py-2 bg-white dark:bg-[#18181b]">
|
||||
<div className="rounded-md my-5 px-5 py-2 bg-white dark:bg-[#18181b] flex flex-row gap-3">
|
||||
<Link href="/admin/article/create">
|
||||
<Button size="md" color="primary" className="w-min">
|
||||
<AddIcon />
|
||||
New Article
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/admin/article/generate">
|
||||
<Button size="md" color="primary" className="w-min">
|
||||
<AddIcon />
|
||||
Generate Article
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-[#18181b] rounded-xl my-5 p-2">
|
||||
<ArticleTable />
|
||||
|
|
|
|||
|
|
@ -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<string>("");
|
||||
const [article, setArticle] = React.useState<Selection>(new Set([]));
|
||||
const [slug, setSlug] = useState<string>("");
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [newTags, setNewTags] = useState<string>("");
|
||||
const editor = useRef(null);
|
||||
const [content, setContent] = useState("");
|
||||
const MySwal = withReactContent(Swal);
|
||||
const [selectedImages, setSelectedImages] = useState<File[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<any>();
|
||||
const [selectedContentType, setSelectedContentType] =
|
||||
useState("single-article");
|
||||
|
||||
const [collectSingleArticleId, setCollectSingleArticleId] = useState<
|
||||
number[]
|
||||
>([]);
|
||||
const [collectBulkArticleId, setCollectBulkArticleId] = useState<number[]>(
|
||||
[]
|
||||
);
|
||||
const [collectRewriteArticleId, setCollectRewriteArticleId] = useState<
|
||||
number[]
|
||||
>([]);
|
||||
|
||||
const [generatedTranscriptId, setGeneratedTranscriptId] = useState<number>();
|
||||
|
||||
const generatedArticleIdStore = generatedArticleIds(
|
||||
(state) => state.articleIds
|
||||
);
|
||||
const setGeneratedArticleIdStore = generatedArticleIds(
|
||||
(state) => state.setArticleIds
|
||||
);
|
||||
|
||||
const [selectedGeneratedArticleId, setSelectedGeneratedArticleId] =
|
||||
useState<number>();
|
||||
|
||||
const formOptions = { resolver: zodResolver(articleSchema) };
|
||||
type MicroIssueSchema = z.infer<typeof articleSchema>;
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<MicroIssueSchema>(formOptions);
|
||||
|
||||
const TypeId = [
|
||||
{
|
||||
key: 1,
|
||||
label: "Article",
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
label: "Magazine",
|
||||
},
|
||||
];
|
||||
|
||||
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="mx-5 my-5 overflow-y-auto">
|
||||
<div>
|
||||
<Card className="rounded-md p-5 space-y-5">
|
||||
<div>
|
||||
<Input
|
||||
type="title"
|
||||
{...register("title")}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
label="Judul"
|
||||
variant="bordered"
|
||||
placeholder="Enter Text"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{title.length === 0 && errors.title && errors.title.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
label="Jenis Artikel"
|
||||
{...register("article")}
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder="Select"
|
||||
selectedKeys={article}
|
||||
className="max-w-xs"
|
||||
onSelectionChange={setArticle}
|
||||
>
|
||||
{TypeId.map((data) => (
|
||||
<SelectItem key={data.key} value={data.key}>
|
||||
{data.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<div className="text-sm text-red-500">
|
||||
{errors.article?.message}
|
||||
</div>
|
||||
{/* <p>{article}</p> */}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm mb-1">Category</p>
|
||||
<ReactSelect
|
||||
className="basic-single text-black z-50"
|
||||
classNames={{
|
||||
control: (state: any) =>
|
||||
"!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}
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{(!selectedCategory || selectedCategory?.length < 1) && (
|
||||
<p>Required</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
{...register("slug")}
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value)}
|
||||
label="Slug"
|
||||
variant="bordered"
|
||||
placeholder="Enter Text"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{slug.length === 0 && errors.slug && errors.slug.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label="Tags (Optional)"
|
||||
{...register("tags")}
|
||||
labelPlacement="outside"
|
||||
type="text"
|
||||
value={newTags}
|
||||
onChange={(e) => setNewTags(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Tambahkan tag baru dan tekan Enter"
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{tags.length === 0 && errors.tags && errors.tags.message}
|
||||
</div>
|
||||
<div className="flex gap-2 border border-inherit mt-2 rounded-md p-1 items-center h-11">
|
||||
{tags.map((tag, index) => (
|
||||
<Chip
|
||||
color="primary"
|
||||
key={index}
|
||||
onClose={() => handleClose(tag)}
|
||||
>
|
||||
{tag}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
label="Content Type"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedContentType]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedContentType(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full md:w-1/4 py-4"
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="single-article">Single Article</SelectItem>
|
||||
<SelectItem key="bulk-article">Bulk Article</SelectItem>
|
||||
<SelectItem key="rewrite">Content Rewrite</SelectItem>
|
||||
<SelectItem key="speech-to-text">Speech to Text</SelectItem>
|
||||
</SelectSection>
|
||||
</Select>
|
||||
{selectedContentType === "single-article" ? (
|
||||
<div className="flex flex-col">
|
||||
<GenerateSingleArticle
|
||||
articleId={(data) => singleArticleId(data)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{collectSingleArticleId.length > 0 &&
|
||||
collectSingleArticleId.map((data) => (
|
||||
<Button
|
||||
key={data}
|
||||
color={
|
||||
selectedGeneratedArticleId === data
|
||||
? "primary"
|
||||
: "warning"
|
||||
}
|
||||
onPress={() => setSelectedGeneratedArticleId(data)}
|
||||
>
|
||||
{data}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{selectedGeneratedArticleId &&
|
||||
collectSingleArticleId.includes(selectedGeneratedArticleId) &&
|
||||
(selectedGeneratedArticleId && content !== "" ? (
|
||||
<div>
|
||||
<p className="pb-2">Description</p>
|
||||
<JoditEditor
|
||||
ref={editor}
|
||||
value={content}
|
||||
onChange={(newContent) => setContent(newContent)}
|
||||
className="dark:text-black"
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{content?.length === 0 &&
|
||||
errors?.description &&
|
||||
errors?.description?.message}
|
||||
</div>
|
||||
</div>
|
||||
) : selectedGeneratedArticleId && content == "" ? (
|
||||
<Spinner size="lg" />
|
||||
) : collectSingleArticleId.length > 0 ? (
|
||||
<p className="text-lg">Select Article ID</p>
|
||||
) : (
|
||||
""
|
||||
))}
|
||||
</div>
|
||||
) : selectedContentType === "bulk-article" ? (
|
||||
<div className="flex flex-col">
|
||||
<GenerateBulkArticle articleId={(data) => bulkArticleId(data)} />
|
||||
<div className="flex flex-wrap gap-3 mt-3">
|
||||
{collectBulkArticleId.length > 0 &&
|
||||
collectBulkArticleId.map((data) => (
|
||||
<Button
|
||||
key={data}
|
||||
color={
|
||||
selectedGeneratedArticleId === data
|
||||
? "primary"
|
||||
: "warning"
|
||||
}
|
||||
onPress={() => setSelectedGeneratedArticleId(data)}
|
||||
>
|
||||
{data}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{selectedGeneratedArticleId &&
|
||||
collectBulkArticleId.includes(selectedGeneratedArticleId) &&
|
||||
(selectedGeneratedArticleId && content !== "" ? (
|
||||
<div>
|
||||
<p className="pb-2">Description</p>
|
||||
<JoditEditor
|
||||
ref={editor}
|
||||
value={content}
|
||||
onChange={(newContent) => setContent(newContent)}
|
||||
className="dark:text-black"
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{content?.length === 0 &&
|
||||
errors?.description &&
|
||||
errors?.description?.message}
|
||||
</div>
|
||||
</div>
|
||||
) : selectedGeneratedArticleId && content == "" ? (
|
||||
<Spinner size="lg" />
|
||||
) : collectSingleArticleId.length > 0 ? (
|
||||
<p className="text-lg">Select Article ID</p>
|
||||
) : (
|
||||
""
|
||||
))}
|
||||
</div>
|
||||
) : selectedContentType === "rewrite" ? (
|
||||
<div className="flex flex-col">
|
||||
<GenerateRewriteArticle
|
||||
articleId={(data) => rewriteArticleId(data)}
|
||||
initTranscript={generatedTranscriptId}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-3 mt-3">
|
||||
{collectRewriteArticleId.length > 0 &&
|
||||
collectRewriteArticleId.map((data) => (
|
||||
<Button
|
||||
key={data}
|
||||
color={
|
||||
selectedGeneratedArticleId === data
|
||||
? "primary"
|
||||
: "warning"
|
||||
}
|
||||
onPress={() => setSelectedGeneratedArticleId(data)}
|
||||
>
|
||||
{data}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{selectedGeneratedArticleId &&
|
||||
collectRewriteArticleId.includes(selectedGeneratedArticleId) &&
|
||||
(selectedGeneratedArticleId && content !== "" ? (
|
||||
<div>
|
||||
<p className="pb-2">Description</p>
|
||||
<JoditEditor
|
||||
ref={editor}
|
||||
value={content}
|
||||
onChange={(newContent) => setContent(newContent)}
|
||||
className="dark:text-black"
|
||||
/>
|
||||
<div className="text-sm text-red-500">
|
||||
{content?.length === 0 &&
|
||||
errors?.description &&
|
||||
errors?.description?.message}
|
||||
</div>
|
||||
</div>
|
||||
) : selectedGeneratedArticleId && content == "" ? (
|
||||
<Spinner size="lg" />
|
||||
) : collectRewriteArticleId.length > 0 ? (
|
||||
<p className="text-lg">Select Article ID</p>
|
||||
) : (
|
||||
""
|
||||
))}
|
||||
</div>
|
||||
) : selectedContentType === "speech-to-text" ? (
|
||||
<SpeechToText
|
||||
generateArticleFromTranscript={(transcriptId) => {
|
||||
setGeneratedTranscriptId(transcriptId);
|
||||
setSelectedContentType("rewrite");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p>Attachment (Opsional)</p>
|
||||
<div className="flex items-center justify-center w-full pt-2 ">
|
||||
<label
|
||||
htmlFor="dropzone-file"
|
||||
className="flex flex-col items-center justify-center w-full h-36 border border-gray-100 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-200"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6 ">
|
||||
<svg
|
||||
className="w-10 h-10 mb-3 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
></path>
|
||||
</svg>
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Drag and drop files here
|
||||
</p>
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{/* or{" "} */}
|
||||
<span className="font-semibold underline text-amber-800">
|
||||
Click to upload
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="dropzone-file"
|
||||
type="file"
|
||||
onChange={handleImageChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{selectedImages?.length > 0 ? (
|
||||
<div>
|
||||
<h4>Pratinjau:</h4>
|
||||
<div className="flex gap-2 pt-2">
|
||||
{selectedImages.map((image, index) => (
|
||||
<div key={index} className="flex flex-col items-end">
|
||||
<Chip
|
||||
color="danger"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleRemoveImage(index)}
|
||||
>
|
||||
X
|
||||
</Chip>
|
||||
<img
|
||||
src={URL.createObjectURL(image)}
|
||||
alt="Pratinjau Gambar"
|
||||
style={{ maxWidth: "200px", maxHeight: "200px" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Link href={`/admin/article`}>
|
||||
<Button color="danger" variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
</Link>
|
||||
<Button type="button" color="primary" variant="solid">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<typeof formSchema>;
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues,
|
||||
trigger,
|
||||
} = useForm<UserSettingSchema>(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 (
|
||||
<fieldset>
|
||||
<form
|
||||
className="flex flex-col gap-2 w-full"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
|
||||
<Select
|
||||
label="Writing Style"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedWritingSyle]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedWritingStyle(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name}>{style.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Article Size"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedArticleSize]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedArticleSize(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
{articleSize.map((size) => (
|
||||
<SelectItem key={size.value}>{size.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Language"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedLanguage]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== "" ? setSelectedLanguage(e.target.value) : ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="id">Indonesia</SelectItem>
|
||||
<SelectItem key="en">English</SelectItem>
|
||||
</SelectSection>
|
||||
</Select>
|
||||
</div>
|
||||
<div className={`flex flex-col md:gap-3 `}>
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className={` grid grid-cols-1 md:grid-cols-3 gap-5 w-full ${
|
||||
fields.length === 1
|
||||
? ""
|
||||
: "border-b-2 md:border-none py-3 md:py-1"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="text-[10px] md:text-base flex items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<p className="text-sm">Main Keyword</p>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (index === 0) {
|
||||
processAll();
|
||||
} else {
|
||||
processRow(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Process {index === 0 ? "All" : ""}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-[42px] flex flex-row gap-1 md:hidden">
|
||||
{index === fields.length - 1 && (
|
||||
<button
|
||||
className="w-[20px] mb-2 "
|
||||
type="button"
|
||||
onClick={() =>
|
||||
append({
|
||||
mainKeyword: "",
|
||||
title: "",
|
||||
additionalKeyword: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<a
|
||||
onClick={() => remove(index)}
|
||||
className="cursor-pointer mb-2 w-[20px] h-[20px]text-center rounded-full flex justify-center items-center"
|
||||
>
|
||||
-
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`rows.${index}.mainKeyword`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="mainKeyword"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
isDisabled={value === "process..."}
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] md:text-base flex items-center gap-1">
|
||||
<p className="text-sm">Title</p>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
generateTitle(
|
||||
getValues(`rows.${index}.mainKeyword`),
|
||||
index
|
||||
)
|
||||
}
|
||||
>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`rows.${index}.title`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
isDisabled={value === "process..."}
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-row items-end gap-1">
|
||||
<div className="full grow">
|
||||
<div>
|
||||
<div className="text-[10px] md:text-base flex items-center gap-1">
|
||||
<p className="text-sm">SEO</p>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
generateKeywords(
|
||||
getValues(`rows.${index}.mainKeyword`),
|
||||
index
|
||||
)
|
||||
}
|
||||
>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`rows.${index}.additionalKeyword`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
isDisabled={value === "process..."}
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[42px] flex-row gap-1 hidden md:flex">
|
||||
{index === fields.length - 1 && (
|
||||
<button
|
||||
className="w-[20px] mb-2 "
|
||||
type="button"
|
||||
onClick={() =>
|
||||
append({
|
||||
mainKeyword: "",
|
||||
title: "",
|
||||
additionalKeyword: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<a
|
||||
onClick={() => remove(index)}
|
||||
className="cursor-pointer mb-2 w-[20px] h-[20px]text-center rounded-full flex justify-center items-center"
|
||||
>
|
||||
-
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => validateFields()}
|
||||
// disabled={validateFields()}
|
||||
color="primary"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<typeof formSchema>;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useForm<UserSettingSchema>(formOptions);
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
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 (
|
||||
<fieldset>
|
||||
<form
|
||||
className="flex flex-col gap-2 w-full"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-5 w-full">
|
||||
<Select
|
||||
label="Context Type"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[contextType]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== "" ? setContextType(e.target.value) : ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="text">Text</SelectItem>
|
||||
<SelectItem key="article">Article</SelectItem>
|
||||
<SelectItem key="transcript">Transcript</SelectItem>
|
||||
<SelectItem key="url">URL</SelectItem>
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Writing Style"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedWritingSyle]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedWritingStyle(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name}>{style.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Article Size"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedArticleSize]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedArticleSize(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
{articleSize.map((size) => (
|
||||
<SelectItem key={size.value}>{size.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Language"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedLanguage]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== "" ? setSelectedLanguage(e.target.value) : ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="id">Indonesia</SelectItem>
|
||||
<SelectItem key="en">English</SelectItem>
|
||||
</SelectSection>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 mt-3">
|
||||
{(contextType === "text" || contextType === "url") && (
|
||||
<p className="text-sm">
|
||||
{contextType === "text" ? "Enter your text here" : "Insert URL"}
|
||||
</p>
|
||||
)}
|
||||
{(contextType === "text" || contextType === "url") && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="field1"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
contextType === "text" ? (
|
||||
<Textarea
|
||||
label=""
|
||||
id="adv_config"
|
||||
variant="bordered"
|
||||
disableAnimation
|
||||
className="pb-3"
|
||||
placeholder="Type your custom instruction here!"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type="text"
|
||||
id="mainKeyword"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
variant="bordered"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{errors.field1?.message && (
|
||||
<p className="text-red-400 text-sm">{errors.field1?.message}</p>
|
||||
)}
|
||||
|
||||
{contextType === "article" && (
|
||||
<>
|
||||
<ArticleDraftTable
|
||||
articleContent={(data) => setValue("field1", data)}
|
||||
/>
|
||||
{errors.field1?.message && (
|
||||
<p className="text-red-400 text-sm">Select Transcript</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{contextType === "transcript" && (
|
||||
<>
|
||||
<TranscriptDraftTable
|
||||
transriptContent={(data) => setValue("field1", data)}
|
||||
initTranscriptId={String(props.initTranscript)}
|
||||
/>
|
||||
{errors.field1?.message && (
|
||||
<p className="text-red-400 text-sm">Select Transcript</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
className="my-5 w-full py-5 text-xs md:text-base"
|
||||
type="submit"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
"use client";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
SelectSection,
|
||||
} from "@nextui-org/react";
|
||||
import { FormEvent, 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 {
|
||||
generateDataArticle,
|
||||
getGenerateKeywords,
|
||||
getGenerateTitle,
|
||||
} 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 singleArticleSchema = z.object({
|
||||
mainKeyword: z.string().min(2, {
|
||||
message: "Main Keyword must be at least 2 characters.",
|
||||
}),
|
||||
title: z.string().min(2, {
|
||||
message: "Main Keyword must be at least 2 characters.",
|
||||
}),
|
||||
additionalKeyword: z.string().min(2, {
|
||||
message: "Main Keyword must be at least 2 characters.",
|
||||
}),
|
||||
});
|
||||
|
||||
export default function GenerateSingleArticle(props: {
|
||||
articleId: (data: number) => void;
|
||||
}) {
|
||||
const [selectedWritingSyle, setSelectedWritingStyle] =
|
||||
useState("Informational");
|
||||
const [selectedArticleSize, setSelectedArticleSize] = useState("News");
|
||||
const [selectedLanguage, setSelectedLanguage] = useState("id");
|
||||
|
||||
const formOptions = { resolver: zodResolver(singleArticleSchema) };
|
||||
type UserSettingSchema = z.infer<typeof singleArticleSchema>;
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues,
|
||||
} = useForm<UserSettingSchema>(formOptions);
|
||||
|
||||
const generateAll = async (keyword: string | undefined) => {
|
||||
if (keyword) {
|
||||
generateTitle(keyword);
|
||||
generateKeywords(keyword);
|
||||
}
|
||||
};
|
||||
|
||||
const generateTitle = async (keyword: string | undefined) => {
|
||||
if (keyword) {
|
||||
loading();
|
||||
const req = {
|
||||
keyword: keyword,
|
||||
style: selectedWritingSyle,
|
||||
website: "None",
|
||||
connectToWeb: true,
|
||||
lang: selectedLanguage,
|
||||
pointOfView: "None",
|
||||
clientId: "",
|
||||
};
|
||||
const res = await getGenerateTitle(req);
|
||||
const data = res?.data?.data;
|
||||
setValue("title", data);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const generateKeywords = async (keyword: string | undefined) => {
|
||||
if (keyword) {
|
||||
const req = {
|
||||
keyword: keyword,
|
||||
style: selectedWritingSyle,
|
||||
website: "None",
|
||||
connectToWeb: true,
|
||||
lang: selectedLanguage,
|
||||
pointOfView: "0",
|
||||
clientId: "",
|
||||
};
|
||||
loading();
|
||||
const res = await getGenerateKeywords(req);
|
||||
const data = res?.data?.data;
|
||||
setValue("additionalKeyword", data);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof singleArticleSchema>) => {
|
||||
loading();
|
||||
const request = {
|
||||
advConfig: "",
|
||||
style: selectedWritingSyle,
|
||||
website: "None",
|
||||
connectToWeb: true,
|
||||
lang: selectedLanguage,
|
||||
pointOfView: "None",
|
||||
title: values.title,
|
||||
imageSource: "Web",
|
||||
mainKeyword: values.mainKeyword,
|
||||
additionalKeywords: values.additionalKeyword,
|
||||
targetCountry: null,
|
||||
articleSize: selectedArticleSize,
|
||||
projectId: 2,
|
||||
createdBy: "123123",
|
||||
clientId: "humasClientIdtest",
|
||||
};
|
||||
console.log("reqq", request);
|
||||
const res = await generateDataArticle(request);
|
||||
close();
|
||||
console.log("res", res?.data?.data);
|
||||
if (res?.error) {
|
||||
error("Error");
|
||||
}
|
||||
props.articleId(res?.data?.data?.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<form
|
||||
className="flex flex-col gap-2 w-full"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
|
||||
<Select
|
||||
label="Writing Style"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedWritingSyle]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedWritingStyle(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name}>{style.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Article Size"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedArticleSize]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== ""
|
||||
? setSelectedArticleSize(e.target.value)
|
||||
: ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
{articleSize.map((size) => (
|
||||
<SelectItem key={size.value}>{size.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select>
|
||||
<Select
|
||||
label="Language"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedLanguage]}
|
||||
onChange={(e) =>
|
||||
e.target.value !== "" ? setSelectedLanguage(e.target.value) : ""
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="id">Indonesia</SelectItem>
|
||||
<SelectItem key="en">English</SelectItem>
|
||||
</SelectSection>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 mt-3">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-sm">Main Keyword</p>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onPress={() => generateAll(getValues("mainKeyword"))}
|
||||
>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="mainKeyword"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="mainKeyword"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.mainKeyword && (
|
||||
<p className="text-red-400 text-sm">
|
||||
{errors.mainKeyword?.message}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-sm">Title</p>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onPress={() => generateAll(getValues("mainKeyword"))}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="text-red-400 text-sm">{errors.title?.message}</p>
|
||||
)}
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-sm">Additional Keyword</p>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onPress={() => generateAll(getValues("mainKeyword"))}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="additionalKeyword"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="additionalKeyword"
|
||||
placeholder=""
|
||||
label=""
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labelPlacement="outside"
|
||||
className="w-full"
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.additionalKeyword && (
|
||||
<p className="text-red-400 text-sm">
|
||||
{errors.additionalKeyword?.message}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
color="primary"
|
||||
className="my-5 w-full py-5 text-xs md:text-base"
|
||||
type="submit"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
"use client";
|
||||
import { AddIcon, TimesIcon } from "@/components/icons";
|
||||
import { Input, Textarea } from "@nextui-org/input";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Select,
|
||||
SelectItem,
|
||||
SelectSection,
|
||||
} from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useState } from "react";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Cookies from "js-cookie";
|
||||
import { close, error, loading } from "@/config/swal";
|
||||
import {
|
||||
generateSpeechToText,
|
||||
getTranscriptById,
|
||||
} from "@/service/generate-article";
|
||||
import { delay } from "@/utils/global";
|
||||
|
||||
// import OperatorArticleTable from "@/components/table/assistant/create-content/operator/operator-article-table";
|
||||
// import OperatorTranscriptTable from "@/components/table/assistant/create-content/operator/operator-transcript-table";
|
||||
// import OperatorArticleDraftTable from "@/components/table/assistant/create-content/operator/operator-draft-article-table";
|
||||
// import OperatorDetailDraftArticle from "@/components/main/multipool-assistant/operator-detail-draft-article";
|
||||
|
||||
const createContentOnlineMediaSchema = z.object({
|
||||
title: z.string().min(1, { message: "Required" }),
|
||||
prompt: z.string().min(1, { message: "Required" }),
|
||||
});
|
||||
|
||||
export default function SpeechToText(props: {
|
||||
generateArticleFromTranscript: (transcriptId: number) => void;
|
||||
}) {
|
||||
const searchParam = useSearchParams();
|
||||
const transcriptQuery = searchParam.get("transcriptId");
|
||||
const roleId = Cookies.get("urie");
|
||||
|
||||
let [files, setFiles] = useState<File[]>([]);
|
||||
const [handleFileStatus, setHandleFileStatus] = useState("");
|
||||
const [detailTranscript, setDetailTranscript] = useState<string>("");
|
||||
|
||||
const [generatedTranscriptId, setGeneratedTranscriptId] = useState<number>();
|
||||
const [advanceConfig, setAdvanceConfig] = useState(false);
|
||||
const [language, setLanguage] = useState("id");
|
||||
|
||||
const formOptions = { resolver: zodResolver(createContentOnlineMediaSchema) };
|
||||
type CreateContentSchema = z.infer<typeof createContentOnlineMediaSchema>;
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<CreateContentSchema>(formOptions);
|
||||
|
||||
const handleFileChange = (event: any) => {
|
||||
if (files.length < 1) {
|
||||
const newFiles: FileList | null = event.target.files;
|
||||
if (newFiles) {
|
||||
const allowedExtensions = [".mp3"];
|
||||
let temp: File[] = [...files]; // Salin file-file yang sudah ada
|
||||
for (let i = 0; i < newFiles.length; i++) {
|
||||
const file = newFiles[i];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
if (
|
||||
fileExtension &&
|
||||
allowedExtensions.includes(`.${fileExtension}`)
|
||||
) {
|
||||
temp.push(file);
|
||||
} else {
|
||||
alert("The supported File formats are .mp3");
|
||||
}
|
||||
}
|
||||
setFiles(temp);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (name: string) => {
|
||||
const arrayFile: File[] = [];
|
||||
for (const element of files) {
|
||||
if (element.name !== name) {
|
||||
arrayFile.push(element);
|
||||
}
|
||||
}
|
||||
setFiles(arrayFile);
|
||||
};
|
||||
|
||||
const handleGenerateTranscript = (data: any) => {
|
||||
if (files.length > 0) {
|
||||
doGenerate(data);
|
||||
setHandleFileStatus("");
|
||||
} else {
|
||||
setHandleFileStatus("Required");
|
||||
}
|
||||
};
|
||||
|
||||
const doGenerate = async (data: any) => {
|
||||
loading();
|
||||
let formData = new FormData();
|
||||
console.log("data.", data.prompt);
|
||||
formData.append("filename", files[0]);
|
||||
formData.append("title", data.title);
|
||||
formData.append("prompt", data.prompt);
|
||||
formData.append("language", language);
|
||||
formData.append("clientId", "humasClientIdTest");
|
||||
formData.append("createdBy", "123");
|
||||
|
||||
const res = await generateSpeechToText(formData);
|
||||
setGeneratedTranscriptId(res?.data?.data?.id);
|
||||
close();
|
||||
initFetch(res?.data?.data?.id);
|
||||
return false;
|
||||
};
|
||||
|
||||
const checkTranscriptStatus = async (id: number) => {
|
||||
delay(7000).then(() => {
|
||||
initFetch(id);
|
||||
});
|
||||
};
|
||||
|
||||
const initFetch = async (id?: number) => {
|
||||
if (id) {
|
||||
const res = await getTranscriptById(id);
|
||||
console.log("stat", res?.data?.data);
|
||||
if (res?.data?.data?.status === 0) {
|
||||
setDetailTranscript("");
|
||||
checkTranscriptStatus(id);
|
||||
} else {
|
||||
setDetailTranscript(res?.data?.data.content);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="w-full">
|
||||
<div className="w-full text-sm">
|
||||
<form method="POST" onSubmit={handleSubmit(handleGenerateTranscript)}>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold">Upload an Audio File</p>
|
||||
<p>The supported File formats are .mp3</p>
|
||||
<p>Max: 25mb</p>
|
||||
{files.length < 1 && (
|
||||
<label htmlFor="dropzone-file">
|
||||
<div className="mt-3 w-[150px] flex justify-center items-center bg-success p-2 text-white rounded-lg gap-2 cursor-pointer">
|
||||
<input
|
||||
id="dropzone-file"
|
||||
type="file"
|
||||
multiple
|
||||
accept=".mp3"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
Add file <AddIcon />
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
{handleFileStatus !== "" && files.length < 1 && (
|
||||
<p className="text-tiny text-danger">{handleFileStatus}</p>
|
||||
)}
|
||||
|
||||
{files.length > 0 &&
|
||||
files.map((list) => (
|
||||
<div
|
||||
key={list.name}
|
||||
className="w-[350px] mt-3 flex flex-row items-center justify-between border-1 rounded-lg p-2"
|
||||
>
|
||||
{list.name}
|
||||
<a
|
||||
className="cursor-pointer"
|
||||
onClick={() => removeFile(list.name)}
|
||||
>
|
||||
<TimesIcon />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="w-full md:w-1/2 pt-2 pb-4">
|
||||
<p className="text-black text-sm mb-1">Language</p>
|
||||
<Select
|
||||
label=""
|
||||
labelPlacement="outside"
|
||||
selectedKeys={[language]}
|
||||
className="w-full"
|
||||
variant="bordered"
|
||||
onChange={(e) =>
|
||||
e.target.value == "" ? "" : setLanguage(e.target.value)
|
||||
}
|
||||
renderValue={(items) => {
|
||||
return items.map((item) => (
|
||||
<span
|
||||
key={item.props?.value}
|
||||
className="text-black text-xs"
|
||||
>
|
||||
{item.textValue}
|
||||
</span>
|
||||
));
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="id">Indonesia</SelectItem>
|
||||
<SelectItem key="en">English</SelectItem>
|
||||
</SelectSection>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2 mt-3">
|
||||
<p className="text-black text-sm mb-1">
|
||||
Title<span className="text-red-500">*</span>
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
id="keyword"
|
||||
label=""
|
||||
className="mb-3"
|
||||
placeholder="Enter title"
|
||||
labelPlacement="outside"
|
||||
variant="bordered"
|
||||
{...register("title")}
|
||||
/>
|
||||
{errors.title?.message && (
|
||||
<p className="text-tiny text-danger">
|
||||
{errors.title?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/2 mt-3">
|
||||
<p className="text-black text-sm mb-1">
|
||||
Keyword<span className="text-red-500">*</span>
|
||||
</p>
|
||||
<Textarea
|
||||
label=""
|
||||
id="seo"
|
||||
variant="bordered"
|
||||
disableAnimation
|
||||
disableAutosize
|
||||
minRows={10}
|
||||
placeholder="Write your text"
|
||||
{...register("prompt")}
|
||||
/>
|
||||
{errors.prompt?.message && (
|
||||
<p className="text-tiny text-danger">
|
||||
{errors.prompt?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-3">
|
||||
<Button className="w-[200px]" color="primary" type="submit">
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{generatedTranscriptId && (
|
||||
<div className="flex flex-col gap-3 my-3">
|
||||
<p className="text-xl font-semibold">Result</p>
|
||||
{detailTranscript === "" ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<p>{detailTranscript}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{detailTranscript !== "" && (
|
||||
<Button
|
||||
className="w-[200px]"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
generatedTranscriptId &&
|
||||
props.generateArticleFromTranscript(generatedTranscriptId);
|
||||
}}
|
||||
>
|
||||
Generate Article
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -346,6 +346,7 @@ export default function FormArticle() {
|
|||
<input
|
||||
id="dropzone-file"
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export const AdminLayout = ({ children }: Props) => {
|
|||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex items-center justify-between h-screen p-1 md:p-4">
|
||||
<div className="h-screen flex items-center flex-row p-1 md:p-4">
|
||||
<Sidebar sidebarData={isOpen} updateSidebarData={updateSidebarData} />
|
||||
<div className={`h-full w-full flex flex-col gap-4`}>
|
||||
<div className={`w-full h-full flex flex-col gap-4`}>
|
||||
<Breadcrumb />
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -221,15 +221,16 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`hidden md:flex h-full ${
|
||||
className={`hidden md:flex h-full flex-grow ${
|
||||
isOpen ? "min-w-[290px]" : "min-w-[80px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`will-change relative flex h-full flex-col rounded-lg p-4 mb-0 bg-gray-100 dark:bg-stone-950 z-40 transition-width !ease-in-out ${
|
||||
className={` flex h-full flex-col rounded-lg p-4 mb-0 bg-gray-100 dark:bg-stone-950 z-40 transition-width !ease-in-out justify-between ${
|
||||
isOpen ? "w-[288px]" : "w-[80px]"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
{!isOpen && (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<button
|
||||
|
|
@ -245,10 +246,13 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
|
|||
isOpen ? "justify-between" : "justify-center"
|
||||
} w-full items-center px-2`}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-3 font-bold">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex flex-row items-center gap-3 font-bold"
|
||||
>
|
||||
<img src="/logohumas.png" className="w-20" />
|
||||
{/* {isOpen && <span>ACME</span>} */}
|
||||
</div>
|
||||
</Link>
|
||||
{isOpen && (
|
||||
<button
|
||||
className="w-5 h-5 text-zinc-400 dark:text-zinc-400 z-50 border-1 border-zinc-400 rounded-full flex justify-center items-center"
|
||||
|
|
@ -258,50 +262,16 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className={`flex ${isOpen ? "justify-between" : "justify-center"} w-full items-center px-2 mt-4 mb-4`}>
|
||||
<div className='flex flex-row items-center gap-3 font-bold'>
|
||||
<Dropdown placement="bottom-start">
|
||||
<DropdownTrigger>
|
||||
<User
|
||||
as="button"
|
||||
avatarProps={{
|
||||
isBordered: true,
|
||||
src: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
}}
|
||||
classNames={{
|
||||
base: `transition-transform gap-0`,
|
||||
wrapper: `${isOpen && "pl-3"}`
|
||||
}}
|
||||
description={isOpen && "@tonyreichert"}
|
||||
name={isOpen && "Tony Reichert"}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="User Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-bold">Signed in as</p>
|
||||
<p className="font-bold">@tonyreichert</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="team_settings">Profile Settings</DropdownItem>
|
||||
<DropdownItem key="analytics">
|
||||
Analytics
|
||||
</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback">
|
||||
Help & Feedback
|
||||
</DropdownItem>
|
||||
<DropdownItem key="logout" color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<SidebarMenu>
|
||||
{sideBarDummyData
|
||||
? sideBarDummyData?.map((list: any, index: number) =>
|
||||
list.isGroup ? (
|
||||
<p
|
||||
key={list}
|
||||
className={`font-bold mr-4 ${!isOpen ? "text-center" : ""}`}
|
||||
className={`font-bold mr-4 ${
|
||||
!isOpen ? "text-center" : ""
|
||||
}`}
|
||||
>
|
||||
{isOpen ? list.name : "..."}
|
||||
</p>
|
||||
|
|
@ -352,7 +322,9 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
|
|||
key={item.id}
|
||||
title={item?.name}
|
||||
isActive={pathname.includes(item.modulePathUrl)}
|
||||
isParentActive={pathname.includes(list.modulePathUrl)}
|
||||
isParentActive={pathname.includes(
|
||||
list.modulePathUrl
|
||||
)}
|
||||
path={item.modulePathUrl}
|
||||
icon={item.icon}
|
||||
/>
|
||||
|
|
@ -363,6 +335,7 @@ const Sidebar: React.FC<SidebarProps> = ({ updateSidebarData }) => {
|
|||
)
|
||||
: ""}
|
||||
</SidebarMenu>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-12 p-2 flex ${
|
||||
isOpen ? "justify-start ml-2" : "justify-center"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
"use client";
|
||||
import {
|
||||
TableCell,
|
||||
TableRow,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
Pagination,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Input,
|
||||
} from "@nextui-org/react";
|
||||
import { Button } from "@nextui-org/button";
|
||||
import React, {
|
||||
Key,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { SearchIcon, UserIcon } from "@/components/icons";
|
||||
|
||||
import { close, loading } from "@/config/swal";
|
||||
|
||||
import {
|
||||
getListArticleDraft,
|
||||
getListTranscript,
|
||||
} from "@/service/generate-article";
|
||||
import { textEllipsis } from "@/utils/global";
|
||||
import { getListArticle } from "@/service/article";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Main Keyword", uid: "mainKeyword" },
|
||||
{ name: "Additional Keywords", uid: "additionalKeywords" },
|
||||
{ name: "Article", uid: "articleBody" },
|
||||
];
|
||||
|
||||
export default function ArticleDraftTable(props: {
|
||||
articleContent: (data: string) => void;
|
||||
}) {
|
||||
const [selectedTableRow, setSelectedTableRow] = useState<any>(new Set([""]));
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [dataTableTranscript, setDataTableTranscript] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
initFetch();
|
||||
}, [page]);
|
||||
|
||||
const initFetch = async () => {
|
||||
loading();
|
||||
const res = await getListArticleDraft({
|
||||
query: search,
|
||||
page: page,
|
||||
userId: "humasClientIdtest",
|
||||
limit: 10,
|
||||
status: [0, 2],
|
||||
isSingle: true,
|
||||
createdBy: "123123",
|
||||
sort: [
|
||||
{
|
||||
column: "id",
|
||||
sort: "desc",
|
||||
},
|
||||
],
|
||||
});
|
||||
close();
|
||||
setTotalPage(Math.ceil(res?.data?.total / 10));
|
||||
const data = res?.data?.data;
|
||||
const startIndex = 10 * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data?.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
setDataTableTranscript(newData);
|
||||
};
|
||||
|
||||
const renderCell = useCallback((article: any, columnKey: any) => {
|
||||
const cellValue = article[columnKey];
|
||||
|
||||
switch (columnKey) {
|
||||
case "articleBody":
|
||||
return <p>{textEllipsis(cellValue, 200)}</p>;
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelected = (e: any) => {
|
||||
setSelectedTableRow(new Set([e.currentKey]));
|
||||
const now = findDataTable(e.currentKey);
|
||||
props.articleContent(now?.articleBody ? now?.articleBody : "");
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
initFetch();
|
||||
};
|
||||
|
||||
const findDataTable = (data: any) => {
|
||||
const temp = dataTableTranscript.find((item: any) => item.id == data);
|
||||
return temp;
|
||||
};
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="mt-3 flex flex-row gap-3">
|
||||
<div className="w-full">
|
||||
<Input
|
||||
type="title"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
label=""
|
||||
variant="bordered"
|
||||
placeholder="Search"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
</div>
|
||||
<Button color="primary" onPress={handleSearch}>
|
||||
Search
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex flex-col items-center rounded-2xl">
|
||||
<Table
|
||||
selectionMode="multiple"
|
||||
aria-label="transcript-table"
|
||||
className="rounded-xl"
|
||||
classNames={{
|
||||
th: "bg-white text-black border-b-1",
|
||||
base: "bg-white",
|
||||
wrapper: "min-h-[50px] bg-transpararent text-black",
|
||||
}}
|
||||
checkboxesProps={{
|
||||
classNames: {
|
||||
wrapper:
|
||||
"after:bg-foreground after:text-background text-background",
|
||||
},
|
||||
}}
|
||||
selectedKeys={selectedTableRow}
|
||||
onSelectionChange={handleSelected}
|
||||
>
|
||||
<TableHeader columns={columns}>
|
||||
{(column) => (
|
||||
<TableColumn key={column.uid}>{column.name}</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
items={dataTableTranscript}
|
||||
emptyContent={"No data to display."}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-2">
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
"use client";
|
||||
import {
|
||||
TableCell,
|
||||
TableRow,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
Pagination,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Input,
|
||||
} from "@nextui-org/react";
|
||||
import { Button } from "@nextui-org/button";
|
||||
import React, {
|
||||
Key,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { SearchIcon, UserIcon } from "@/components/icons";
|
||||
|
||||
import { close, loading } from "@/config/swal";
|
||||
|
||||
import { getListTranscript } from "@/service/generate-article";
|
||||
import { textEllipsis } from "@/utils/global";
|
||||
|
||||
const columns = [
|
||||
{ name: "No", uid: "no" },
|
||||
{ name: "Title", uid: "title" },
|
||||
{ name: "Prompt", uid: "prompt" },
|
||||
{ name: "Content", uid: "content" },
|
||||
];
|
||||
|
||||
export default function TranscriptDraftTable(props: {
|
||||
initTranscriptId?: string;
|
||||
transriptContent: (data: string) => void;
|
||||
}) {
|
||||
const [selectedTableRow, setSelectedTableRow] = useState<any>(new Set([""]));
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
const [dataTableTranscript, setDataTableTranscript] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTableRow(new Set([props.initTranscriptId]));
|
||||
const now = findDataTable(Array.from(selectedTableRow)[0]);
|
||||
props.transriptContent(now?.content ? now?.content : "");
|
||||
}, [dataTableTranscript, props.initTranscriptId]);
|
||||
|
||||
useEffect(() => {
|
||||
initFetch();
|
||||
}, [page]);
|
||||
|
||||
const initFetch = async () => {
|
||||
loading();
|
||||
const res = await getListTranscript({
|
||||
query: search || "",
|
||||
page: page,
|
||||
clientId: "humasClientIdTest",
|
||||
limit: 10,
|
||||
createdBy: null,
|
||||
|
||||
status: [0, 2],
|
||||
sort: [
|
||||
{
|
||||
column: "id",
|
||||
sort: "desc",
|
||||
},
|
||||
],
|
||||
});
|
||||
close();
|
||||
setTotalPage(Math.ceil(res?.data?.total / 10));
|
||||
const data = res?.data?.data;
|
||||
const startIndex = 10 * (page - 1);
|
||||
let iterate = 0;
|
||||
const newData = data?.map((value: any) => {
|
||||
iterate++;
|
||||
value.no = startIndex + iterate;
|
||||
return value;
|
||||
});
|
||||
setDataTableTranscript(newData);
|
||||
};
|
||||
|
||||
const renderCell = useCallback((article: any, columnKey: any) => {
|
||||
const cellValue = article[columnKey];
|
||||
|
||||
switch (columnKey) {
|
||||
case "content":
|
||||
return <p>{textEllipsis(cellValue, 200)}</p>;
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelected = (e: any) => {
|
||||
setSelectedTableRow(new Set([e.currentKey]));
|
||||
const now = findDataTable(e.currentKey);
|
||||
props.transriptContent(now?.content ? now?.content : "");
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
initFetch();
|
||||
};
|
||||
|
||||
const findDataTable = (data: any) => {
|
||||
const temp = dataTableTranscript.find((item: any) => item.id == data);
|
||||
return temp;
|
||||
};
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="mt-3 flex flex-row gap-3">
|
||||
<div className="w-full">
|
||||
<Input
|
||||
type="title"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
label=""
|
||||
variant="bordered"
|
||||
placeholder="Search"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
</div>
|
||||
<Button color="primary" onPress={handleSearch}>
|
||||
Search
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex flex-col items-center rounded-2xl">
|
||||
<Table
|
||||
selectionMode="multiple"
|
||||
aria-label="transcript-table"
|
||||
className="rounded-xl"
|
||||
classNames={{
|
||||
th: "bg-white text-black border-b-1",
|
||||
base: "bg-white",
|
||||
wrapper: "min-h-[50px] bg-transpararent text-black",
|
||||
}}
|
||||
checkboxesProps={{
|
||||
classNames: {
|
||||
wrapper:
|
||||
"after:bg-foreground after:text-background text-background",
|
||||
},
|
||||
}}
|
||||
selectedKeys={selectedTableRow}
|
||||
onSelectionChange={handleSelected}
|
||||
>
|
||||
<TableHeader columns={columns}>
|
||||
{(column) => (
|
||||
<TableColumn key={column.uid}>{column.name}</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
items={dataTableTranscript}
|
||||
emptyContent={"No data to display."}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell>{renderCell(item, columnKey)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-2">
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
wrapper: "bg-transparent",
|
||||
}}
|
||||
page={page}
|
||||
total={totalPage}
|
||||
onChange={(page) => setPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ export const Breadcrumb = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-[100px] gap-0 grid rounded-lg border-small">
|
||||
<div className="h-[100px] rounded-lg border-small">
|
||||
<div className="px-4 md:px-8">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="pt-1">
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@
|
|||
"tailwind-variants": "^0.1.18",
|
||||
"tailwindcss": "3.3.5",
|
||||
"typescript": "5.0.4",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
|
@ -7290,6 +7291,34 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz",
|
||||
"integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
"tailwind-variants": "^0.1.18",
|
||||
"tailwindcss": "3.3.5",
|
||||
"typescript": "5.0.4",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
import { httpGet, httpPost } from "@/services/http-config/disestages-services";
|
||||
|
||||
interface GenerateKeywordsAndTitleRequestData {
|
||||
keyword: string;
|
||||
style: string;
|
||||
website: string;
|
||||
connectToWeb: boolean;
|
||||
lang: string;
|
||||
pointOfView: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
interface GenerateArticleRequest {
|
||||
advConfig: string;
|
||||
style: string;
|
||||
website: string;
|
||||
connectToWeb: boolean;
|
||||
lang: string;
|
||||
pointOfView: string;
|
||||
title: string;
|
||||
imageSource: string;
|
||||
mainKeyword: string;
|
||||
additionalKeywords: string;
|
||||
targetCountry: null;
|
||||
articleSize: string;
|
||||
projectId: number;
|
||||
createdBy: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
type BulkArticleRequest = {
|
||||
style: string;
|
||||
website: string;
|
||||
connectToWeb: boolean;
|
||||
lang: string;
|
||||
pointOfView: string;
|
||||
imageSource: string;
|
||||
targetCountry: null;
|
||||
articleSize: string;
|
||||
projectId: number;
|
||||
data: { title: string; mainKeyword: string; additionalKeywords: string }[];
|
||||
createdBy: string;
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
interface ContentRewriteRequest {
|
||||
advConfig: string;
|
||||
context: string | null;
|
||||
style: string;
|
||||
sentiment: string;
|
||||
clientId: string;
|
||||
createdBy: string;
|
||||
contextType: string;
|
||||
urlContext: string | null;
|
||||
lang: string;
|
||||
}
|
||||
|
||||
export type ContentBankRequest = {
|
||||
query: string;
|
||||
page: number;
|
||||
userId: string;
|
||||
limit: number;
|
||||
status: number[];
|
||||
isSingle: boolean;
|
||||
createdBy: string;
|
||||
sort: { column: string; sort: string }[];
|
||||
};
|
||||
|
||||
export async function getGenerateTitle(
|
||||
data: GenerateKeywordsAndTitleRequestData
|
||||
) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/generate-title", headers, data);
|
||||
}
|
||||
|
||||
export async function getGenerateKeywords(
|
||||
data: GenerateKeywordsAndTitleRequestData
|
||||
) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/generate-keywords", headers, data);
|
||||
}
|
||||
|
||||
export async function generateDataArticle(data: GenerateArticleRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/save-article", headers, data);
|
||||
}
|
||||
|
||||
export async function approveArticle(props: { id: number[] }) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/approve-article", headers, props);
|
||||
}
|
||||
export async function rejectArticle(props: { id: number[] }) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/reject-article", headers, props);
|
||||
}
|
||||
|
||||
export async function getGenerateTopicKeywords(data: {
|
||||
keyword: string;
|
||||
count: number;
|
||||
}) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/generate-topic-keywords", headers, data);
|
||||
}
|
||||
|
||||
export async function saveBulkArticle(data: BulkArticleRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/save-bulk-article", headers, data);
|
||||
}
|
||||
|
||||
export async function getDetailArticle(id: number) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpGet(`ai-writer/article/findArticleById/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function generateSpeechToText(data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/speech-to-text", headers, data);
|
||||
}
|
||||
|
||||
export async function getTranscriptById(id: number) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpGet(`ai-writer/speech-to-text/findById/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function getGenerateRewriter(data: ContentRewriteRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/create-rewriter", headers, data);
|
||||
}
|
||||
|
||||
export async function getListTranscript(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/speech-to-text/datatable", headers, data);
|
||||
}
|
||||
|
||||
export async function getListArticleDraft(data: ContentBankRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/article/datatable", headers, data);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import axios from "axios";
|
||||
|
||||
const baseURL = "https://disestages.com/api";
|
||||
|
||||
const axiosDisestagesInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export default axiosDisestagesInstance;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import axiosDisestagesInstance from "./disestages-instance";
|
||||
|
||||
export async function httpPost(pathUrl: any, headers: any, data?: any) {
|
||||
const response = await axiosDisestagesInstance
|
||||
.post(pathUrl, data, { headers })
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpGet(pathUrl: any, headers: any) {
|
||||
const response = await axiosDisestagesInstance
|
||||
.get(pathUrl, { headers })
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
interface targetStore {
|
||||
articleIds: { singleArticle: number[]; bulkArticle: number[] };
|
||||
setArticleIds: (newTarget: {
|
||||
singleArticle: number[];
|
||||
bulkArticle: number[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const getInitialTarget = () => {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const stored = localStorage.getItem("generated-article");
|
||||
const initial = stored
|
||||
? JSON.parse(stored)
|
||||
: { singleArticle: [], bulkArticle: [] };
|
||||
return initial;
|
||||
}
|
||||
};
|
||||
|
||||
const generatedArticleIds = create<targetStore>((set) => ({
|
||||
articleIds: getInitialTarget(),
|
||||
setArticleIds: (newTarget: {
|
||||
singleArticle: number[];
|
||||
bulkArticle: number[];
|
||||
}) => {
|
||||
localStorage.setItem("generated-article", JSON.stringify(newTarget));
|
||||
set({ articleIds: newTarget });
|
||||
},
|
||||
}));
|
||||
|
||||
export default generatedArticleIds;
|
||||
|
|
@ -70,3 +70,25 @@ const LoadScript = () => {
|
|||
};
|
||||
|
||||
export default LoadScript;
|
||||
|
||||
export function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function textEllipsis(
|
||||
str: string,
|
||||
maxLength: number,
|
||||
{ side = "end", ellipsis = "..." } = {}
|
||||
) {
|
||||
if (str !== undefined && str?.length > maxLength) {
|
||||
switch (side) {
|
||||
case "start":
|
||||
return ellipsis + str.slice(-(maxLength - ellipsis.length));
|
||||
|
||||
case "end":
|
||||
default:
|
||||
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue