-
Notifications
You must be signed in to change notification settings - Fork 221
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DOC-2494: Add AI Assistant to full-featured demo
- Loading branch information
1 parent
b61c847
commit e173f8a
Showing
3 changed files
with
292 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,23 @@ | ||
const fetchApi = import( | ||
"https://unpkg.com/@microsoft/[email protected]/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 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 export footnotes mergetags autocorrect typography advtemplate', | ||
plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link 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 export footnotes mergetags autocorrect typography advtemplate', | ||
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 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 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: { | ||
|
@@ -148,5 +156,134 @@ tinymce.init({ | |
value: 'Salutation', | ||
title: 'Salutation' | ||
} | ||
] | ||
], | ||
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); | ||
}); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
const fetchApi = import( | ||
"https://unpkg.com/@microsoft/[email protected]/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,9 +144,139 @@ 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); | ||
}); | ||
}; | ||
|
||
tinymce.init({ | ||
selector: 'textarea#full-featured', | ||
plugins: 'preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link 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 export footnotes mergetags autocorrect typography advtemplate', | ||
plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link 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 export footnotes mergetags autocorrect typography advtemplate', | ||
editimage_cors_hosts: ['picsum.photos'], | ||
tinydrive_token_provider: (success, failure) => { | ||
success({ token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ks_BdfH4CWilyzLNk8S2gDARFhuxIauLa8PwhdEQhEo' }); | ||
|
@@ -152,7 +286,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 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 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: { | ||
|
@@ -291,6 +425,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak | |
value: 'Salutation', | ||
title: 'Salutation' | ||
} | ||
] | ||
], | ||
ai_request, | ||
}); | ||
}); |