Skip to content

Commit 9bbf2a3

Browse files
author
Farzad Hayat
authored
DOC-2453: Port AI integration guides and gpt-4o upgrade into 6 Docs (#3369)
1 parent 14b9409 commit 9bbf2a3

File tree

16 files changed

+1238
-205
lines changed

16 files changed

+1238
-205
lines changed

antora.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ asciidoc:
1414
tinydrive_live_demo_url: https://cdn.tiny.cloud/1/qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc/tinydrive/6/tinydrive.min.js
1515
webcomponent_url: https://cdn.jsdelivr.net/npm/@tinymce/tinymce-webcomponent@2/dist/tinymce-webcomponent.min.js
1616
jquery_url: https://cdn.jsdelivr.net/npm/@tinymce/tinymce-jquery@2/dist/tinymce-jquery.min.js
17+
openai_proxy_url: https://openai.ai-demo-proxy.tiny.cloud/v1/chat/completions
18+
openai_proxy_token: eyJhbGciOiJFUzM4NCJ9.eyJhdWQiOlsiaHR0cHM6Ly9vcGVuYWkuYWktZGVtby1wcm94eS50aW55LmNsb3VkLyJdLCJleHAiOjE3MTk3NTYwMDAsImh0dHBzOi8vb3BlbmFpLmFpLWRlbW8tcHJveHkudGlueS5jbG91ZC9yb2xlIjoicHVibGljLWRlbW8iLCJpc3MiOiJodHRwczovL2FpLWRlbW8tcHJveHkudGlueS5jbG91ZC8iLCJqdGkiOiJlYWU5MjBhMy05ZGQ0LTRkZjUtYTM4Yy04ODY5ZTJkZGIwYTQiLCJzdWIiOiJhaS1hc3Npc3RhbnQtZGVtbyJ9.hX44J5hPOCLOidK8mBAcnlAVh-ae4nVTq3W92xRLuGiF_4mGZWn1W3Ihgd7unlffz7GDayGxD_3hoQx8f64bTJg5hyHjwjKH1AZj1EKRal-NCNbqmoUa0TOm6_VXpRl2
1719
default_meta_keywords: tinymce, documentation, docs, plugins, customizable skins, configuration, examples, html, php, java, javascript, image editor, inline editor, distraction-free editor, classic editor, wysiwyg
1820
# product variables
1921
productname: TinyMCE

modules/ROOT/examples/live-demos/ai-demo/index.html

Lines changed: 0 additions & 30 deletions
This file was deleted.

modules/ROOT/examples/live-demos/ai-demo/index.js

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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 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 api_key = '<INSERT_API_KEY_HERE>';
8+
9+
const ai_request = (request, respondWith) => {
10+
respondWith.stream((signal, streamMessage) => {
11+
// Adds each previous query and response as individual messages
12+
const conversation = request.thread.flatMap((event) => {
13+
if (event.response) {
14+
return [
15+
{ role: 'user', content: event.request.query },
16+
{ role: 'assistant', content: event.response.data },
17+
];
18+
} else {
19+
return [];
20+
}
21+
});
22+
23+
// System messages provided by the plugin to format the output as HTML content.
24+
const systemMessages = request.system.map((content) => ({
25+
role: 'system',
26+
content,
27+
}));
28+
29+
// Forms the new query sent to the API
30+
const content =
31+
request.context.length === 0 || conversation.length > 0
32+
? request.query
33+
: `Question: ${request.query} Context: """${request.context}"""`;
34+
35+
const messages = [
36+
...conversation,
37+
...systemMessages,
38+
{ role: 'user', content },
39+
];
40+
41+
let hasHead = false;
42+
let markdownHead = '';
43+
44+
const hasMarkdown = (message) => {
45+
if (message.includes('`') && markdownHead !== '```') {
46+
const numBackticks = message.split('`').length - 1;
47+
markdownHead += '`'.repeat(numBackticks);
48+
if (hasHead && markdownHead === '```') {
49+
markdownHead = '';
50+
hasHead = false;
51+
}
52+
return true;
53+
} else if (message.includes('html') && markdownHead === '```') {
54+
markdownHead = '';
55+
hasHead = true;
56+
return true;
57+
}
58+
return false;
59+
};
60+
61+
const requestBody = {
62+
model: 'gpt-4o',
63+
temperature: 0.7,
64+
max_tokens: 4000,
65+
messages,
66+
stream: true,
67+
};
68+
69+
const openAiOptions = {
70+
signal,
71+
method: 'POST',
72+
headers: {
73+
'Content-Type': 'application/json',
74+
'Authorization': `Bearer ${api_key}`,
75+
},
76+
body: JSON.stringify(requestBody),
77+
};
78+
79+
const onopen = async (response) => {
80+
if (response) {
81+
const contentType = response.headers.get('content-type');
82+
if (response.ok && contentType?.includes('text/event-stream')) {
83+
return;
84+
} else if (contentType?.includes('application/json')) {
85+
const data = await response.json();
86+
if (data.error) {
87+
throw new Error(
88+
`${data.error.type}: ${data.error.message}`
89+
);
90+
}
91+
}
92+
} else {
93+
throw new Error('Failed to communicate with the ChatGPT API');
94+
}
95+
};
96+
97+
// This function passes each new message into the plugin via the `streamMessage` callback.
98+
const onmessage = (ev) => {
99+
const data = ev.data;
100+
if (data !== '[DONE]') {
101+
const parsedData = JSON.parse(data);
102+
const firstChoice = parsedData?.choices[0];
103+
const message = firstChoice?.delta?.content;
104+
if (message && message !== '') {
105+
if (!hasMarkdown(message)) {
106+
streamMessage(message);
107+
}
108+
}
109+
}
110+
};
111+
112+
const onerror = (error) => {
113+
// Stop operation and do not retry by the fetch-event-source
114+
throw error;
115+
};
116+
117+
// Use microsoft's fetch-event-source library to work around the 2000 character limit
118+
// of the browser `EventSource` API, which requires query strings
119+
return fetchApi
120+
.then((fetchEventSource) =>
121+
fetchEventSource('https://api.openai.com/v1/chat/completions', {
122+
...openAiOptions,
123+
openWhenHidden: true,
124+
onopen,
125+
onmessage,
126+
onerror,
127+
})
128+
)
129+
.then(async (response) => {
130+
if (response && !response.ok) {
131+
const data = await response.json();
132+
if (data.error) {
133+
throw new Error(
134+
`${data.error.type}: ${data.error.message}`
135+
);
136+
}
137+
}
138+
})
139+
.catch(onerror);
140+
});
141+
};
142+
143+
tinymce.init({
144+
selector: 'textarea', // change this value according to your HTML
145+
plugins: 'ai advlist anchor autolink charmap advcode emoticons fullscreen help image link lists media preview searchreplace table',
146+
toolbar: 'undo redo | aidialog aishortcuts | styles fontsizeinput | bold italic | align bullist numlist | table link image | code',
147+
height: 650,
148+
ai_request,
149+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<textarea id="ai">
2+
<h1 class="p1"><span class="s1">🤖</span><span class="s2"><strong> Try out AI Assistant!</strong></span></h1>
3+
<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>
4+
<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>
5+
<blockquote>
6+
<p class="p2"><span class="s2">The 3Q23 financial results followed a predictable trend, reflecting the status quo from previous years.</span></p>
7+
</blockquote>
8+
<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>
9+
<blockquote>
10+
<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>
11+
</blockquote>
12+
<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>
13+
</textarea>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
const fetchApi = import(
2+
'https://unpkg.com/@microsoft/[email protected]/lib/esm/index.js'
3+
).then((module) => module.fetchEventSource);
4+
5+
const ai_request = (request, respondWith) => {
6+
respondWith.stream((signal, streamMessage) => {
7+
// Adds each previous query and response as individual messages
8+
const conversation = request.thread.flatMap((event) => {
9+
if (event.response) {
10+
return [
11+
{ role: 'user', content: event.request.query },
12+
{ role: 'assistant', content: event.response.data },
13+
];
14+
} else {
15+
return [];
16+
}
17+
});
18+
19+
// System messages provided by the plugin to format the output as HTML content.
20+
const systemMessages = request.system.map((content) => ({
21+
role: 'system',
22+
content,
23+
}));
24+
25+
// Forms the new query sent to the API
26+
const content =
27+
request.context.length === 0 || conversation.length > 0
28+
? request.query
29+
: `Question: ${request.query} Context: """${request.context}"""`;
30+
31+
const messages = [
32+
...conversation,
33+
...systemMessages,
34+
{ role: 'user', content },
35+
];
36+
37+
let hasHead = false;
38+
let markdownHead = '';
39+
40+
const hasMarkdown = (message) => {
41+
if (message.includes('`') && markdownHead !== '```') {
42+
const numBackticks = message.split('`').length - 1;
43+
markdownHead += '`'.repeat(numBackticks);
44+
if (hasHead && markdownHead === '```') {
45+
markdownHead = '';
46+
hasHead = false;
47+
}
48+
return true;
49+
} else if (message.includes('html') && markdownHead === '```') {
50+
markdownHead = '';
51+
hasHead = true;
52+
return true;
53+
}
54+
return false;
55+
};
56+
57+
const requestBody = {
58+
model: 'gpt-4o',
59+
temperature: 0.7,
60+
max_tokens: 4000,
61+
messages,
62+
stream: true,
63+
};
64+
65+
const openAiOptions = {
66+
signal,
67+
method: 'POST',
68+
headers: {
69+
'Content-Type': 'application/json',
70+
'Authorization': `Bearer {{ openai_proxy_token }}`,
71+
},
72+
body: JSON.stringify(requestBody),
73+
};
74+
75+
const onopen = async (response) => {
76+
if (response) {
77+
const contentType = response.headers.get('content-type');
78+
if (response.ok && contentType?.includes('text/event-stream')) {
79+
return;
80+
} else if (contentType?.includes('application/json')) {
81+
const data = await response.json();
82+
if (data.error) {
83+
throw new Error(
84+
`${data.error.type}: ${data.error.message}`
85+
);
86+
}
87+
}
88+
} else {
89+
throw new Error('Failed to communicate with the ChatGPT API');
90+
}
91+
};
92+
93+
// This function passes each new message into the plugin via the `streamMessage` callback.
94+
const onmessage = (ev) => {
95+
const data = ev.data;
96+
if (data !== '[DONE]') {
97+
const parsedData = JSON.parse(data);
98+
const firstChoice = parsedData?.choices[0];
99+
const message = firstChoice?.delta?.content;
100+
if (message && message !== '') {
101+
if (!hasMarkdown(message)) {
102+
streamMessage(message);
103+
}
104+
}
105+
}
106+
};
107+
108+
const onerror = (error) => {
109+
// Stop operation and do not retry by the fetch-event-source
110+
throw error;
111+
};
112+
113+
// Use microsoft's fetch-event-source library to work around the 2000 character limit
114+
// of the browser `EventSource` API, which requires query strings
115+
return fetchApi
116+
.then((fetchEventSource) =>
117+
fetchEventSource('{{ openai_proxy_url }}', {
118+
...openAiOptions,
119+
openWhenHidden: true,
120+
onopen,
121+
onmessage,
122+
onerror,
123+
})
124+
)
125+
.then(async (response) => {
126+
if (response && !response.ok) {
127+
const data = await response.json();
128+
if (data.error) {
129+
throw new Error(
130+
`${data.error.type}: ${data.error.message}`
131+
);
132+
}
133+
}
134+
})
135+
.catch(onerror);
136+
});
137+
};
138+
139+
tinymce.init({
140+
selector: 'textarea', // change this value according to your HTML
141+
plugins: 'ai advlist anchor autolink charmap advcode emoticons fullscreen help image link lists media preview searchreplace table',
142+
toolbar: 'undo redo | aidialog aishortcuts | styles fontsizeinput | bold italic | align bullist numlist | table link image | code',
143+
height: 650,
144+
ai_request,
145+
});
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 15 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)