diff --git a/antora.yml b/antora.yml index 3a5f74aec9..31c676dd42 100644 --- a/antora.yml +++ b/antora.yml @@ -25,6 +25,9 @@ asciidoc: exportpdf_service_url: exportpdf_service_url placeholder # TinyMCE AI demo API key tinymceai_demo_api_key: qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc + # TinyMCE AI MCP demo API key - only for MCP live demos, which require additional service access + tinymceai_mcp_demo_api_key: 3ibmjj7hijc8a4pgc2er8hgq781a2q2r81aj5dbj0wzq9a6c + tinymceai_mcp_live_demo_url: https://cdn.tiny.cloud/1/3ibmjj7hijc8a4pgc2er8hgq781a2q2r81aj5dbj0wzq9a6c/tinymce/8/tinymce.min.js # product variables productname: TinyMCE productmajorversion: 8 diff --git a/modules/ROOT/attachments/tinymceai-mcp-demo/content.css b/modules/ROOT/attachments/tinymceai-mcp-demo/content.css new file mode 100644 index 0000000000..63d0a8cd77 --- /dev/null +++ b/modules/ROOT/attachments/tinymceai-mcp-demo/content.css @@ -0,0 +1,366 @@ +/* Main demo content styles */ + +/* Fixing always showing comments */ +.tox-comment[contenteditable=false]:not([data-mce-selected]), span.tox-comment img:not([data-mce-selected]), span.tox-comment span.mce-preview-object:not([data-mce-selected]), span.tox-comment > audio:not([data-mce-selected]), span.tox-comment > video:not([data-mce-selected]) { + outline:3px solid #ffe796 +} +span.tox-comment:not([data-mce-selected]) { + background-color: #ffe796; + outline:0 +} +body:not(.tox-comments-visible) span.tox-comment[data-mce-selected] { + background-color: #ffe796; + outline:0 +} + +/* Fix issue with overlapping comments */ +.tox-comments-visible span.tox-comment[data-mce-selected] { + z-index: 1; + position: relative; +} + +/* Hide comments in AI previews */ +.tox-tinymceai__preview-body span.tox-comment { + background-color: transparent; + outline: 0; +} + +/* Fixing placeholder */ +.mce-content-body:not([dir=rtl])[data-mce-placeholder]:not(.mce-visualblocks)::before { + left: calc(var(--root-padding) * 2) !important; +} + +/* Improvement to mergetag making them selectable */ +.mce-mergetag { + user-select: all !important; + -webkit-user-select: all !important; +} + +/* Make mergetag background appear selected on firefox */ +.mce-mergetag::selection { + background-color: #b3d8ff; +} + +/* --- CONTENT STYLES -- */ + +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + --root-padding: 2rem; + --bg-color: #f7faff; + --dash-color: #d4e1f9; + --contrast-text-color: #1a1a1a; + background: var(--bg-color); + padding: var(--root-padding); + + &::before, + &::after { + content: ''; + display: block; + margin: 0 calc(50% - 50vw); + border-bottom: 1px dashed var(--dash-color); + pointer-events: none; + } + @media (width < 750px) { + --root-padding: 1rem; + } +} + +body { + font: 16px / 28px 'Inter', Arial, sans-serif; + max-width: 900px; + margin: 0 auto; + padding: var(--root-padding); + color: #2c2c2c; + background: #fff; + background-clip: padding-box; + border-right: 1px dashed var(--dash-color); + border-left: 1px dashed var(--dash-color); + overflow-x: hidden; +} + +.hero { + container-type: inline-size; + container-name: hero; + position: relative; + display: grid; + grid-template-columns: 6fr 4fr; + gap: 2rem; + margin: 0 calc(var(--root-padding) * -1) 1rem; + padding: calc(var(--root-padding) / 2) var(--root-padding); + background: #fff; + border-bottom: var(--root-padding) solid var(--bg-color); + + &:first-child, + [data-mce-bogus] + &:nth-child(2) { + margin-top: calc(var(--root-padding) * -1 - 1px); + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + width: 100vw; + margin-left: -50vw; + border-top: 1px dashed var(--dash-color); + pointer-events: none; + } + &::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + width: 100vw; + height: var(--root-padding); + margin-left: -50vw; + border: 1px dashed var(--dash-color); + border-style: dashed none; + pointer-events: none; + } +} + +@media (width >= 750px) { + .hero__aside { + text-align: right; + margin: 0; + padding-top: 1rem; + } +} + +@media (width < 750px) { + .hero__main { + grid-column: 1 / -1; + } + .hero__aside { + grid-column: 1 / -1; + } +} + +h1 { + font-family: 'Inter', Arial, sans-serif; + font-weight: 200; + font-size: 2.6rem; + line-height: 1.2; + margin-bottom: 0.25em; + color: var(--contrast-text-color); + + @media (width < 750px) { + font-size: 2rem; + } +} + +h1 strong { + font-size: inherit; + font-weight: 700; + text-transform: none; + letter-spacing: normal; +} + +h2 { + font-family: 'Inter', Arial, sans-serif; + font-weight: 700; + font-size: 1.35rem; + margin-top: 2.25rem; + margin-bottom: 0.2rem; + color: var(--contrast-text-color); +} + +h3 { + font-family: 'Inter', Arial, sans-serif; + font-weight: 700; + font-size: 1.1rem; + margin-top: 1.5rem; + margin-bottom: 0.2rem; + color: var(--contrast-text-color); +} + +h4 { + margin: 0.75em 0; +} + +h2:first-child, +h3:first-child, +h4:first-child { + margin-top: 0; +} + +p + p { + margin: 0.75em 0; +} + +hr { + border: none; + height: 4px; + background: linear-gradient(135deg, #ED716A 20%, #EEB83F 80%); + margin: 2rem 0; +} + +.smallcaps { + font-size: 0.8em; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +em { + font-style: italic; + color: inherit; +} + +.highlight { + color: #335dff; +} + +h1 .highlight { + color: var(--contrast-text-color); + background: linear-gradient(#EEB83F 20%, #ED716A 80%); + padding: 0 0.2em; + margin: 0 -0.2em; +} + +.subtitle { + font-size: 1.15rem; + color: #888cab; + font-weight: 200; + margin: 0; +} + +a { + color: #335dff; + text-decoration: underline; + text-decoration-color: #335dff80; +} + +a:hover { + color: #233477; + text-decoration-color: #233477; +} + +ul, ol { + padding-left: 1.25rem; + margin: 1rem 0; +} + +li { + margin-bottom: 0.5em; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1.25rem 0; + font-size: 0.95rem; + border: 1px dashed var(--dash-color); + + thead { + border-bottom: 3px solid #335dff; + } + + th, + thead td { + font-size: 0.8em; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + text-align: left; + color: var(--contrast-text-color); + border: 1px dashed var(--dash-color); + + &.center { + text-align: center; + } + } + + thead th, + thead td { + background: var(--bg-color); + padding: 0.6rem 0.75rem; + } + + tbody th, td { + padding: 0.5rem 0.75rem; + border: 1px dashed var(--dash-color); + border-bottom: 1px solid var(--dash-color); + } + + .number { + text-align: right; + } + + tbody tr { + background: #fff; + } + + tbody tr:nth-child(even) { + background: var(--bg-color); + } + + tfoot td { + border: 1px dashed var(--dash-color); + border-bottom: none; + border-top: 2px solid var(--dash-color); + padding-top: 0.65rem; + } +} + +blockquote { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin: 1.25rem 0; + + &::before, + &::after { + content: ''; + width: 60px; + height: 4px; + background: linear-gradient(135deg, #ED716A 20%, #EEB83F 80%); + } +} + +blockquote p { + width: 80%; + margin: 0; + font-size: 1.3rem; +} + +img, +figure.image { + display: block; + max-width: 100%; + height: auto; + margin-bottom: 0.75em; +} +img[align="left"], +figure.image.align-left { + float: left; + margin-right: 0.75em; +} +img[align="right"], +figure.image.align-right { + float: right; + margin-left: 0.75em; +} + +figure figcaption { + font: 12px / 20px 'Inter', Arial, sans-serif; + text-align: center; + color: #888cab; +} + +:not(.tox-tinymceai__annotation) > .placeholder { + background: rgba(from var(--dash-color) r g b / .3); + outline: 1px solid var(--dash-color); + padding: 0.1rem 0.3rem; + margin: 0 -0.3rem; + border-radius: 0.25rem; + display: block; + color: #888cab; + color: lch(from var(--dash-color) calc(l / 2) c h) +} \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/tinymceai-mcp/example.js b/modules/ROOT/examples/live-demos/tinymceai-mcp/example.js new file mode 100644 index 0000000000..1c051730e1 --- /dev/null +++ b/modules/ROOT/examples/live-demos/tinymceai-mcp/example.js @@ -0,0 +1,65 @@ +// The MCP server is configured on the AI service, not in the editor. The editor only +// needs the tinymceai plugin and a token provider. +// The demo-only data plumbing (merge tag list, mentions directory, revision history, +// and template storage) is omitted below to keep the AI configuration in focus. +tinymce.init({ + selector: 'textarea#tinymceai-mcp', + height: 'clamp(650px, 80vh, 1200px)', + plugins: [ + 'autolink', 'link', 'emoticons', 'lists', 'table', 'advlist', 'searchreplace', 'wordcount', 'autocorrect', 'tinymcespellchecker', 'charmap', 'fullscreen', 'advcode', 'accordion', 'anchor', 'footnotes', 'powerpaste', 'importword', 'exportpdf', 'exportword', 'markdown', 'a11ychecker', 'typography', 'casechange', 'checklist', 'advtable', 'formatpainter', 'permanentpen', 'tableofcontents', 'help', 'math', 'linkchecker', 'media', 'mediaembed', + 'quickbars', 'tinymceai', 'tinycomments', 'uploadcare' + ], + toolbar: 'undo redo | tinymceai-chat tinymceai-review | ai-quickactions-translate | styles fontsizeinput | bold italic underline forecolor backcolor casechange | link uploadcare table addcomment | align bullist numlist checklist removeformat | code fullscreen help', + quickbars_selection_toolbar: 'tinymceai-quickactions ai-quickactions-improve-writing addcomment', + body_class: 'tox-comments-visible', // Ensure comments show up on init + sidebar_show: 'tinymceai-chat', + toolbar_mode: 'sliding', + visual: false, + table_default_attributes: {}, // Prevents inserting border=1 attribute on a new table + content_css: 'content.css', + user_id: 'james-wilson', + advcode_inline: true, + highlight_on_focus: false, + tinycomments_mode: 'embedded', + tinymceai_default_model: 'claude-4-6-sonnet', + tinymceai_allow_model_selection: false, + tinymceai_chat_welcome_message: 'This demo is connected to an Airtable database containing acquisition deals and related companies information.', + tinymceai_chat_welcome_actions: [ + { text: 'Try using the following actions with this database:' }, + { text: '**Get help editing your document**' }, + { + title: 'Fill with "Nuvo Technologies" account details', + command: 'TinyMCEAIChatPrompt', + value: { + prompt: 'Pull the "Nuvo Technologies" record from "TinyMCE demo data" Airtable database and fill in all placeholders in this document', + displayedPrompt: 'Pull the "Nuvo Technologies" record from Airtable and fill in all placeholders in this document' + }, + icon: 'document-properties' + }, + { text: '**Explore data via chat**' }, + { + title: 'Compile an open deals summary report', + command: 'TinyMCEAIChatPrompt', + value: { + prompt: 'Find all deals in "proposal sent" status and provide a summary in chat with their deal value, notes, confidence level and potential red flags' + }, + icon: 'comment' + } + ], + tinymceai_languages: [ + { title: 'English', language: 'english' }, + { title: 'Chinese (Simplified)', language: 'chinese' }, + { title: 'Spanish', language: 'spanish' }, + { title: 'German', language: 'german' }, + { title: 'Japanese', language: 'japanese' }, + { title: 'Portuguese', language: 'portuguese' }, + { title: 'Korean', language: 'korean' }, + { title: 'Italian', language: 'italian' } + ], + // Required for authentication. Replace with the token endpoint for the AI service. + tinymceai_token_provider: () => { + return fetch('/api/tinymceai-token', { credentials: 'include' }) + .then(resp => resp.text()) + .then(token => ({ token })); + } +}); diff --git a/modules/ROOT/examples/live-demos/tinymceai-mcp/index.html b/modules/ROOT/examples/live-demos/tinymceai-mcp/index.html new file mode 100644 index 0000000000..1404834227 --- /dev/null +++ b/modules/ROOT/examples/live-demos/tinymceai-mcp/index.html @@ -0,0 +1,46 @@ + diff --git a/modules/ROOT/examples/live-demos/tinymceai-mcp/index.js b/modules/ROOT/examples/live-demos/tinymceai-mcp/index.js new file mode 100644 index 0000000000..955eca2241 --- /dev/null +++ b/modules/ROOT/examples/live-demos/tinymceai-mcp/index.js @@ -0,0 +1,527 @@ +const apiKey = '{{tinymceai_mcp_demo_api_key}}'; + +// Once per page, set up an auth session for TestDrive so we can fetch a JWT later. +// More than once per page probably won't break anything, but there's a chance things might get out of sync. +// So to ensure demos run well, we recommend only fetching this once per page. +// This function is more complex than we recommend in the docs because we've included error handling. The error will appear as a banner if hit. +// Docs example for reference: const isLoggedIn = fetch(`https://demo.api.tiny.cloud/1/${apiKey}/auth/random`, { method: "POST", credentials: "include" }); +async function getAuthSession() { + const res = await fetch(`https://demo.api.tiny.cloud/1/${apiKey}/auth/random`, { method: 'POST', credentials: 'include' }); + if (!res.ok) throw new Error(`TestDrive auth session request failed: ${res.status}`); + const { token } = await res.json(); + return token; +} + +// Fetch a TinyMCE AI JWT from TestDrive. Returns { token } as the plugin's token +// provider requires. +// This is more complex than we recommend in the docs because we've included error handling. The error will appear as a banner if hit. +const tinymceaiTokenProvider = async () => { + // Check we've got an auth session before fetching a JWT. + await getAuthSession(); + const res = await fetch(`https://demo.api.tiny.cloud/1/${apiKey}/jwt/tinymceai`, { credentials: 'include' }); + if (!res.ok) throw new Error(`TestDrive JWT request failed: ${res.status}`); + return res.text().then(token => ({ token })); +}; + +// Tracks the visual viewport height (which excludes the on-screen keyboard on mobile) +// and exposes it as a CSS custom property --vvh. This allows the editor height to +// shrink correctly when the keyboard appears, unlike 100vh which ignores it. +const vv = window.visualViewport; +function updateVvh() { + document.documentElement.style.setProperty('--vvh', vv.height + 'px'); +} +vv.addEventListener('resize', updateVvh); +updateVvh(); + +// The Editor implementation + +// The demo document is provided by the textarea in index.html. +const initialContent = document.querySelector('textarea#tinymceai-mcp').value; + +// Merge tags +const mergetags_list = [ + { + title: 'Recipient', + menu: [ + { value: 'Recipient.FirstName', title: 'First name' }, + { value: 'Recipient.LastName', title: 'Last name' }, + { value: 'Recipient.FullName', title: 'Full name' }, + { value: 'Recipient.Salutation', title: 'Salutation (Mr/Ms/Dr …)' }, + { value: 'Recipient.Email', title: 'Email address' }, + { value: 'Recipient.Phone', title: 'Phone number' }, + { value: 'Recipient.JobTitle', title: 'Job title' }, + { value: 'Recipient.Company', title: 'Company name' }, + ] + }, + { + title: 'Sender', + menu: [ + { value: 'Sender.FirstName', title: 'First name' }, + { value: 'Sender.LastName', title: 'Last name' }, + { value: 'Sender.FullName', title: 'Full name' }, + { value: 'Sender.Email', title: 'Email address' }, + { value: 'Sender.Phone', title: 'Phone number' }, + { value: 'Sender.JobTitle', title: 'Job title' }, + { value: 'Sender.Department', title: 'Department' }, + { value: 'Sender.Signature', title: 'Email signature block' }, + ] + }, + { + title: 'Organization', + menu: [ + { value: 'Org.Name', title: 'Organization name' }, + { value: 'Org.LogoUrl', title: 'Logo URL' }, + { value: 'Org.Website', title: 'Website URL' }, + { value: 'Org.Address', title: 'Street address' }, + { value: 'Org.City', title: 'City' }, + { value: 'Org.Country', title: 'Country' }, + { value: 'Org.SupportEmail', title: 'Support email' }, + { value: 'Org.Phone', title: 'Main phone number' }, + ] + }, + { + title: 'Document', + menu: [ + { value: 'Document.Title', title: 'Document title' }, + { value: 'Document.ID', title: 'Document ID / reference' }, + { value: 'Document.Version', title: 'Version number' }, + { value: 'Document.CreatedDate', title: 'Created date' }, + { value: 'Document.DueDate', title: 'Due / expiry date' }, + { value: 'Document.Status', title: 'Status' }, + { value: 'Document.OwnerName', title: 'Document owner name' }, + ] + }, + { + title: 'Date & Time', + menu: [ + { value: 'Date.Today', title: 'Today\'s date' }, + { value: 'Date.CurrentYear', title: 'Current year' }, + { value: 'Date.CurrentMonth', title: 'Current month' }, + { value: 'Date.CurrentDay', title: 'Day of the week' }, + { value: 'DateTime.Now', title: 'Current date & time' }, + ] + }, + { + title: 'Contract & Legal', + menu: [ + { value: 'Contract.Number', title: 'Contract number' }, + { value: 'Contract.StartDate', title: 'Contract start date' }, + { value: 'Contract.EndDate', title: 'Contract end date' }, + { value: 'Contract.Value', title: 'Contract value' }, + { value: 'Contract.Currency', title: 'Currency' }, + { value: 'Contract.PaymentTerms', title: 'Payment terms' }, + { value: 'Contract.GoverningLaw', title: 'Governing law / jurisdiction' }, + ] + }, + { + title: 'Product & Order', + menu: [ + { value: 'Order.ID', title: 'Order ID' }, + { value: 'Order.Date', title: 'Order date' }, + { value: 'Order.Total', title: 'Order total' }, + { value: 'Order.ItemCount', title: 'Number of items' }, + { value: 'Product.Name', title: 'Product name' }, + { value: 'Product.SKU', title: 'Product SKU' }, + { value: 'Product.Price', title: 'Unit price' }, + { value: 'Order.TrackingURL', title: 'Shipment tracking URL' }, + ] + } +]; + +// Advanced Templates store (sessionStorage-backed) +const SESSION_KEY = 'tinymce_templates'; +const SEED_TEMPLATES = [ + { + id: '1', + title: 'Resolving tickets', + content: '
Hi {{prefix}}Customer.FirstName{{suffix}},
Thank you for reaching out. I\'m happy to let you know that your ticket has been resolved. Please don\'t hesitate to get in touch if you need further assistance.
Best regards,
{{prefix}}Agent.FirstName{{suffix}}
Hi {{prefix}}Customer.FirstName{{suffix}},
Thanks for your message — we\'ve received it and will get back to you as soon as possible.
Best regards,
{{prefix}}Agent.FirstName{{suffix}}
Hi {{prefix}}Customer.FirstName{{suffix}},
Just a quick note to let you know we\'re still working on your request. We\'ll follow up with an update shortly.
Best regards,
{{prefix}}Agent.FirstName{{suffix}}
Write your subtitle here
+' + userInfo.description + '
' + + '