1import React from 'react';
2import {ChatProvider, Frame, Launcher, useChat} from '@booper/react';
3
4const ChatHeader = () => {
5 return (
6 <div className="bg-gradient-to-r from-indigo-700 via-indigo-600 to-indigo-500 px-4 py-4 text-zinc-100 shadow-lg dark:border-zinc-700">
7 <h1 className="mt-1 text-xl font-bold text-white">
8 Welcome to Booper ๐Ÿ‘‹
9 </h1>
10 <p className="text-sm text-zinc-200">Ask us anything below.</p>
11 <button
12 className="absolute right-2 top-2 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-zinc-800"
13 onClick={toggle}
14 >
15 <XIcon className="h-4 w-4" />
16 <span className="sr-only">Close</span>
17 </button>
18 </div>
19 );
20};
21
22const ChatBody = () => {
23 const {messages} = useChat();
24 const scrollRef = React.useRef<HTMLDivElement | null>(null);
25
26 React.useEffect(() => {
27 scrollRef.current?.scrollIntoView();
28 }, [messages.length]);
29
30 return (
31 <div className="flex-1 overflow-scroll bg-white px-4 py-4 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100">
32 {messages.map((message, index) => {
33 const {id, txn_id: txnId, content, source, metadata = {}} = message;
34 const key = txnId || id;
35 const isMe = source === 'chat';
36 const avatar =
37 metadata.author?.avatarURL || metadata.author?.defaultAvatarURL;
38 const prev = messages[index - 1];
39 const isPrevSame = prev && prev.source === source;
40
41 if (isMe) {
42 return (
43 <div
44 key={key}
45 className={`${
46 isPrevSame ? 'mt-1' : 'mt-4'
47 } flex items-center justify-end pl-12 animate-in fade-in-0 slide-in-from-right-4 duration-300`}
48 >
49 <div className="rounded-md bg-indigo-600 px-3 py-2 text-sm text-zinc-50 dark:bg-indigo-700">
50 {content}
51 </div>
52 </div>
53 );
54 }
55
56 return (
57 <div
58 key={key}
59 className={`${
60 isPrevSame ? 'mt-1' : 'mt-4'
61 } flex items-end justify-start gap-3 pr-12 animate-in fade-in-0 slide-in-from-left-4 duration-300`}
62 >
63 <span className="flex-shrink-0">
64 <img
65 className="mb-0.5 flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"
66 src={avatar}
67 />
68 </span>
69
70 <div className="rounded-md bg-zinc-200 px-3 py-2 text-sm text-zinc-900 dark:bg-zinc-700 dark:text-zinc-100">
71 {content}
72 </div>
73 </div>
74 );
75 })}
76 <div ref={scrollRef} />
77 </div>
78 );
79};
80
81const ChatFooter = () => {
82 const [content, setMessageContent] = React.useState('');
83 const {send, refresh} = useChat();
84
85 const handleSubmit = async (e: React.FormEvent) => {
86 e.preventDefault();
87 const message = {content};
88
89 try {
90 setMessageContent('');
91 await send(message);
92 await refresh();
93 } catch (err) {
94 console.error('Failed to send or refresh:', err);
95 setMessageContent(message.content); // reset
96 }
97 };
98
99 return (
100 <form
101 className="border-t border-zinc-200 bg-zinc-50 py-3 text-zinc-900 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"
102 onSubmit={handleSubmit}
103 >
104 <input
105 className="flex h-8 w-full select-none appearance-none rounded-md border-none bg-transparent px-4 py-2 text-sm text-zinc-800 outline-none transition duration-200 ease-in-out placeholder:text-zinc-500 focus:border-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:text-zinc-100"
106 placeholder="Type your message..."
107 autoFocus
108 value={content}
109 onChange={(e) => setMessageContent(e.target.value)}
110 />
111 </form>
112 );
113};
114
115const Chat = () => {
116 const {open, toggle} = useChat();
117
118 return (
119 <>
120 <Launcher open={open}>
121 <button
122 className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500 text-sm font-medium text-zinc-50 ring-offset-zinc-950 transition-colors hover:bg-indigo-500/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-70"
123 onClick={toggle}
124 >
125 {open ? (
126 <XIcon className="h-5 w-5 animate-in fade-in-0 duration-200" />
127 ) : (
128 <ChatIcon className="h-5 w-5 animate-in fade-in-0 duration-200" />
129 )}
130 </button>
131 </Launcher>
132
133 <Frame className="dark:border dark:border-zinc-700" open={open}>
134 <div className="flex h-full w-full flex-1 flex-col">
135 <ChatHeader />
136 <ChatBody />
137 <ChatFooter />
138 </div>
139 </Frame>
140 </>
141 );
142};
143
144export default function App() {
145 return (
146 <ChatProvider appId={YOUR_APP_ID}>
147 <Chat />
148 </ChatProvider>
149 );
150};

Welcome to Booper ๐Ÿ‘‹

Ask us anything below.