Browse Source

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
pull/3431/head
Farzad Hayat 12 months ago
committed by GitHub
parent
commit
13ffdd8098
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 165
      modules/ROOT/examples/live-demos/full-featured/example.js
  2. 14
      modules/ROOT/examples/live-demos/full-featured/index.html
  3. 139
      modules/ROOT/examples/live-demos/full-featured/index.js
  4. 3
      modules/ROOT/pages/full-featured-premium-demo.adoc

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

@ -1,15 +1,23 @@
const fetchApi = import(
"https://unpkg.com/@microsoft/fetch-event-source@2.0.1/lib/esm/index.js"
).then((module) => module.fetchEventSource);
// This example stores the OpenAI API key in the client side integration. This is not recommended for any purpose.
// Instead, an alternate method for retrieving the API key should be used.
const openai_api_key = "<INSERT_OPENAI_API_KEY_HERE>";
const useDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches;
tinymce.init({
selector: 'textarea#full-featured',
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',
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',
tinydrive_token_provider: 'URL_TO_YOUR_TOKEN_PROVIDER',
tinydrive_dropbox_app_key: 'YOUR_DROPBOX_APP_KEY',
tinydrive_google_drive_key: 'YOUR_GOOGLE_DRIVE_KEY',
tinydrive_google_drive_client_id: 'YOUR_GOOGLE_DRIVE_CLIENT_ID',
mobile: {
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',
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',
},
menu: {
tc: {
@ -105,17 +113,6 @@ tinymce.init({
a11y_advanced_options: true,
skin: useDarkMode ? 'oxide-dark' : 'oxide',
content_css: useDarkMode ? 'dark' : 'default',
/*
The following settings require more configuration than shown here.
For information on configuring the mentions plugin, see:
https://www.tiny.cloud/docs/tinymce/6/mentions/.
*/
mentions_selector: '.mymention',
mentions_fetch: mentions_fetch, // TODO: Implement mentions_fetch
mentions_menu_hover: mentions_menu_hover, // TODO: Implement mentions_menu_hover
mentions_menu_complete: mentions_menu_complete, // TODO: Implement mentions_menu_complete
mentions_select: mentions_select, // TODO: Implement mentions_select
mentions_item_type: 'profile',
autocorrect_capitalize: true,
mergetags_list: [
{
@ -157,5 +154,145 @@ tinymce.init({
content: '<p>Initial content</p>'
},
]);
}
},
ai_request: (request, respondWith) => {
respondWith.stream((signal, streamMessage) => {
// Adds each previous query and response as individual messages
const conversation = request.thread.flatMap((event) => {
if (event.response) {
return [
{ role: "user", content: event.request.query },
{ role: "assistant", content: event.response.data },
];
} else {
return [];
}
});
// System messages provided by the plugin to format the output as HTML content.
const systemMessages = request.system.map((content) => ({
role: "system",
content,
}));
// Forms the new query sent to the API
const content =
request.context.length === 0 || conversation.length > 0
? request.query
: `Question: ${request.query} Context: """${request.context}"""`;
const messages = [
...conversation,
...systemMessages,
{ role: "user", content },
];
let hasHead = false;
let markdownHead = "";
const hasMarkdown = (message) => {
if (message.includes("`") && markdownHead !== "```") {
const numBackticks = message.split("`").length - 1;
markdownHead += "`".repeat(numBackticks);
if (hasHead && markdownHead === "```") {
markdownHead = "";
hasHead = false;
}
return true;
} else if (message.includes("html") && markdownHead === "```") {
markdownHead = "";
hasHead = true;
return true;
}
return false;
};
const requestBody = {
model: "gpt-4o",
temperature: 0.7,
max_tokens: 4000,
messages,
stream: true,
};
const openAiOptions = {
signal,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${openai_api_key}`,
},
body: JSON.stringify(requestBody),
};
const onopen = async (response) => {
if (response) {
const contentType = response.headers.get("content-type");
if (response.ok && contentType?.includes("text/event-stream")) {
return;
} else if (contentType?.includes("application/json")) {
const data = await response.json();
if (data.error) {
throw new Error(`${data.error.type}: ${data.error.message}`);
}
}
} else {
throw new Error("Failed to communicate with the ChatGPT API");
}
};
// This function passes each new message into the plugin via the `streamMessage` callback.
const onmessage = (ev) => {
const data = ev.data;
if (data !== "[DONE]") {
const parsedData = JSON.parse(data);
const firstChoice = parsedData?.choices[0];
const message = firstChoice?.delta?.content;
if (message && message !== "") {
if (!hasMarkdown(message)) {
streamMessage(message);
}
}
}
};
const onerror = (error) => {
// Stop operation and do not retry by the fetch-event-source
throw error;
};
// Use microsoft's fetch-event-source library to work around the 2000 character limit
// of the browser `EventSource` API, which requires query strings
return fetchApi
.then((fetchEventSource) =>
fetchEventSource("https://api.openai.com/v1/chat/completions", {
...openAiOptions,
openWhenHidden: true,
onopen,
onmessage,
onerror,
})
)
.then(async (response) => {
if (response && !response.ok) {
const data = await response.json();
if (data.error) {
throw new Error(`${data.error.type}: ${data.error.message}`);
}
}
})
.catch(onerror);
});
},
/*
The following settings require more configuration than shown here.
For information on configuring the mentions plugin, see:
https://www.tiny.cloud/docs/tinymce/latest/mentions/.
*/
mentions_selector: ".mymention",
mentions_fetch: mentions_fetch, // TODO: Implement mentions_fetch
mentions_menu_hover: mentions_menu_hover, // TODO: Implement mentions_menu_hover
mentions_menu_complete: mentions_menu_complete, // TODO: Implement mentions_menu_complete
mentions_select: mentions_select, // TODO: Implement mentions_select
mentions_item_type: "profile",
});

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

@ -54,6 +54,20 @@
<li>30C is 86F</li>
</ul>
<h2 class="p1"><span class="s1">🤖</span><span class="s2"><strong> Try out AI Assistant!</strong></span></h2>
<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>
<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>
<blockquote>
<p class="p2"><span class="s2">The 3Q23 financial results followed a predictable trend, reflecting the status quo from previous years.</span></p>
</blockquote>
<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>
<blockquote>
<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>
</blockquote>
<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>
<p class="p2">&nbsp;</p>
<h2>Note on the included Templates demonstration</h2>
<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>

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

@ -1,3 +1,7 @@
const fetchApi = import(
"https://unpkg.com/@microsoft/fetch-event-source@2.0.1/lib/esm/index.js"
).then((module) => module.fetchEventSource);
/* Script to import faker.js for generating random data for demonstration purposes */
tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/faker.min.js']).then(() => {
@ -140,6 +144,136 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
const useDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches;
const ai_request = (request, respondWith) => {
respondWith.stream((signal, streamMessage) => {
// Adds each previous query and response as individual messages
const conversation = request.thread.flatMap((event) => {
if (event.response) {
return [
{ role: "user", content: event.request.query },
{ role: "assistant", content: event.response.data },
];
} else {
return [];
}
});
// System messages provided by the plugin to format the output as HTML content.
const systemMessages = request.system.map((content) => ({
role: "system",
content,
}));
// Forms the new query sent to the API
const content =
request.context.length === 0 || conversation.length > 0
? request.query
: `Question: ${request.query} Context: """${request.context}"""`;
const messages = [
...conversation,
...systemMessages,
{ role: "user", content },
];
let hasHead = false;
let markdownHead = "";
const hasMarkdown = (message) => {
if (message.includes("`") && markdownHead !== "```") {
const numBackticks = message.split("`").length - 1;
markdownHead += "`".repeat(numBackticks);
if (hasHead && markdownHead === "```") {
markdownHead = "";
hasHead = false;
}
return true;
} else if (message.includes("html") && markdownHead === "```") {
markdownHead = "";
hasHead = true;
return true;
}
return false;
};
const requestBody = {
model: "gpt-4o",
temperature: 0.7,
max_tokens: 4000,
messages,
stream: true,
};
const openAiOptions = {
signal,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer {{ openai_proxy_token }}`,
},
body: JSON.stringify(requestBody),
};
const onopen = async (response) => {
if (response) {
const contentType = response.headers.get("content-type");
if (response.ok && contentType?.includes("text/event-stream")) {
return;
} else if (contentType?.includes("application/json")) {
const data = await response.json();
if (data.error) {
throw new Error(`${data.error.type}: ${data.error.message}`);
}
}
} else {
throw new Error("Failed to communicate with the ChatGPT API");
}
};
// This function passes each new message into the plugin via the `streamMessage` callback.
const onmessage = (ev) => {
const data = ev.data;
if (data !== "[DONE]") {
const parsedData = JSON.parse(data);
const firstChoice = parsedData?.choices[0];
const message = firstChoice?.delta?.content;
if (message && message !== "") {
if (!hasMarkdown(message)) {
streamMessage(message);
}
}
}
};
const onerror = (error) => {
// Stop operation and do not retry by the fetch-event-source
throw error;
};
// Use microsoft's fetch-event-source library to work around the 2000 character limit
// of the browser `EventSource` API, which requires query strings
return fetchApi
.then((fetchEventSource) =>
fetchEventSource("{{ openai_proxy_url }}", {
...openAiOptions,
openWhenHidden: true,
onopen,
onmessage,
onerror,
})
)
.then(async (response) => {
if (response && !response.ok) {
const data = await response.json();
if (data.error) {
throw new Error(`${data.error.type}: ${data.error.message}`);
}
}
})
.catch(onerror);
});
};
// revisionhistory_fetch
const fetchRevisions = () => {
return new Promise((resolve, _reject) => {
@ -285,7 +419,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
tinymce.init({
selector: 'textarea#full-featured',
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',
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',
editimage_cors_hosts: ['picsum.photos'],
tinydrive_token_provider: (success, failure) => {
success({ token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ks_BdfH4CWilyzLNk8S2gDARFhuxIauLa8PwhdEQhEo' });
@ -295,7 +429,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
tinydrive_google_drive_key: 'AIzaSyAsVRuCBc-BLQ1xNKtnLHB3AeoK-xmOrTc',
tinydrive_google_drive_client_id: '748627179519-p9vv3va1mppc66fikai92b3ru73mpukf.apps.googleusercontent.com',
mobile: {
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',
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',
},
menu: {
tc: {
@ -435,6 +569,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak
title: 'Salutation'
}
],
ai_request,
revisionhistory_fetch: fetchRevisions,
revisionhistory_author: {
id: 'john.doe',

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

@ -18,9 +18,6 @@ The following plugins are excluded from this example:
|Excluded plugins |Notes
|xref:ai.adoc[AI Assistant]
|Logistical concerns regarding exposing API keys preclude adding this.
|xref:autoresize.adoc[Autoresize]
|Resizes the editor to fit the content.

Loading…
Cancel
Save