Skip to content

Commit 13ffdd8

Browse files
author
Farzad Hayat
authored
DOC-2494: Add AI Assistant to full-featured demo (7 Docs) (#3428)
* DOC-2494: Add AI Assistant to full-featured demo * DOC-2494: Remove AI Assistant from Excluded plugins in full-featured-premium-demo.adoc
1 parent daa732e commit 13ffdd8

File tree

4 files changed

+302
-19
lines changed

4 files changed

+302
-19
lines changed

modules/ROOT/examples/live-demos/full-featured/example.js

Lines changed: 151 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
const fetchApi = import(
2+
"https://unpkg.com/@microsoft/[email protected]/lib/esm/index.js"
3+
).then((module) => module.fetchEventSource);
4+
5+
// This example stores the OpenAI API key in the client side integration. This is not recommended for any purpose.
6+
// Instead, an alternate method for retrieving the API key should be used.
7+
const openai_api_key = "<INSERT_OPENAI_API_KEY_HERE>";
8+
19
const useDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
210
const isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches;
311

412
tinymce.init({
513
selector: 'textarea#full-featured',
6-
plugins: 'preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker editimage help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate markdown revisionhistory',
14+
plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker editimage help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate markdown revisionhistory',
715
tinydrive_token_provider: 'URL_TO_YOUR_TOKEN_PROVIDER',
816
tinydrive_dropbox_app_key: 'YOUR_DROPBOX_APP_KEY',
917
tinydrive_google_drive_key: 'YOUR_GOOGLE_DRIVE_KEY',
1018
tinydrive_google_drive_client_id: 'YOUR_GOOGLE_DRIVE_CLIENT_ID',
1119
mobile: {
12-
plugins: 'preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate',
20+
plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate',
1321
},
1422
menu: {
1523
tc: {
@@ -105,17 +113,6 @@ tinymce.init({
105113
a11y_advanced_options: true,
106114
skin: useDarkMode ? 'oxide-dark' : 'oxide',
107115
content_css: useDarkMode ? 'dark' : 'default',
108-
/*
109-
The following settings require more configuration than shown here.
110-
For information on configuring the mentions plugin, see:
111-
https://www.tiny.cloud/docs/tinymce/6/mentions/.
112-
*/
113-
mentions_selector: '.mymention',
114-
mentions_fetch: mentions_fetch, // TODO: Implement mentions_fetch
115-
mentions_menu_hover: mentions_menu_hover, // TODO: Implement mentions_menu_hover
116-
mentions_menu_complete: mentions_menu_complete, // TODO: Implement mentions_menu_complete
117-
mentions_select: mentions_select, // TODO: Implement mentions_select
118-
mentions_item_type: 'profile',
119116
autocorrect_capitalize: true,
120117
mergetags_list: [
121118
{
@@ -157,5 +154,145 @@ tinymce.init({
157154
content: '<p>Initial content</p>'
158155
},
159156
]);
160-
}
157+
},
158+
ai_request: (request, respondWith) => {
159+
respondWith.stream((signal, streamMessage) => {
160+
// Adds each previous query and response as individual messages
161+
const conversation = request.thread.flatMap((event) => {
162+
if (event.response) {
163+
return [
164+
{ role: "user", content: event.request.query },
165+
{ role: "assistant", content: event.response.data },
166+
];
167+
} else {
168+
return [];
169+
}
170+
});
171+
172+
// System messages provided by the plugin to format the output as HTML content.
173+
const systemMessages = request.system.map((content) => ({
174+
role: "system",
175+
content,
176+
}));
177+
178+
// Forms the new query sent to the API
179+
const content =
180+
request.context.length === 0 || conversation.length > 0
181+
? request.query
182+
: `Question: ${request.query} Context: """${request.context}"""`;
183+
184+
const messages = [
185+
...conversation,
186+
...systemMessages,
187+
{ role: "user", content },
188+
];
189+
190+
let hasHead = false;
191+
let markdownHead = "";
192+
193+
const hasMarkdown = (message) => {
194+
if (message.includes("`") && markdownHead !== "```") {
195+
const numBackticks = message.split("`").length - 1;
196+
markdownHead += "`".repeat(numBackticks);
197+
if (hasHead && markdownHead === "```") {
198+
markdownHead = "";
199+
hasHead = false;
200+
}
201+
return true;
202+
} else if (message.includes("html") && markdownHead === "```") {
203+
markdownHead = "";
204+
hasHead = true;
205+
return true;
206+
}
207+
return false;
208+
};
209+
210+
const requestBody = {
211+
model: "gpt-4o",
212+
temperature: 0.7,
213+
max_tokens: 4000,
214+
messages,
215+
stream: true,
216+
};
217+
218+
const openAiOptions = {
219+
signal,
220+
method: "POST",
221+
headers: {
222+
"Content-Type": "application/json",
223+
Authorization: `Bearer ${openai_api_key}`,
224+
},
225+
body: JSON.stringify(requestBody),
226+
};
227+
228+
const onopen = async (response) => {
229+
if (response) {
230+
const contentType = response.headers.get("content-type");
231+
if (response.ok && contentType?.includes("text/event-stream")) {
232+
return;
233+
} else if (contentType?.includes("application/json")) {
234+
const data = await response.json();
235+
if (data.error) {
236+
throw new Error(`${data.error.type}: ${data.error.message}`);
237+
}
238+
}
239+
} else {
240+
throw new Error("Failed to communicate with the ChatGPT API");
241+
}
242+
};
243+
244+
// This function passes each new message into the plugin via the `streamMessage` callback.
245+
const onmessage = (ev) => {
246+
const data = ev.data;
247+
if (data !== "[DONE]") {
248+
const parsedData = JSON.parse(data);
249+
const firstChoice = parsedData?.choices[0];
250+
const message = firstChoice?.delta?.content;
251+
if (message && message !== "") {
252+
if (!hasMarkdown(message)) {
253+
streamMessage(message);
254+
}
255+
}
256+
}
257+
};
258+
259+
const onerror = (error) => {
260+
// Stop operation and do not retry by the fetch-event-source
261+
throw error;
262+
};
263+
264+
// Use microsoft's fetch-event-source library to work around the 2000 character limit
265+
// of the browser `EventSource` API, which requires query strings
266+
return fetchApi
267+
.then((fetchEventSource) =>
268+
fetchEventSource("https://api.openai.com/v1/chat/completions", {
269+
...openAiOptions,
270+
openWhenHidden: true,
271+
onopen,
272+
onmessage,
273+
onerror,
274+
})
275+
)
276+
.then(async (response) => {
277+
if (response && !response.ok) {
278+
const data = await response.json();
279+
if (data.error) {
280+
throw new Error(`${data.error.type}: ${data.error.message}`);
281+
}
282+
}
283+
})
284+
.catch(onerror);
285+
});
286+
},
287+
/*
288+
The following settings require more configuration than shown here.
289+
For information on configuring the mentions plugin, see:
290+
https://www.tiny.cloud/docs/tinymce/latest/mentions/.
291+
*/
292+
mentions_selector: ".mymention",
293+
mentions_fetch: mentions_fetch, // TODO: Implement mentions_fetch
294+
mentions_menu_hover: mentions_menu_hover, // TODO: Implement mentions_menu_hover
295+
mentions_menu_complete: mentions_menu_complete, // TODO: Implement mentions_menu_complete
296+
mentions_select: mentions_select, // TODO: Implement mentions_select
297+
mentions_item_type: "profile",
161298
});

modules/ROOT/examples/live-demos/full-featured/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ <h2>Character strings to demonstrate some of the Advanced Typography plugin’s
5454
<li>30C is 86F</li>
5555
</ul>
5656

57+
<h2 class="p1"><span class="s1">🤖</span><span class="s2"><strong> Try out AI Assistant!</strong></span></h2>
58+
59+
<p class="p2"><span class="s2">Below are just a few of the ways you can use AI Assistant within your app. Since you can define your own custom prompts, the sky really is the limit!</span></p>
60+
<p class="p2"><span class="s2"><strong>&nbsp;</strong></span><span class="s3">🎭</span><span class="s2"><strong> Changing tone </strong>&ndash;<strong>&nbsp;</strong>Lighten up the sentence below by selecting the text, clicking <img src="{{imagesdir}}/ai-plugin/wand-icon.svg" width="20" height="20"/>,&nbsp;and choosing <em>Change tone &gt; Friendly</em>.</span></p>
61+
<blockquote>
62+
<p class="p2"><span class="s2">The 3Q23 financial results followed a predictable trend, reflecting the status quo from previous years.</span></p>
63+
</blockquote>
64+
<p class="p2"><span class="s3">📝</span><span class="s2"><strong> Summarizing&nbsp;</strong>&ndash; Below is a long paragraph that people may not want to read from start to finish. Get a quick summary by selecting the text, clicking <img src="{{imagesdir}}/ai-plugin/wand-icon.svg" width="20" height="20"/>,&nbsp;and choosing <em>Summarize content</em>.</span></p>
65+
<blockquote>
66+
<p class="p2"><span class="s2">Population growth in the 17th century was marked by significant increment in the number of people around the world. Various factors contributed to this demographic trend. Firstly, advancements in agriculture and technology resulted in increased food production and improved living conditions. This led to decreased mortality rates and better overall health, allowing for more individuals to survive and thrive. Additionally, the exploration and expansion of European powers, such as colonization efforts, fostered migration and settlement in new territories.</span></p>
67+
</blockquote>
68+
<p class="p2"><span class="s3">💡</span><span class="s2"><strong> Writing from scratch</strong> &ndash; Ask AI Assistant to generate content from scratch by clicking <img src="{{imagesdir}}/ai-plugin/ai-icon.svg" width="20" height="20"/>, and typing&nbsp;<em>Write a marketing email announcing TinyMCE's new AI Assistant plugin</em>.</span></p>
69+
<p class="p2">&nbsp;</p>
70+
5771
<h2>Note on the included Templates demonstration</h2>
5872

5973
<p>The included Templates demonstration uses the <a class="mceNonEditable" href="{{site-url}}/tinymce/7/advanced-templates/#advtemplate_list"><code>advtemplate_list</code></a> configuration option to return a local promise containing a basic Template structure with self-contained examples.</p>

modules/ROOT/examples/live-demos/full-featured/index.js

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
const fetchApi = import(
2+
"https://unpkg.com/@microsoft/[email protected]/lib/esm/index.js"
3+
).then((module) => module.fetchEventSource);
4+
15
/* Script to import faker.js for generating random data for demonstration purposes */
26
tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/faker.min.js']).then(() => {
37

@@ -140,6 +144,136 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
140144
const useDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
141145
const isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches;
142146

147+
const ai_request = (request, respondWith) => {
148+
respondWith.stream((signal, streamMessage) => {
149+
// Adds each previous query and response as individual messages
150+
const conversation = request.thread.flatMap((event) => {
151+
if (event.response) {
152+
return [
153+
{ role: "user", content: event.request.query },
154+
{ role: "assistant", content: event.response.data },
155+
];
156+
} else {
157+
return [];
158+
}
159+
});
160+
161+
// System messages provided by the plugin to format the output as HTML content.
162+
const systemMessages = request.system.map((content) => ({
163+
role: "system",
164+
content,
165+
}));
166+
167+
// Forms the new query sent to the API
168+
const content =
169+
request.context.length === 0 || conversation.length > 0
170+
? request.query
171+
: `Question: ${request.query} Context: """${request.context}"""`;
172+
173+
const messages = [
174+
...conversation,
175+
...systemMessages,
176+
{ role: "user", content },
177+
];
178+
179+
let hasHead = false;
180+
let markdownHead = "";
181+
182+
const hasMarkdown = (message) => {
183+
if (message.includes("`") && markdownHead !== "```") {
184+
const numBackticks = message.split("`").length - 1;
185+
markdownHead += "`".repeat(numBackticks);
186+
if (hasHead && markdownHead === "```") {
187+
markdownHead = "";
188+
hasHead = false;
189+
}
190+
return true;
191+
} else if (message.includes("html") && markdownHead === "```") {
192+
markdownHead = "";
193+
hasHead = true;
194+
return true;
195+
}
196+
return false;
197+
};
198+
199+
const requestBody = {
200+
model: "gpt-4o",
201+
temperature: 0.7,
202+
max_tokens: 4000,
203+
messages,
204+
stream: true,
205+
};
206+
207+
const openAiOptions = {
208+
signal,
209+
method: "POST",
210+
headers: {
211+
"Content-Type": "application/json",
212+
Authorization: `Bearer {{ openai_proxy_token }}`,
213+
},
214+
body: JSON.stringify(requestBody),
215+
};
216+
217+
const onopen = async (response) => {
218+
if (response) {
219+
const contentType = response.headers.get("content-type");
220+
if (response.ok && contentType?.includes("text/event-stream")) {
221+
return;
222+
} else if (contentType?.includes("application/json")) {
223+
const data = await response.json();
224+
if (data.error) {
225+
throw new Error(`${data.error.type}: ${data.error.message}`);
226+
}
227+
}
228+
} else {
229+
throw new Error("Failed to communicate with the ChatGPT API");
230+
}
231+
};
232+
233+
// This function passes each new message into the plugin via the `streamMessage` callback.
234+
const onmessage = (ev) => {
235+
const data = ev.data;
236+
if (data !== "[DONE]") {
237+
const parsedData = JSON.parse(data);
238+
const firstChoice = parsedData?.choices[0];
239+
const message = firstChoice?.delta?.content;
240+
if (message && message !== "") {
241+
if (!hasMarkdown(message)) {
242+
streamMessage(message);
243+
}
244+
}
245+
}
246+
};
247+
248+
const onerror = (error) => {
249+
// Stop operation and do not retry by the fetch-event-source
250+
throw error;
251+
};
252+
253+
// Use microsoft's fetch-event-source library to work around the 2000 character limit
254+
// of the browser `EventSource` API, which requires query strings
255+
return fetchApi
256+
.then((fetchEventSource) =>
257+
fetchEventSource("{{ openai_proxy_url }}", {
258+
...openAiOptions,
259+
openWhenHidden: true,
260+
onopen,
261+
onmessage,
262+
onerror,
263+
})
264+
)
265+
.then(async (response) => {
266+
if (response && !response.ok) {
267+
const data = await response.json();
268+
if (data.error) {
269+
throw new Error(`${data.error.type}: ${data.error.message}`);
270+
}
271+
}
272+
})
273+
.catch(onerror);
274+
});
275+
};
276+
143277
// revisionhistory_fetch
144278
const fetchRevisions = () => {
145279
return new Promise((resolve, _reject) => {
@@ -285,7 +419,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
285419

286420
tinymce.init({
287421
selector: 'textarea#full-featured',
288-
plugins: 'preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker editimage help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate markdown revisionhistory',
422+
plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker editimage help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate markdown revisionhistory',
289423
editimage_cors_hosts: ['picsum.photos'],
290424
tinydrive_token_provider: (success, failure) => {
291425
success({ token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ks_BdfH4CWilyzLNk8S2gDARFhuxIauLa8PwhdEQhEo' });
@@ -295,7 +429,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
295429
tinydrive_google_drive_key: 'AIzaSyAsVRuCBc-BLQ1xNKtnLHB3AeoK-xmOrTc',
296430
tinydrive_google_drive_client_id: '748627179519-p9vv3va1mppc66fikai92b3ru73mpukf.apps.googleusercontent.com',
297431
mobile: {
298-
plugins: 'preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate',
432+
plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate',
299433
},
300434
menu: {
301435
tc: {
@@ -435,6 +569,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
435569
title: 'Salutation'
436570
}
437571
],
572+
ai_request,
438573
revisionhistory_fetch: fetchRevisions,
439574
revisionhistory_author: {
440575
id: 'john.doe',

modules/ROOT/pages/full-featured-premium-demo.adoc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ The following plugins are excluded from this example:
1818
|Excluded plugins |Notes
1919

2020

21-
|xref:ai.adoc[AI Assistant]
22-
|Logistical concerns regarding exposing API keys preclude adding this.
23-
2421
|xref:autoresize.adoc[Autoresize]
2522
|Resizes the editor to fit the content.
2623

0 commit comments

Comments
 (0)