Skip to content

Instantly share code, notes, and snippets.

@ahmadrosid
Created July 6, 2023 16:49
Show Gist options
  • Save ahmadrosid/328ed02279219b48a9edea261c72c4ca to your computer and use it in GitHub Desktop.
Save ahmadrosid/328ed02279219b48a9edea261c72c4ca to your computer and use it in GitHub Desktop.
import type { ConfigData, Conversation } from '@/Components/ChatWidget';
import { ChatWidget } from '@/Components/ChatWidget';
import useConfigReducer from '@/Hooks/useConfigReducer';
import DialogModal from '@/Components/DialogModal';
import InputLabel from '@/Components/InputLabel';
import Popover from '@/Components/Popover';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import useRoute from '@/Hooks/useRoute';
import { errorStyle, successStyle } from '@/Utils/toast-style';
import { Switch } from '@headlessui/react';
import { useForm } from '@inertiajs/react';
import axios from 'axios';
import { Edit, Loader2, PlusCircle, Trash2 } from 'lucide-react';
import { ChangeEvent, useEffect, useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import toast from 'react-hot-toast';
type Props = { chatbotId: string };
export default function ThemeEditor({ chatbotId }: Props) {
const route = useRoute();
const form = useForm({
id: chatbotId,
config: undefined as unknown as ConfigData,
});
const [config, dispatch] = useConfigReducer();
const [isOpenAddQeustion, setIsOpenAddQuestion] = useState(false);
const initialConversations: Conversation[] = [
{
role: 'bot',
message: config.welcome_text,
},
{
role: 'user',
message: 'Hi, what is GPTin? How can I used it on my website?',
},
{
role: 'bot',
message:
'A GPTin is a software application that can simulate conversation with human.',
},
];
const handleSaveConfig = () => {
form.put(route('theme.update', chatbotId), {
preserveState: false,
onError: e => {
toast.error(e.message, errorStyle);
},
onSuccess: e => {
toast.success('Configuration saved!', successStyle);
},
});
};
const fetchConfigData = (chatbot_id: string) => {
axios
.get(route('config.show', chatbot_id))
.then(response => {
dispatch({ type: 'ALL', payload: response.data.data });
})
.catch(e => {
toast.error(e.message, errorStyle);
});
};
const onChangeUserIcon = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files && event.target.files[0];
if (file && file.size <= 1024 * 1024) {
dispatch({
type: 'UPDATE_ICON_USER',
payload: file,
});
} else {
console.log('Invalid file!');
}
};
const onChangeRobotIcon = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files && event.target.files[0];
try {
if (file && file.size <= 1024 * 1024) {
let formData = new FormData();
formData.append('file', file);
formData.append('chatbot_id', chatbotId);
const { data } = await axios.post(
'/api/chatbot/upload/icon',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
);
dispatch({
type: 'UPDATE_ROBOT_ICON',
payload: data.link,
});
} else {
toast.error('Image upload must be under 1 MB');
}
} catch (e) {
console.log(e);
toast.error('Failed to upload icon', errorStyle);
}
};
useEffect(() => {
if (config) {
form.setData('config', config);
}
}, [config]);
useEffect(() => {
fetchConfigData(chatbotId);
}, []);
return (
<div>
<div className="bg-white dark:bg-gray-800">
<div className="flex justify-between flex-col-reverse lg:flex-row">
<div className="p-6 flex flex-col gap-4 w-full h-[89vh] overflow-y-auto">
<div className="flex flex-col gap-2 lg:flex-row border-b dark:border-gray-700 pb-4">
<h3 className="text-gray-800 dark:text-gray-300 font-bold text-xl">
Customize your chatbot
</h3>
<PrimaryButton
onClick={handleSaveConfig}
disabled={form.processing}
className="lg:ml-auto justify-center"
>
{form.processing && (
<Loader2 className="animate-spin w-4 -h-4 mr-2" />
)}
Save changes
</PrimaryButton>
</div>
<div className="leading-tight">
<InputLabel htmlFor="input_update_text">Chatbot Name</InputLabel>
<TextInput
id="input_update_text"
defaultValue={config.title}
onChange={e =>
dispatch({
type: 'UPDATE_TITLE',
payload: e.target.value,
})
}
className="border border-gray-300 dark:border-gray-600 p-2 rounded-md mt-2 w-full text-gray-500 focus:ring-0 focus:outline-none"
/>
</div>
<div className="leading-tight">
<InputLabel htmlFor="input_placeholder_chat">
Placeholder text
</InputLabel>
<TextInput
id="input_placeholder_chat"
defaultValue={config.placeholder}
onChange={e =>
dispatch({
type: 'UPDATE_PLACEHOLDER',
payload: e.target.value,
})
}
className="border border-gray-300 p-2 rounded-md mt-2 w-full text-gray-500 focus:ring-0 focus:outline-none"
/>
</div>
<div className="leading-tight">
<InputLabel htmlFor="input_welcom_text">
Welcome message
</InputLabel>
<TextInput
id="input_welcom_text"
defaultValue={config.welcome_text}
onChange={e =>
dispatch({
type: 'UPDATE_WELCOME_TEXT',
payload: e.target.value,
})
}
className="border border-gray-300 p-2 rounded-md mt-2 w-full text-gray-500 focus:ring-0 focus:outline-none"
/>
</div>
<div className="mt-4 space-y-2">
<h3 className="text-gray-600 dark:text-gray-300 font-medium text-lg">
Customize Colors
</h3>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<div className="leading-tight">
<InputLabel htmlFor="input_font_color">Font color</InputLabel>
<div className="flex gap-2 items-center mt-2">
<TextInput
id="input_font_color"
className="border border-gray-300 dark:border-gray-700 h-10 px-2 rounded-md flex-1 text-gray-500 focus:ring-0 focus:outline-none"
value={config.color.font}
onChange={e =>
dispatch({
type: 'UPDATE_FONT_COLOR',
payload: e.target.value,
})
}
/>
<Popover
label={
<span
className="block border rounded-md border-gray-300 dark:border-gray-700 w-16 h-10 focus:outline-none mt-1"
style={{ backgroundColor: config.color.font }}
/>
}
>
<HexColorPicker
className="w-full "
color={config.color.font}
onChange={e =>
dispatch({ type: 'UPDATE_FONT_COLOR', payload: e })
}
/>
</Popover>
</div>
</div>
<div className="leading-tight">
<InputLabel htmlFor="input_background_color">
Background color
</InputLabel>
<div className="flex gap-2 items-center mt-2">
<TextInput
id="input_background_color"
className="border border-gray-300 dark:border-gray-600 h-10 px-2 rounded-md flex-1 text-gray-500 focus:ring-0 focus:outline-none"
value={config.color.background}
onChange={e =>
dispatch({
type: 'UPDATE_BACKGROUND_COLOR',
payload: e.target.value,
})
}
/>
<Popover
label={
<span
className="block border rounded-md border-gray-300 dark:border-gray-700 w-16 h-10 focus:outline-none mt-1"
style={{ backgroundColor: config.color.background }}
/>
}
>
<HexColorPicker
className="w-full"
color={config.color.background}
onChange={e =>
dispatch({
type: 'UPDATE_BACKGROUND_COLOR',
payload: e,
})
}
/>
</Popover>
</div>
</div>
<div className="leading-tight">
<InputLabel htmlFor="input_primary_color">
Primary color
</InputLabel>
<div className="flex gap-2 items-center mt-2">
<TextInput
id="input_primary_color"
className="border border-gray-300 dark:border-gray-600 h-10 px-2 rounded-md flex-1 text-gray-500 focus:ring-0 focus:outline-none"
value={config.color.primary}
onChange={e =>
dispatch({
type: 'UPDATE_PRIMARY_COLOR',
payload: e.target.value,
})
}
/>
<Popover
label={
<span
className="block border rounded-md border-gray-300 dark:border-gray-700 w-16 h-10 focus:outline-none mt-1"
style={{ backgroundColor: config.color.primary }}
/>
}
>
<HexColorPicker
className="w-full"
color={config.color.primary}
onChange={e =>
dispatch({
type: 'UPDATE_PRIMARY_COLOR',
payload: e,
})
}
/>
</Popover>
</div>
</div>
<div className="leading-tight">
<InputLabel htmlFor="input_secondary_color">
Secondary color
</InputLabel>
<div className="flex gap-2 items-center mt-2">
<TextInput
id="input_secondary_color"
className="border border-gray-300 h-10 px-2 rounded-md flex-1 text-gray-500 focus:ring-0 focus:outline-none"
value={config.color.secondary}
onChange={e =>
dispatch({
type: 'UPDATE_SECONDARY_COLOR',
payload: e.target.value,
})
}
/>
<Popover
label={
<span
className="block border rounded-md border-gray-300 dark:border-gray-700 w-16 h-10 focus:outline-none mt-1"
style={{ backgroundColor: config.color.secondary }}
/>
}
>
<HexColorPicker
className="w-full"
color={config.color.secondary}
onChange={e =>
dispatch({
type: 'UPDATE_SECONDARY_COLOR',
payload: e,
})
}
/>
</Popover>
</div>
</div>
</div>
</div>
<div className="mt-4 space-y-2">
<h3 className="text-gray-600 dark:text-gray-300 font-medium text-lg">
Robot Icon
</h3>
<div className="flex items-start justify-start gap-x-4 w-full">
<input
type="file"
accept="image/*"
onChange={onChangeRobotIcon}
/>
</div>
</div>
<div className="mt-4 space-y-2">
<h3 className="text-gray-600 dark:text-gray-300 font-medium text-lg">
User Icon
</h3>
<div className="flex items-start justify-start gap-x-4 w-full">
<input
type="file"
accept="image/*"
onChange={onChangeUserIcon}
/>
</div>
</div>
<div className="mt-4 space-y-2">
<h3 className="text-gray-600 dark:text-gray-300 font-medium text-lg">
Format Footer
</h3>
<div className="flex items-start justify-start gap-x-4 w-full">
<Switch
checked={config.show_footer}
onChange={e =>
dispatch({
type: 'UPDATE_FOOTER_STATUS',
payload: !config.show_footer,
})
}
className={`${
config.show_footer ? 'bg-blue-600' : 'bg-gray-200'
} relative inline-flex h-6 w-11 items-center rounded-full`}
>
<span className="sr-only">Enable notifications</span>
<span
className={`${
config.show_footer ? 'translate-x-6' : 'translate-x-1'
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
/>
</Switch>
{config.show_footer ? 'disabled' : 'enabled'}
</div>
</div>
<div className="mt-4 space-y-2">
<h3 className="text-gray-600 dark:text-gray-300 font-medium text-lg">
Predefined Question
</h3>
<div>
<PrimaryButton
onClick={() => setIsOpenAddQuestion(true)}
className="pl-3"
>
<PlusCircle className="w-4 h-4 mr-2" /> Add
</PrimaryButton>
<DialogModal
isOpen={isOpenAddQeustion}
onClose={() => setIsOpenAddQuestion(false)}
>
<DialogModal.Content title="Add new predefined question">
<form
onSubmit={e => {
e.preventDefault();
if (!e.currentTarget.default_question) return;
if (!e.currentTarget.default_prompt.value) return;
dispatch({
type: 'ADD_PREDEFINED_QUESTION',
payload: {
question: e.currentTarget.default_question.value,
prompt: e.currentTarget.default_prompt.value,
},
});
setIsOpenAddQuestion(false);
}}
className="leading-tight w-full bg-white py-2"
>
<div className="grid items-center mt-2 gap-2 w-full">
<InputLabel htmlFor="input_predefined_question">
Display text
</InputLabel>
<TextInput
id="input_predefined_question"
name="default_question"
placeholder="ex. Pricing"
className="border border-gray-300 p-2 rounded-md w-full text-gray-500 focus:ring-0 focus:outline-none"
/>
<InputLabel htmlFor="input_predefined_prompt">
Prompt
</InputLabel>
<TextInput
id="input_predefined_prompt"
name="default_prompt"
placeholder="ex. Display pricing info in markdown table."
className="border border-gray-300 p-2 rounded-md w-full text-gray-500 focus:ring-0 focus:outline-none"
/>
<PrimaryButton className="h-10 mt-6 justify-center">
Submit
</PrimaryButton>
</div>
</form>
</DialogModal.Content>
</DialogModal>
<div className="grid gap-2 mt-4">
{config.predefined_questions.map((item, idx) => (
<div
key={idx}
className="flex justify-between border-b dark:border-b-gray-600 items-center pb-2"
>
<div>
<p className="text-gray-800 font-medium dark:text-gray-300">
{item.question}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{item.prompt}
</p>
</div>
<div className="flex gap-4">
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-900 rounded-md">
<Edit className="w-4 h-4 dark:text-gray-400" />
</button>
<button
onClick={() =>
dispatch({
type: 'DELETE_PREDEFINED_QUESTION',
payload: idx,
})
}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-900 rounded-md"
>
<Trash2 className="w-4 h-4 dark:text-gray-400" />
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="px-8 py-2 w-full bg-gray-50 dark:bg-gray-800 grid place-content-center border-b lg:border-b-0 lg:border-l">
<div className="mx-auto relative h-full border rounded-md overflow-hidden">
<div className="h-full bg-white font-sans text-gray-900 antialiased overflow-hidden">
<div
style={{
position: 'fixed',
zIndex: 9999,
top: '16px',
left: '16px',
right: '16px',
bottom: '16px',
pointerEvents: 'none',
}}
></div>
<ChatWidget
chatbotId={chatbotId}
config={config}
conversations={initialConversations}
baseUrl={window.location.origin + '/api'}
customHeight="h-[75vh]"
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment