kontenhumas-fe/components/form/content/audio/audio-form.tsx

1696 lines
57 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, {
ChangeEvent,
useEffect,
useRef,
Fragment,
useState,
} from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Upload } from "tus-js-client";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { redirect, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { register } from "module";
import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie";
import {
createMedia,
createArticle,
getTagsBySubCategoryId,
listEnableCategory,
listArticleCategories,
uploadThumbnail,
uploadArticleFiles,
uploadArticleThumbnail,
CreateArticleData,
} from "@/service/content/content";
import { Textarea } from "@/components/ui/textarea";
import {
generateDataArticle,
generateDataRewrite,
getDetailArticle,
getGenerateKeywords,
getGenerateTitle,
} from "@/service/content/ai";
import { getCookiesDecrypt } from "@/lib/utils";
import { useDropzone } from "react-dropzone";
import { Icon } from "@iconify/react";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { error, loading } from "@/config/swal";
import { Item } from "@radix-ui/react-dropdown-menu";
import dynamic from "next/dynamic";
import { getCsrfToken } from "@/service/auth";
import { useParams } from "next/navigation";
import { request } from "http";
import { toast } from "sonner";
import { htmlToString } from "@/utils/globals";
import Link from "next/link";
interface FileWithPreview extends File {
preview: string;
}
type Category = {
id: string;
name: string;
};
type Option = {
id: string;
label: string;
};
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false },
);
export default function FormAudio() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
type AudioSchema = z.infer<typeof audioSchema>;
const params = useParams();
const locale = params?.locale;
const [selectedFileType, setSelectedFileType] = useState("original");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [tags, setTags] = useState<string[]>([]);
const roleId = getCookiesDecrypt("urie");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [thumbnail, setThumbnail] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional");
const [editorContent, setEditorContent] = useState("");
const [rewriteEditorContent, setRewriteEditorContent] = useState("");
const [selectedSEO, setSelectedSEO] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [selectedAdvConfig, setSelectedAdvConfig] = useState<string>("");
const [editingArticleId, setEditingArticleId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoadingData, setIsLoadingData] = useState<boolean>(false);
const [articleIds, setArticleIds] = useState<string[]>([]);
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
const [articleBody, setArticleBody] = useState<string>("");
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null,
);
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
const [selectedSize, setSelectedSize] = useState("");
const [detailData, setDetailData] = useState<any>(null);
const [articleImages, setArticleImages] = useState<string[]>([]);
const [isSwitchOn, setIsSwitchOn] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
const [selectedTarget, setSelectedTarget] = useState("");
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
});
let fileTypeId = "4";
let progressInfo: any = [];
let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]);
let uploadPersen = 0;
const [isStartUpload, setIsStartUpload] = useState(false);
const [counterProgress, setCounterProgress] = useState(0);
const [isContentRewriteClicked, setIsContentRewriteClicked] = useState(false);
const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [fileError, setFileError] = useState<string | null>(null);
type FileWithPreview = File & { preview: string };
const userId = Cookies.get("userId");
const options: Option[] = [
{ id: "all", label: "SEMUA" },
{ id: "4", label: "UMUM" },
{ id: "5", label: "JOURNALIS" },
];
const audioRefs = useRef<HTMLAudioElement[]>([]);
const { getRootProps, getInputProps } = useDropzone({
accept: {
"audio/mpeg": [".mp3"],
"audio/wav": [".wav"],
},
maxSize: 100 * 1024 * 1024,
multiple: true,
onDrop: (acceptedFiles, fileRejections) => {
setFileError(null);
if (fileRejections.length > 0) {
const messages = fileRejections
.map((rej) => rej.errors.map((e) => e.message).join(", "))
.join(", ");
setFileError(messages || "File tidak valid");
return;
}
if (acceptedFiles.length === 0) {
setFileError("Wajib upload minimal 1 file mp3 atau wav");
return;
}
const filesWithPreview = acceptedFiles.map((file) =>
Object.assign(file, { preview: URL.createObjectURL(file) }),
);
setFiles((prevFiles) => [...prevFiles, ...filesWithPreview]);
const prevFiles = getValues("files") || [];
setValue("files", [...prevFiles, ...filesWithPreview], {
shouldValidate: true,
});
},
});
const handlePlay = (index: number) => {
audioRefs.current.forEach((audio, i) => {
if (audio && i !== index) {
audio.pause();
audio.currentTime = 0;
}
});
};
const audioSchema = z.object({
title: z.string().min(1, { message: "titleRequired" }),
description: z.string().optional(),
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "creatorRequired" }),
files: z
.array(z.any())
.min(1, { message: "Minimal 1 file harus diunggah." })
.refine(
(files) =>
files.every(
(file: File) =>
["audio/mpeg", "audio/wav", "audio/mp3"].includes(file.type) &&
file.size <= 100 * 1024 * 1024,
),
{
message: "Hanya file .mp3, .wav, maksimal 100MB yang diperbolehkan.",
},
),
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
tags: z
.array(z.string())
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
publishedFor: z
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
});
const {
control,
handleSubmit,
getValues,
setValue,
formState: { errors },
} = useForm<AudioSchema>({
resolver: zodResolver(audioSchema),
defaultValues: {
title: "",
description: "",
descriptionOri: "",
rewriteDescription: "",
creatorName: "",
files: [],
categoryId: "",
tags: [],
publishedFor: [],
},
});
const doGenerateMainKeyword = async () => {
console.log(selectedMainKeyword);
if (selectedMainKeyword?.length > 1) {
try {
setIsLoading(true);
const titleData = {
keyword: selectedMainKeyword,
style: selectedWritingStyle,
website: "0",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
clientId: "",
};
console.log("Sending request for title with data:", titleData);
const titleRes = await getGenerateTitle(titleData);
setTitle(titleRes?.data?.data || "");
console.log("Generated title:", titleRes?.data?.data);
const keywordsData = {
keyword: selectedMainKeyword,
style: selectedWritingStyle,
website: "0",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
clientId: "",
};
console.log("Sending request for keywords with data:", keywordsData);
const keywordsRes = await getGenerateKeywords(keywordsData);
setSelectedSEO(keywordsRes?.data?.data || []);
console.log("Generated keywords:", keywordsRes?.data?.data);
} catch (error) {
console.error("Error during generation process:", error);
} finally {
setIsLoading(false);
}
} else {
Swal.fire({
icon: "warning",
title: "WARNING",
text: "Please provide a valid main keyword.",
});
console.error("Please provide a valid main keyword.");
}
};
const doGenerateTitle = async () => {
if (selectedMainKeyword?.length > 1) {
try {
setIsLoading(true);
const titleData = {
keyword: selectedMainKeyword,
style: selectedWritingStyle,
website: "0",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
clientId: "",
};
console.log("Sending request for title with data:", titleData);
const titleRes = await getGenerateTitle(titleData);
setTitle(titleRes?.data?.data || "");
console.log("Generated title:", titleRes?.data?.data);
} catch (error) {
console.error("Error generating title:", error);
} finally {
setIsLoading(false);
}
} else {
Swal.fire({
icon: "warning",
title: "WARNING",
text: "Please provide a valid title.",
});
console.error("Please provide a valid main keyword.");
}
};
const doGenerateKeyword = async () => {
if (selectedMainKeyword?.length > 1) {
try {
setIsLoading(true);
const keywordsData = {
keyword: selectedMainKeyword,
style: selectedWritingStyle,
website: "0",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
clientId: "",
};
console.log("Sending request for keywords with data:", keywordsData);
const keywordsRes = await getGenerateKeywords(keywordsData);
setSelectedSEO(keywordsRes?.data?.data || []);
console.log("Generated keywords:", keywordsRes?.data?.data);
} catch (error) {
console.error("Error generating keywords:", error);
} finally {
setIsLoading(false);
}
} else {
Swal.fire({
icon: "warning",
title: "WARNING",
text: "Please provide a valid keyword.",
});
console.error("Please provide a valid main keyword.");
}
};
const handleGenerateArtikel = async () => {
const request = {
advConfig: selectedAdvConfig,
style: selectedWritingStyle,
website: "None",
connectToWeb: true,
lang: selectedLanguage,
pointOfView: "None",
title: title,
imageSource: "Web",
mainKeyword: selectedMainKeyword,
additionalKeywords: selectedSEO,
targetCountry: null,
articleSize: selectedSize,
projectId: 2,
createdBy: roleId,
clientId: "ngDLPPiorplznw2jTqVe3YFCz5xqKfUJ",
};
const res = await generateDataArticle(request);
close();
if (res?.error) {
console.error(res.message);
return false;
}
const newArticleId = res?.data?.data?.id;
setIsGeneratedArticle(true);
setArticleIds((prevIds: string[]) => {
if (prevIds.length < 3) {
return [...prevIds, newArticleId];
} else {
const updatedIds = [...prevIds];
updatedIds[2] = newArticleId;
return updatedIds;
}
});
Cookies.set("nulisAIArticleIdTemp", JSON.stringify(articleIds));
};
const handleArticleIdClick = async (id: string) => {
setIsLoadingData(true);
let retryCount = 0;
const maxRetries = 20;
try {
const waitForStatusUpdate = async () => {
while (retryCount < maxRetries) {
const res = await getDetailArticle(id);
const articleData = res?.data?.data;
if (articleData?.status === 2) {
return articleData;
}
retryCount++;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
throw new Error("Timeout: Artikel belum selesai diproses.");
};
const articleData = await waitForStatusUpdate();
const cleanArticleBody = articleData?.articleBody?.replace(
/<img[^>]*>/g,
"",
);
const articleImagesData = articleData?.imagesUrl?.split(",");
setArticleBody(cleanArticleBody || "");
setDetailData(articleData);
setSelectedArticleId(id);
setArticleImages(articleImagesData || []);
} catch (error) {
console.error("Error fetching article details:", error);
} finally {
setIsLoadingData(false);
}
};
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
e.preventDefault();
const newTags = [...tags, e.currentTarget.value.trim()];
setTags(newTags);
setValue("tags", newTags, { shouldValidate: true });
e.currentTarget.value = "";
}
};
const handleRemoveTag = (index: number) => {
const newTags = tags.filter((_, i) => i !== index);
setTags(newTags);
setValue("tags", newTags, { shouldValidate: true });
};
const handleRemoveImage = (index: number) => {
setSelectedFiles((prevImages) => prevImages.filter((_, i) => i !== index));
};
useEffect(() => {
async function initState() {
getCategories();
// setVideoActive(fileTypeId == '2');
// getRoles();
}
initState();
}, []);
const getCategories = async () => {
try {
// Use new Article Categories API
const category = await listArticleCategories(1, 100);
console.log("Article categories response:", category);
if (category?.error) {
console.error("Failed to fetch article categories:", category.message);
// Fallback to old API if new one fails
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] =
fallbackCategory?.data.data.content || [];
setCategories(resCategory);
return;
}
// Handle new API response structure
const resCategory: Category[] =
category?.data?.data?.map((item: any) => ({
id: item.id,
name: item.title, // map title to name for backward compatibility
title: item.title,
description: item.description,
...item,
})) || [];
setCategories(resCategory);
console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis"),
);
if (findCategory) {
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (error) {
console.error("Failed to fetch categories:", error);
// Fallback to old API if error occurs
try {
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] =
fallbackCategory?.data.data.content || [];
setCategories(resCategory);
} catch (fallbackError) {
console.error("Fallback category fetch also failed:", fallbackError);
}
}
};
const handleCheckboxChange = (id: string): void => {
if (id === "all") {
if (publishedFor.includes("all")) {
// Uncheck all checkboxes
setPublishedFor([]);
} else {
// Select all checkboxes
setPublishedFor(
options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id),
);
}
} else {
const updatedPublishedFor = publishedFor.includes(id)
? publishedFor.filter((item) => item !== id)
: [...publishedFor, id];
// Remove "all" if any checkbox is unchecked
if (publishedFor.includes("all") && id !== "all") {
setPublishedFor(updatedPublishedFor.filter((item) => item !== "all"));
} else {
setPublishedFor(updatedPublishedFor);
}
}
};
useEffect(() => {
if (articleBody) {
// Set ke dua field jika rewrite juga aktif
setValue("description", articleBody);
setValue("rewriteDescription", articleBody);
}
}, [articleBody, setValue]);
const save = async (data: AudioSchema) => {
loading();
const finalTags = tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title;
const finalDescription = isSwitchOn
? data.description
: selectedFileType === "rewrite"
? data.rewriteDescription
: data.descriptionOri;
if (!finalDescription?.trim()) {
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
return;
}
function formatDateForBackend(date: Date) {
const pad = (n: number) => (n < 10 ? "0" + n : n);
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
" " +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
}
let requestData: {
title: string;
description: string;
htmlDescription: string;
fileTypeId: string;
categoryId: any;
subCategoryId: any;
uploadedBy: string;
statusId: string;
publishedFor: string;
creatorName: string;
tags: string;
isYoutube: boolean;
isInternationalMedia: boolean;
attachFromScheduleId?: number;
} = {
...data,
title: finalTitle,
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
fileTypeId,
categoryId: selectedCategory,
subCategoryId: selectedCategory,
uploadedBy: "2b7c8d83-d298-4b19-9f74-b07924506b58",
statusId: "1",
publishedFor: publishedFor.join(","),
creatorName: data.creatorName,
tags: finalTags,
isYoutube: false,
isInternationalMedia: false,
};
let id = Cookies.get("idCreate");
if (scheduleId !== undefined) {
requestData.attachFromScheduleId = Number(scheduleId);
}
if (id == undefined) {
// New Articles API request data structure
const articleData: CreateArticleData = {
aiArticleId: 0, // default 0
categoryIds: selectedCategory.toString(),
createdAt: formatDateForBackend(new Date()), // ✅ format sesuai backend
createdById: Number(userId), // isi dengan userId valid
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
isDraft: true,
isPublish: false,
oldId: 0,
slug: finalTitle
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, ""),
tags: finalTags,
title: finalTitle,
typeId: 4,
publishedFor: data.publishedFor.join(","),
};
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
if (response?.error) {
MySwal.fire(
"Error",
response.message || "Failed to create article",
"error",
);
return false;
}
// Get the article ID from the new API response
const articleId = response?.data?.data?.id;
Cookies.set("idCreate", articleId, { expires: 1 });
id = articleId;
// Upload files using new article-files API
const formData = new FormData();
// Add all files to FormData
files.forEach((file, index) => {
formData.append("files", file);
});
console.log("Uploading files to article:", articleId);
console.log("Files to upload:", files.length);
try {
const uploadResponse = await uploadArticleFiles(articleId, formData);
if (uploadResponse?.error) {
MySwal.fire(
"Error",
uploadResponse.message || "Failed to upload files",
"error",
);
return false;
}
console.log("Files uploaded successfully:", uploadResponse);
// Upload thumbnail using first file as thumbnail
if (files.length > 0) {
const thumbnailFormData = new FormData();
thumbnailFormData.append("files", files[0]); // Use first file as thumbnail
console.log("Uploading thumbnail for article:", articleId);
try {
const thumbnailResponse = await uploadArticleThumbnail(
articleId,
thumbnailFormData,
);
if (thumbnailResponse?.error) {
console.warn(
"Thumbnail upload failed:",
thumbnailResponse.message,
);
// Don't fail the whole process if thumbnail upload fails
} else {
console.log(
"Thumbnail uploaded successfully:",
thumbnailResponse,
);
}
} catch (thumbnailError) {
console.warn("Thumbnail upload error:", thumbnailError);
// Don't fail the whole process if thumbnail upload fails
}
}
} catch (uploadError) {
console.error("Upload error:", uploadError);
MySwal.fire(
"Error",
"Failed to upload files. Please try again.",
"error",
);
return false;
}
// Show success message
MySwal.fire({
title: "Sukses",
text: "Article dan files berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/audio");
});
Cookies.remove("idCreate");
return;
}
Cookies.remove("idCreate");
};
const onSubmit = (data: AudioSchema) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
async function uploadResumableFile(
idx: number,
id: string,
file: any,
duration: string,
) {
console.log(idx, id, file, duration);
// const placements = getPlacement(file.placements);
// console.log("Placementttt: : ", placements);
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
const headers = {
"X-XSRF-TOKEN": csrfToken,
};
const upload = new Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API}/media/file/upload`,
headers: headers,
retryDelays: [0, 3000, 6000, 12_000, 24_000],
chunkSize: 20_000,
metadata: {
mediaid: id,
filename: file.name,
filetype: file.type,
duration,
isWatermark: "false",
},
onBeforeRequest: function (req) {
var xhr = req.getUnderlyingObject();
xhr.withCredentials = true;
},
onError: async (e: any) => {
console.log("Error upload :", e);
error(e);
},
onChunkComplete: (
chunkSize: any,
bytesAccepted: any,
bytesTotal: any,
) => {
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
progressInfo[idx].percentage = uploadPersen;
counterUpdateProgress++;
console.log(counterUpdateProgress);
setProgressList(progressInfo);
setCounterProgress(counterUpdateProgress);
},
onSuccess: async () => {
uploadPersen = 100;
progressInfo[idx].percentage = 100;
counterUpdateProgress++;
setCounterProgress(counterUpdateProgress);
successTodo();
},
});
upload.start();
}
const successSubmit = (redirect: string) => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
});
};
function successTodo() {
let counter = 0;
for (const element of progressInfo) {
if (element.percentage == 100) {
counter++;
}
}
if (counter == progressInfo.length) {
setIsStartUpload(false);
// hideProgress();
Cookies.remove("idCreate");
successSubmit("/admin/content/audio");
}
}
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setThumbnail(file);
console.log("Selected Thumbnail:", file);
}
if (file) {
setPreview(URL.createObjectURL(file));
}
};
const renderFilePreview = (file: FileWithPreview) => {
if (file.type.startsWith("image")) {
return (
<Image
width={48}
height={48}
alt={file.name}
src={URL.createObjectURL(file)}
className=" rounded border p-0.5"
/>
);
} else {
return <Icon icon="tabler:file-description" />;
}
};
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
const fileList = files.map((file) => (
<div
key={file.name}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
<div className="flex gap-3 items-center">
{/* <div className="file-preview">{renderFilePreview(file)}</div> */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
/>
</svg>{" "}
<div>
<div className=" text-sm text-card-foreground">{file.name}</div>
<div className=" text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)}
{" kb"}
</div>
</div>
</div>
<Button
size="icon"
color="destructive"
variant="outline"
className=" border-none rounded-full"
onClick={() => handleRemoveFile(file)}
>
<Icon icon="tabler:x" className=" h-5 w-5" />
</Button>
</div>
));
const handleRemoveAllFiles = () => {
setFiles([]);
};
useEffect(() => {
if (!getValues("title") && title) {
setValue("title", title);
}
}, [title, getValues, setValue]);
const handleRewriteClick = async () => {
setIsContentRewriteClicked(true);
const request = {
style: selectedWritingStyle,
lang: "id",
contextType: "text",
urlContext: null,
context: editorContent,
createdBy: roleId,
sentiment: "Humorous",
clientId: "7QTW8cMojyayt6qnhqTOeJaBI70W4EaQ",
};
const res = await generateDataRewrite(request);
close();
if (res?.error) {
console.error(res.message);
return false;
}
const newArticleId = res?.data?.data?.id;
setIsGeneratedArticle(true);
setArticleIds((prevIds: string[]) => {
if (prevIds.length < 3) {
return [...prevIds, newArticleId];
} else {
const updatedIds = [...prevIds];
updatedIds[2] = newArticleId;
return updatedIds;
}
});
Cookies.set("nulisAIArticleIdTemp", JSON.stringify(articleIds));
setShowRewriteEditor(true);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col lg:flex-row gap-10 border rounded-lg">
<Card className="w-full lg:w-8/12 m-2">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Audio</p>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Controller
control={control}
name="categoryId"
render={({ field }) => (
<div className="w-full">
<Select
value={field.value}
onValueChange={(value) => {
field.onChange(value);
setSelectedCategory(value);
}}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.categoryId && (
<p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p>
)}
</div>
)}
/>
</div>
</div>
<div className="flex flex-row items-center gap-3 py-2">
<Label>Ai Assistance</Label>
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={isSwitchOn}
onChange={(e) => setIsSwitchOn(e.target.checked)}
className="sr-only peer"
/>
<div
className="
w-11 h-6
bg-gray-300
rounded-full
peer
peer-checked:bg-blue-600
transition-colors
after:content-['']
after:absolute
after:top-[2px]
after:left-[2px]
after:bg-white
after:rounded-full
after:h-5
after:w-5
after:transition-transform
peer-checked:after:translate-x-5
"
/>
</label>
</div>
{/* <div className="flex items-center gap-3">
<Switch
defaultChecked={isSwitchOn}
color="primary"
id="c2"
onCheckedChange={(checked: boolean) =>
setIsSwitchOn(checked)
}
/>
</div> */}
</div>
{isSwitchOn && (
<div>
<div className="flex flex-row gap-3">
<div className="space-y-2 py-3 w-4/12">
<Label>Language</Label>
<Select onValueChange={setSelectedLanguage}>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="id">Indonesia</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 py-3 w-4/12">
<Label>Writing Style</Label>
<Select onValueChange={setSelectedWritingStyle}>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="friendly">Friendly</SelectItem>
<SelectItem value="profesional">
Profesional
</SelectItem>
<SelectItem value="informational">
Informational
</SelectItem>
<SelectItem value="neutral">Neutral</SelectItem>
<SelectItem value="witty">Witty</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 py-3 w-4/12">
<Label>Article Size</Label>
<Select onValueChange={setSelectedSize}>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="news">
News (300 - 900 words)
</SelectItem>
<SelectItem value="info">
Info (900 - 2000 words)
</SelectItem>
<SelectItem value="detail">
Detail (2000 - 5000 words)
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3">
<Label>Main Keyword</Label>
<Button
variant="outline"
color="primary"
onClick={doGenerateMainKeyword}
disabled={isLoading}
>
{isLoading ? "Processing..." : "Proses"}
</Button>
</div>
<div>
<Input
type="text"
value={selectedMainKeyword}
onChange={(e) => setSelectedMainKeyword(e.target.value)}
placeholder="Enter Main Keyword"
/>
{/* )}
/> */}
</div>
</div>
<div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3">
<Label>Title</Label>
<Button
variant="outline"
color="primary"
onClick={doGenerateTitle}
disabled={isLoading}
>
{isLoading ? "Generating..." : "Generate"}
</Button>
</div>
<div>
<Input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Generated Title"
/>
</div>
</div>
<div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3">
<Label>Seo</Label>
<Button
variant={"outline"}
color="primary"
onClick={doGenerateKeyword}
disabled={isLoading}
>
{isLoading ? "Generating..." : "Generate"}
</Button>
</div>
<p className="font-semibold">
Keywords To Include In The Text
</p>
<p className="text-sm">Title Key</p>
<div className="mt-3">
<Textarea
value={selectedSEO}
onChange={(e) => setSelectedSEO(e.target.value)}
placeholder="Enter Title"
/>
</div>
</div>
<div className="mt-5">
<Label>Special Instructions (Optional)</Label>
<div className="mt-3">
<Controller
control={control}
name="title"
render={({ field }) => (
<Textarea
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
</div>
</div>
<div>
<div className="my-5">
<Button
// variant={"outline"}
color="primary"
onClick={handleGenerateArtikel}
size="sm"
type="button"
>
Generate Article
</Button>
</div>
{isGeneratedArticle && (
<div className="mt-3 pb-0 flex flex-row">
{articleIds.map((id: string, index: number) => (
<p
key={index}
className={`mr-3 px-3 py-2 rounded-md ${
selectedArticleId === id
? "bg-green-500 text-white"
: "border-2 border-green-500 text-green-500"
}`}
onClick={() => handleArticleIdClick(id)}
>
{"Narasi " + (index + 1)}
</p>
))}
</div>
)}
<div className="pt-3">
<div className="flex flex-row justify-between items-center">
{selectedArticleId && (
<Button
className="mb-2"
size="sm"
variant={"outline"}
color="primary"
onClick={() => {
const url = `/${locale}/contributor/content/image/update-seo/${selectedArticleId}`;
window.open(url, "_blank", "noopener,noreferrer");
}}
>
Update
</Button>
)}
</div>
</div>
</div>
<div className="py-3 space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) =>
isLoadingData ? (
<div className="flex justify-center items-center h-40">
<p className="text-gray-500">
Loading Proses Data...
</p>
</div>
) : (
<CustomEditor
onChange={(value: any) => {
onChange(value);
setEditorContent(value);
}}
initialData={articleBody || value}
/>
)
}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</div>
)}
{!isSwitchOn && (
<>
<RadioGroup
onValueChange={(value) => setSelectedFileType(value)}
value={selectedFileType}
className=" grid-cols-1"
>
<div className="">
<RadioGroupItem value="original" id="original-file" />
<Label htmlFor="original-file">
Select Original Description
</Label>
</div>
<div className="py-3 space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="descriptionOri"
render={({ field: { onChange, value } }) => (
<CustomEditor
onChange={(value: any) => {
onChange(value);
setEditorContent(value);
}}
initialData={value}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
<p className="text-sm font-semibold">Content Rewrite</p>
<div className="my-2">
<Button
size="sm"
type="button"
onClick={handleRewriteClick}
className="bg-blue-500 text-white py-2 px-4 rounded"
>
Content Rewrite
</Button>
</div>
{showRewriteEditor && (
<div>
{isGeneratedArticle && (
<div className="mt-3 pb-0 flex flex-row ">
{articleIds.map((id: string, index: number) => (
<Button
type="button"
key={index}
className={`mr-3 px-3 py-2 rounded-md ${
selectedArticleId === id
? "bg-green-500 text-white"
: "border-2 border-green-500 bg-white text-green-500 hover:bg-green-500 hover:text-white hover:border-green-500"
}`}
onClick={() => handleArticleIdClick(id)}
>
{"Narasi " + (index + 1)}
</Button>
))}
</div>
)}
<div className="flex items-center space-x-2 mt-3">
<RadioGroupItem value="rewrite" id="rewrite-file" />
<Label htmlFor="rewrite-file">
Select Description Rewrite
</Label>
</div>
<div className="py-3 space-y-2">
<Label>File Rewrite</Label>
<Controller
control={control}
name="rewriteDescription"
render={({ field: { onChange, value } }) =>
isLoadingData ? (
<div className="flex justify-center items-center h-40">
<p className="text-gray-500">
Loading Proses Data...
</p>
</div>
) : (
<CustomEditor
onChange={(value: any) => {
onChange(value);
setRewriteEditorContent(value);
}}
initialData={articleBody || value}
/>
)
}
/>
</div>
</div>
)}
</RadioGroup>
</>
)}
<div className="py-3 space-y-2">
<Label>Select File</Label>
<Controller
name="files"
control={control}
rules={{
required: "Wajib upload minimal 1 file mp3 atau wav",
}}
render={({ field, fieldState }) => (
<>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps({ multiple: true })} />
<div className="w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Drag File
</h4>
<div className="text-xs text-muted-foreground">
Upload File Audio Max
</div>
</div>
</div>
{/* pesan error dari validasi dropzone */}
{fileError && (
<p className="text-sm text-red-500 mt-1">{fileError}</p>
)}
{/* pesan error dari schema */}
{fieldState.error && (
<p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)}
{files.length ? (
<div className="space-y-4 mt-4">
{files.map((file, idx) => (
<div
key={idx}
className="flex flex-col gap-2 border p-2 rounded-md"
>
<p className="text-sm font-medium truncate">
{file.name}
</p>
<audio
controls
src={file.preview}
onPlay={() => handlePlay(idx)}
className="w-full rounded"
>
Your browser does not support the audio element.
</audio>
</div>
))}
</div>
) : null}
{files.length > 0 && (
<div className="flex justify-between gap-2 mt-3">
<Button
color="destructive"
onClick={() => {
setFiles([]);
setFileError(null);
setValue("files", [], { shouldValidate: true });
}}
>
Remove All
</Button>
</div>
)}
</>
)}
/>
</div>
</div>
</div>
</Card>
<div className="w-full lg:w-4/12 m-2">
<Card className="pb-3">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
</div>
<div className="px-3 py-3 space-y-2">
<Label htmlFor="tags">Tags</Label>
<Input
type="text"
id="tags"
placeholder="Add a tag and press Enter"
onKeyDown={handleAddTag}
ref={inputRef}
/>
<div className="mt-3">
{tags.map((tag, index) => (
<span
key={index}
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{tag}{" "}
<button
type="button"
onClick={() => handleRemoveTag(index)}
className="remove-tag-button"
>
×
</button>
</span>
))}
</div>
{errors.tags && (
<p className="text-sm text-red-500 mt-1">
{errors.tags.message}
</p>
)}
</div>
{/* <div className="px-3 py-3">
<div className="flex flex-col gap-3 space-y-2">
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => (
<div key={option.id} className="flex gap-2 items-center">
<Checkbox
id={option.id}
checked={
option.id === "all"
? publishedFor.length ===
options.filter((opt: any) => opt.id !== "all")
.length
: publishedFor.includes(option.id)
}
onCheckedChange={() => handleCheckboxChange(option.id)}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
))}
</div>
</div> */}
<Controller
control={control}
name="publishedFor"
render={({ field }) => (
<div className="px-3 py-3">
<div className="flex flex-col gap-3 space-y-2">
<Label>Publish Target</Label>
{options.map((option) => {
const isAllChecked =
field.value.length ===
options.filter((opt: any) => opt.id !== "all").length;
const isChecked =
option.id === "all"
? isAllChecked
: field.value.includes(option.id);
const handleChange = (checked: boolean) => {
let updated: string[] = [];
if (option.id === "all") {
updated = checked
? options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id)
: [];
} else {
updated = checked
? [...field.value, option.id]
: field.value.filter((val) => val !== option.id);
if (isAllChecked && option.id !== "all") {
updated = updated.filter((val) => val !== "all");
}
}
field.onChange(updated);
setPublishedFor(updated);
};
// const handleChange = () => {
// let updated: string[] = [];
// if (option.id === "all") {
// updated = isAllChecked
// ? []
// : options
// .filter((opt: any) => opt.id !== "all")
// .map((opt: any) => opt.id);
// } else {
// updated = isChecked
// ? field.value.filter((val) => val !== option.id)
// : [...field.value, option.id];
// if (isAllChecked && option.id !== "all") {
// updated = updated.filter((val) => val !== "all");
// }
// }
// field.onChange(updated);
// setPublishedFor(updated);
// };
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<input
type="checkbox"
id={option.id}
checked={isChecked}
onChange={(e) => handleChange(e.target.checked)}
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
/>
{/* <Checkbox
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
className="border"
/> */}
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
</div>
)}
/>
</Card>
<div className="flex flex-row justify-end gap-3">
<div className="mt-4">
<Button type="submit" color="primary">
Submit
</Button>
</div>
<div className="mt-4">
<Link href={"/admin/content/audio"}>
<Button type="button" color="primary" variant="outline">
Cancel
</Button>
</Link>
</div>
</div>
</div>
</div>
</form>
);
}