PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / HelpCenter / pages / AIChat.jsx

AIChat.jsx in Extendify 3.2.1, at src/HelpCenter/pages/AIChat.jsx

187 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { Answer } from '@help-center/components/ai-chat/Answer';
2 import { History } from '@help-center/components/ai-chat/History';
3 import { Nav } from '@help-center/components/ai-chat/Nav';
4 import { Question } from '@help-center/components/ai-chat/Question';
5 import { getAnswer } from '@help-center/lib/api';
6 import { useAIChatStore } from '@help-center/state/ai-chat';
7 import { useAIConsentStore } from '@shared/state/ai-consent';
8 import { useLayoutEffect, useState } from '@wordpress/element';
9 import { __, isRTL } from '@wordpress/i18n';
10 import {
11 chevronLeft,
12 chevronRight,
13 Icon,
14 postComments,
15 } from '@wordpress/icons';
16 import { AnimatePresence, motion } from 'framer-motion';
17
18 export const AIChatDashboard = ({ onOpen }) => {
19 return (
20 <section className="">
21 <button
22 data-test="help-center-dashboard-ai-chat-button"
23 type="button"
24 onClick={onOpen}
25 className="m-0 flex w-full justify-between gap-2 rounded-md border border-gray-200 bg-transparent p-2.5 text-left hover:bg-gray-100 rtl:text-right"
26 >
27 <Icon
28 icon={postComments}
29 className="rounded-full border-0 bg-design-main fill-design-text p-2"
30 size={48}
31 />
32 <div className="grow pl-1">
33 <h1 className="m-0 p-0 text-lg font-medium">
34 {__('Ask AI', 'extendify-local')}
35 </h1>
36 <p className="m-0 p-0 text-xs text-gray-800">
37 {__('Got questions? Ask our AI chatbot', 'extendify-local')}
38 </p>
39 </div>
40 <div className="flex h-12 grow-0 items-center justify-end">
41 <Icon
42 icon={isRTL() ? chevronLeft : chevronRight}
43 size={24}
44 className="fill-current text-gray-700"
45 />
46 </div>
47 </button>
48 </section>
49 );
50 };
51
52 export const AIChat = () => {
53 const [question, setQuestion] = useState(undefined);
54 const [answer, setAnswer] = useState(undefined);
55 const [answerId, setAnswerId] = useState(undefined);
56 const [error, setError] = useState(false);
57
58 const [showHistory, setShowHistory] = useState(false);
59 const { experienceLevel, currentQuestion, setCurrentQuestion } =
60 useAIChatStore();
61
62 const shouldShowAIConsent = useAIConsentStore((state) =>
63 state.shouldShowAIConsent('help-center'),
64 );
65
66 const reset = () => {
67 setQuestion(undefined);
68 setAnswer(undefined);
69 setAnswerId(undefined);
70 setError(false);
71 setShowHistory(false);
72 setCurrentQuestion(undefined);
73 };
74
75 const handleSubmit = async (formSubmitEvent) => {
76 formSubmitEvent.preventDefault();
77 const q = formSubmitEvent.target?.[0]?.value ?? '';
78 if (!q) return;
79 setAnswer('...');
80 setQuestion(q);
81 const response = await getAnswer({ question: q, experienceLevel });
82 if (!response.ok) {
83 setError(true);
84 return;
85 }
86 try {
87 const reader = response.body.getReader();
88 const decoder = new TextDecoder();
89 while (true) {
90 const { value, done } = await reader.read();
91 if (done) break;
92 const chunk = decoder.decode(value);
93 setAnswer((v) => {
94 if (v === '...') return chunk;
95 // For backward compatibility we remove the json appended to the end
96 return (v + chunk).replace(/\{"id":"[a-zA-Z0-9]+"\}/g, '');
97 });
98 }
99 setAnswerId(response.headers.get('x-extendify-chat-id') || undefined);
100 } catch (e) {
101 console.error(e);
102 }
103 };
104
105 useLayoutEffect(() => {
106 setQuestion(currentQuestion?.question);
107 setAnswer(currentQuestion?.htmlAnswer);
108 setShowHistory(false);
109 }, [currentQuestion]);
110
111 if (shouldShowAIConsent) {
112 return <ConsentOverlay />;
113 }
114
115 if (question) {
116 return (
117 <Answer
118 question={question}
119 answer={answer}
120 answerId={answerId}
121 reset={reset}
122 error={error}
123 />
124 );
125 }
126
127 return (
128 <>
129 <section className="flex h-full flex-col">
130 <Nav setShowHistory={setShowHistory} showHistory={showHistory} />
131 <div className="flex grow items-center bg-design-main p-6 text-design-text">
132 <Question onSubmit={handleSubmit} />
133 </div>
134 </section>
135 <AnimatePresence>
136 {showHistory && (
137 <motion.section
138 // slide up from bottom 100%
139 initial={{ x: 50 }}
140 animate={{ x: 0 }}
141 exit={{ x: 0 }}
142 transition={{ duration: 0.2 }}
143 style={{ '--ext-design-text': '#000000' }}
144 className="absolute bottom-0 left-0 right-0 top-0 z-20 ml-4 mt-4 flex h-full flex-col overflow-hidden rounded-tl-lg bg-white shadow-2xl"
145 >
146 <History setShowHistory={setShowHistory} />
147 </motion.section>
148 )}
149 </AnimatePresence>
150 </>
151 );
152 };
153
154 const ConsentOverlay = () => {
155 const { consentTerms, setUserGaveConsent } = useAIConsentStore();
156
157 return (
158 <div
159 data-test="help-center-ai-chat-consent-prompt"
160 className="absolute inset-0 flex items-center justify-center bg-black/75 p-6"
161 >
162 <div className="rounded-sm bg-white p-4">
163 <h2 className="mb-2 mt-0 text-lg">
164 {__('Terms of Use', 'extendify-local')}
165 </h2>
166 <p className="m-0" dangerouslySetInnerHTML={{ __html: consentTerms }} />
167 <button
168 data-test="help-center-ai-chat-consent-accept-button"
169 className="mt-4 w-full rounded-sm border-0 bg-design-main px-4 py-2 text-center text-white"
170 type="button"
171 onClick={() => setUserGaveConsent(true)}
172 >
173 {__('Accept', 'extendify-local')}
174 </button>
175 </div>
176 </div>
177 );
178 };
179
180 export const routes = [
181 {
182 slug: 'ai-chat',
183 title: __('AI Chatbot', 'extendify-local'),
184 component: AIChat,
185 },
186 ];
187