Skip to content

Commit a843b61

Browse files
authored
Add AI Toolbar (#2673)
* Add DebugComponent * Add first skeleton of AiToolbar component and its logic * Add Copy Markdown button logic and style and test it on CMS intro page * Improve button styles * Swizzle heading component to include AiToolbar in all h1s * Add LLMs buttons * Fix dark mode * Remove Copy Markdown button from secondary TOC * Remove LLM txt files from footer * Move AiToolbar to left * Add special hook to ensure badges are displayed before AiToolbar * Add missing h1s to some pages so that the AiToolbar is displayed, because it's part of headings * Fix margin around badges container * Add description to hook * Update tip in CMS intro. page
1 parent 77f42e0 commit a843b61

File tree

21 files changed

+833
-23
lines changed

21 files changed

+833
-23
lines changed

docusaurus/docs/cloud/getting-started/cloud-fundamentals.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ tags:
88
- concepts
99
---
1010

11-
# Strapi Cloud fundamentals <UpdatedBadge />
11+
# Strapi Cloud fundamentals
1212

1313
Before going any further into this Strapi Cloud documentation, we recommend you to acknowledge the main concepts below. They will help you to understand how Strapi Cloud works, and ensure a smooth Strapi Cloud experience.
1414

docusaurus/docs/cms/admin-panel-customization/bundlers.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ tags:
1313

1414
import FeedbackCallout from '/docs/snippets/backend-customization-feedback-cta.md'
1515

16+
# Admin panel bundlers
17+
1618
Strapi's [admin panel](/cms/admin-panel-customization) is a React-based single-page application that encapsulates all the features and installed plugins of a Strapi application. 2 different bundlers can be used with your Strapi 5 application, [Vite](#vite) (the default one) and [webpack](#webpack). Both bundlers can be configured to suit your needs.
1719

1820
:::info

docusaurus/docs/cms/api/document.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ tags:
1616

1717
<div className="document-concept-page custom-mermaid-layout">
1818

19+
# Documents
20+
1921
A **document** in Strapi 5 is an API-only concept. A document represents all the different variations of content for a given entry of a content-type.
2022

2123
A single type contains a unique document, and a collection type can contain several documents.

docusaurus/docs/cms/backend-customization.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ tags:
1919

2020
<div className="custom-mermaid-layout">
2121

22+
# Backend customization
23+
2224
:::strapi Disambiguation: Strapi back end
2325
As a headless CMS, the Strapi software as a whole can be considered as the "back end" of your website or application.
2426
But the Strapi software itself includes 2 different parts:

docusaurus/docs/cms/intro.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,6 @@ tags:
1212

1313
# Welcome to the Strapi CMS Documentation!
1414

15-
<!--
16-
<SubtleCallout title="Strapi CMS & Strapi Cloud docs" emoji="📍">
17-
18-
There are 2 Strapi documentations, one for each Strapi product:
19-
20-
- <Icon name="feather" /> The **CMS documentation**, that you're currently reading, which contains all the information related to a Strapi 5 project (installation, setup, deployment, content management in admin panel, etc).
21-
- <Icon name="cloud" /> The **[Cloud documentation](/cloud/intro)**, which is about deploying your Strapi application to Strapi Cloud and managing your Strapi Cloud projects and settings.
22-
23-
</SubtleCallout>
24-
-->
25-
2615
The Strapi CMS documentation focuses on Strapi 5 and will take you through the complete journey of your Strapi 5 project. From the technical information related to the setup, advanced usage, customization and update of your project, to the management of the admin panel and its content and users.
2716

2817
<ThemedImage
@@ -66,7 +55,7 @@ The table of content of the Strapi CMS documentation displays 7 main sections in
6655
- If you prefer learning more about Strapi while looking at the project code structure, you can use the interactive [project structure](/cms/project-structure) page.
6756
- If demos are more your thing, feel free to watch the <ExternalLink to="https://youtu.be/zd0_S_FPzKg" text="video demo"/>, or you can request a <ExternalLink to="https://strapi.io/demo" text="live demo"/>.
6857
- Try our AI assistant: Click or tap the **Ask AI** button and ask your questions using natural language. Watch it answer you in real time, then read recommended sources for more details.
69-
- To help you integrate Strapi Docs with your favorite AI models, you can use the **Copy Markdown** button or visit the [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt) pages.
58+
- To help you integrate Strapi Docs with your favorite AI models, you can use the dropdown at the top of each page to **Copy Markdown** or visit the [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt) pages.
7059
:::
7160

7261
:::strapi Information for beginner developers

docusaurus/docusaurus.config.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,6 @@ const config = {
264264
{
265265
title: 'Additional resources',
266266
items: [
267-
{
268-
label: 'LLMs.txt',
269-
href: 'https://docs.strapi.io/llms.txt',
270-
},
271-
{
272-
label: 'LLMs-full.txt',
273-
href: 'https://docs.strapi.io/llms-full.txt',
274-
},
275267
{
276268
label: 'v4 Docs',
277269
href: 'https://docs-v4.strapi.io',
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import { getPrimaryAction, getDropdownActions } from './utils/aiToolsHelpers';
3+
import { executeAction, getActionDisplay } from './actions/actionRegistry';
4+
import Icon from '../Icon';
5+
6+
const AiToolbar = () => {
7+
const [actionStates, setActionStates] = useState({
8+
'copy-markdown': 'idle'
9+
});
10+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
11+
const dropdownRef = useRef(null);
12+
13+
const primaryAction = getPrimaryAction();
14+
const dropdownActions = getDropdownActions();
15+
16+
// Close dropdown when clicking outside
17+
useEffect(() => {
18+
const handleClickOutside = (event) => {
19+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
20+
setIsDropdownOpen(false);
21+
}
22+
};
23+
24+
document.addEventListener('mousedown', handleClickOutside);
25+
return () => {
26+
document.removeEventListener('mousedown', handleClickOutside);
27+
};
28+
}, []);
29+
30+
// Function to update action state
31+
const updateActionState = (actionId, state) => {
32+
setActionStates(prev => ({
33+
...prev,
34+
[actionId]: state
35+
}));
36+
};
37+
38+
// Function to close dropdown
39+
const closeDropdown = () => {
40+
setIsDropdownOpen(false);
41+
};
42+
43+
// Handle primary action click
44+
const handlePrimaryAction = async () => {
45+
if (!primaryAction) {
46+
console.error('No primary action configured');
47+
return;
48+
}
49+
50+
const context = {
51+
updateActionState,
52+
closeDropdown,
53+
docId: null, // Will be auto-detected by the action
54+
docPath: null, // Will be auto-detected by the action
55+
};
56+
57+
await executeAction(primaryAction, context);
58+
};
59+
60+
// Handle dropdown action click
61+
const handleDropdownAction = async (action) => {
62+
const context = {
63+
updateActionState,
64+
closeDropdown,
65+
url: action.url, // For navigate actions
66+
};
67+
68+
await executeAction(action, context);
69+
};
70+
71+
// Toggle dropdown
72+
const toggleDropdown = () => {
73+
setIsDropdownOpen(!isDropdownOpen);
74+
};
75+
76+
// Don't render if no primary action
77+
if (!primaryAction) {
78+
return null;
79+
}
80+
81+
const currentState = actionStates[primaryAction.id] || 'idle';
82+
const displayConfig = getActionDisplay(primaryAction.id, currentState);
83+
const isDisabled = currentState === 'loading' || currentState === 'success';
84+
85+
return (
86+
<div className="ai-toolbar">
87+
<div className="ai-toolbar__container" ref={dropdownRef}>
88+
<button
89+
onClick={handlePrimaryAction}
90+
disabled={isDisabled}
91+
className={`ai-toolbar__button ${displayConfig.className}`}
92+
title={primaryAction.description}
93+
>
94+
<Icon
95+
name={displayConfig.icon}
96+
classes={displayConfig.iconClasses}
97+
color={currentState === 'success' ? 'var(--strapi-success-700)' : 'inherit'}
98+
/>
99+
<span className="ai-toolbar__button-text">
100+
{displayConfig.label}
101+
</span>
102+
</button>
103+
104+
{/* Dropdown button */}
105+
<button
106+
onClick={toggleDropdown}
107+
className={`ai-toolbar__dropdown-trigger ${isDropdownOpen ? 'ai-toolbar__dropdown-trigger--open' : ''}`}
108+
title="More AI options"
109+
style={{
110+
borderColor: currentState === 'success' ? 'var(--strapi-neutral-200)' : undefined
111+
}}
112+
>
113+
<Icon
114+
name="caret-down"
115+
classes="ph-bold"
116+
color="inherit"
117+
/>
118+
</button>
119+
120+
{/* Dropdown menu */}
121+
{isDropdownOpen && (
122+
<div className="ai-toolbar__dropdown">
123+
{dropdownActions.map((action) => (
124+
<button
125+
key={action.id}
126+
onClick={() => handleDropdownAction(action)}
127+
className="ai-toolbar__dropdown-item"
128+
title={action.description}
129+
>
130+
<Icon
131+
name={action.icon}
132+
classes="ph-bold"
133+
color="inherit"
134+
/>
135+
<div className="ai-toolbar__dropdown-item-content">
136+
<span className="ai-toolbar__dropdown-item-label">
137+
{action.label}
138+
</span>
139+
{action.description && (
140+
<span className="ai-toolbar__dropdown-item-description">
141+
{action.description}
142+
</span>
143+
)}
144+
</div>
145+
</button>
146+
))}
147+
</div>
148+
)}
149+
</div>
150+
</div>
151+
);
152+
};
153+
154+
export default AiToolbar;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { copyMarkdownAction } from './copyMarkdown';
2+
import { navigateAction } from './navigate';
3+
4+
// Central registry of all available actions
5+
export const actionHandlers = {
6+
'copy-markdown': copyMarkdownAction,
7+
'navigate': navigateAction,
8+
};
9+
10+
// Main function to execute an action
11+
export const executeAction = async (actionConfig, additionalContext = {}) => {
12+
const handler = actionHandlers[actionConfig.actionType];
13+
14+
if (!handler) {
15+
console.warn(`Unknown action type: ${actionConfig.actionType}`);
16+
return;
17+
}
18+
19+
const context = {
20+
...actionConfig, // url, etc.
21+
...additionalContext, // docId, docPath, updateActionState, closeDropdown, etc.
22+
};
23+
24+
try {
25+
await handler(context);
26+
} catch (error) {
27+
console.error(`Error executing action ${actionConfig.actionType}:`, error);
28+
}
29+
};
30+
31+
// Utility to get display information based on action state
32+
export const getActionDisplay = (actionId, currentState = 'idle') => {
33+
switch (actionId) {
34+
case 'copy-markdown':
35+
switch (currentState) {
36+
case 'loading':
37+
return {
38+
icon: 'circle-notch',
39+
iconClasses: 'ph-bold spinning',
40+
label: 'Copying...',
41+
className: 'ai-toolbar-button--loading'
42+
};
43+
case 'success':
44+
return {
45+
icon: 'check-circle',
46+
iconClasses: 'ph-fill',
47+
label: 'Copied!',
48+
className: 'ai-toolbar-button--success'
49+
};
50+
case 'error':
51+
return {
52+
icon: 'warning-circle',
53+
iconClasses: 'ph-fill',
54+
label: 'Copy failed',
55+
className: 'ai-toolbar-button--error'
56+
};
57+
default: // 'idle'
58+
return {
59+
icon: 'copy',
60+
iconClasses: 'ph-bold',
61+
label: 'Copy Markdown',
62+
className: 'ai-toolbar-button--idle'
63+
};
64+
}
65+
default:
66+
// For other actions, just return their base configuration
67+
return {
68+
icon: 'question',
69+
iconClasses: 'ph-bold',
70+
label: 'Unknown',
71+
className: 'ai-toolbar-button--idle'
72+
};
73+
}
74+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export const copyMarkdownAction = async (context) => {
2+
const { docId, docPath, updateActionState } = context;
3+
4+
try {
5+
updateActionState('copy-markdown', 'loading');
6+
7+
// Helper functions to get current document info from URL (reused from CopyMarkdownButton)
8+
const getCurrentDocId = () => {
9+
if (typeof window === 'undefined') return null;
10+
const path = window.location.pathname;
11+
// Remove leading/trailing slashes and split
12+
const segments = path.replace(/^\/|\/$/g, '').split('/');
13+
// For paths like /cms/api/rest or /cloud/getting-started/intro
14+
if (segments.length >= 2) {
15+
return segments.join('/');
16+
}
17+
return null;
18+
};
19+
20+
const getCurrentDocPath = () => {
21+
if (typeof window === 'undefined') return null;
22+
const path = window.location.pathname;
23+
// Convert URL path to docs file path
24+
const cleanPath = path.replace(/^\/|\/$/g, '');
25+
return cleanPath ? `docs/${cleanPath}.md` : null;
26+
};
27+
28+
// Use props or try to get from current URL
29+
const currentDocId = docId || getCurrentDocId();
30+
const currentDocPath = docPath || getCurrentDocPath();
31+
32+
if (!currentDocId && !currentDocPath) {
33+
throw new Error('Unable to determine document path');
34+
}
35+
36+
// Build the raw markdown URL from GitHub
37+
const baseUrl = 'https://raw.githubusercontent.com/strapi/documentation/main/docusaurus';
38+
const markdownUrl = currentDocPath
39+
? `${baseUrl}/${currentDocPath}`
40+
: `${baseUrl}/docs/${currentDocId}.md`;
41+
42+
// Fetch the raw markdown content
43+
const response = await fetch(markdownUrl);
44+
45+
if (!response.ok) {
46+
throw new Error(`Failed to fetch markdown: ${response.status}`);
47+
}
48+
49+
const markdownContent = await response.text();
50+
51+
// Copy to clipboard
52+
await navigator.clipboard.writeText(markdownContent);
53+
54+
updateActionState('copy-markdown', 'success');
55+
setTimeout(() => updateActionState('copy-markdown', 'idle'), 3000);
56+
57+
} catch (error) {
58+
console.error('Error copying markdown:', error);
59+
updateActionState('copy-markdown', 'error');
60+
setTimeout(() => updateActionState('copy-markdown', 'idle'), 3000);
61+
}
62+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const navigateAction = (context) => {
2+
const { url, closeDropdown } = context;
3+
4+
if (!url) {
5+
console.error('Navigate action requires a URL');
6+
return;
7+
}
8+
9+
// Open URL in new tab
10+
window.open(url, '_blank');
11+
12+
// Close dropdown immediately for navigation actions
13+
if (closeDropdown) {
14+
closeDropdown();
15+
}
16+
};

0 commit comments

Comments
 (0)