Created
July 6, 2023 16:49
-
-
Save ahmadrosid/328ed02279219b48a9edea261c72c4ca to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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