diff --git a/package-lock.json b/package-lock.json index 1803ce0609..f78e9f2a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,6 +224,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.41.0.tgz", "integrity": "sha512-G9I2atg1ShtFp0t7zwleP6aPS4DcZvsV4uoQOripp16aR6VJzbEnKFPLW4OFXzX7avgZSpYeBAS+Zx4FOgmpPw==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.41.0", "@algolia/requester-browser-xhr": "5.41.0", @@ -371,6 +372,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2199,6 +2201,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2221,6 +2224,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2330,6 +2334,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2751,6 +2756,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3641,6 +3647,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -3909,6 +3916,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4921,6 +4929,7 @@ "node_modules/@mdx-js/react": { "version": "3.1.0", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -5267,6 +5276,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5828,6 +5838,7 @@ "node_modules/@types/react": { "version": "19.1.5", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6118,6 +6129,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6181,6 +6193,7 @@ "node_modules/ajv": { "version": "8.17.1", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6222,6 +6235,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.41.0.tgz", "integrity": "sha512-9E4b3rJmYbBkn7e3aAPt1as+VVnRhsR4qwRRgOzpeyz4PAOuwKh0HI4AN6mTrqK0S0M9fCCSTOUnuJ8gPY/tvA==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.7.0", "@algolia/client-abtesting": "5.41.0", @@ -6699,6 +6713,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6974,6 +6989,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7645,6 +7661,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7954,6 +7971,7 @@ "node_modules/cytoscape": { "version": "3.32.0", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -8357,6 +8375,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -9442,6 +9461,7 @@ "node_modules/file-loader/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13790,6 +13810,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14338,6 +14359,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15241,6 +15263,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16024,6 +16047,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16033,6 +16057,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -16078,6 +16103,7 @@ "name": "@docusaurus/react-loadable", "version": "6.0.0", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -16102,7 +16128,6 @@ "node_modules/react-router": { "version": "7.6.0", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -16175,7 +16200,6 @@ "node_modules/react-router/node_modules/cookie": { "version": "1.0.2", "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -17088,8 +17112,7 @@ }, "node_modules/set-cookie-parser": { "version": "2.7.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -17842,7 +17865,8 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.6", @@ -17996,6 +18020,7 @@ "version": "5.8.2", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18377,6 +18402,7 @@ "node_modules/url-loader/node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -18594,6 +18620,7 @@ "node_modules/webpack": { "version": "5.99.9", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -19179,6 +19206,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/teams.md/LANGUAGE-INCLUDE.md b/teams.md/LANGUAGE-INCLUDE.md index 740e080c50..a542a30c16 100644 --- a/teams.md/LANGUAGE-INCLUDE.md +++ b/teams.md/LANGUAGE-INCLUDE.md @@ -63,6 +63,7 @@ Shared content for all languages. The package name is . + The context type is . ``` @@ -103,15 +104,17 @@ N/A For simple, short language-specific text (like API names, method names, or simple phrases), you can use inline content directly in templates without creating separate include files: ```mdx - + ``` **When to use inline content:** + - Short text snippets (API names, method names, parameter names) - Simple differences between languages - Content that's easier to read inline than in separate files **When to use include files:** + - Code examples - Complex or multi-line content - Content that benefits from syntax highlighting diff --git a/teams.md/docs/main/privacy.md b/teams.md/docs/main/privacy.md index 3ddaf4b1f8..14acb92bfa 100644 --- a/teams.md/docs/main/privacy.md +++ b/teams.md/docs/main/privacy.md @@ -6,4 +6,4 @@ llms: ignore # Privacy Policy -We partner with Microsoft Clarity to capture how you use and interact with our website through behavioral metrics, heatmaps, and session replay to improve and market our products/services. Website usage data is captured using first and third-party cookies and other tracking technologies to determine the popularity of products/services and online activity. Additionally, we use this information for site optimization and fraud/security purposes. For more information about how Microsoft collects and uses your data, visit the [Microsoft Privacy Statement](https://www.microsoft.com/en-us/privacy/privacystatement). \ No newline at end of file +We partner with Microsoft Clarity to capture how you use and interact with our website through behavioral metrics, heatmaps, and session replay to improve and market our products/services. Website usage data is captured using first and third-party cookies and other tracking technologies to determine the popularity of products/services and online activity. Additionally, we use this information for site optimization and fraud/security purposes. For more information about how Microsoft collects and uses your data, visit the [Microsoft Privacy Statement](https://www.microsoft.com/en-us/privacy/privacystatement). diff --git a/teams.md/docs/main/teams/app-authentication/client-secret.md b/teams.md/docs/main/teams/app-authentication/client-secret.md index 8910619299..45e0bcf838 100644 --- a/teams.md/docs/main/teams/app-authentication/client-secret.md +++ b/teams.md/docs/main/teams/app-authentication/client-secret.md @@ -14,6 +14,7 @@ Client Secret authentication is the simplest method, using a password-like secre ## Prerequisites Before you begin, ensure you have: + - An Azure subscription - Permissions to create App Registrations and Azure Bot Services diff --git a/teams.md/docs/main/teams/app-authentication/federated-identity-credentials.md b/teams.md/docs/main/teams/app-authentication/federated-identity-credentials.md index d4d95116d8..fc24b16265 100644 --- a/teams.md/docs/main/teams/app-authentication/federated-identity-credentials.md +++ b/teams.md/docs/main/teams/app-authentication/federated-identity-credentials.md @@ -14,6 +14,7 @@ Federated Identity Credentials (FIC) allows you to assign managed identities dir ## Prerequisites Before you begin, ensure you have: + - An Azure subscription - Permissions to create App Registrations, Azure Bot Services, and manage identities - A compute resource where your bot will be hosted (App Service, Container App, VM, etc.) diff --git a/teams.md/docs/main/teams/app-authentication/troubleshooting.md b/teams.md/docs/main/teams/app-authentication/troubleshooting.md index c75a08d482..29dec2dcc4 100644 --- a/teams.md/docs/main/teams/app-authentication/troubleshooting.md +++ b/teams.md/docs/main/teams/app-authentication/troubleshooting.md @@ -76,6 +76,7 @@ This error occurs when the application has a single-tenant Azure Bot Service (`m 3. **Search for your application** Use the **BOT_ID** from your environment file: + - Local development → `env/.env.local` - Azure deployment → `env/.env.dev` diff --git a/teams.md/docs/main/teams/app-authentication/user-managed-identity.md b/teams.md/docs/main/teams/app-authentication/user-managed-identity.md index fffa4072dd..7b514f3d06 100644 --- a/teams.md/docs/main/teams/app-authentication/user-managed-identity.md +++ b/teams.md/docs/main/teams/app-authentication/user-managed-identity.md @@ -14,6 +14,7 @@ User Managed Identity authentication eliminates the need for secrets or password ## Prerequisites Before you begin, ensure you have: + - An Azure subscription - Permissions to create App Registrations, Azure Bot Services, and manage identities - A compute resource where your bot will be hosted (App Service, Container App, VM, etc.) diff --git a/teams.md/docs/main/teams/core-concepts.md b/teams.md/docs/main/teams/core-concepts.md index eb5d41c93e..d1bdd7b121 100644 --- a/teams.md/docs/main/teams/core-concepts.md +++ b/teams.md/docs/main/teams/core-concepts.md @@ -115,4 +115,4 @@ Sideloading needs to be enabled in your tenant. If this is not the case, then yo ## Provisioning and Deployment -To test your app in Teams, you will at minimum need to have a provisioned Azure bot. You are likely to have other provisionied resources such as storage. Please see the Microsoft Learn [Provision cloud resources](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/provision) documentation for provisioning and deployment using Visual Studio Code and to a container service. \ No newline at end of file +To test your app in Teams, you will at minimum need to have a provisioned Azure bot. You are likely to have other provisionied resources such as storage. Please see the Microsoft Learn [Provision cloud resources](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/provision) documentation for provisioning and deployment using Visual Studio Code and to a container service. diff --git a/teams.md/docs/main/teams/enabling-in-copilot.md b/teams.md/docs/main/teams/enabling-in-copilot.md index 44c7b6bd7b..b7a6241334 100644 --- a/teams.md/docs/main/teams/enabling-in-copilot.md +++ b/teams.md/docs/main/teams/enabling-in-copilot.md @@ -68,11 +68,7 @@ Here's how the `copilotAgents` section fits into the overall manifest structure: "bots": [ { "botId": "${{BOT_ID}}", - "scopes": [ - "personal", - "team", - "groupchat" - ], + "scopes": ["personal", "team", "groupchat"], "supportsFiles": false, "isNotificationOnly": false } diff --git a/teams.md/docs/main/teams/user-authentication/_category_.json b/teams.md/docs/main/teams/user-authentication/_category_.json index 2374d63e67..bdc8d624ee 100644 --- a/teams.md/docs/main/teams/user-authentication/_category_.json +++ b/teams.md/docs/main/teams/user-authentication/_category_.json @@ -1,5 +1,5 @@ { "label": "User Authentication Configuration", - "collapsed": true, - "position": 4, + "collapsed": true, + "position": 4 } diff --git a/teams.md/docs/main/teams/user-authentication/sso-setup.md b/teams.md/docs/main/teams/user-authentication/sso-setup.md index 9e40f804b5..fa2aafc5c1 100644 --- a/teams.md/docs/main/teams/user-authentication/sso-setup.md +++ b/teams.md/docs/main/teams/user-authentication/sso-setup.md @@ -20,7 +20,7 @@ You need an Entra ID App Registration to configure the OAuth Connection in Azure ![Entra client secret](/screenshots/entra-client-secret.png) -4. Configure the API. From `Expose an API`, Click `Add` to Application ID URI and accept the default value that will look like `api://`. Add the scope `access_as_user` and select who can _consent_. +4. Configure the API. From `Expose an API`, Click `Add` to Application ID URI and accept the default value that will look like `api://`. Add the scope `access_as_user` and select who can _consent_. ![Entra oauth scopes](/screenshots/entra-oauth-scopes.png) @@ -64,10 +64,9 @@ az bot authsetting create \ --parameters "clientId=$appId" "clientSecret=$clientSecret" "tenantId=$tenantId" "tokenExchangeUrl=api://$appId" ``` - ## Configure the App Manifest -The Teams application manifest needs to be updated to reflect the settings configure above, with the `Application Id` and `Application ID URI`, if not using `devtunnels`, replace the valid domain with the domain hosting your application. +The Teams application manifest needs to be updated to reflect the settings configure above, with the `Application Id` and `Application ID URI`, if not using `devtunnels`, replace the valid domain with the domain hosting your application. ```json "validDomains": [ @@ -79,4 +78,3 @@ The Teams application manifest needs to be updated to reflect the settings confi "resource": "api://" } ``` - diff --git a/teams.md/docusaurus.config.ts b/teams.md/docusaurus.config.ts index 47fa542dda..9901b29bd3 100644 --- a/teams.md/docusaurus.config.ts +++ b/teams.md/docusaurus.config.ts @@ -6,204 +6,204 @@ import { themes as prismThemes } from 'prism-react-renderer'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) const baseUrl = '/teams-sdk/'; const config: Config = { - title: 'Teams SDK', - favicon: 'img/msft-logo-48x48.png', + title: 'Teams SDK', + favicon: 'img/msft-logo-48x48.png', - // Set the production url of your site here - url: 'https://microsoft.github.io/', - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl, + // Set the production url of your site here + url: 'https://microsoft.github.io/', + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl, - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: 'microsoft', // Usually your GitHub org/user name. - projectName: baseUrl, // Usually your repo name. + // GitHub pages deployment config. + // If you aren't using GitHub pages, you don't need these. + organizationName: 'microsoft', // Usually your GitHub org/user name. + projectName: baseUrl, // Usually your repo name. - onBrokenLinks: 'throw', + onBrokenLinks: 'throw', - // Even if you don't use internationalization, you can use this field to set - // useful metadata like html lang. For example, if your site is Chinese, you - // may want to replace "en" with "zh-Hans". - i18n: { - defaultLocale: 'en', - locales: ['en'], + // Even if you don't use internationalization, you can use this field to set + // useful metadata like html lang. For example, if your site is Chinese, you + // may want to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'en', + locales: ['en'], + }, + + markdown: { + mermaid: true, + hooks: { + onBrokenMarkdownLinks: 'throw', + }, + }, + headTags: [ + { + tagName: 'link', + attributes: { + rel: 'llms.txt', + href: 'https://microsoft.github.io/teams-sdk/llms_docs/llms.txt', + }, }, + ], + scripts: [path.join(baseUrl, '/scripts/clarity.js')], - markdown: { - mermaid: true, - hooks: { - onBrokenMarkdownLinks: 'throw', + presets: [ + [ + 'classic', + { + blog: false, + docs: { + routeBasePath: '/', + path: 'docs/main', + sidebarPath: './sidebars.ts', + sidebarCollapsed: false, + editUrl: 'https://github.com/microsoft/teams-sdk/tree/main/teams.md/', + // Temporary exclude until generate-LLMs script is fully tested + exclude: ['**/LLMs.md'], }, - }, - headTags: [ - { - tagName: 'link', - attributes: { - rel: 'llms.txt', - href: 'https://microsoft.github.io/teams-sdk/llms_docs/llms.txt' - } - } + pages: { + exclude: ['**/templates/**'], + }, + theme: { + customCss: ['./src/css/custom.css', './src/css/code-blocks.css'], + }, + } satisfies Preset.Options, ], - scripts: [path.join(baseUrl, '/scripts/clarity.js')], + ], - presets: [ - [ - 'classic', - { - blog: false, - docs: { - routeBasePath: '/', - path: 'docs/main', - sidebarPath: './sidebars.ts', - sidebarCollapsed: false, - editUrl: 'https://github.com/microsoft/teams-sdk/tree/main/teams.md/', - // Temporary exclude until generate-LLMs script is fully tested - exclude: ['**/LLMs.md'], - }, - pages: { - exclude: ['**/templates/**'], - }, - theme: { - customCss: ['./src/css/custom.css', './src/css/code-blocks.css'], - }, - } satisfies Preset.Options, - ], + themes: [ + '@docusaurus/theme-mermaid', + [ + require.resolve('@easyops-cn/docusaurus-search-local'), + /** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */ + { + hashed: true, + language: ['en'], + docsRouteBasePath: ['/', '/typescript', '/csharp', '/python'], + indexDocs: true, + indexPages: true, + highlightSearchTermsOnTargetPage: true, + }, ], - - themes: [ - '@docusaurus/theme-mermaid', - [ - require.resolve('@easyops-cn/docusaurus-search-local'), - /** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */ + ], + themeConfig: { + colorMode: { + respectPrefersColorScheme: true, + }, + navbar: { + title: 'Teams SDK', + hideOnScroll: true, + logo: { + alt: 'Teams SDK', + src: 'img/teams.png', + }, + items: [ + { + href: 'https://github.com/microsoft/teams-sdk/tree/main', + position: 'right', + className: 'header-github-link', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: 'Docs', + items: [ { - hashed: true, - language: ['en'], - docsRouteBasePath: ['/', '/typescript', '/csharp', '/python'], - indexDocs: true, - indexPages: true, - highlightSearchTermsOnTargetPage: true, + label: 'Getting Started', + to: '/', }, - ], - ], - themeConfig: { - colorMode: { - respectPrefersColorScheme: true, + { + label: 'TypeScript', + to: '/typescript/getting-started', + }, + { + label: 'C#', + to: '/csharp/getting-started', + }, + { + label: 'Python', + to: '/python/getting-started', + }, + { + label: 'Privacy policy', + to: '/privacy', + }, + ], }, - navbar: { - title: 'Teams SDK', - hideOnScroll: true, - logo: { - alt: 'Teams SDK', - src: 'img/teams.png', + { + title: 'More', + items: [ + { + label: 'GitHub', + href: 'https://github.com/microsoft/teams-sdk/tree/main', + }, + { + label: 'Contributing', + href: 'https://github.com/microsoft/teams-sdk/blob/main/CONTRIBUTING.md', + }, + { + label: 'Blog', + href: 'https://devblogs.microsoft.com/microsoft365dev/announcing-the-updated-teams-ai-library-and-mcp-support/', + }, + { + label: 'Teams agent accelerator templates', + href: 'https://microsoft.github.io/teams-agent-accelerator-templates/', }, - items: [ - { - href: 'https://github.com/microsoft/teams-sdk/tree/main', - position: 'right', - className: 'header-github-link', - }, - ], + ], }, - footer: { - style: 'dark', - links: [ - { - title: 'Docs', - items: [ - { - label: 'Getting Started', - to: '/', - }, - { - label: 'TypeScript', - to: '/typescript/getting-started', - }, - { - label: 'C#', - to: '/csharp/getting-started', - }, - { - label: 'Python', - to: '/python/getting-started', - }, - { - label: 'Privacy policy', - to: '/privacy', - }, - ], - }, - { - title: 'More', - items: [ - { - label: 'GitHub', - href: 'https://github.com/microsoft/teams-sdk/tree/main', - }, - { - label: 'Contributing', - href: 'https://github.com/microsoft/teams-sdk/blob/main/CONTRIBUTING.md', - }, - { - label: 'Blog', - href: 'https://devblogs.microsoft.com/microsoft365dev/announcing-the-updated-teams-ai-library-and-mcp-support/', - }, - { - label: 'Teams agent accelerator templates', - href: 'https://microsoft.github.io/teams-agent-accelerator-templates/', - }, - ], - }, - ], - copyright: `Copyright Ā© ${new Date().getFullYear()} Microsoft Corporation. All rights reserved.`, + ], + copyright: `Copyright Ā© ${new Date().getFullYear()} Microsoft Corporation. All rights reserved.`, + }, + prism: { + theme: prismThemes.dracula, + darkTheme: prismThemes.vsDark, + magicComments: [ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: { + start: 'highlight-start', + end: 'highlight-end', + }, }, - prism: { - theme: prismThemes.dracula, - darkTheme: prismThemes.vsDark, - magicComments: [ - { - className: 'theme-code-block-highlighted-line', - line: 'highlight-next-line', - block: { - start: 'highlight-start', - end: 'highlight-end', - }, - }, - { - className: 'code-block-error-line', - line: 'highlight-error-line', - block: { - start: 'highlight-error-start', - end: 'highlight-error-end', - }, - }, - { - className: 'code-block-success-line', - line: 'highlight-success-line', - block: { - start: 'highlight-success-start', - end: 'highlight-success-end', - }, - }, - ], - additionalLanguages: [ - 'typescript', - 'javascript', - 'csharp', - 'python', - 'bash', - 'markdown', - 'json', - ], + { + className: 'code-block-error-line', + line: 'highlight-error-line', + block: { + start: 'highlight-error-start', + end: 'highlight-error-end', + }, }, - announcementBar: { - id: 'teams-sdk-rename', - content: 'We have been renamed to Teams SDK! šŸŽ‰ 🄳', - isCloseable: true, - backgroundColor: '#515cc6', - textColor: '#fff' + { + className: 'code-block-success-line', + line: 'highlight-success-line', + block: { + start: 'highlight-success-start', + end: 'highlight-success-end', + }, }, - } satisfies Preset.ThemeConfig, + ], + additionalLanguages: [ + 'typescript', + 'javascript', + 'csharp', + 'python', + 'bash', + 'markdown', + 'json', + ], + }, + announcementBar: { + id: 'teams-sdk-rename', + content: 'We have been renamed to Teams SDK! šŸŽ‰ 🄳', + isCloseable: true, + backgroundColor: '#515cc6', + textColor: '#fff', + }, + } satisfies Preset.ThemeConfig, }; export default config; diff --git a/teams.md/scripts/generate-language-docs.ts b/teams.md/scripts/generate-language-docs.ts index 2b4eea75ba..0552831f5b 100644 --- a/teams.md/scripts/generate-language-docs.ts +++ b/teams.md/scripts/generate-language-docs.ts @@ -39,7 +39,8 @@ const SECTION_REGEX = (sectionName: string) => new RegExp(`\\s*([\\s\\S]*?)(?=|$)`, 'i'); // Regex to find LanguageInclude tags (supports both section and content props) -const LANGUAGE_INCLUDE_REGEX = //g; +const LANGUAGE_INCLUDE_REGEX = + //g; const languagePattern = LANGUAGES.join('|'); const LANGUAGE_INCL_FILENAME_REGEX = new RegExp(`^(${languagePattern})\\.incl\\.md$`); @@ -142,13 +143,13 @@ function processLanguageIncludeTags( try { // Parse the inline content object const contentObj = JSON.parse(inlineContent); - + // Production mode with target language if (isProduction && targetLanguage) { const content = contentObj[targetLanguage]; return content || ''; } - + // Development mode: generate Language components for all languages const languageComponents: string[] = []; for (const lang of LANGUAGES) { @@ -183,7 +184,11 @@ function processLanguageIncludeTags( const fileContent = readFileUtf8Normalized(inclPath); const sectionContent = extractSection(fileContent, sectionName); - if (sectionContent === null || sectionContent === '' || sectionContent === 'EMPTY_SECTION') { + if ( + sectionContent === null || + sectionContent === '' || + sectionContent === 'EMPTY_SECTION' + ) { // Skip missing sections (null), intentional N/A content (empty string), or empty sections return ''; } @@ -464,12 +469,16 @@ function generateDocsForTemplate(templatePath: string): void { const processedContent = processLanguageIncludeTags(templateContent, templatePath, lang); // Extract frontmatter if exists - const { frontmatter, hasFrontmatter, content: contentWithoutFrontmatter } = FrontmatterParser.extract(processedContent); + const { + frontmatter, + hasFrontmatter, + content: contentWithoutFrontmatter, + } = FrontmatterParser.extract(processedContent); let content = contentWithoutFrontmatter; let frontmatterRaw = ''; if (hasFrontmatter && frontmatter && Object.keys(frontmatter).length > 0) { - frontmatterRaw = `---\n${yaml.dump(frontmatter)}---\n` + frontmatterRaw = `---\n${yaml.dump(frontmatter)}---\n`; } const outputDir = path.join(DOCS_BASE, lang, path.dirname(relativePath)); @@ -645,7 +654,9 @@ function warnOrphanedIncludeFiles() { } scanDir(FRAGMENTS_DIR); if (orphanedFiles.length > 0) { - console.warn(`\n[DevMode] Orphaned include files were found. These files are not referenced by any template (possibly due to 'language' frontmatter restrictions):`); + console.warn( + `\n[DevMode] Orphaned include files were found. These files are not referenced by any template (possibly due to 'language' frontmatter restrictions):` + ); orphanedFiles.forEach(({ lang, fullPath, relTemplate }) => { console.warn(` - [${lang}] ${fullPath}\n Template: ${relTemplate}`); }); @@ -701,7 +712,7 @@ function writeContentGapsManifest(): void { for (const [templatePath, sections] of Object.entries(contentGapsManifest)) { markdownContent += `## \`${templatePath}\`\n\n`; for (const [sectionName, languages] of Object.entries(sections)) { - const langNames = languages.map(lang => LANGUAGE_NAMES[lang]).join(', '); + const langNames = languages.map((lang) => LANGUAGE_NAMES[lang]).join(', '); markdownContent += `- **\`${sectionName}\`**: Missing in ${langNames}\n`; } markdownContent += `\n`; @@ -710,7 +721,7 @@ function writeContentGapsManifest(): void { markdownContent += `## Summary\n\n`; const totalSectionGaps = Object.values(contentGapsManifest) - .flatMap(sections => Object.values(sections)) + .flatMap((sections) => Object.values(sections)) .reduce((total, langs: Language[]) => total + langs.length, 0); markdownContent += `- **${totalGaps}** templates with gaps\n`; markdownContent += `- **${totalSectionGaps}** total missing sections\n`; @@ -871,7 +882,9 @@ if (require.main === module) { // In production mode, fail the build if there are content gaps if (isProduction && result.contentGapsFound > 0) { - console.error(`\nāŒ Build failed: Found ${result.contentGapsFound} template(s) with content gaps in production mode.`); + console.error( + `\nāŒ Build failed: Found ${result.contentGapsFound} template(s) with content gaps in production mode.` + ); console.error('Please fill in all missing sections or mark them as N/A before deploying.\n'); process.exit(1); } diff --git a/teams.md/scripts/generate-llms-txt.ts b/teams.md/scripts/generate-llms-txt.ts index c0d3023f9d..c641c868c9 100644 --- a/teams.md/scripts/generate-llms-txt.ts +++ b/teams.md/scripts/generate-llms-txt.ts @@ -10,25 +10,25 @@ import { LANGUAGES, Language, LANGUAGE_NAMES } from '../src/constants/languages' import readFileUtf8Normalized from '../src/utils/readFileUtf8Normalized'; const LANGUAGE_SPECIFIC_TIPS: Record = { - typescript: [ - "It's a good idea to build the application using `npm run build` and fix compile time errors to help ensure the app works as expected.", - "The SDK uses typescript to help you make the right decisions when using the APIs. You may check type definitions and type checkers to make sure your code is correct." - ], - python: [ - "It's a good idea to run `pyright` to make sure the code is correctly typed and fix any type errors.", - ], - csharp: [ - "It's a good idea to build the application and fix compile time errors to help ensure the app works as expected.", - "It is helpful to inspect NuGet packages folder to get exact types for a given namesapce" - ] + typescript: [ + "It's a good idea to build the application using `npm run build` and fix compile time errors to help ensure the app works as expected.", + 'The SDK uses typescript to help you make the right decisions when using the APIs. You may check type definitions and type checkers to make sure your code is correct.', + ], + python: [ + "It's a good idea to run `pyright` to make sure the code is correctly typed and fix any type errors.", + ], + csharp: [ + "It's a good idea to build the application and fix compile time errors to help ensure the app works as expected.", + 'It is helpful to inspect NuGet packages folder to get exact types for a given namesapce', + ], }; const COMMON_OVERALL_SUMMARY = (language: Language) => { - const langName = LANGUAGE_NAMES[language]; - const tips = LANGUAGE_SPECIFIC_TIPS[language]; - const formattedTips = tips.map(tip => `- ${tip}`).join('\n'); + const langName = LANGUAGE_NAMES[language]; + const tips = LANGUAGE_SPECIFIC_TIPS[language]; + const formattedTips = tips.map((tip) => `- ${tip}`).join('\n'); - return `> Microsoft Teams SDK - A comprehensive framework for building AI-powered Teams applications using ${langName}. Using this SDK, you can easily build and integrate a variety of features in Microsoft Teams by building Agents or Tools. The documentation here helps by giving background information and code samples on how best to do this. + return `> Microsoft Teams SDK - A comprehensive framework for building AI-powered Teams applications using ${langName}. Using this SDK, you can easily build and integrate a variety of features in Microsoft Teams by building Agents or Tools. The documentation here helps by giving background information and code samples on how best to do this. IMPORTANT THINGS TO REMEMBER: - This SDK is NOT based off of BotFramework (which the _previous_ version of the Teams SDK was based on). This SDK is a completely new framework. Use this guide to find snippets to drive your decisions. @@ -39,32 +39,32 @@ YOU MUST FOLLOW THE ABOVE GUIDANCE.`; }; interface DocusaurusConfig { - url: string; - baseUrl: string; + url: string; + baseUrl: string; } interface ProcessedFile { - title: string; - content: string; - frontmatter: { [key: string]: any }; - filePath: string; - sidebarPosition: number; - relativeUrl: string; + title: string; + content: string; + frontmatter: { [key: string]: any }; + filePath: string; + sidebarPosition: number; + relativeUrl: string; } interface FileInfo { - name: string; - title: string; - path: string; - order: number; + name: string; + title: string; + path: string; + order: number; } interface FolderStructure { - title: string; - order: number; - path: string; - files: FileInfo[]; - children: { [key: string]: FolderStructure }; + title: string; + order: number; + path: string; + files: FileInfo[]; + children: { [key: string]: FolderStructure }; } /** @@ -73,25 +73,25 @@ interface FolderStructure { * @returns Config object with url and baseUrl */ function getDocusaurusConfig(baseDir: string): DocusaurusConfig { - try { + try { // Read the docusaurus.config.ts file const configPath = path.join(baseDir, 'docusaurus.config.ts'); const configContent = readFileUtf8Normalized(configPath); - // Extract URL and baseUrl using regex (simple approach) - const urlMatch = configContent.match(/url:\s*['"]([^'"]+)['"]/); - const baseUrlMatch = - configContent.match(/baseUrl\s*=\s*['"]([^'"]+)['"]/) || - configContent.match(/baseUrl:\s*['"]([^'"]+)['"]/); + // Extract URL and baseUrl using regex (simple approach) + const urlMatch = configContent.match(/url:\s*['"]([^'"]+)['"]/); + const baseUrlMatch = + configContent.match(/baseUrl\s*=\s*['"]([^'"]+)['"]/) || + configContent.match(/baseUrl:\s*['"]([^'"]+)['"]/); - const url = urlMatch ? urlMatch[1] : 'https://microsoft.github.io'; - const baseUrl = baseUrlMatch ? baseUrlMatch[1] : '/teams-sdk/'; + const url = urlMatch ? urlMatch[1] : 'https://microsoft.github.io'; + const baseUrl = baseUrlMatch ? baseUrlMatch[1] : '/teams-sdk/'; - return { url, baseUrl }; - } catch (error) { - console.warn('āš ļø Could not read Docusaurus config, using defaults'); - return { url: 'https://microsoft.github.io', baseUrl: '/teams-sdk/' }; - } + return { url, baseUrl }; + } catch (error) { + console.warn('āš ļø Could not read Docusaurus config, using defaults'); + return { url: 'https://microsoft.github.io', baseUrl: '/teams-sdk/' }; + } } /** @@ -99,35 +99,35 @@ function getDocusaurusConfig(baseDir: string): DocusaurusConfig { * Creates both small and full versions for TypeScript and C# docs */ async function generateLlmsTxt(): Promise { - console.log('šŸš€ Starting llms.txt generation...'); - - const baseDir = path.join(__dirname, '..'); - const outputDir = path.join(baseDir, 'static', 'llms_docs'); - - // Get Docusaurus configuration - const config = getDocusaurusConfig(baseDir); - const cleanUrl = config.url.replace(/\/$/, ''); - const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; - console.log(`šŸ“ Using base URL: ${cleanUrl}${cleanBaseUrl}`); - - // Ensure output directory exists - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); + console.log('šŸš€ Starting llms.txt generation...'); + + const baseDir = path.join(__dirname, '..'); + const outputDir = path.join(baseDir, 'static', 'llms_docs'); + + // Get Docusaurus configuration + const config = getDocusaurusConfig(baseDir); + const cleanUrl = config.url.replace(/\/$/, ''); + const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; + console.log(`šŸ“ Using base URL: ${cleanUrl}${cleanBaseUrl}`); + + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + try { + // Generate llms.txt files for all languages + for (const language of LANGUAGES) { + const langName = LANGUAGE_NAMES[language]; + console.log(`šŸ“ Generating ${langName} llms.txt files...`); + await generateLanguageFiles(language, baseDir, outputDir, config); } - try { - // Generate llms.txt files for all languages - for (const language of LANGUAGES) { - const langName = LANGUAGE_NAMES[language]; - console.log(`šŸ“ Generating ${langName} llms.txt files...`); - await generateLanguageFiles(language, baseDir, outputDir, config); - } - - console.log('āœ… Successfully generated all llms.txt files!'); - } catch (error) { - console.error('āŒ Error generating llms.txt files:', error); - process.exit(1); - } + console.log('āœ… Successfully generated all llms.txt files!'); + } catch (error) { + console.error('āŒ Error generating llms.txt files:', error); + process.exit(1); + } } /** @@ -137,49 +137,54 @@ async function generateLlmsTxt(): Promise { * @param outputDir - Output directory path * @param config - Docusaurus config object */ -async function generateLanguageFiles(language: Language, baseDir: string, outputDir: string, config: DocusaurusConfig): Promise { - // Collect all relevant files - const mainFiles: string[] = []; - const langFiles = collectFiles(path.join(baseDir, 'docs', 'main', language)); - - // Process all files to get metadata and file mapping - const { processedFiles, fileMapping } = await processAllFiles( - [...mainFiles, ...langFiles], - baseDir, - language - ); - - // Generate individual TXT files for each doc - await generateIndividualTxtFiles( - processedFiles, - outputDir, - language, - baseDir, - config, - fileMapping - ); - - // Process content for small version (navigation index) - const smallContent = await generateSmallVersionHierarchical( - language, - baseDir, - config, - fileMapping - ); - - // Process content for full version (all documentation) - const fullContent = await generateFullVersion(language, processedFiles, baseDir); - - // Write main llms.txt files - const smallPath = path.join(outputDir, `llms_${language}.txt`); - const fullPath = path.join(outputDir, `llms_${language}_full.txt`); - - fs.writeFileSync(smallPath, smallContent, 'utf8'); - fs.writeFileSync(fullPath, fullContent, 'utf8'); - - console.log(` āœ“ Generated ${path.basename(smallPath)} (${formatBytes(smallContent.length)})`); - console.log(` āœ“ Generated ${path.basename(fullPath)} (${formatBytes(fullContent.length)})`); - console.log(` āœ“ Generated ${processedFiles.length} individual .txt files`); +async function generateLanguageFiles( + language: Language, + baseDir: string, + outputDir: string, + config: DocusaurusConfig +): Promise { + // Collect all relevant files + const mainFiles: string[] = []; + const langFiles = collectFiles(path.join(baseDir, 'docs', 'main', language)); + + // Process all files to get metadata and file mapping + const { processedFiles, fileMapping } = await processAllFiles( + [...mainFiles, ...langFiles], + baseDir, + language + ); + + // Generate individual TXT files for each doc + await generateIndividualTxtFiles( + processedFiles, + outputDir, + language, + baseDir, + config, + fileMapping + ); + + // Process content for small version (navigation index) + const smallContent = await generateSmallVersionHierarchical( + language, + baseDir, + config, + fileMapping + ); + + // Process content for full version (all documentation) + const fullContent = await generateFullVersion(language, processedFiles, baseDir); + + // Write main llms.txt files + const smallPath = path.join(outputDir, `llms_${language}.txt`); + const fullPath = path.join(outputDir, `llms_${language}_full.txt`); + + fs.writeFileSync(smallPath, smallContent, 'utf8'); + fs.writeFileSync(fullPath, fullContent, 'utf8'); + + console.log(` āœ“ Generated ${path.basename(smallPath)} (${formatBytes(smallContent.length)})`); + console.log(` āœ“ Generated ${path.basename(fullPath)} (${formatBytes(fullContent.length)})`); + console.log(` āœ“ Generated ${processedFiles.length} individual .txt files`); } /** @@ -189,48 +194,52 @@ async function generateLanguageFiles(language: Language, baseDir: string, output * @param language - Language identifier for filtering language-specific files * @returns Object with processedFiles array and fileMapping Map */ -async function processAllFiles(allFiles: string[], baseDir: string, language: Language): Promise<{ processedFiles: ProcessedFile[]; fileMapping: Map }> { - const processedFiles: ProcessedFile[] = []; - - // First pass: build file mapping - const fileMapping = new Map(); - for (const file of allFiles) { - // Generate the same filename logic as used in generateIndividualTxtFiles - const tempProcessed = await processContent(file, baseDir, false, null, null, language); // Quick pass for title with language filtering - if (tempProcessed) { - // Only process files that aren't marked to ignore - let fileName: string; - if (path.basename(file) === 'README.md') { - const parentDir = path.basename(path.dirname(file)); - fileName = generateSafeFileName(parentDir); - } else { - fileName = generateSafeFileName(tempProcessed.title || file); - } - fileMapping.set(file, fileName); - } +async function processAllFiles( + allFiles: string[], + baseDir: string, + language: Language +): Promise<{ processedFiles: ProcessedFile[]; fileMapping: Map }> { + const processedFiles: ProcessedFile[] = []; + + // First pass: build file mapping + const fileMapping = new Map(); + for (const file of allFiles) { + // Generate the same filename logic as used in generateIndividualTxtFiles + const tempProcessed = await processContent(file, baseDir, false, null, null, language); // Quick pass for title with language filtering + if (tempProcessed) { + // Only process files that aren't marked to ignore + let fileName: string; + if (path.basename(file) === 'README.md') { + const parentDir = path.basename(path.dirname(file)); + fileName = generateSafeFileName(parentDir); + } else { + fileName = generateSafeFileName(tempProcessed.title || file); + } + fileMapping.set(file, fileName); } + } - // Second pass: process files with mapping for link resolution - for (const file of allFiles) { - const processed = await processContent(file, baseDir, true, fileMapping, null, language); - if (processed && (processed.title || processed.content)) { - processedFiles.push(processed); - } + // Second pass: process files with mapping for link resolution + for (const file of allFiles) { + const processed = await processContent(file, baseDir, true, fileMapping, null, language); + if (processed && (processed.title || processed.content)) { + processedFiles.push(processed); } + } - // Sort by sidebar position, then by title - const sortedFiles = processedFiles.sort((a, b) => { - const posA = a.sidebarPosition || 999; - const posB = b.sidebarPosition || 999; + // Sort by sidebar position, then by title + const sortedFiles = processedFiles.sort((a, b) => { + const posA = a.sidebarPosition || 999; + const posB = b.sidebarPosition || 999; - if (posA !== posB) { - return posA - posB; - } + if (posA !== posB) { + return posA - posB; + } - return (a.title || '').localeCompare(b.title || ''); - }); + return (a.title || '').localeCompare(b.title || ''); + }); - return { processedFiles: sortedFiles, fileMapping }; + return { processedFiles: sortedFiles, fileMapping }; } /** @@ -243,53 +252,53 @@ async function processAllFiles(allFiles: string[], baseDir: string, language: La * @param fileMapping - File mapping for link resolution */ async function generateIndividualTxtFiles( - processedFiles: ProcessedFile[], - outputDir: string, - language: Language, - baseDir: string, - config: DocusaurusConfig, - fileMapping: Map + processedFiles: ProcessedFile[], + outputDir: string, + language: Language, + baseDir: string, + config: DocusaurusConfig, + fileMapping: Map ): Promise { - const docsDir = path.join(outputDir, `docs_${language}`); - - // Clean and recreate docs directory to remove old files - if (fs.existsSync(docsDir)) { - fs.rmSync(docsDir, { recursive: true }); - } - fs.mkdirSync(docsDir, { recursive: true }); - - for (const file of processedFiles) { - if (!file.content) continue; - - // Re-process the file with full URL generation for individual files - const reprocessed = await processContent( - file.filePath, - baseDir, - true, - fileMapping, - config, - language - ); + const docsDir = path.join(outputDir, `docs_${language}`); + + // Clean and recreate docs directory to remove old files + if (fs.existsSync(docsDir)) { + fs.rmSync(docsDir, { recursive: true }); + } + fs.mkdirSync(docsDir, { recursive: true }); + + for (const file of processedFiles) { + if (!file.content) continue; + + // Re-process the file with full URL generation for individual files + const reprocessed = await processContent( + file.filePath, + baseDir, + true, + fileMapping, + config, + language + ); - if (!reprocessed) continue; + if (!reprocessed) continue; - // Generate safe filename - use folder name for README.md files - let fileName: string; - if (path.basename(file.filePath) === 'README.md') { - // Use parent folder name for README files - const parentDir = path.basename(path.dirname(file.filePath)); - fileName = generateSafeFileName(parentDir); - } else { - fileName = generateSafeFileName(file.title || file.filePath); - } + // Generate safe filename - use folder name for README.md files + let fileName: string; + if (path.basename(file.filePath) === 'README.md') { + // Use parent folder name for README files + const parentDir = path.basename(path.dirname(file.filePath)); + fileName = generateSafeFileName(parentDir); + } else { + fileName = generateSafeFileName(file.title || file.filePath); + } - const outputPath = path.join(docsDir, `${fileName}.txt`); + const outputPath = path.join(docsDir, `${fileName}.txt`); - // Use the reprocessed content directly without adding metadata header - let txtContent = reprocessed.content || file.content; // Use reprocessed content with full URLs + // Use the reprocessed content directly without adding metadata header + let txtContent = reprocessed.content || file.content; // Use reprocessed content with full URLs - fs.writeFileSync(outputPath, txtContent, 'utf8'); - } + fs.writeFileSync(outputPath, txtContent, 'utf8'); + } } /** @@ -300,23 +309,34 @@ async function generateIndividualTxtFiles( * @param fileMapping - Mapping of source files to generated filenames * @returns Generated navigation content */ -async function generateSmallVersionHierarchical(language: Language, baseDir: string, config: DocusaurusConfig, fileMapping: Map): Promise { - const langName = LANGUAGE_NAMES[language]; - // Remove trailing slash from URL and ensure baseUrl starts with slash - const cleanUrl = config.url.replace(/\/$/, ''); - const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; - const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; - - let content = `# Teams SDK - ${langName} Documentation\n\n`; - content += COMMON_OVERALL_SUMMARY(language) + '\n\n'; - - // Get hierarchical structure - const hierarchical = getHierarchicalFiles(baseDir, `main/${language}`); - - // Add Language-specific Documentation - content += renderHierarchicalStructure(hierarchical.language, fullBaseUrl, language, fileMapping, 0); - - return content; +async function generateSmallVersionHierarchical( + language: Language, + baseDir: string, + config: DocusaurusConfig, + fileMapping: Map +): Promise { + const langName = LANGUAGE_NAMES[language]; + // Remove trailing slash from URL and ensure baseUrl starts with slash + const cleanUrl = config.url.replace(/\/$/, ''); + const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; + const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; + + let content = `# Teams SDK - ${langName} Documentation\n\n`; + content += COMMON_OVERALL_SUMMARY(language) + '\n\n'; + + // Get hierarchical structure + const hierarchical = getHierarchicalFiles(baseDir, `main/${language}`); + + // Add Language-specific Documentation + content += renderHierarchicalStructure( + hierarchical.language, + fullBaseUrl, + language, + fileMapping, + 0 + ); + + return content; } /** @@ -328,130 +348,142 @@ async function generateSmallVersionHierarchical(language: Language, baseDir: str * @param indentLevel - Current indentation level (0 = section headers, 1+ = bullet points) * @returns Rendered content with proper hierarchy */ -function renderHierarchicalStructure(structure: { [key: string]: FolderStructure }, baseUrl: string, language: Language, fileMapping: Map, indentLevel: number = 0): string { - let content = ''; +function renderHierarchicalStructure( + structure: { [key: string]: FolderStructure }, + baseUrl: string, + language: Language, + fileMapping: Map, + indentLevel: number = 0 +): string { + let content = ''; + + // Helper function for folder name formatting + function formatFolderName(name: string): string { + return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); + } + + // Convert structure to sorted array + const folders = Object.entries(structure) + .map(([key, value]) => ({ key, ...value })) + .sort((a, b) => { + const orderA = a.order || 999; + const orderB = b.order || 999; + if (orderA !== orderB) return orderA - orderB; + return a.key.localeCompare(b.key); + }); - // Helper function for folder name formatting - function formatFolderName(name: string): string { - return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); - } + for (const folder of folders) { + // Check if this folder has any content (files or children) + const hasFiles = folder.files && folder.files.length > 0; + const hasChildren = folder.children && Object.keys(folder.children).length > 0; + const hasContent = hasFiles || hasChildren; + + if (hasContent) { + // Check if this folder has a README file to make the header clickable + const readmeFile = hasFiles + ? folder.files.find((f) => path.basename(f.path) === 'README.md') + : null; + const displayTitle = + folder.title && folder.title !== folder.key ? folder.title : formatFolderName(folder.key); + + // Generate indent for nested folders (use spaces, 2 per level) + const indent = ' '.repeat(indentLevel); + + if (readmeFile) { + // Make folder header clickable by linking to the README + let folderFileName: string; + if (fileMapping && fileMapping.has(readmeFile.path)) { + folderFileName = fileMapping.get(readmeFile.path)!; + } else { + folderFileName = generateSafeFileName(folder.key); + } - // Convert structure to sorted array - const folders = Object.entries(structure) - .map(([key, value]) => ({ key, ...value })) - .sort((a, b) => { - const orderA = a.order || 999; - const orderB = b.order || 999; - if (orderA !== orderB) return orderA - orderB; - return a.key.localeCompare(b.key); - }); - - for (const folder of folders) { - // Check if this folder has any content (files or children) - const hasFiles = folder.files && folder.files.length > 0; - const hasChildren = folder.children && Object.keys(folder.children).length > 0; - const hasContent = hasFiles || hasChildren; - - if (hasContent) { - // Check if this folder has a README file to make the header clickable - const readmeFile = hasFiles - ? folder.files.find((f) => path.basename(f.path) === 'README.md') - : null; - const displayTitle = - folder.title && folder.title !== folder.key ? folder.title : formatFolderName(folder.key); - - // Generate indent for nested folders (use spaces, 2 per level) - const indent = ' '.repeat(indentLevel); - - if (readmeFile) { - // Make folder header clickable by linking to the README - let folderFileName: string; - if (fileMapping && fileMapping.has(readmeFile.path)) { - folderFileName = fileMapping.get(readmeFile.path)!; - } else { - folderFileName = generateSafeFileName(folder.key); - } - - // Use ### header for top-level sections (indentLevel 0), bullet points for nested - if (indentLevel === 0) { - content += `### [${displayTitle}](${baseUrl}llms_docs/docs_${language}/${folderFileName}.txt)\n\n`; - } else { - content += `${indent}- [${displayTitle}](${baseUrl}llms_docs/docs_${language}/${folderFileName}.txt)`; - } - - // Add summary from README if available - try { - const readmeContent = readFileUtf8Normalized(readmeFile.path); - const { frontmatter } = FrontmatterParser.extract(readmeContent); - const summary = frontmatter.summary; - if (summary) { - if (indentLevel === 0) { - content += `${summary}\n\n`; - } else { - content += `: ${summary}`; - } - } - } catch (error) { - // Ignore errors reading README summary - } - - if (indentLevel > 0) { - content += '\n'; - } - } else { - // No README - if (indentLevel === 0) { - content += `### ${displayTitle}\n\n`; - } else { - content += `${indent}- ${displayTitle}\n`; - } - } + // Use ### header for top-level sections (indentLevel 0), bullet points for nested + if (indentLevel === 0) { + content += `### [${displayTitle}](${baseUrl}llms_docs/docs_${language}/${folderFileName}.txt)\n\n`; + } else { + content += `${indent}- [${displayTitle}](${baseUrl}llms_docs/docs_${language}/${folderFileName}.txt)`; + } - // Add files in this folder (sorted by order), excluding README - if (hasFiles) { - const sortedFiles = [...folder.files] - .filter((f) => path.basename(f.path) !== 'README.md') // Exclude README since it's now the header link - .sort((a, b) => { - const orderA = a.order || 999; - const orderB = b.order || 999; - if (orderA !== orderB) return orderA - orderB; - return a.name.localeCompare(b.name); - }); - - for (const file of sortedFiles) { - // Use file mapping to get the correct generated filename - let fileName: string; - if (fileMapping && fileMapping.has(file.path)) { - fileName = fileMapping.get(file.path)!; - } else { - fileName = generateSafeFileName(file.title || file.name); - } - - const summary = extractSummaryFromFile(file.path); - - // Files are always indented one level deeper than their parent folder - const fileIndent = ' '.repeat(indentLevel + 1); - content += `${fileIndent}- [${file.title}](${baseUrl}llms_docs/docs_${language}/${fileName}.txt)`; - if (summary) { - content += `: ${summary}`; - } - content += '\n'; - } + // Add summary from README if available + try { + const readmeContent = readFileUtf8Normalized(readmeFile.path); + const { frontmatter } = FrontmatterParser.extract(readmeContent); + const summary = frontmatter.summary; + if (summary) { + if (indentLevel === 0) { + content += `${summary}\n\n`; + } else { + content += `: ${summary}`; } + } + } catch (error) { + // Ignore errors reading README summary + } - // Recursively render children with increased indent - if (hasChildren) { - content += renderHierarchicalStructure(folder.children, baseUrl, language, fileMapping, indentLevel + 1); - } + if (indentLevel > 0) { + content += '\n'; + } + } else { + // No README + if (indentLevel === 0) { + content += `### ${displayTitle}\n\n`; + } else { + content += `${indent}- ${displayTitle}\n`; + } + } - // Add spacing after top-level sections - if (indentLevel === 0) { - content += '\n'; - } + // Add files in this folder (sorted by order), excluding README + if (hasFiles) { + const sortedFiles = [...folder.files] + .filter((f) => path.basename(f.path) !== 'README.md') // Exclude README since it's now the header link + .sort((a, b) => { + const orderA = a.order || 999; + const orderB = b.order || 999; + if (orderA !== orderB) return orderA - orderB; + return a.name.localeCompare(b.name); + }); + + for (const file of sortedFiles) { + // Use file mapping to get the correct generated filename + let fileName: string; + if (fileMapping && fileMapping.has(file.path)) { + fileName = fileMapping.get(file.path)!; + } else { + fileName = generateSafeFileName(file.title || file.name); + } + + const summary = extractSummaryFromFile(file.path); + + // Files are always indented one level deeper than their parent folder + const fileIndent = ' '.repeat(indentLevel + 1); + content += `${fileIndent}- [${file.title}](${baseUrl}llms_docs/docs_${language}/${fileName}.txt)`; + if (summary) { + content += `: ${summary}`; + } + content += '\n'; } + } + + // Recursively render children with increased indent + if (hasChildren) { + content += renderHierarchicalStructure( + folder.children, + baseUrl, + language, + fileMapping, + indentLevel + 1 + ); + } + + // Add spacing after top-level sections + if (indentLevel === 0) { + content += '\n'; + } } + } - return content; + return content; } /** @@ -460,40 +492,40 @@ function renderHierarchicalStructure(structure: { [key: string]: FolderStructure * @returns File summary or empty string */ function extractSummaryFromFile(filePath: string): string { - try { - const fileContent = readFileUtf8Normalized(filePath); - - // First check for summary in frontmatter - const { frontmatter, content } = FrontmatterParser.extract(fileContent); - const summary = frontmatter.summary; - if (summary && typeof summary === 'string') { - return summary; - } + try { + const fileContent = readFileUtf8Normalized(filePath); + + // First check for summary in frontmatter + const { frontmatter, content } = FrontmatterParser.extract(fileContent); + const summary = frontmatter.summary; + if (summary && typeof summary === 'string') { + return summary; + } - // Remove HTML comments before extracting summary - // Generated .mdx files contain AUTO-GENERATED warnings that shouldn't appear in summaries - const cleanContent = content.replace(//g, ''); - - // Fallback to extracting first meaningful paragraph if no summary in frontmatter - const paragraphs = cleanContent.split('\n\n'); - for (const paragraph of paragraphs) { - const clean = paragraph - .replace(/#+\s*/g, '') // Remove headers - .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold - .replace(/\*(.+?)\*/g, '$1') // Remove italic - .replace(/`(.+?)`/g, '$1') // Remove inline code - .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links, keep text - .trim(); - - if (clean.length > 20 && !clean.startsWith('```') && !clean.startsWith('import')) { - return clean.length > 100 ? clean.substring(0, 100) + '...' : clean; - } - } - } catch (error) { - // Ignore file read errors + // Remove HTML comments before extracting summary + // Generated .mdx files contain AUTO-GENERATED warnings that shouldn't appear in summaries + const cleanContent = content.replace(//g, ''); + + // Fallback to extracting first meaningful paragraph if no summary in frontmatter + const paragraphs = cleanContent.split('\n\n'); + for (const paragraph of paragraphs) { + const clean = paragraph + .replace(/#+\s*/g, '') // Remove headers + .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold + .replace(/\*(.+?)\*/g, '$1') // Remove italic + .replace(/`(.+?)`/g, '$1') // Remove inline code + .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links, keep text + .trim(); + + if (clean.length > 20 && !clean.startsWith('```') && !clean.startsWith('import')) { + return clean.length > 100 ? clean.substring(0, 100) + '...' : clean; + } } + } catch (error) { + // Ignore file read errors + } - return ''; + return ''; } /** @@ -503,28 +535,32 @@ function extractSummaryFromFile(filePath: string): string { * @param baseDir - Base directory path * @returns Generated content */ -async function generateFullVersion(language: Language, processedFiles: ProcessedFile[], baseDir: string): Promise { - const langName = LANGUAGE_NAMES[language] - let content = `# Teams SDK - ${langName} Documentation (Complete)\n\n`; - content += COMMON_OVERALL_SUMMARY(language) + '\n\n'; - - // Group files by section - const sections = groupFilesBySection(processedFiles, baseDir); - - // Process all sections - for (const [sectionName, files] of Object.entries(sections)) { - if (!files || files.length === 0) continue; - - content += `## ${formatSectionName(sectionName)}\n\n`; - - for (const file of files) { - if (file.content) { - content += `### ${file.title}\n\n${file.content}\n\n---\n\n`; - } - } +async function generateFullVersion( + language: Language, + processedFiles: ProcessedFile[], + baseDir: string +): Promise { + const langName = LANGUAGE_NAMES[language]; + let content = `# Teams SDK - ${langName} Documentation (Complete)\n\n`; + content += COMMON_OVERALL_SUMMARY(language) + '\n\n'; + + // Group files by section + const sections = groupFilesBySection(processedFiles, baseDir); + + // Process all sections + for (const [sectionName, files] of Object.entries(sections)) { + if (!files || files.length === 0) continue; + + content += `## ${formatSectionName(sectionName)}\n\n`; + + for (const file of files) { + if (file.content) { + content += `### ${file.title}\n\n${file.content}\n\n---\n\n`; + } } + } - return content; + return content; } /** @@ -533,56 +569,59 @@ async function generateFullVersion(language: Language, processedFiles: Processed * @param baseDir - Base directory path * @returns Grouped files by section */ -function groupFilesBySection(processedFiles: ProcessedFile[], baseDir: string): { [key: string]: ProcessedFile[] } { - const sections: { [key: string]: ProcessedFile[] } = { - main: [], - gettingStarted: [], - essentials: [], - inDepthGuides: [], - migrations: [], - }; - - for (const file of processedFiles) { - const relativePath = path.relative(path.join(baseDir, 'docs'), file.filePath); - - if (relativePath.startsWith('main/')) { - sections.main.push(file); - } else if (relativePath.includes('getting-started/')) { - sections.gettingStarted.push(file); - } else if (relativePath.includes('essentials/')) { - sections.essentials.push(file); - } else if (relativePath.includes('in-depth-guides/')) { - sections.inDepthGuides.push(file); - } else if (relativePath.includes('migrations/')) { - sections.migrations.push(file); - } else { - // Create dynamic section based on directory - const parts = relativePath.split('/'); - if (parts.length > 1) { - const sectionKey = parts[1].replace(/[^a-zA-Z0-9]/g, ''); - if (!sections[sectionKey]) { - sections[sectionKey] = []; - } - sections[sectionKey].push(file); - } +function groupFilesBySection( + processedFiles: ProcessedFile[], + baseDir: string +): { [key: string]: ProcessedFile[] } { + const sections: { [key: string]: ProcessedFile[] } = { + main: [], + gettingStarted: [], + essentials: [], + inDepthGuides: [], + migrations: [], + }; + + for (const file of processedFiles) { + const relativePath = path.relative(path.join(baseDir, 'docs'), file.filePath); + + if (relativePath.startsWith('main/')) { + sections.main.push(file); + } else if (relativePath.includes('getting-started/')) { + sections.gettingStarted.push(file); + } else if (relativePath.includes('essentials/')) { + sections.essentials.push(file); + } else if (relativePath.includes('in-depth-guides/')) { + sections.inDepthGuides.push(file); + } else if (relativePath.includes('migrations/')) { + sections.migrations.push(file); + } else { + // Create dynamic section based on directory + const parts = relativePath.split('/'); + if (parts.length > 1) { + const sectionKey = parts[1].replace(/[^a-zA-Z0-9]/g, ''); + if (!sections[sectionKey]) { + sections[sectionKey] = []; } + sections[sectionKey].push(file); + } } + } - // Sort files within each section by sidebar position - for (const sectionFiles of Object.values(sections)) { - sectionFiles.sort((a, b) => { - const posA = a.sidebarPosition || 999; - const posB = b.sidebarPosition || 999; + // Sort files within each section by sidebar position + for (const sectionFiles of Object.values(sections)) { + sectionFiles.sort((a, b) => { + const posA = a.sidebarPosition || 999; + const posB = b.sidebarPosition || 999; - if (posA !== posB) { - return posA - posB; - } + if (posA !== posB) { + return posA - posB; + } - return (a.title || '').localeCompare(b.title || ''); - }); - } + return (a.title || '').localeCompare(b.title || ''); + }); + } - return sections; + return sections; } /** @@ -591,16 +630,16 @@ function groupFilesBySection(processedFiles: ProcessedFile[], baseDir: string): * @returns Safe filename */ function generateSafeFileName(title: string): string { - return ( - title - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special characters - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single - .replace(/^-|-$/g, '') // Remove leading/trailing hyphens - .substring(0, 50) || // Limit length - 'untitled' - ); + return ( + title + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // Remove special characters + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single + .replace(/^-|-$/g, '') // Remove leading/trailing hyphens + .substring(0, 50) || // Limit length + 'untitled' + ); } /** @@ -609,21 +648,21 @@ function generateSafeFileName(title: string): string { * @returns Formatted section name */ function formatSectionName(sectionName: string): string { - const nameMap: { [key: string]: string } = { - main: 'Main Documentation', - gettingStarted: 'Getting Started', - essentials: 'Essentials', - inDepthGuides: 'In-Depth Guides', - migrations: 'Migrations', - }; - - return ( - nameMap[sectionName] || - sectionName - .replace(/([A-Z])/g, ' $1') // Add spaces before capitals - .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter - .trim() - ); + const nameMap: { [key: string]: string } = { + main: 'Main Documentation', + gettingStarted: 'Getting Started', + essentials: 'Essentials', + inDepthGuides: 'In-Depth Guides', + migrations: 'Migrations', + }; + + return ( + nameMap[sectionName] || + sectionName + .replace(/([A-Z])/g, ' $1') // Add spaces before capitals + .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter + .trim() + ); } /** @@ -632,16 +671,16 @@ function formatSectionName(sectionName: string): string { * @returns Formatted string */ function formatBytes(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // Run the generator if this file is executed directly if (require.main === module) { - generateLlmsTxt(); + generateLlmsTxt(); } export { generateLlmsTxt }; diff --git a/teams.md/scripts/lib/content-processor.ts b/teams.md/scripts/lib/content-processor.ts index edadb408c2..11bcd4879e 100644 --- a/teams.md/scripts/lib/content-processor.ts +++ b/teams.md/scripts/lib/content-processor.ts @@ -5,23 +5,23 @@ import { FrontmatterParser } from './frontmatter-parser'; import readFileUtf8Normalized from '../../src/utils/readFileUtf8Normalized'; interface ProcessedContent { - title: string; - content: string; - frontmatter: { [key: string]: any }; - filePath: string; - sidebarPosition: number; - relativeUrl: string; + title: string; + content: string; + frontmatter: { [key: string]: any }; + filePath: string; + sidebarPosition: number; + relativeUrl: string; } interface ParsedMarkdown { - title: string; - content: string; - frontmatter: { [key: string]: any }; + title: string; + content: string; + frontmatter: { [key: string]: any }; } interface DocusaurusConfig { - url: string; - baseUrl: string; + url: string; + baseUrl: string; } /** @@ -30,35 +30,39 @@ interface DocusaurusConfig { * @returns True if file should be ignored due to section filtering */ export function shouldIgnoreFileBySection(filePath: string): boolean { - // Get the directory path - let currentDir = path.dirname(filePath); - - // Walk up the directory tree looking for README.md or index.mdx files - while (currentDir && currentDir !== path.dirname(currentDir)) { - const readmePath = path.join(currentDir, 'README.md'); - const indexPath = path.join(currentDir, 'index.mdx'); - - // Check README.md first, then index.mdx - const indexFilePath = fs.existsSync(readmePath) ? readmePath : (fs.existsSync(indexPath) ? indexPath : null); - - if (indexFilePath) { - try { - const indexContent = readFileUtf8Normalized(indexFilePath); - const indexFrontmatter = FrontmatterParser.extract(indexContent).frontmatter; - // Only ignore entire section if index file has 'llms: ignore' (not 'ignore-file') - if (indexFrontmatter.llms === 'ignore' || indexFrontmatter.llms === false) { - return true; - } - } catch (error) { - // Ignore errors reading index file - } + // Get the directory path + let currentDir = path.dirname(filePath); + + // Walk up the directory tree looking for README.md or index.mdx files + while (currentDir && currentDir !== path.dirname(currentDir)) { + const readmePath = path.join(currentDir, 'README.md'); + const indexPath = path.join(currentDir, 'index.mdx'); + + // Check README.md first, then index.mdx + const indexFilePath = fs.existsSync(readmePath) + ? readmePath + : fs.existsSync(indexPath) + ? indexPath + : null; + + if (indexFilePath) { + try { + const indexContent = readFileUtf8Normalized(indexFilePath); + const indexFrontmatter = FrontmatterParser.extract(indexContent).frontmatter; + // Only ignore entire section if index file has 'llms: ignore' (not 'ignore-file') + if (indexFrontmatter.llms === 'ignore' || indexFrontmatter.llms === false) { + return true; } - - // Move up one directory - currentDir = path.dirname(currentDir); + } catch (error) { + // Ignore errors reading index file + } } - return false; + // Move up one directory + currentDir = path.dirname(currentDir); + } + + return false; } /** @@ -72,56 +76,71 @@ export function shouldIgnoreFileBySection(filePath: string): boolean { * @returns Processed content with title, content, and metadata */ export async function processContent( - filePath: string, - baseDir: string, - includeCodeBlocks: boolean = false, - fileMapping: Map | null = null, - config: DocusaurusConfig | null = null, - language: string | null = null + filePath: string, + baseDir: string, + includeCodeBlocks: boolean = false, + fileMapping: Map | null = null, + config: DocusaurusConfig | null = null, + language: string | null = null ): Promise { - try { - if (!fs.existsSync(filePath)) { - console.warn(`āš ļø File not found: ${filePath}`); - return { title: '', content: '', frontmatter: {}, filePath, sidebarPosition: 999, relativeUrl: '' }; - } + try { + if (!fs.existsSync(filePath)) { + console.warn(`āš ļø File not found: ${filePath}`); + return { + title: '', + content: '', + frontmatter: {}, + filePath, + sidebarPosition: 999, + relativeUrl: '', + }; + } const rawContent = readFileUtf8Normalized(filePath); - // Check if this file should be excluded from LLM output - if (FrontmatterParser.shouldIgnore(rawContent)) { - return null; // Return null to indicate this file should be skipped - } - - // Check if language filtering is enabled and if this file is for a different language - const { frontmatter: earlyFrontmatter } = FrontmatterParser.extract(rawContent); - if (language && earlyFrontmatter.languages) { - const fileLanguages = Array.isArray(earlyFrontmatter.languages) - ? earlyFrontmatter.languages - : [earlyFrontmatter.languages]; - if (!fileLanguages.includes(language)) { - return null; // Skip this file as it's not for the current language - } - } - - const { title, content, frontmatter } = await parseMarkdownContent(rawContent, baseDir, includeCodeBlocks, filePath, fileMapping, config, language); + // Check if this file should be excluded from LLM output + if (FrontmatterParser.shouldIgnore(rawContent)) { + return null; // Return null to indicate this file should be skipped + } - // Check if this file should be ignored due to section-wide filtering - if (shouldIgnoreFileBySection(filePath)) { - return null; // Return null to indicate this file should be skipped - } + // Check if language filtering is enabled and if this file is for a different language + const { frontmatter: earlyFrontmatter } = FrontmatterParser.extract(rawContent); + if (language && earlyFrontmatter.languages) { + const fileLanguages = Array.isArray(earlyFrontmatter.languages) + ? earlyFrontmatter.languages + : [earlyFrontmatter.languages]; + if (!fileLanguages.includes(language)) { + return null; // Skip this file as it's not for the current language + } + } - return { - title: title, - content: content || '', - frontmatter: frontmatter || {}, - filePath, - sidebarPosition: (frontmatter.sidebar_position as number) || 999, - relativeUrl: generateRelativeUrl(filePath, baseDir) - }; - } catch (error) { - console.error(`āŒ Error processing file ${filePath}:`, (error as Error).message); - throw error; // Re-throw to fail the build + const { title, content, frontmatter } = await parseMarkdownContent( + rawContent, + baseDir, + includeCodeBlocks, + filePath, + fileMapping, + config, + language + ); + + // Check if this file should be ignored due to section-wide filtering + if (shouldIgnoreFileBySection(filePath)) { + return null; // Return null to indicate this file should be skipped } + + return { + title: title, + content: content || '', + frontmatter: frontmatter || {}, + filePath, + sidebarPosition: (frontmatter.sidebar_position as number) || 999, + relativeUrl: generateRelativeUrl(filePath, baseDir), + }; + } catch (error) { + console.error(`āŒ Error processing file ${filePath}:`, (error as Error).message); + throw error; // Re-throw to fail the build + } } /** @@ -136,46 +155,46 @@ export async function processContent( * @returns Parsed title, content, and frontmatter */ async function parseMarkdownContent( - rawContent: string, - baseDir: string, - includeCodeBlocks: boolean, - filePath: string, - fileMapping: Map | null, - config: DocusaurusConfig | null, - language: string | null + rawContent: string, + baseDir: string, + includeCodeBlocks: boolean, + filePath: string, + fileMapping: Map | null, + config: DocusaurusConfig | null, + language: string | null ): Promise { - // Extract and remove frontmatter using enhanced parser with js-yaml - const { frontmatter, content: contentWithoutFrontmatter } = FrontmatterParser.extract(rawContent); - let content = contentWithoutFrontmatter; - - // Extract title from first H1 (always required) - const h1Match = content.match(/^#\s+(.+)$/m); - if (!h1Match) { - throw new Error(`No # header found in file: ${filePath}`); - } - const title = h1Match[1].trim(); - - // Remove import statements - content = content.replace(/^import\s+.*$/gm, ''); - - // Process FileCodeBlock components if requested - if (includeCodeBlocks) { - content = await processFileCodeBlocks(content, baseDir); - } else { - // Remove FileCodeBlock components for small version - content = content.replace(//g, '[Code example removed for brevity]'); - } - - // Clean up MDX-specific syntax while preserving markdown - content = cleanMdxSyntax(content); - - // Fix internal relative links - content = fixInternalLinks(content, filePath, fileMapping, config, language); - - // Remove excessive whitespace - content = content.replace(/\n{3,}/g, '\n\n').trim(); - - return { title, content, frontmatter }; + // Extract and remove frontmatter using enhanced parser with js-yaml + const { frontmatter, content: contentWithoutFrontmatter } = FrontmatterParser.extract(rawContent); + let content = contentWithoutFrontmatter; + + // Extract title from first H1 (always required) + const h1Match = content.match(/^#\s+(.+)$/m); + if (!h1Match) { + throw new Error(`No # header found in file: ${filePath}`); + } + const title = h1Match[1].trim(); + + // Remove import statements + content = content.replace(/^import\s+.*$/gm, ''); + + // Process FileCodeBlock components if requested + if (includeCodeBlocks) { + content = await processFileCodeBlocks(content, baseDir); + } else { + // Remove FileCodeBlock components for small version + content = content.replace(//g, '[Code example removed for brevity]'); + } + + // Clean up MDX-specific syntax while preserving markdown + content = cleanMdxSyntax(content); + + // Fix internal relative links + content = fixInternalLinks(content, filePath, fileMapping, config, language); + + // Remove excessive whitespace + content = content.replace(/\n{3,}/g, '\n\n').trim(); + + return { title, content, frontmatter }; } /** @@ -185,27 +204,27 @@ async function parseMarkdownContent( * @returns Content with FileCodeBlocks replaced by actual code */ async function processFileCodeBlocks(content: string, baseDir: string): Promise { - const fileCodeBlockRegex = /]+)\/>/g; - let processedContent = content; - let match; - - while ((match = fileCodeBlockRegex.exec(content)) !== null) { - const attributes = parseAttributes(match[1]); - const { src, lang } = attributes; - - if (src) { - try { - const codeContent = await loadCodeFile(src, baseDir); - const codeBlock = `\`\`\`${lang || 'typescript'}\n${codeContent}\n\`\`\``; - processedContent = processedContent.replace(match[0], codeBlock); - } catch (error) { - console.warn(`āš ļø Could not load code file ${src}:`, (error as Error).message); - processedContent = processedContent.replace(match[0], `[Code file not found: ${src}]`); - } - } + const fileCodeBlockRegex = /]+)\/>/g; + let processedContent = content; + let match; + + while ((match = fileCodeBlockRegex.exec(content)) !== null) { + const attributes = parseAttributes(match[1]); + const { src, lang } = attributes; + + if (src) { + try { + const codeContent = await loadCodeFile(src, baseDir); + const codeBlock = `\`\`\`${lang || 'typescript'}\n${codeContent}\n\`\`\``; + processedContent = processedContent.replace(match[0], codeBlock); + } catch (error) { + console.warn(`āš ļø Could not load code file ${src}:`, (error as Error).message); + processedContent = processedContent.replace(match[0], `[Code file not found: ${src}]`); + } } + } - return processedContent; + return processedContent; } /** @@ -215,19 +234,19 @@ async function processFileCodeBlocks(content: string, baseDir: string): Promise< * @returns Code content */ async function loadCodeFile(src: string, baseDir: string): Promise { - // Convert src path to local file path - let filePath: string; - if (src.startsWith('/')) { - filePath = path.join(baseDir, 'static', src.substring(1)); - } else { - filePath = path.join(baseDir, 'static', src); - } - - if (fs.existsSync(filePath)) { - return readFileUtf8Normalized(filePath).trim(); - } else { - throw new Error(`File not found: ${filePath}`); - } + // Convert src path to local file path + let filePath: string; + if (src.startsWith('/')) { + filePath = path.join(baseDir, 'static', src.substring(1)); + } else { + filePath = path.join(baseDir, 'static', src); + } + + if (fs.existsSync(filePath)) { + return readFileUtf8Normalized(filePath).trim(); + } else { + throw new Error(`File not found: ${filePath}`); + } } /** @@ -236,15 +255,15 @@ async function loadCodeFile(src: string, baseDir: string): Promise { * @returns Parsed attributes */ function parseAttributes(attributeString: string): { [key: string]: string } { - const attributes: { [key: string]: string } = {}; - const regex = /(\w+)=["']([^"']+)["']/g; - let match; + const attributes: { [key: string]: string } = {}; + const regex = /(\w+)=["']([^"']+)["']/g; + let match; - while ((match = regex.exec(attributeString)) !== null) { - attributes[match[1]] = match[2]; - } + while ((match = regex.exec(attributeString)) !== null) { + attributes[match[1]] = match[2]; + } - return attributes; + return attributes; } /** @@ -253,53 +272,52 @@ function parseAttributes(attributeString: string): { [key: string]: string } { * @returns Cleaned content */ function cleanMdxSyntax(content: string): string { - let cleaned = content; - - // Remove HTML comments from llms.txt output - // Generated .mdx files contain AUTO-GENERATED warnings as HTML comments for developers, - // but these developer notes don't belong in AI-friendly documentation files. - // Note: Section markers () in .incl.md source files are processed - // earlier by generate-language-docs.ts and never appear in generated .mdx files. - cleaned = cleaned.replace(//g, ''); - - // Remove JSX components (except code blocks which are handled separately) - cleaned = cleaned.replace(/<\/?[A-Z][^>]*>/g, ''); - - // Remove empty JSX fragments - cleaned = cleaned.replace(/<>\s*<\/>/g, ''); - - // Remove JSX expressions but keep the content if it's simple text - // IMPORTANT: Don't process content inside code blocks (```) - const codeBlockRegex = /```[\s\S]*?```/g; - const codeBlocks: string[] = []; - - // Extract code blocks temporarily - cleaned = cleaned.replace(codeBlockRegex, (match) => { - codeBlocks.push(match); - return `___CODE_BLOCK_${codeBlocks.length - 1}___`; - }); - - // Now remove JSX expressions outside code blocks - cleaned = cleaned.replace(/\{([^{}]+)\}/g, (match, expr) => { - // Keep simple text expressions, remove complex ones - if (expr.includes('(') || expr.includes('.') || expr.includes('[')) { - return ''; - } - return expr; - }); + let cleaned = content; + + // Remove HTML comments from llms.txt output + // Generated .mdx files contain AUTO-GENERATED warnings as HTML comments for developers, + // but these developer notes don't belong in AI-friendly documentation files. + // Note: Section markers () in .incl.md source files are processed + // earlier by generate-language-docs.ts and never appear in generated .mdx files. + cleaned = cleaned.replace(//g, ''); + + // Remove JSX components (except code blocks which are handled separately) + cleaned = cleaned.replace(/<\/?[A-Z][^>]*>/g, ''); + + // Remove empty JSX fragments + cleaned = cleaned.replace(/<>\s*<\/>/g, ''); + + // Remove JSX expressions but keep the content if it's simple text + // IMPORTANT: Don't process content inside code blocks (```) + const codeBlockRegex = /```[\s\S]*?```/g; + const codeBlocks: string[] = []; + + // Extract code blocks temporarily + cleaned = cleaned.replace(codeBlockRegex, (match) => { + codeBlocks.push(match); + return `___CODE_BLOCK_${codeBlocks.length - 1}___`; + }); + + // Now remove JSX expressions outside code blocks + cleaned = cleaned.replace(/\{([^{}]+)\}/g, (match, expr) => { + // Keep simple text expressions, remove complex ones + if (expr.includes('(') || expr.includes('.') || expr.includes('[')) { + return ''; + } + return expr; + }); - // Restore code blocks - cleaned = cleaned.replace(/___CODE_BLOCK_(\d+)___/g, (match, index) => { - return codeBlocks[parseInt(index)]; - }); + // Restore code blocks + cleaned = cleaned.replace(/___CODE_BLOCK_(\d+)___/g, (match, index) => { + return codeBlocks[parseInt(index)]; + }); - // Clean up multiple empty lines - cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n'); + // Clean up multiple empty lines + cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n'); - return cleaned; + return cleaned; } - /** * Generates a relative URL for a documentation file * @param filePath - Full path to the file @@ -307,26 +325,26 @@ function cleanMdxSyntax(content: string): string { * @returns Relative URL for the file */ export function generateRelativeUrl(filePath: string, baseDir: string): string { - const relativePath = path.relative(path.join(baseDir, 'docs'), filePath); - - // Convert file path to URL format - let url = relativePath - .replace(/\\/g, '/') // Convert Windows paths - .replace(/\.mdx?$/, '') // Remove .md/.mdx extension - .replace(/\/README$/i, '') // Remove /README from end - .replace(/\/index$/i, ''); // Remove /index from end - - // Add leading slash - if (!url.startsWith('/')) { - url = '/' + url; - } - - // Handle empty URL (root README) - if (url === '/') { - url = ''; - } - - return url; + const relativePath = path.relative(path.join(baseDir, 'docs'), filePath); + + // Convert file path to URL format + let url = relativePath + .replace(/\\/g, '/') // Convert Windows paths + .replace(/\.mdx?$/, '') // Remove .md/.mdx extension + .replace(/\/README$/i, '') // Remove /README from end + .replace(/\/index$/i, ''); // Remove /index from end + + // Add leading slash + if (!url.startsWith('/')) { + url = '/' + url; + } + + // Handle empty URL (root README) + if (url === '/') { + url = ''; + } + + return url; } /** @@ -339,85 +357,90 @@ export function generateRelativeUrl(filePath: string, baseDir: string): string { * @returns Content with fixed links */ export function fixInternalLinks( - content: string, - currentFilePath: string, - fileMapping: Map | null, - config: DocusaurusConfig | null, - language: string | null + content: string, + currentFilePath: string, + fileMapping: Map | null, + config: DocusaurusConfig | null, + language: string | null ): string { - // Pattern to match markdown links: [text](link) - const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + // Pattern to match markdown links: [text](link) + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; - return content.replace(linkRegex, (match, text, link) => { - // Skip external links (http/https/mailto/etc) - if (link.startsWith('http') || link.startsWith('mailto') || link.startsWith('#')) { - return match; - } + return content.replace(linkRegex, (match, text, link) => { + // Skip external links (http/https/mailto/etc) + if (link.startsWith('http') || link.startsWith('mailto') || link.startsWith('#')) { + return match; + } - // Skip absolute paths starting with / - if (link.startsWith('/') && !link.startsWith('//')) { - return match; - } + // Skip absolute paths starting with / + if (link.startsWith('/') && !link.startsWith('//')) { + return match; + } - // Handle relative links - if (!link.includes('://')) { - // Remove any file extensions and anchors - const cleanLink = link.split('#')[0].replace(/\.(md|mdx)$/, ''); - - // If it's just a filename without path separators, it's likely a sibling file - if (!cleanLink.includes('/')) { - // Try to resolve using file mapping first - if (fileMapping) { - const currentDir = path.dirname(currentFilePath); - const possiblePath = path.join(currentDir, cleanLink + '.md'); - const possibleMdxPath = path.join(currentDir, cleanLink + '.mdx'); - - // Look for the file in the mapping - for (const [sourcePath, generatedName] of fileMapping.entries()) { - // Check exact path match or basename match - if (sourcePath === possiblePath || - sourcePath === possibleMdxPath || - path.basename(sourcePath, '.md') === cleanLink || - path.basename(sourcePath, '.mdx') === cleanLink) { - - // Generate full URL if config and language are provided - if (config && language) { - const cleanUrl = config.url.replace(/\/$/, ''); - const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; - const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; - return `[${text}](${fullBaseUrl}llms_docs/docs_${language}/${generatedName}.txt)`; - } else { - return `[${text}](${generatedName}.txt)`; - } - } - } - } - - // Fallback to simple conversion - const safeFileName = cleanLink - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') - .substring(0, 50) - || 'untitled'; - - // Generate full URL if config and language are provided - if (config && language) { - const cleanUrl = config.url.replace(/\/$/, ''); - const cleanBaseUrl = config.baseUrl.startsWith('/') ? config.baseUrl : '/' + config.baseUrl; - const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; - return `[${text}](${fullBaseUrl}llms_docs/docs_${language}/${safeFileName}.txt)`; - } else { - return `[${text}](${safeFileName}.txt)`; - } + // Handle relative links + if (!link.includes('://')) { + // Remove any file extensions and anchors + const cleanLink = link.split('#')[0].replace(/\.(md|mdx)$/, ''); + + // If it's just a filename without path separators, it's likely a sibling file + if (!cleanLink.includes('/')) { + // Try to resolve using file mapping first + if (fileMapping) { + const currentDir = path.dirname(currentFilePath); + const possiblePath = path.join(currentDir, cleanLink + '.md'); + const possibleMdxPath = path.join(currentDir, cleanLink + '.mdx'); + + // Look for the file in the mapping + for (const [sourcePath, generatedName] of fileMapping.entries()) { + // Check exact path match or basename match + if ( + sourcePath === possiblePath || + sourcePath === possibleMdxPath || + path.basename(sourcePath, '.md') === cleanLink || + path.basename(sourcePath, '.mdx') === cleanLink + ) { + // Generate full URL if config and language are provided + if (config && language) { + const cleanUrl = config.url.replace(/\/$/, ''); + const cleanBaseUrl = config.baseUrl.startsWith('/') + ? config.baseUrl + : '/' + config.baseUrl; + const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; + return `[${text}](${fullBaseUrl}llms_docs/docs_${language}/${generatedName}.txt)`; + } else { + return `[${text}](${generatedName}.txt)`; + } } + } } - // Return original link if we can't process it - return match; - }); + // Fallback to simple conversion + const safeFileName = + cleanLink + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') + .substring(0, 50) || 'untitled'; + + // Generate full URL if config and language are provided + if (config && language) { + const cleanUrl = config.url.replace(/\/$/, ''); + const cleanBaseUrl = config.baseUrl.startsWith('/') + ? config.baseUrl + : '/' + config.baseUrl; + const fullBaseUrl = `${cleanUrl}${cleanBaseUrl}`; + return `[${text}](${fullBaseUrl}llms_docs/docs_${language}/${safeFileName}.txt)`; + } else { + return `[${text}](${safeFileName}.txt)`; + } + } + } + + // Return original link if we can't process it + return match; + }); } /** @@ -427,22 +450,22 @@ export function fixInternalLinks( * @returns Content summary */ export function extractSummary(content: string, maxLength: number = 200): string { - // Remove markdown formatting for summary - let summary = content - .replace(/#+\s*/g, '') // Remove headers - .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold - .replace(/\*(.+?)\*/g, '$1') // Remove italic - .replace(/`(.+?)`/g, '$1') // Remove inline code - .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links, keep text - .trim(); - - // Get first paragraph - const firstParagraph = summary.split('\n\n')[0]; - - // Truncate if too long - if (firstParagraph.length > maxLength) { - return firstParagraph.substring(0, maxLength).trim() + '...'; - } - - return firstParagraph; + // Remove markdown formatting for summary + let summary = content + .replace(/#+\s*/g, '') // Remove headers + .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold + .replace(/\*(.+?)\*/g, '$1') // Remove italic + .replace(/`(.+?)`/g, '$1') // Remove inline code + .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links, keep text + .trim(); + + // Get first paragraph + const firstParagraph = summary.split('\n\n')[0]; + + // Truncate if too long + if (firstParagraph.length > maxLength) { + return firstParagraph.substring(0, maxLength).trim() + '...'; + } + + return firstParagraph; } diff --git a/teams.md/scripts/lib/file-collector.ts b/teams.md/scripts/lib/file-collector.ts index 5ece9f4c1c..11311fdc66 100644 --- a/teams.md/scripts/lib/file-collector.ts +++ b/teams.md/scripts/lib/file-collector.ts @@ -5,22 +5,22 @@ import { FrontmatterParser } from './frontmatter-parser'; import readFileUtf8Normalized from '../../src/utils/readFileUtf8Normalized'; interface FileInfo { - name: string; - title: string; - path: string; - order: number; + name: string; + title: string; + path: string; + order: number; } interface FolderStructure { - title: string; - order: number; - path: string; - files: FileInfo[]; - children: { [key: string]: FolderStructure }; + title: string; + order: number; + path: string; + files: FileInfo[]; + children: { [key: string]: FolderStructure }; } interface HierarchicalFiles { - language: { [key: string]: FolderStructure }; + language: { [key: string]: FolderStructure }; } /** @@ -30,41 +30,41 @@ interface HierarchicalFiles { * @returns Array of file paths */ export function collectFiles(dirPath: string, extensions: string[] = ['.md', '.mdx']): string[] { - const files: string[] = []; - - if (!fs.existsSync(dirPath)) { - console.warn(`āš ļø Directory not found: ${dirPath}`); - return files; - } - - /** - * Recursively traverse directory - * @param currentPath - Current directory path - */ - function traverse(currentPath: string): void { - const items = fs.readdirSync(currentPath, { withFileTypes: true }); - - for (const item of items) { - const fullPath = path.join(currentPath, item.name); - - if (item.isDirectory()) { - // Skip common directories that don't contain docs - if (!shouldSkipDirectory(item.name)) { - traverse(fullPath); - } - } else if (item.isFile()) { - const ext = path.extname(item.name).toLowerCase(); - if (extensions.includes(ext)) { - files.push(fullPath); - } - } + const files: string[] = []; + + if (!fs.existsSync(dirPath)) { + console.warn(`āš ļø Directory not found: ${dirPath}`); + return files; + } + + /** + * Recursively traverse directory + * @param currentPath - Current directory path + */ + function traverse(currentPath: string): void { + const items = fs.readdirSync(currentPath, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(currentPath, item.name); + + if (item.isDirectory()) { + // Skip common directories that don't contain docs + if (!shouldSkipDirectory(item.name)) { + traverse(fullPath); } + } else if (item.isFile()) { + const ext = path.extname(item.name).toLowerCase(); + if (extensions.includes(ext)) { + files.push(fullPath); + } + } } + } - traverse(dirPath); + traverse(dirPath); - // Sort files for consistent ordering - return files.sort(); + // Sort files for consistent ordering + return files.sort(); } /** @@ -73,18 +73,18 @@ export function collectFiles(dirPath: string, extensions: string[] = ['.md', '.m * @returns True if directory should be skipped */ export function shouldSkipDirectory(dirName: string): boolean { - const skipDirs = [ - 'node_modules', - '.git', - 'build', - 'dist', - '.next', - '.docusaurus', - 'coverage', - '__pycache__' - ]; - - return skipDirs.includes(dirName) || dirName.startsWith('.'); + const skipDirs = [ + 'node_modules', + '.git', + 'build', + 'dist', + '.next', + '.docusaurus', + 'coverage', + '__pycache__', + ]; + + return skipDirs.includes(dirName) || dirName.startsWith('.'); } /** @@ -94,13 +94,13 @@ export function shouldSkipDirectory(dirName: string): boolean { * @returns Hierarchically organized file structure */ export function getHierarchicalFiles(basePath: string, language: string): HierarchicalFiles { - const langPath = path.join(basePath, 'docs', language); + const langPath = path.join(basePath, 'docs', language); - const structure: HierarchicalFiles = { - language: buildHierarchicalStructure(langPath) - }; + const structure: HierarchicalFiles = { + language: buildHierarchicalStructure(langPath), + }; - return structure; + return structure; } /** @@ -109,160 +109,174 @@ export function getHierarchicalFiles(basePath: string, language: string): Hierar * @returns Hierarchical structure with folders and files */ export function buildHierarchicalStructure(rootPath: string): { [key: string]: FolderStructure } { - if (!fs.existsSync(rootPath)) { - return {}; - } + if (!fs.existsSync(rootPath)) { + return {}; + } + + const structure: { [key: string]: FolderStructure } = {}; + const seenTitles = new Map(); // Track titles and their file paths for duplicate detection + + /** + * Recursively processes a directory + * @param dirPath - Current directory path + * @param currentLevel - Current level in the structure + */ + function processDirectory( + dirPath: string, + currentLevel: { files: FileInfo[]; children: { [key: string]: FolderStructure } } + ): void { + const items = fs.readdirSync(dirPath, { withFileTypes: true }); + + // Collect folders and files separately + const folders: FileInfo[] = []; + const files: FileInfo[] = []; + + for (const item of items) { + const fullPath = path.join(dirPath, item.name); + + if (item.isDirectory() && !shouldSkipDirectory(item.name)) { + // Process subdirectory + // Check for both README.md and index.mdx + const readmePath = path.join(fullPath, 'README.md'); + const indexPath = path.join(fullPath, 'index.mdx'); + let folderOrder = 999; + let folderTitle = item.name; + + // Get folder ordering from README.md or index.mdx + const indexFilePath = fs.existsSync(readmePath) + ? readmePath + : fs.existsSync(indexPath) + ? indexPath + : null; + + if (indexFilePath) { + try { + const indexContent = readFileUtf8Normalized(indexFilePath); + const { frontmatter, content } = FrontmatterParser.extract(indexContent); + + // Skip this entire folder if index file is marked to ignore + if (frontmatter.llms === 'ignore' || frontmatter.llms === false) { + continue; // Skip this folder entirely + } - const structure: { [key: string]: FolderStructure } = {}; - const seenTitles = new Map(); // Track titles and their file paths for duplicate detection - - /** - * Recursively processes a directory - * @param dirPath - Current directory path - * @param currentLevel - Current level in the structure - */ - function processDirectory(dirPath: string, currentLevel: { files: FileInfo[]; children: { [key: string]: FolderStructure } }): void { - const items = fs.readdirSync(dirPath, { withFileTypes: true }); - - // Collect folders and files separately - const folders: FileInfo[] = []; - const files: FileInfo[] = []; - - for (const item of items) { - const fullPath = path.join(dirPath, item.name); - - if (item.isDirectory() && !shouldSkipDirectory(item.name)) { - // Process subdirectory - // Check for both README.md and index.mdx - const readmePath = path.join(fullPath, 'README.md'); - const indexPath = path.join(fullPath, 'index.mdx'); - let folderOrder = 999; - let folderTitle = item.name; - - // Get folder ordering from README.md or index.mdx - const indexFilePath = fs.existsSync(readmePath) ? readmePath : (fs.existsSync(indexPath) ? indexPath : null); - - if (indexFilePath) { - try { - const indexContent = readFileUtf8Normalized(indexFilePath); - const { frontmatter, content } = FrontmatterParser.extract(indexContent); - - // Skip this entire folder if index file is marked to ignore - if (frontmatter.llms === 'ignore' || frontmatter.llms === false) { - continue; // Skip this folder entirely - } - - // If index file is marked ignore-file, skip just the file but process folder - // (folderOrder and folderTitle will use defaults) - - folderOrder = (frontmatter.sidebar_position as number) || 999; - - // Extract title from frontmatter or first # header - if (frontmatter.title || frontmatter.sidebar_label) { - folderTitle = (frontmatter.title || frontmatter.sidebar_label) as string; - } else { - // Extract from first # header - const headerMatch = content.match(/^#\s+(.+)$/m); - if (headerMatch) { - folderTitle = headerMatch[1].trim(); - } - } - } catch (error) { - // Ignore errors reading index file - } - } - - folders.push({ - name: item.name, - title: folderTitle, - path: fullPath, - order: folderOrder - }); - } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) { - // Process file - let fileOrder = 999; - let fileTitle = item.name; - - try { - const fileContent = readFileUtf8Normalized(fullPath); - const { frontmatter, content } = FrontmatterParser.extract(fileContent); - - // Skip this file if marked to ignore (including ignore-file) - if (frontmatter.llms === 'ignore' || frontmatter.llms === 'ignore-file' || frontmatter.llms === false) { - continue; // Skip this file - } - - fileOrder = (frontmatter.sidebar_position as number) || 999; - - // Extract title from first # header - const headerMatch = content.match(/^#\s+(.+)$/m); - if (headerMatch) { - fileTitle = headerMatch[1].trim(); - } - - // Check for duplicate titles - if (seenTitles.has(fileTitle)) { - const existingPath = seenTitles.get(fileTitle)!; - throw new Error( - `Duplicate title found: "${fileTitle}"\n` + - ` First occurrence: ${existingPath}\n` + - ` Duplicate found in: ${fullPath}` - ); - } - seenTitles.set(fileTitle, fullPath); - } catch (error) { - // Re-throw to fail the build - throw error; - } - - files.push({ - name: item.name, - title: fileTitle, - path: fullPath, - order: fileOrder - }); + // If index file is marked ignore-file, skip just the file but process folder + // (folderOrder and folderTitle will use defaults) + + folderOrder = (frontmatter.sidebar_position as number) || 999; + + // Extract title from frontmatter or first # header + if (frontmatter.title || frontmatter.sidebar_label) { + folderTitle = (frontmatter.title || frontmatter.sidebar_label) as string; + } else { + // Extract from first # header + const headerMatch = content.match(/^#\s+(.+)$/m); + if (headerMatch) { + folderTitle = headerMatch[1].trim(); + } } + } catch (error) { + // Ignore errors reading index file + } } - // Sort files by order and add to current level - files.sort((a, b) => { - if (a.order !== b.order) return a.order - b.order; - return a.name.localeCompare(b.name); + folders.push({ + name: item.name, + title: folderTitle, + path: fullPath, + order: folderOrder, }); - - if (files.length > 0) { - if (!currentLevel.files) currentLevel.files = []; - currentLevel.files.push(...files); + } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) { + // Process file + let fileOrder = 999; + let fileTitle = item.name; + + try { + const fileContent = readFileUtf8Normalized(fullPath); + const { frontmatter, content } = FrontmatterParser.extract(fileContent); + + // Skip this file if marked to ignore (including ignore-file) + if ( + frontmatter.llms === 'ignore' || + frontmatter.llms === 'ignore-file' || + frontmatter.llms === false + ) { + continue; // Skip this file + } + + fileOrder = (frontmatter.sidebar_position as number) || 999; + + // Extract title from first # header + const headerMatch = content.match(/^#\s+(.+)$/m); + if (headerMatch) { + fileTitle = headerMatch[1].trim(); + } + + // Check for duplicate titles + if (seenTitles.has(fileTitle)) { + const existingPath = seenTitles.get(fileTitle)!; + throw new Error( + `Duplicate title found: "${fileTitle}"\n` + + ` First occurrence: ${existingPath}\n` + + ` Duplicate found in: ${fullPath}` + ); + } + seenTitles.set(fileTitle, fullPath); + } catch (error) { + // Re-throw to fail the build + throw error; } - // Sort folders by order and process each one - folders.sort((a, b) => { - if (a.order !== b.order) return a.order - b.order; - return a.name.localeCompare(b.name); + files.push({ + name: item.name, + title: fileTitle, + path: fullPath, + order: fileOrder, }); + } + } - if (!currentLevel.children) currentLevel.children = {}; + // Sort files by order and add to current level + files.sort((a, b) => { + if (a.order !== b.order) return a.order - b.order; + return a.name.localeCompare(b.name); + }); - for (const folder of folders) { - currentLevel.children[folder.name] = { - title: folder.title, - order: folder.order, - path: folder.path, - files: [], - children: {} - }; + if (files.length > 0) { + if (!currentLevel.files) currentLevel.files = []; + currentLevel.files.push(...files); + } - // Recursively process subdirectory - processDirectory(folder.path, currentLevel.children[folder.name]); - } + // Sort folders by order and process each one + folders.sort((a, b) => { + if (a.order !== b.order) return a.order - b.order; + return a.name.localeCompare(b.name); + }); + + if (!currentLevel.children) currentLevel.children = {}; + + for (const folder of folders) { + currentLevel.children[folder.name] = { + title: folder.title, + order: folder.order, + path: folder.path, + files: [], + children: {}, + }; + + // Recursively process subdirectory + processDirectory(folder.path, currentLevel.children[folder.name]); } + } - // Create a temporary wrapper to handle the root properly - const tempWrapper: { files: FileInfo[]; children: { [key: string]: FolderStructure } } = { files: [], children: {} }; - processDirectory(rootPath, tempWrapper); + // Create a temporary wrapper to handle the root properly + const tempWrapper: { files: FileInfo[]; children: { [key: string]: FolderStructure } } = { + files: [], + children: {}, + }; + processDirectory(rootPath, tempWrapper); - // Return the children (which contain the actual folder structure) - return tempWrapper.children; + // Return the children (which contain the actual folder structure) + return tempWrapper.children; } /** @@ -271,27 +285,25 @@ export function buildHierarchicalStructure(rootPath: string): { [key: string]: F * @returns Priority files for small version */ export function getPriorityFiles(organized: any): string[] { - const priorityFiles: string[] = []; + const priorityFiles: string[] = []; - // Add welcome/overview files - priorityFiles.push(...organized.main.welcome); + // Add welcome/overview files + priorityFiles.push(...organized.main.welcome); - // Add key team concepts - const keyTeamFiles = organized.main.teams.filter((file: string) => - file.includes('core-concepts') || - file.includes('README.md') - ); - priorityFiles.push(...keyTeamFiles); + // Add key team concepts + const keyTeamFiles = organized.main.teams.filter( + (file: string) => file.includes('core-concepts') || file.includes('README.md') + ); + priorityFiles.push(...keyTeamFiles); - // Add getting started files - priorityFiles.push(...organized.language.gettingStarted); + // Add getting started files + priorityFiles.push(...organized.language.gettingStarted); - // Add essential README files - const essentialReadmes = organized.language.essentials.filter((file: string) => - file.includes('README.md') || - file.includes('app-basics') - ); - priorityFiles.push(...essentialReadmes); + // Add essential README files + const essentialReadmes = organized.language.essentials.filter( + (file: string) => file.includes('README.md') || file.includes('app-basics') + ); + priorityFiles.push(...essentialReadmes); - return priorityFiles; + return priorityFiles; } diff --git a/teams.md/scripts/lib/frontmatter-parser.ts b/teams.md/scripts/lib/frontmatter-parser.ts index ff8dddb7e0..926ee3fffc 100644 --- a/teams.md/scripts/lib/frontmatter-parser.ts +++ b/teams.md/scripts/lib/frontmatter-parser.ts @@ -10,166 +10,177 @@ import readFileUtf8Normalized from '../../src/utils/readFileUtf8Normalized'; export const FRONTMATTER_REGEX = /^---\s*\r?\n([\s\S]*?)\r?\n---/; interface FrontmatterData { - [key: string]: string | number | boolean; + [key: string]: string | number | boolean; } interface ExtractResult { - frontmatter: FrontmatterData; - content: string; - hasFrontmatter: boolean; + frontmatter: FrontmatterData; + content: string; + hasFrontmatter: boolean; } /** * Parser for YAML frontmatter in markdown/MDX files */ export class FrontmatterParser { - /** - * Extracts and parses frontmatter from content using js-yaml - * @param content - Raw file content - * @returns Object with parsed frontmatter and content without frontmatter - */ - static extract(content: string): ExtractResult { - const match = content.match(FRONTMATTER_REGEX); - - if (!match) { - return { - frontmatter: {}, - content: content, - hasFrontmatter: false - }; - } - - try { - // Use js-yaml for robust YAML parsing instead of custom parser - const frontmatter = yaml.load(match[1]) as FrontmatterData || {}; - const contentWithoutFrontmatter = content.replace(match[0], '').trimStart(); - - return { - frontmatter, - content: contentWithoutFrontmatter, - hasFrontmatter: true - }; - } catch (error) { - console.warn(`Warning: Error parsing frontmatter with js-yaml, falling back to simple parser:`, error); - // Fallback to simple parser - const frontmatter = this.parseSimple(match[1]); - const contentWithoutFrontmatter = content.replace(match[0], '').trimStart(); - - return { - frontmatter, - content: contentWithoutFrontmatter, - hasFrontmatter: true - }; - } + /** + * Extracts and parses frontmatter from content using js-yaml + * @param content - Raw file content + * @returns Object with parsed frontmatter and content without frontmatter + */ + static extract(content: string): ExtractResult { + const match = content.match(FRONTMATTER_REGEX); + + if (!match) { + return { + frontmatter: {}, + content: content, + hasFrontmatter: false, + }; } - /** - * Parses frontmatter text into an object (simple parser - kept for fallback) - * @param frontmatterText - Raw frontmatter content (without --- delimiters) - * @returns Parsed frontmatter object - */ - static parseSimple(frontmatterText: string): FrontmatterData { - const frontmatter: FrontmatterData = {}; - const lines = frontmatterText.split('\n'); - - for (const line of lines) { - const match = line.match(/^(\w+):\s*(.+)$/); - if (!match) continue; - - const key = match[1]; - let value: string | number | boolean = match[2].trim(); - - value = this._parseValue(value); - frontmatter[key] = value; - } + try { + // Use js-yaml for robust YAML parsing instead of custom parser + const frontmatter = (yaml.load(match[1]) as FrontmatterData) || {}; + const contentWithoutFrontmatter = content.replace(match[0], '').trimStart(); + + return { + frontmatter, + content: contentWithoutFrontmatter, + hasFrontmatter: true, + }; + } catch (error) { + console.warn( + `Warning: Error parsing frontmatter with js-yaml, falling back to simple parser:`, + error + ); + // Fallback to simple parser + const frontmatter = this.parseSimple(match[1]); + const contentWithoutFrontmatter = content.replace(match[0], '').trimStart(); + + return { + frontmatter, + content: contentWithoutFrontmatter, + hasFrontmatter: true, + }; + } + } + + /** + * Parses frontmatter text into an object (simple parser - kept for fallback) + * @param frontmatterText - Raw frontmatter content (without --- delimiters) + * @returns Parsed frontmatter object + */ + static parseSimple(frontmatterText: string): FrontmatterData { + const frontmatter: FrontmatterData = {}; + const lines = frontmatterText.split('\n'); + + for (const line of lines) { + const match = line.match(/^(\w+):\s*(.+)$/); + if (!match) continue; + + const key = match[1]; + let value: string | number | boolean = match[2].trim(); + + value = this._parseValue(value); + frontmatter[key] = value; + } - return frontmatter; + return frontmatter; + } + + /** + * Extracts frontmatter from a file + * @param filePath - Path to the file + * @returns Parsed frontmatter or null if file doesn't exist + */ + static extractFromFile(filePath: string): ExtractResult | null { + if (!fs.existsSync(filePath)) { + return null; } - /** - * Extracts frontmatter from a file - * @param filePath - Path to the file - * @returns Parsed frontmatter or null if file doesn't exist - */ - static extractFromFile(filePath: string): ExtractResult | null { - if (!fs.existsSync(filePath)) { - return null; - } - - try { - const content = readFileUtf8Normalized(filePath); - return this.extract(content); - } catch (error) { - console.warn(`āš ļø Error reading frontmatter from ${filePath}:`, (error as Error).message); - return null; - } + try { + const content = readFileUtf8Normalized(filePath); + return this.extract(content); + } catch (error) { + console.warn(`āš ļø Error reading frontmatter from ${filePath}:`, (error as Error).message); + return null; + } + } + + /** + * Gets a specific property from frontmatter + * @param content - File content or path + * @param propertyName - Property to extract + * @param defaultValue - Default value if property not found + * @returns Property value or default + */ + static getProperty( + content: string, + propertyName: string, + defaultValue?: T + ): T | undefined { + const result = + typeof content === 'string' && fs.existsSync(content) + ? this.extractFromFile(content) + : this.extract(content); + + if (!result) { + return defaultValue; } - /** - * Gets a specific property from frontmatter - * @param content - File content or path - * @param propertyName - Property to extract - * @param defaultValue - Default value if property not found - * @returns Property value or default - */ - static getProperty(content: string, propertyName: string, defaultValue?: T): T | undefined { - const result = typeof content === 'string' && fs.existsSync(content) - ? this.extractFromFile(content) - : this.extract(content); - - if (!result) { - return defaultValue; - } - - const { frontmatter } = result; - - return frontmatter[propertyName] !== undefined - ? (frontmatter[propertyName] as T) - : defaultValue; + const { frontmatter } = result; + + return frontmatter[propertyName] !== undefined + ? (frontmatter[propertyName] as T) + : defaultValue; + } + + /** + * Checks if a file or content should be ignored based on frontmatter + * @param content - File content or path + * @returns True if should be ignored + */ + static shouldIgnore(content: string): boolean { + const result = + typeof content === 'string' && fs.existsSync(content) + ? this.extractFromFile(content) + : this.extract(content); + + if (!result) { + return false; } - /** - * Checks if a file or content should be ignored based on frontmatter - * @param content - File content or path - * @returns True if should be ignored - */ - static shouldIgnore(content: string): boolean { - const result = typeof content === 'string' && fs.existsSync(content) - ? this.extractFromFile(content) - : this.extract(content); - - if (!result) { - return false; - } - - const { frontmatter } = result; - const llmsValue = frontmatter.llms; - return llmsValue === 'ignore' || llmsValue === 'ignore-file' || llmsValue === false; + const { frontmatter } = result; + const llmsValue = frontmatter.llms; + return llmsValue === 'ignore' || llmsValue === 'ignore-file' || llmsValue === false; + } + + /** + * Parses a value from frontmatter, converting types as needed + * @private + * @param value - Raw value string + * @returns Parsed value (string, number, boolean) + */ + private static _parseValue(value: string): string | number | boolean { + // Remove surrounding quotes + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1); } - /** - * Parses a value from frontmatter, converting types as needed - * @private - * @param value - Raw value string - * @returns Parsed value (string, number, boolean) - */ - private static _parseValue(value: string): string | number | boolean { - // Remove surrounding quotes - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - return value.slice(1, -1); - } - - // Parse booleans - if (value === 'true') return true; - if (value === 'false') return false; - - // Parse integers - if (/^\d+$/.test(value)) { - return parseInt(value, 10); - } - - // Return as string - return value; + // Parse booleans + if (value === 'true') return true; + if (value === 'false') return false; + + // Parse integers + if (/^\d+$/.test(value)) { + return parseInt(value, 10); } + + // Return as string + return value; + } } diff --git a/teams.md/sidebars.ts b/teams.md/sidebars.ts index be0203d6bd..c4244ef674 100644 --- a/teams.md/sidebars.ts +++ b/teams.md/sidebars.ts @@ -1,4 +1,4 @@ -import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; +import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; export default { default: [{ type: 'autogenerated', dirName: '.' }], diff --git a/teams.md/src/components/FileCodeBlock.tsx b/teams.md/src/components/FileCodeBlock.tsx index 713298d7b0..5d312f9a82 100644 --- a/teams.md/src/components/FileCodeBlock.tsx +++ b/teams.md/src/components/FileCodeBlock.tsx @@ -1,37 +1,37 @@ -import { useEffect, useState, PropsWithChildren } from "react"; -import CodeBlock from "@theme/CodeBlock"; -import useBaseUrl from "@docusaurus/useBaseUrl"; +import { useEffect, useState, PropsWithChildren } from 'react'; +import CodeBlock from '@theme/CodeBlock'; +import useBaseUrl from '@docusaurus/useBaseUrl'; export type FileCodeBlockParams = { - readonly src: string; - readonly lang?: string; + readonly src: string; + readonly lang?: string; }; export default function FileCodeBlock({ src, lang }: PropsWithChildren) { - const [code, setCode] = useState(); - const url = useBaseUrl(src); + const [code, setCode] = useState(); + const url = useBaseUrl(src); - useEffect(() => { - (async () => { - try { - const res = await fetch(url); + useEffect(() => { + (async () => { + try { + const res = await fetch(url); - if (!res.ok || res.status != 200) { - throw new Error(`failed to load file code block with status "${res.status}"`); - } + if (!res.ok || res.status != 200) { + throw new Error(`failed to load file code block with status "${res.status}"`); + } - const blob = await res.blob(); - const data = await blob.text(); - setCode(data.trim()); - } catch (err) { - console.error('failed to load file code block', err); - } - })(); - }, [src]); + const blob = await res.blob(); + const data = await blob.text(); + setCode(data.trim()); + } catch (err) { + console.error('failed to load file code block', err); + } + })(); + }, [src]); - return ( - - {code} - - ); + return ( + + {code} + + ); } diff --git a/teams.md/src/components/include/essentials/api/csharp.incl.md b/teams.md/src/components/include/essentials/api/csharp.incl.md index 8cc589390d..a93cb02ee3 100644 --- a/teams.md/src/components/include/essentials/api/csharp.incl.md +++ b/teams.md/src/components/include/essentials/api/csharp.incl.md @@ -4,11 +4,11 @@ -| Area | Description | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | -| `Meetings` | Gives your application access to meeting details and participant information via `GetByIdAsync` and `GetParticipantAsync` | -| `Teams` | Gives your application access to team or channel details | +| Area | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user). Also includes `Reactions` for adding/removing emoji reactions to messages | +| `Meetings` | Gives your application access to meeting details and participant information via `GetByIdAsync` and `GetParticipantAsync` | +| `Teams` | Gives your application access to team or channel details | @@ -19,7 +19,6 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - ```csharp app.OnMessage(async context => { @@ -27,7 +26,6 @@ app.OnMessage(async context => }); ``` - ```csharp diff --git a/teams.md/src/components/include/essentials/api/python.incl.md b/teams.md/src/components/include/essentials/api/python.incl.md index 4fc536918a..8021ea0f40 100644 --- a/teams.md/src/components/include/essentials/api/python.incl.md +++ b/teams.md/src/components/include/essentials/api/python.incl.md @@ -4,11 +4,11 @@ -| Area | Description | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | -| `meetings` | Gives your application access to meeting details and participant information via `get_by_id` and `get_participant` | -| `teams` | Gives your application access to team or channel details | +| Area | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user). Also includes `reactions` for adding/removing emoji reactions to messages | +| `meetings` | Gives your application access to meeting details and participant information via `get_by_id` and `get_participant` | +| `teams` | Gives your application access to team or channel details | diff --git a/teams.md/src/components/include/essentials/api/typescript.incl.md b/teams.md/src/components/include/essentials/api/typescript.incl.md index a440f973ab..5173ff84ce 100644 --- a/teams.md/src/components/include/essentials/api/typescript.incl.md +++ b/teams.md/src/components/include/essentials/api/typescript.incl.md @@ -4,11 +4,11 @@ -| Area | Description | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) | -| `meetings` | Gives your application access to meeting details and participant information via `getById` and `getParticipant` | -| `teams` | Gives your application access to team or channel details | +| Area | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user). Also includes `reactions` for adding/removing emoji reactions to messages | +| `meetings` | Gives your application access to meeting details and participant information via `getById` and `getParticipant` | +| `teams` | Gives your application access to team or channel details | diff --git a/teams.md/src/components/include/essentials/app-authentication/csharp.incl.md b/teams.md/src/components/include/essentials/app-authentication/csharp.incl.md index 3c50dc100f..689c1f0767 100644 --- a/teams.md/src/components/include/essentials/app-authentication/csharp.incl.md +++ b/teams.md/src/components/include/essentials/app-authentication/csharp.incl.md @@ -5,11 +5,14 @@ The environment file approach is not yet supported for C#. You need to configure ::: In your `Program.cs`, replace the initialization: + ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddTeams(); ``` + with the following code to enable User Assigned Managed Identity authentication: + ```csharp var builder = WebApplication.CreateBuilder(args); diff --git a/teams.md/src/components/include/essentials/graph/csharp.incl.md b/teams.md/src/components/include/essentials/graph/csharp.incl.md index 282323470a..2f7cfb181f 100644 --- a/teams.md/src/components/include/essentials/graph/csharp.incl.md +++ b/teams.md/src/components/include/essentials/graph/csharp.incl.md @@ -39,7 +39,6 @@ To access the graph using the user's token, you need to do this as part of a mes import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - ```csharp app.OnMessage(async context => { @@ -51,7 +50,6 @@ app.OnMessage(async context => }); ``` - `userGraph` diff --git a/teams.md/src/components/include/essentials/on-activity/csharp.incl.md b/teams.md/src/components/include/essentials/on-activity/csharp.incl.md index c8ad8866aa..de878e11f6 100644 --- a/teams.md/src/components/include/essentials/on-activity/csharp.incl.md +++ b/teams.md/src/components/include/essentials/on-activity/csharp.incl.md @@ -20,14 +20,15 @@ In the above example, the `context.activity` parameter is of type `MessageActivi The `OnActivity` activity handlers (and attributes) follow a [middleware](https://www.patterns.dev/vanilla/mediator-pattern/) pattern similar to how `dotnet` middlewares work. This means that for each activity handler, a `Next` function is passed in which can be called to pass control to the next handler. This allows you to build a chain of handlers that can process the same activity in different ways. - ```csharp - app.OnMessage(async context => - { - Console.WriteLine("global logger"); - context.Next(); // pass control onward - return Task.CompletedTask; - }); - ``` + +```csharp +app.OnMessage(async context => +{ + Console.WriteLine("global logger"); + context.Next(); // pass control onward + return Task.CompletedTask; +}); +``` ```csharp app.OnMessage(async context => @@ -40,14 +41,13 @@ app.OnMessage(async context => // Conditionally pass control to the next handler context.Next(); }); - + app.OnMessage(async context => { // Fallthrough to the final handler await context.Send($"Hello! you said {context.Activity.Text}"); }); - ``` - +``` diff --git a/teams.md/src/components/include/essentials/sending-messages/csharp.incl.md b/teams.md/src/components/include/essentials/sending-messages/csharp.incl.md index 8c817f0f8d..05f8ecb5a9 100644 --- a/teams.md/src/components/include/essentials/sending-messages/csharp.incl.md +++ b/teams.md/src/components/include/essentials/sending-messages/csharp.incl.md @@ -9,12 +9,12 @@ app.OnMessage(async context => - ```csharp - app.OnVerifyState(async context => - { - await context.Send("You have successfully signed in!"); - }); - ``` +```csharp +app.OnVerifyState(async context => +{ + await context.Send("You have successfully signed in!"); +}); +``` diff --git a/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/csharp.incl.md b/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/csharp.incl.md index 70087bdb21..b2b09a4698 100644 --- a/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/csharp.incl.md +++ b/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/csharp.incl.md @@ -50,4 +50,4 @@ public static async Task SendTargetedNotification(string conversationId, Account .WithRecipient(recipient, isTargeted: true) ); } -``` \ No newline at end of file +``` diff --git a/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/python.incl.md b/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/python.incl.md index 3078b055a4..0b42970621 100644 --- a/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/python.incl.md +++ b/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/python.incl.md @@ -48,4 +48,4 @@ async def send_targeted_notification(conversation_id: str, recipient: Account): MessageActivityInput(text="This is a private notification just for you!") .with_recipient(recipient, is_targeted=True) ) -``` \ No newline at end of file +``` diff --git a/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/typescript.incl.md b/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/typescript.incl.md index 6bfff73a1a..ce702de266 100644 --- a/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/typescript.incl.md +++ b/teams.md/src/components/include/essentials/sending-messages/proactive-messaging/typescript.incl.md @@ -35,7 +35,7 @@ const sendProactiveNotification = async (userId: string) => { if (!conversationId) { return; } - const activity = new MessageActivity('Hey! It\'s been a while. How are you?'); + const activity = new MessageActivity("Hey! It's been a while. How are you?"); await app.send(conversationId, activity); }; ``` @@ -49,8 +49,10 @@ import { MessageActivity, Account } from '@microsoft/teams.api'; const sendTargetedNotification = async (conversationId: string, recipient: Account) => { await app.send( conversationId, - new MessageActivity('This is a private notification just for you!') - .withRecipient(recipient, true) + new MessageActivity('This is a private notification just for you!').withRecipient( + recipient, + true + ) ); }; -``` \ No newline at end of file +``` diff --git a/teams.md/src/components/include/essentials/sending-messages/typescript.incl.md b/teams.md/src/components/include/essentials/sending-messages/typescript.incl.md index d6abc899b7..228908d064 100644 --- a/teams.md/src/components/include/essentials/sending-messages/typescript.incl.md +++ b/teams.md/src/components/include/essentials/sending-messages/typescript.incl.md @@ -54,8 +54,7 @@ import { MessageActivity } from '@microsoft/teams.api'; app.on('message', async ({ send, activity }) => { // Using withRecipient with isTargeted=true explicitly targets the specified recipient await send( - new MessageActivity('This message is only visible to you!') - .withRecipient(activity.from, true) + new MessageActivity('This message is only visible to you!').withRecipient(activity.from, true) ); }); ``` diff --git a/teams.md/src/components/include/getting-started/code-basics/csharp.incl.md b/teams.md/src/components/include/getting-started/code-basics/csharp.incl.md index 514f91e22e..2fe476d887 100644 --- a/teams.md/src/components/include/getting-started/code-basics/csharp.incl.md +++ b/teams.md/src/components/include/getting-started/code-basics/csharp.incl.md @@ -49,7 +49,6 @@ teams.OnMessage(async context => }); ``` - Listens for all incoming messages using `onMessage` handler. diff --git a/teams.md/src/components/include/getting-started/running-in-teams/csharp.incl.md b/teams.md/src/components/include/getting-started/running-in-teams/csharp.incl.md index 4abc4d8bac..8d695efa23 100644 --- a/teams.md/src/components/include/getting-started/running-in-teams/csharp.incl.md +++ b/teams.md/src/components/include/getting-started/running-in-teams/csharp.incl.md @@ -6,4 +6,4 @@ [INFO] Echo.Microsoft.Teams.Plugins.AspNetCore.DevTools Available at http://localhost:3979/devtools [INFO] Microsoft.Hosting.Lifetime Application started. Press Ctrl+C to shut down. [INFO] Microsoft.Hosting.Lifetime Hosting environment: Development -``` \ No newline at end of file +``` diff --git a/teams.md/src/components/include/getting-started/running-in-teams/python.incl.md b/teams.md/src/components/include/getting-started/running-in-teams/python.incl.md index 6212035421..f056058806 100644 --- a/teams.md/src/components/include/getting-started/running-in-teams/python.incl.md +++ b/teams.md/src/components/include/getting-started/running-in-teams/python.incl.md @@ -9,4 +9,4 @@ INFO: Waiting for application startup. [INFO] @teams/app Teams app started successfully INFO: Application startup complete.. INFO: Uvicorn running on http://0.0.0.0:3979 (Press CTRL+C to quit) -``` \ No newline at end of file +``` diff --git a/teams.md/src/components/include/getting-started/running-in-teams/typescript.incl.md b/teams.md/src/components/include/getting-started/running-in-teams/typescript.incl.md index dfeddde4aa..ae8f743453 100644 --- a/teams.md/src/components/include/getting-started/running-in-teams/typescript.incl.md +++ b/teams.md/src/components/include/getting-started/running-in-teams/typescript.incl.md @@ -9,4 +9,4 @@ [WARN] @teams/app/devtools āš ļø Devtools are not secure and should not be used production environments āš ļø [INFO] @teams/app/http listening on port 3978 šŸš€ [INFO] @teams/app/devtools available at http://localhost:3979/devtools -``` \ No newline at end of file +``` diff --git a/teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/python.incl.md b/teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/python.incl.md index 7c48395112..9adab5ec61 100644 --- a/teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/python.incl.md +++ b/teams.md/src/components/include/in-depth-guides/adaptive-cards/executing-actions/python.incl.md @@ -162,4 +162,4 @@ async def handle_card_action(ctx: ActivityContext[AdaptiveCardInvokeActivity]) - :::note The `data` values are accessible as a dictionary and can be accessed using `.get()` method for safe access. -::: \ No newline at end of file +::: diff --git a/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-client/python.incl.md b/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-client/python.incl.md index 2668803b23..52d5b756e8 100644 --- a/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-client/python.incl.md +++ b/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-client/python.incl.md @@ -141,7 +141,6 @@ advanced_plugin.on_use_plugin( advanced_prompt = ChatPrompt(model=completions_model, plugins=[advanced_plugin]) ``` - ```mermaid diff --git a/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-server/typescript.incl.md b/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-server/typescript.incl.md index cb9b295dec..a99ccfe73f 100644 --- a/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-server/typescript.incl.md +++ b/teams.md/src/components/include/in-depth-guides/ai/a2a/a2a-server/typescript.incl.md @@ -59,4 +59,3 @@ app.event('a2a:message', async ({ respond, requestContext }) => { await respond(result); }); ``` - diff --git a/teams.md/src/components/include/in-depth-guides/ai/function-calling/csharp.incl.md b/teams.md/src/components/include/in-depth-guides/ai/function-calling/csharp.incl.md index 0c3a1e86e9..b987f23832 100644 --- a/teams.md/src/components/include/in-depth-guides/ai/function-calling/csharp.incl.md +++ b/teams.md/src/components/include/in-depth-guides/ai/function-calling/csharp.incl.md @@ -379,4 +379,5 @@ The LLM can call functions sequentially - using the output of one function as in ::: + N/A diff --git a/teams.md/src/components/include/in-depth-guides/ai/function-calling/python.incl.md b/teams.md/src/components/include/in-depth-guides/ai/function-calling/python.incl.md index 188f1be595..5fff31923d 100644 --- a/teams.md/src/components/include/in-depth-guides/ai/function-calling/python.incl.md +++ b/teams.md/src/components/include/in-depth-guides/ai/function-calling/python.incl.md @@ -162,4 +162,5 @@ async def handle_multiple_functions(ctx: ActivityContext[MessageActivity]): ``` + N/A diff --git a/teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/typescript.incl.md b/teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/typescript.incl.md index deeafd709a..dfd6241ef3 100644 --- a/teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/typescript.incl.md +++ b/teams.md/src/components/include/in-depth-guides/ai/setup-and-prereqs/typescript.incl.md @@ -66,15 +66,16 @@ const model = new OpenAIChatModel({ const model = new OpenAIChatModel({ apiKey: 'your-api-key', model: 'gpt-4o', - endpoint: 'your-endpoint', // Azure only + endpoint: 'your-endpoint', // Azure only apiVersion: 'your-api-version', // Azure only - baseUrl: 'your-base-url', // Custom base URL - organization: 'your-org-id', // Optional - project: 'your-project-id', // Optional + baseUrl: 'your-base-url', // Custom base URL + organization: 'your-org-id', // Optional + project: 'your-project-id', // Optional }); ``` **Environment variables automatically loaded:** + - `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` - `AZURE_OPENAI_ENDPOINT` (Azure only) - `OPENAI_API_VERSION` (Azure only) diff --git a/teams.md/src/components/include/in-depth-guides/meeting-events/typescript.incl.md b/teams.md/src/components/include/in-depth-guides/meeting-events/typescript.incl.md index 57e57483e1..510430b9b0 100644 --- a/teams.md/src/components/include/in-depth-guides/meeting-events/typescript.incl.md +++ b/teams.md/src/components/include/in-depth-guides/meeting-events/typescript.incl.md @@ -13,11 +13,9 @@ app.on('meetingStart', async ({ activity, send }) => { const card = new AdaptiveCard( new TextBlock(`'${meetingData.Title}' has started at ${startTime}.`, { wrap: true, - weight: 'Bolder' + weight: 'Bolder', }), - new ActionSet( - new OpenUrlAction(meetingData.JoinUrl).withTitle('Join the meeting') - ) + new ActionSet(new OpenUrlAction(meetingData.JoinUrl).withTitle('Join the meeting')) ); await send(card); @@ -39,7 +37,7 @@ app.on('meetingEnd', async ({ activity, send }) => { const card = new AdaptiveCard( new TextBlock(`'${meetingData.Title}' has ended at ${endTime}.`, { wrap: true, - weight: 'Bolder' + weight: 'Bolder', }) ); @@ -63,7 +61,7 @@ app.on('meetingParticipantJoin', async ({ activity, send }) => { const card = new AdaptiveCard( new TextBlock(`${member} has joined the meeting as ${role}.`, { wrap: true, - weight: 'Bolder' + weight: 'Bolder', }) ); @@ -86,7 +84,7 @@ app.on('meetingParticipantLeave', async ({ activity, send }) => { const card = new AdaptiveCard( new TextBlock(`${member} has left the meeting.`, { wrap: true, - weight: 'Bolder' + weight: 'Bolder', }) ); diff --git a/teams.md/src/components/include/in-depth-guides/message-reactions/csharp.incl.md b/teams.md/src/components/include/in-depth-guides/message-reactions/csharp.incl.md new file mode 100644 index 0000000000..cc89719f86 --- /dev/null +++ b/teams.md/src/components/include/in-depth-guides/message-reactions/csharp.incl.md @@ -0,0 +1,80 @@ + + +```csharp +app.OnMessage(async context => +{ + await context.Send("Hello! I'll react to this message."); + + // Add a reaction to the incoming message + await context.Api.Conversations.Reactions.AddAsync( + context.Activity.Conversation.Id, + context.Activity.Id, + ReactionType.Like + ); +}); +``` + + + +```csharp +app.OnMessage(async context => +{ + // First, add a reaction + await context.Api.Conversations.Reactions.AddAsync( + context.Activity.Conversation.Id, + context.Activity.Id, + ReactionType.Heart + ); + + // Wait a bit, then remove it + await Task.Delay(2000); + await context.Api.Conversations.Reactions.DeleteAsync( + context.Activity.Conversation.Id, + context.Activity.Id, + ReactionType.Heart + ); +}); +``` + + + +The following reaction types are available: + +- `ReactionType.Like` — šŸ‘ +- `ReactionType.Heart` — ā¤ļø +- `ReactionType.Checkmark` — āœ… +- `ReactionType.Hourglass` — ā³ +- `ReactionType.Pushpin` — šŸ“Œ +- `ReactionType.Exclamation` — ā— +- `ReactionType.Laugh` — šŸ˜† +- `ReactionType.Surprise` — 😮 +- `ReactionType.Sad` — šŸ™ +- `ReactionType.Angry` — 😠 + +You can also use custom reaction types by creating a new `ReactionType` instance: + +```csharp +// Use a custom emoji reaction +var customReaction = new ReactionType("1f44b_wavinghand-tone4"); + +await context.Api.Conversations.Reactions.AddAsync( + context.Activity.Conversation.Id, + context.Activity.Id, + customReaction +); +``` + + + +For advanced scenarios where you need to use a custom service URL or access the HTTP client directly, you can use the `ApiClient.Client` property: + +```csharp +// Access the underlying HTTP client for custom requests +var api = new ApiClient(context.Activity.ServiceUrl, context.Api.Client); + +await api.Conversations.Reactions.AddAsync( + context.Activity.Conversation.Id, + context.Activity.Id, + ReactionType.Like +); +``` diff --git a/teams.md/src/components/include/in-depth-guides/message-reactions/python.incl.md b/teams.md/src/components/include/in-depth-guides/message-reactions/python.incl.md new file mode 100644 index 0000000000..e3cc555692 --- /dev/null +++ b/teams.md/src/components/include/in-depth-guides/message-reactions/python.incl.md @@ -0,0 +1,79 @@ + + +```python +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + await ctx.send("Hello! I'll react to this message.") + + # Add a reaction to the incoming message + await ctx.api.conversations.reactions.add( + ctx.activity.conversation.id, + ctx.activity.id, + 'like' + ) +``` + + + +```python +import asyncio + +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + # First, add a reaction + await ctx.api.conversations.reactions.add( + ctx.activity.conversation.id, + ctx.activity.id, + 'heart' + ) + + # Wait a bit, then remove it + await asyncio.sleep(2) + await ctx.api.conversations.reactions.delete( + ctx.activity.conversation.id, + ctx.activity.id, + 'heart' + ) +``` + + + +The following reaction types are available: + +- `'like'` — šŸ‘ +- `'heart'` — ā¤ļø +- `'checkmark'` — āœ… +- `'hourglass'` — ā³ +- `'pushpin'` — šŸ“Œ +- `'exclamation'` — ā— +- `'laugh'` — šŸ˜† +- `'surprise'` — 😮 +- `'sad'` — šŸ™ +- `'angry'` — 😠 + +You can also use custom emoji reactions by providing the emoji code: + +```python +# Use a custom emoji reaction +await ctx.api.conversations.reactions.add( + ctx.activity.conversation.id, + ctx.activity.id, + '1f44b_wavinghand-tone4' +) +``` + + + +For advanced scenarios, you can access the API client from the context: + +```python +# The API client is available through the context +api = ctx.api + +# Use reactions API +await api.conversations.reactions.add( + conversation_id, + activity_id, + 'like' +) +``` diff --git a/teams.md/src/components/include/in-depth-guides/message-reactions/typescript.incl.md b/teams.md/src/components/include/in-depth-guides/message-reactions/typescript.incl.md new file mode 100644 index 0000000000..d78725951a --- /dev/null +++ b/teams.md/src/components/include/in-depth-guides/message-reactions/typescript.incl.md @@ -0,0 +1,61 @@ + + +```typescript +app.on('message', async ({ activity, api, send }) => { + await send("Hello! I'll react to this message."); + + // Add a reaction to the incoming message + await api.conversations.reactions.add(activity.conversation.id, activity.id, 'like'); +}); +``` + + + +```typescript +app.on('message', async ({ activity, api }) => { + // First, add a reaction + await api.conversations.reactions.add(activity.conversation.id, activity.id, 'heart'); + + // Wait a bit, then remove it + await new Promise((resolve) => setTimeout(resolve, 2000)); + await api.conversations.reactions.delete(activity.conversation.id, activity.id, 'heart'); +}); +``` + + + +The following reaction types are available: + +- `'like'` — šŸ‘ +- `'heart'` — ā¤ļø +- `'checkmark'` — āœ… +- `'hourglass'` — ā³ +- `'pushpin'` — šŸ“Œ +- `'exclamation'` — ā— +- `'laugh'` — šŸ˜† +- `'surprise'` — 😮 +- `'sad'` — šŸ™ +- `'angry'` — 😠 + +You can also use custom emoji reactions by providing the emoji code: + +```typescript +// Use a custom emoji reaction +await api.conversations.reactions.add( + activity.conversation.id, + activity.id, + '1f44b_wavinghand-tone4' +); +``` + + + +For advanced scenarios, you can access the underlying HTTP client or create a custom API client instance: + +```typescript +// The API client is available through the context +const { api } = context; + +// Use reactions API +await api.conversations.reactions.add(conversationId, activityId, 'like'); +``` diff --git a/teams.md/src/components/include/in-depth-guides/user-authentication/csharp.incl.md b/teams.md/src/components/include/in-depth-guides/user-authentication/csharp.incl.md index 1222be1cf3..b9d31584bf 100644 --- a/teams.md/src/components/include/in-depth-guides/user-authentication/csharp.incl.md +++ b/teams.md/src/components/include/in-depth-guides/user-authentication/csharp.incl.md @@ -88,6 +88,7 @@ teams.OnMessage("/signout", async context => await context.Send("you have been signed out!"); }); ``` + -N/A \ No newline at end of file +N/A diff --git a/teams.md/src/components/include/migrations/botbuilder/integration/csharp.incl.md b/teams.md/src/components/include/migrations/botbuilder/integration/csharp.incl.md index 302ab1c034..7deaae9ffb 100644 --- a/teams.md/src/components/include/migrations/botbuilder/integration/csharp.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/integration/csharp.incl.md @@ -80,4 +80,4 @@ ``` - \ No newline at end of file + diff --git a/teams.md/src/components/include/migrations/botbuilder/integration/python.incl.md b/teams.md/src/components/include/migrations/botbuilder/integration/python.incl.md index 272d59b9bf..861264af59 100644 --- a/teams.md/src/components/include/migrations/botbuilder/integration/python.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/integration/python.incl.md @@ -67,4 +67,4 @@ ``` - \ No newline at end of file + diff --git a/teams.md/src/components/include/migrations/botbuilder/proactive-activities/csharp.incl.md b/teams.md/src/components/include/migrations/botbuilder/proactive-activities/csharp.incl.md index e3752983bc..084b5fcee6 100644 --- a/teams.md/src/components/include/migrations/botbuilder/proactive-activities/csharp.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/proactive-activities/csharp.incl.md @@ -12,16 +12,17 @@ + using Microsoft.Teams.Apps; // highlight-error-start -- var conversationReference = new ConversationReference -- { + +- var conversationReference = new ConversationReference +- { - ServiceUrl = "...", - Bot = new ChannelAccount { ... }, - ChannelId = "msteams", - Conversation = new ConversationAccount { ... }, - User = new ChannelAccount { ... } -- }; +- }; - -- await adapter.ContinueConversationAsync( +- await adapter.ContinueConversationAsync( - configuration["MicrosoftAppId"], - conversationReference, - async (turnContext, cancellationToken) => @@ -29,48 +30,50 @@ - await turnContext.SendActivityAsync("proactive hello", cancellationToken: cancellationToken); - }, - default); - // highlight-error-end - // highlight-success-start -+ var teams = app.UseTeams(); -+ await teams.Send("your-conversation-id", "proactive hello"); - // highlight-success-end - ``` - + // highlight-error-end + // highlight-success-start + +* var teams = app.UseTeams(); +* await teams.Send("your-conversation-id", "proactive hello"); + // highlight-success-end + ` - ```csharp showLineNumbers - using Microsoft.Bot.Builder; - using Microsoft.Bot.Builder.Integration.AspNet.Core; - using Microsoft.Bot.Schema; + `csharp showLineNumbers + using Microsoft.Bot.Builder; + using Microsoft.Bot.Builder.Integration.AspNet.Core; + using Microsoft.Bot.Schema; + + // highlight-start + var conversationReference = new ConversationReference + { + ServiceUrl = "...", + Bot = new ChannelAccount { ... }, + ChannelId = "msteams", + Conversation = new ConversationAccount { ... }, + User = new ChannelAccount { ... } + }; + + await adapter.ContinueConversationAsync( + configuration["MicrosoftAppId"], + conversationReference, + async (turnContext, cancellationToken) => + { + await turnContext.SendActivityAsync("proactive hello", cancellationToken: cancellationToken); + }, + default); + // highlight-end + ``` - // highlight-start - var conversationReference = new ConversationReference - { - ServiceUrl = "...", - Bot = new ChannelAccount { ... }, - ChannelId = "msteams", - Conversation = new ConversationAccount { ... }, - User = new ChannelAccount { ... } - }; + + + ```csharp showLineNumbers + using Microsoft.Teams.Apps; - await adapter.ContinueConversationAsync( - configuration["MicrosoftAppId"], - conversationReference, - async (turnContext, cancellationToken) => - { - await turnContext.SendActivityAsync("proactive hello", cancellationToken: cancellationToken); - }, - default); - // highlight-end - ``` - - - ```csharp showLineNumbers - using Microsoft.Teams.Apps; + // highlight-start + var teams = app.UseTeams(); + await teams.Send("your-conversation-id", "proactive hello"); + // highlight-end + ``` - // highlight-start - var teams = app.UseTeams(); - await teams.Send("your-conversation-id", "proactive hello"); - // highlight-end - ``` - - \ No newline at end of file + + diff --git a/teams.md/src/components/include/migrations/botbuilder/proactive-activities/python.incl.md b/teams.md/src/components/include/migrations/botbuilder/proactive-activities/python.incl.md index 94a2e3e5e3..5724ed051e 100644 --- a/teams.md/src/components/include/migrations/botbuilder/proactive-activities/python.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/proactive-activities/python.incl.md @@ -12,69 +12,73 @@ + from microsoft_teams.apps import App # highlight-error-start -- adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) - # highlight-error-end - # highlight-success-line -+ app = App() - # highlight-error-start -- conversation_reference = ConversationReference( +- adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) + # highlight-error-end + # highlight-success-line + +* app = App() + + # highlight-error-start + +- conversation_reference = ConversationReference( - service_url="...", - bot=ChannelAccount(...), - channel_id="msteams", - conversation=ConversationAccount(...), - user=ChannelAccount(...) -- ) +- ) - -- async def send_proactive(turn_context: TurnContext): +- async def send_proactive(turn_context: TurnContext): - await turn_context.send_activity("proactive hello") - -- await adapter.continue_conversation( +- await adapter.continue_conversation( - conversation_reference, - send_proactive, -- ) - # highlight-error-end - # highlight-success-start -+ await app.send("your-conversation-id", "proactive hello") - # highlight-success-end - ``` - +- ) + # highlight-error-end + # highlight-success-start + +* await app.send("your-conversation-id", "proactive hello") # highlight-success-end + ` - ```python showLineNumbers - from botbuilder.core import TurnContext - from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationBotFrameworkAuthentication - from botbuilder.schema import ChannelAccount, ConversationAccount, ConversationReference + `python showLineNumbers + from botbuilder.core import TurnContext + from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationBotFrameworkAuthentication + from botbuilder.schema import ChannelAccount, ConversationAccount, ConversationReference + + adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) + + # highlight-start + conversation_reference = ConversationReference( + service_url="...", + bot=ChannelAccount(...), + channel_id="msteams", + conversation=ConversationAccount(...), + user=ChannelAccount(...) + ) - adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) + async def send_proactive(turn_context: TurnContext): + await turn_context.send_activity("proactive hello") - # highlight-start - conversation_reference = ConversationReference( - service_url="...", - bot=ChannelAccount(...), - channel_id="msteams", - conversation=ConversationAccount(...), - user=ChannelAccount(...) - ) + await adapter.continue_conversation( + conversation_reference, + send_proactive + ) + # highlight-end + ``` - async def send_proactive(turn_context: TurnContext): - await turn_context.send_activity("proactive hello") + + + ```python showLineNumbers + from microsoft_teams.apps import App - await adapter.continue_conversation( - conversation_reference, - send_proactive - ) - # highlight-end - ``` - - - ```python showLineNumbers - from microsoft_teams.apps import App + app = App() - app = App() + # highlight-start + await app.send("your-conversation-id", "proactive hello") + # highlight-end + ``` - # highlight-start - await app.send("your-conversation-id", "proactive hello") - # highlight-end - ``` - - \ No newline at end of file + + diff --git a/teams.md/src/components/include/migrations/botbuilder/proactive-activities/typescript.incl.md b/teams.md/src/components/include/migrations/botbuilder/proactive-activities/typescript.incl.md index df978fa60e..e1f8253fb3 100644 --- a/teams.md/src/components/include/migrations/botbuilder/proactive-activities/typescript.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/proactive-activities/typescript.incl.md @@ -14,14 +14,18 @@ + import { App } from '@microsoft/teams.apps'; // highlight-error-start -- const auth = new ConfigurationBotFrameworkAuthentication(process.env); -- const adapter = new CloudAdapter(auth); - // highlight-error-end - // highlight-success-line -+ const app = new App(); + +- const auth = new ConfigurationBotFrameworkAuthentication(process.env); +- const adapter = new CloudAdapter(auth); + // highlight-error-end + // highlight-success-line + +* const app = new App(); + (async () => { // highlight-error-start + - const conversationReference: ConversationReference = { - serviceUrl: '...', - bot: { ... }, @@ -35,52 +39,56 @@ - }); // highlight-error-end // highlight-success-start -+ await app.start(); -+ await app.send('your-conversation-id', 'proactive hello'); - // highlight-success-end - }()); - ``` - - - ```typescript showLineNumbers - import { - CloudAdapter, - ConfigurationBotFrameworkAuthentication, - ConversationReference, - } from 'botbuilder'; - - const auth = new ConfigurationBotFrameworkAuthentication(process.env); - const adapter = new CloudAdapter(auth); - - // highlight-start - (async () => { - const conversationReference: ConversationReference = { - serviceUrl: '...', - bot: { ... }, - channelId: 'msteams', - conversation: { ... }, - user: { ... }, - }; - - await adapter.continueConversationAsync(process.env.MicrosoftAppId ?? '', conversationReference, async context => { - await context.sendActivity('proactive hello'); - }); - }()); - // highlight-end - ``` - - - ```typescript showLineNumbers - import { App } from '@microsoft/teams.apps'; - - const app = new App(); - - // highlight-start - (async () => { - await app.start(); - await app.send('your-conversation-id', 'proactive hello'); - }()); - // highlight-end - ``` - - \ No newline at end of file + +* await app.start(); +* await app.send('your-conversation-id', 'proactive hello'); + // highlight-success-end + }()); + ``` + + + + ```typescript showLineNumbers + import { + CloudAdapter, + ConfigurationBotFrameworkAuthentication, + ConversationReference, + } from 'botbuilder'; + + const auth = new ConfigurationBotFrameworkAuthentication(process.env); + const adapter = new CloudAdapter(auth); + + // highlight-start + (async () => { + const conversationReference: ConversationReference = { + serviceUrl: '...', + bot: { ... }, + channelId: 'msteams', + conversation: { ... }, + user: { ... }, + }; + + await adapter.continueConversationAsync(process.env.MicrosoftAppId ?? '', conversationReference, async context => { + await context.sendActivity('proactive hello'); + }); + }()); + // highlight-end + ``` + + + + ```typescript showLineNumbers + import { App } from '@microsoft/teams.apps'; + + const app = new App(); + + // highlight-start + (async () => { + await app.start(); + await app.send('your-conversation-id', 'proactive hello'); + }()); + // highlight-end + ``` + + + diff --git a/teams.md/src/components/include/migrations/botbuilder/sending-activities/csharp.incl.md b/teams.md/src/components/include/migrations/botbuilder/sending-activities/csharp.incl.md index ddb2aad34e..9a40ebe16e 100644 --- a/teams.md/src/components/include/migrations/botbuilder/sending-activities/csharp.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/sending-activities/csharp.incl.md @@ -14,61 +14,64 @@ //highlight-success-end // highlight-error-start -- public class MyActivityHandler : ActivityHandler -- { + +- public class MyActivityHandler : ActivityHandler +- { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - await turnContext.SendActivityAsync( -- Activity.CreateTypingActivity(), +- Activity.CreateTypingActivity(), - cancellationToken: cancellationToken); - } -- } - // highlight-error-end - // highlight-success-start -+ var teams = app.UseTeams(); -+ teams.OnMessage(async (context) => -+ { -+ await context.Send(new Activity(type:"typing")); -+ }); - // highlight-success-end - ``` - +- } + // highlight-error-end + // highlight-success-start + +* var teams = app.UseTeams(); +* teams.OnMessage(async (context) => +* { +* await context.Send(new Activity(type:"typing")); +* }); + // highlight-success-end + ` - ```csharp showLineNumbers - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; - - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - // highlight-next-line - await turnContext.SendActivityAsync( - Activity.CreateTypingActivity(), - cancellationToken: cancellationToken); - } - } - ``` - - - ```csharp showLineNumbers - using Microsoft.Teams.Apps; - using Microsoft.Teams.Plugins.AspNetCore.Extensions; - using Microsoft.Teams.Api.Activities; - - var teams = app.UseTeams(); - teams.OnMessage(async (context) => - { - // highlight-next-line - await context.Send(new Activity(type:"typing")); - }); - ``` - - + `csharp showLineNumbers + using Microsoft.Bot.Builder; + using Microsoft.Bot.Schema; + + public class MyActivityHandler : ActivityHandler + { + protected override async Task OnMessageActivityAsync( + ITurnContext turnContext, + CancellationToken cancellationToken) + { + // highlight-next-line + await turnContext.SendActivityAsync( + Activity.CreateTypingActivity(), + cancellationToken: cancellationToken); + } + } + ``` + + + + ```csharp showLineNumbers + using Microsoft.Teams.Apps; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; + using Microsoft.Teams.Api.Activities; + + var teams = app.UseTeams(); + teams.OnMessage(async (context) => + { + // highlight-next-line + await context.Send(new Activity(type:"typing")); + }); + ``` + + + ## Strings @@ -85,56 +88,59 @@ //highlight-success-end // highlight-error-start -- public class MyActivityHandler : ActivityHandler -- { + +- public class MyActivityHandler : ActivityHandler +- { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - await turnContext.SendActivityAsync("hello world", cancellationToken: cancellationToken); - } -- } - // highlight-error-end - // highlight-success-start -+ var teams = app.UseTeams(); -+ teams.OnMessage(async (context) => -+ { -+ await context.Send("hello world"); -+ }); - // highlight-success-end - ``` - +- } + // highlight-error-end + // highlight-success-start + +* var teams = app.UseTeams(); +* teams.OnMessage(async (context) => +* { +* await context.Send("hello world"); +* }); + // highlight-success-end + ` - ```csharp showLineNumbers - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; - - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - // highlight-next-line - await turnContext.SendActivityAsync("hello world", cancellationToken: cancellationToken); - } - } - ``` - - - ```csharp showLineNumbers - using Microsoft.Teams.Apps; - using Microsoft.Teams.Plugins.AspNetCore.Extensions; - - var teams = app.UseTeams(); - teams.OnMessage(async (context) => - { - // highlight-next-line - await context.Send("hello world"); - }); - ``` - - + `csharp showLineNumbers + using Microsoft.Bot.Builder; + using Microsoft.Bot.Schema; + + public class MyActivityHandler : ActivityHandler + { + protected override async Task OnMessageActivityAsync( + ITurnContext turnContext, + CancellationToken cancellationToken) + { + // highlight-next-line + await turnContext.SendActivityAsync("hello world", cancellationToken: cancellationToken); + } + } + ``` + + + + ```csharp showLineNumbers + using Microsoft.Teams.Apps; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; + + var teams = app.UseTeams(); + teams.OnMessage(async (context) => + { + // highlight-next-line + await context.Send("hello world"); + }); + ``` + + + ## Adaptive Cards @@ -152,8 +158,9 @@ // highlight-success-end // highlight-error-start -- public class MyActivityHandler : ActivityHandler -- { + +- public class MyActivityHandler : ActivityHandler +- { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) @@ -175,65 +182,67 @@ - var activity = MessageFactory.Attachment(attachment); - await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); - } -- } - // highlight-error-end - // highlight-success-start -+ var teams = app.UseTeams(); -+ teams.OnMessage(async (context) => -+ { -+ await context.Send(new AdaptiveCard(new TextBlock("hello world"))); -+ }); - // highlight-success-end - ``` - +- } + // highlight-error-end + // highlight-success-start + +* var teams = app.UseTeams(); +* teams.OnMessage(async (context) => +* { +* await context.Send(new AdaptiveCard(new TextBlock("hello world"))); +* }); + // highlight-success-end + ` - ```csharp showLineNumbers - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; - - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - // highlight-start - var card = new - { - type = "AdaptiveCard", - version = "1.0", - body = new[] - { - new { type = "TextBlock", text = "hello world" } - } - }; - var attachment = new Attachment - { - ContentType = "application/vnd.microsoft.card.adaptive", - Content = card - }; - var activity = MessageFactory.Attachment(attachment); - await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); - // highlight-end - } - } - ``` - - - ```csharp showLineNumbers - using Microsoft.Teams.Cards; - using Microsoft.Teams.Apps; - using Microsoft.Teams.Plugins.AspNetCore.Extensions; - - var teams = app.UseTeams(); - teams.OnMessage(async (context) => - { - // highlight-next-line - await context.Send(new AdaptiveCard(new TextBlock("hello world"))); - }); - ``` - - + `csharp showLineNumbers + using Microsoft.Bot.Builder; + using Microsoft.Bot.Schema; + + public class MyActivityHandler : ActivityHandler + { + protected override async Task OnMessageActivityAsync( + ITurnContext turnContext, + CancellationToken cancellationToken) + { + // highlight-start + var card = new + { + type = "AdaptiveCard", + version = "1.0", + body = new[] + { + new { type = "TextBlock", text = "hello world" } + } + }; + var attachment = new Attachment + { + ContentType = "application/vnd.microsoft.card.adaptive", + Content = card + }; + var activity = MessageFactory.Attachment(attachment); + await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); + // highlight-end + } + } + ``` + + + + ```csharp showLineNumbers + using Microsoft.Teams.Cards; + using Microsoft.Teams.Apps; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; + + var teams = app.UseTeams(); + teams.OnMessage(async (context) => + { + // highlight-next-line + await context.Send(new AdaptiveCard(new TextBlock("hello world"))); + }); + ``` + + + ## Attachments @@ -251,8 +260,9 @@ // highlight-success-end // highlight-error-start -- public class MyActivityHandler : ActivityHandler -- { + +- public class MyActivityHandler : ActivityHandler +- { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) @@ -260,53 +270,54 @@ - var activity = MessageFactory.Attachment(new Attachment { /* ... */ }); - await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); - } -- } - // highlight-error-end - // highlight-success-start -+ var teams = app.UseTeams(); -+ teams.OnMessage(async (context) => -+ { -+ var activity = new MessageActivity(); -+ activity.AddAttachment(new Attachment { /* ... */ }); -+ await context.SendAsync(activity); -+ }); - // highlight-success-end - ``` - +- } + // highlight-error-end + // highlight-success-start + +* var teams = app.UseTeams(); +* teams.OnMessage(async (context) => +* { +* var activity = new MessageActivity(); +* activity.AddAttachment(new Attachment { /* ... */ }); +* await context.SendAsync(activity); +* }); + // highlight-success-end + ` - ```csharp showLineNumbers - using Microsoft.Bot.Builder; - using Microsoft.Bot.Schema; - - public class MyActivityHandler : ActivityHandler - { - protected override async Task OnMessageActivityAsync( - ITurnContext turnContext, - CancellationToken cancellationToken) - { - // highlight-start - var activity = MessageFactory.Attachment(new Attachment { /* ... */ }); - await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); - // highlight-end - } - } - ``` - - - ```csharp showLineNumbers - using Microsoft.Teams.Api; - using Microsoft.Teams.Apps; - using Microsoft.Teams.Plugins.AspNetCore.Extensions; - - var teams = app.UseTeams(); - teams.OnMessage(async (context) => - { - // highlight-start - var activity = new MessageActivity(); - activity.AddAttachment(new Attachment { /* ... */ }); - await context.SendAsync(activity); - // highlight-end - }); - ``` - - + `csharp showLineNumbers + using Microsoft.Bot.Builder; + using Microsoft.Bot.Schema; + + public class MyActivityHandler : ActivityHandler + { + protected override async Task OnMessageActivityAsync( + ITurnContext turnContext, + CancellationToken cancellationToken) + { + // highlight-start + var activity = MessageFactory.Attachment(new Attachment { /* ... */ }); + await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken); + // highlight-end + } + } + ``` + + + + ```csharp showLineNumbers + using Microsoft.Teams.Api; + using Microsoft.Teams.Apps; + using Microsoft.Teams.Plugins.AspNetCore.Extensions; + + var teams = app.UseTeams(); + teams.OnMessage(async (context) => + { + // highlight-start + var activity = new MessageActivity(); + activity.AddAttachment(new Attachment { /* ... */ }); + await context.SendAsync(activity); + // highlight-end + }); + ``` + + diff --git a/teams.md/src/components/include/migrations/botbuilder/sending-activities/python.incl.md b/teams.md/src/components/include/migrations/botbuilder/sending-activities/python.incl.md index f9ccb41072..079f014282 100644 --- a/teams.md/src/components/include/migrations/botbuilder/sending-activities/python.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/sending-activities/python.incl.md @@ -13,40 +13,45 @@ # highlight-success-end # highlight-error-start -- class MyActivityHandler(ActivityHandler): + +- class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - await turn_context.send_activity(Activity(type="typing")) - # highlight-error-end - # highlight-success-start -+ @app.on_message -+ async def on_message(context: ActivityContext[MessageActivity]): -+ await context.send(TypingActivityInput()) - # highlight-success-end - ``` - - - ```python showLineNumbers - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.schema import Activity - - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - # highlight-next-line - await turn_context.send_activity(Activity(type="typing")) - ``` - - - ```python showLineNumbers - from microsoft_teams.api import MessageActivity, TypingActivityInput - from microsoft_teams.apps import ActivityContext, App - - @app.on_message - async def on_message(context: ActivityContext[MessageActivity]): - # highlight-next-line - await context.send(TypingActivityInput()) - ``` - - + # highlight-error-end + # highlight-success-start + +* @app.on_message +* async def on_message(context: ActivityContext[MessageActivity]): +* await context.send(TypingActivityInput()) + # highlight-success-end + ``` + + + + ```python showLineNumbers + from botbuilder.core import ActivityHandler, TurnContext + from botbuilder.schema import Activity + + class MyActivityHandler(ActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + # highlight-next-line + await turn_context.send_activity(Activity(type="typing")) + ``` + + + + ```python showLineNumbers + from microsoft_teams.api import MessageActivity, TypingActivityInput + from microsoft_teams.apps import ActivityContext, App + + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + # highlight-next-line + await context.send(TypingActivityInput()) + ``` + + + ## Strings @@ -62,39 +67,44 @@ # highlight-success-end # highlight-error-start -- class MyActivityHandler(ActivityHandler): + +- class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - await turn_context.send_activity("hello world") - # highlight-error-end - # highlight-success-start -+ @app.on_message -+ async def on_message(context: ActivityContext[MessageActivity]): -+ await context.send("hello world") - # highlight-success-end - ``` - - - ```python showLineNumbers - from botbuilder.core import ActivityHandler, TurnContext - - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - # highlight-next-line - await turn_context.send_activity("hello world") - ``` - - - ```python showLineNumbers - from microsoft_teams.api import MessageActivity - from microsoft_teams.apps import ActivityContext, App - - @app.on_message - async def on_message(context: ActivityContext[MessageActivity]): - # highlight-next-line - await context.send("hello world") - ``` - - + # highlight-error-end + # highlight-success-start + +* @app.on_message +* async def on_message(context: ActivityContext[MessageActivity]): +* await context.send("hello world") + # highlight-success-end + ``` + + + + ```python showLineNumbers + from botbuilder.core import ActivityHandler, TurnContext + + class MyActivityHandler(ActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + # highlight-next-line + await turn_context.send_activity("hello world") + ``` + + + + ```python showLineNumbers + from microsoft_teams.api import MessageActivity + from microsoft_teams.apps import ActivityContext, App + + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + # highlight-next-line + await context.send("hello world") + ``` + + + ## Adaptive Cards @@ -112,48 +122,53 @@ # highlight-success-end # highlight-error-start -- class MyActivityHandler(ActivityHandler): + +- class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - card = {"type": "AdaptiveCard", "version": "1.0", "body": [{"type": "TextBlock", "text": "hello world"}]} - attachment = Attachment(content_type="application/vnd.microsoft.card.adaptive", content=card) - activity = Activity(type="message", attachments=[attachment]) - await turn_context.send_activity(activity) - # highlight-error-end - # highlight-success-start -+ @app.on_message -+ async def on_message(context: ActivityContext[MessageActivity]): -+ await context.send(AdaptiveCard().with_body([TextBlock(text="Hello from Adaptive Card!")])) - # highlight-success-end - ``` - - - ```python showLineNumbers - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.schema import Activity, Attachment - - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - # hightlight-start - card = {"type": "AdaptiveCard", "version": "1.0", "body": [{"type": "TextBlock", "text": "hello world"}]} - attachment = Attachment(content_type="application/vnd.microsoft.card.adaptive", content=card) - activity = Activity(type="message", attachments=[attachment]) - await turn_context.send_activity(activity) - # highlight-end - ``` - - - ```python showLineNumbers - from microsoft_teams.api import MessageActivity - from microsoft_teams.apps import ActivityContext, App - from microsoft_teams.cards import AdaptiveCard, TextBlock - - @app.on_message - async def on_message(context: ActivityContext[MessageActivity]): - # highlight-next-line - await context.send(AdaptiveCard(body=[TextBlock(text="Hello from Adaptive Card!")])) - ``` - - + # highlight-error-end + # highlight-success-start + +* @app.on_message +* async def on_message(context: ActivityContext[MessageActivity]): +* await context.send(AdaptiveCard().with_body([TextBlock(text="Hello from Adaptive Card!")])) + # highlight-success-end + ``` + + + + ```python showLineNumbers + from botbuilder.core import ActivityHandler, TurnContext + from botbuilder.schema import Activity, Attachment + + class MyActivityHandler(ActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + # hightlight-start + card = {"type": "AdaptiveCard", "version": "1.0", "body": [{"type": "TextBlock", "text": "hello world"}]} + attachment = Attachment(content_type="application/vnd.microsoft.card.adaptive", content=card) + activity = Activity(type="message", attachments=[attachment]) + await turn_context.send_activity(activity) + # highlight-end + ``` + + + + ```python showLineNumbers + from microsoft_teams.api import MessageActivity + from microsoft_teams.apps import ActivityContext, App + from microsoft_teams.cards import AdaptiveCard, TextBlock + + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + # highlight-next-line + await context.send(AdaptiveCard(body=[TextBlock(text="Hello from Adaptive Card!")])) + ``` + + + ## Attachments @@ -170,47 +185,52 @@ # highlight-success-end # highlight-error-start -- class MyActivityHandler(ActivityHandler): + +- class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - attachment = Attachment(...) - activity = Activity(type="message", attachments=[attachment]) - await turn_context.send_activity(activity) - # highlight-error-end - # highlight-success-start -+ @app.on_message -+ async def on_message(context: ActivityContext[MessageActivity]): -+ attachment = Attachment(...) -+ activity = MessageActivityInput().add_attachments([attachment]) -+ await context.send(activity) - # highlight-success-end - ``` - - - ```python showLineNumbers - from botbuilder.core import ActivityHandler, TurnContext - from botbuilder.schema import Activity, Attachment - - class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - # highlight-start - attachment = Attachment(...) - activity = Activity(type="message", attachments=[attachment]) - await turn_context.send_activity(activity) - # highlight-end - ``` - - - ```python showLineNumbers - from microsoft_teams.api import Attachment, MessageActivity, MessageActivityInput - from microsoft_teams.apps import ActivityContext, App - - @app.on_message - async def on_message(context: ActivityContext[MessageActivity]): - # highlight-start - attachment = Attachment(...) - activity = MessageActivityInput().add_attachments([attachment]) - await context.send(activity) - # highlight-end - ``` - - + # highlight-error-end + # highlight-success-start + +* @app.on_message +* async def on_message(context: ActivityContext[MessageActivity]): +* attachment = Attachment(...) +* activity = MessageActivityInput().add_attachments([attachment]) +* await context.send(activity) + # highlight-success-end + ``` + + + + ```python showLineNumbers + from botbuilder.core import ActivityHandler, TurnContext + from botbuilder.schema import Activity, Attachment + + class MyActivityHandler(ActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + # highlight-start + attachment = Attachment(...) + activity = Activity(type="message", attachments=[attachment]) + await turn_context.send_activity(activity) + # highlight-end + ``` + + + + ```python showLineNumbers + from microsoft_teams.api import Attachment, MessageActivity, MessageActivityInput + from microsoft_teams.apps import ActivityContext, App + + @app.on_message + async def on_message(context: ActivityContext[MessageActivity]): + # highlight-start + attachment = Attachment(...) + activity = MessageActivityInput().add_attachments([attachment]) + await context.send(activity) + # highlight-end + ``` + + + diff --git a/teams.md/src/components/include/migrations/botbuilder/sending-activities/typescript.incl.md b/teams.md/src/components/include/migrations/botbuilder/sending-activities/typescript.incl.md index 1d0e72b102..1bccaa45e9 100644 --- a/teams.md/src/components/include/migrations/botbuilder/sending-activities/typescript.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/sending-activities/typescript.incl.md @@ -6,21 +6,25 @@ // highlight-error-start - import { TeamsActivityHandler } from 'botbuilder'; -- export class ActivityHandler extends TeamsActivityHandler { +- export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - await context.sendActivity({ type: 'typing' }); - }); - } -- } - // highlight-error-end - // highlight-success-start -+ app.on('message', async ({ send }) => { -+ await send({ type: 'typing' }); -+ }); - // highlight-success-end +- } + // highlight-error-end + // highlight-success-start + +* app.on('message', async ({ send }) => { +* await send({ type: 'typing' }); +* }); + // highlight-success-end + + ``` + ```typescript showLineNumbers @@ -36,6 +40,7 @@ } } ``` + ```typescript showLineNumbers @@ -55,21 +60,25 @@ // highlight-error-start - import { TeamsActivityHandler } from 'botbuilder'; -- export class ActivityHandler extends TeamsActivityHandler { +- export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { - await context.sendActivity('hello world'); - }); - } -- } - // highlight-error-end - // highlight-success-start -+ app.on('message', async ({ send }) => { -+ await send('hello world'); -+ }); - // highlight-success-end +- } + // highlight-error-end + // highlight-success-start + +* app.on('message', async ({ send }) => { +* await send('hello world'); +* }); + // highlight-success-end + + ``` + ```typescript showLineNumbers @@ -85,6 +94,7 @@ } } ``` + ```typescript showLineNumbers @@ -107,7 +117,8 @@ + import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; // highlight-error-start -- export class ActivityHandler extends TeamsActivityHandler { + +- export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { @@ -127,14 +138,18 @@ - }); - }); - } -- } - // highlight-error-end - // highlight-success-start -+ app.on('message', async ({ send }) => { -+ await send(new AdaptiveCard(new TextBlock('hello world'))); -+ }); - // highlight-success-end +- } + // highlight-error-end + // highlight-success-start + +* app.on('message', async ({ send }) => { +* await send(new AdaptiveCard(new TextBlock('hello world'))); +* }); + // highlight-success-end + + ``` + ```typescript showLineNumbers @@ -164,6 +179,7 @@ } } ``` + ```typescript showLineNumbers @@ -174,6 +190,7 @@ await send(new AdaptiveCard(new TextBlock('hello world'))); }); ``` + @@ -188,7 +205,8 @@ + import { AdaptiveCard, TextBlock } from '@microsoft/teams.cards'; // highlight-error-start -- export class ActivityHandler extends TeamsActivityHandler { + +- export class ActivityHandler extends TeamsActivityHandler { - constructor() { - super(); - this.onMessage(async (context) => { @@ -200,14 +218,18 @@ - }); - }); - } -- } - // highlight-error-end - // highlight-success-start -+ app.on('message', async ({ send }) => { -+ await send(new MessageActivity().addAttachment(...)); -+ }); - // highlight-success-end +- } + // highlight-error-end + // highlight-success-start + +* app.on('message', async ({ send }) => { +* await send(new MessageActivity().addAttachment(...)); +* }); + // highlight-success-end + + ``` + ```typescript showLineNumbers @@ -229,6 +251,7 @@ } } ``` + ```typescript showLineNumbers @@ -238,5 +261,6 @@ await send(new MessageActivity().addAttachment(...)); }); ``` + - \ No newline at end of file + diff --git a/teams.md/src/components/include/migrations/botbuilder/user-authentication/typescript.incl.md b/teams.md/src/components/include/migrations/botbuilder/user-authentication/typescript.incl.md index 716f1b7143..0469a67a2b 100644 --- a/teams.md/src/components/include/migrations/botbuilder/user-authentication/typescript.incl.md +++ b/teams.md/src/components/include/migrations/botbuilder/user-authentication/typescript.incl.md @@ -170,4 +170,5 @@ ``` + diff --git a/teams.md/src/pages/index.tsx b/teams.md/src/pages/index.tsx index ba6bb18de9..7ecec02e8b 100644 --- a/teams.md/src/pages/index.tsx +++ b/teams.md/src/pages/index.tsx @@ -1,5 +1,5 @@ import { Redirect } from '@docusaurus/router'; export default function Home() { - return ; + return ; } diff --git a/teams.md/src/pages/templates/in-depth-guides/message-reactions.mdx b/teams.md/src/pages/templates/in-depth-guides/message-reactions.mdx new file mode 100644 index 0000000000..4dfe32b11f --- /dev/null +++ b/teams.md/src/pages/templates/in-depth-guides/message-reactions.mdx @@ -0,0 +1,52 @@ +--- +sidebar_position: 8 +title: Message Reactions +sidebar_label: Message Reactions +summary: Guide to adding and removing message reactions in Teams applications, including available reaction types and best practices for implementing reaction functionality. +--- + +# Message Reactions + +Message reactions allow your bot to add emoji reactions to messages in Teams conversations. This feature enables your bot to provide quick, non-verbal feedback to user messages, enhancing the interactive experience. + +## Adding a Reaction + +To add a reaction to a message, use the `Reactions` API available through the conversations client: + + + +## Removing a Reaction + +You can also remove reactions that your bot has added: + + + +## Available Reaction Types + + + +:::info Custom Reactions +In addition to the standard reaction types listed above, you can use any valid Teams emoji reaction code. Teams supports a wide variety of emoji reactions beyond the standard set. +::: + +## Advanced Usage + + + +## Best Practices + +When working with message reactions, consider the following: + +- **Use reactions sparingly**: Reactions work best when used to provide meaningful feedback rather than reacting to every message. +- **Consider the context**: Different reaction types convey different meanings. Choose reactions that align with your bot's purpose and the conversation context. +- **Handle errors gracefully**: Reaction operations may fail (e.g., if the message no longer exists). Always handle potential errors appropriately. +- **Be aware of rate limits**: Like other bot operations, reaction operations are subject to Teams API rate limits. + +## Differences from Feedback + +Message reactions are different from the [feedback](./feedback) feature: + +- **Reactions** are quick emoji responses that your bot adds to messages programmatically +- **Feedback** are interactive UI components (like/dislike buttons) that users can click to provide structured feedback on bot responses + +Use reactions when your bot wants to acknowledge or respond to messages with emoji. Use feedback when you want to collect user opinions about your bot's responses. diff --git a/teams.md/src/scripts/scaffold.js b/teams.md/src/scripts/scaffold.js index 39dd2d6325..7607f140bd 100644 --- a/teams.md/src/scripts/scaffold.js +++ b/teams.md/src/scripts/scaffold.js @@ -15,18 +15,21 @@ function createFileIfNotExists(filePath, content = '') { } } - function scaffold(userInput) { // If path starts with src/ but is not a valid base, fail - if (/^src\//.test(userInput) && - !/^src\/pages\/templates\//.test(userInput) && - !/^src\/components\/include\//.test(userInput)) { + if ( + /^src\//.test(userInput) && + !/^src\/pages\/templates\//.test(userInput) && + !/^src\/components\/include\//.test(userInput) + ) { console.error('Error: Path is outside of allowed base directories.'); process.exit(1); } // Normalize input - let relPath = userInput.replace(/^src\/(pages\/templates|components\/include)\/?/, '').replace(/^[/.]+/, ''); + let relPath = userInput + .replace(/^src\/(pages\/templates|components\/include)\/?/, '') + .replace(/^[/.]+/, ''); let isTemplates = false; let isInclude = false; // Detect if user input is for templates or include @@ -80,7 +83,7 @@ function scaffold(userInput) { const includeTarget = walkAndCreate(includeBase, relPath, (dir) => { createFileIfNotExists(path.join(dir, 'typescript.incl.md'), '# Typescript Include\n'); }); - return {templatesTarget, includeTarget}; + return { templatesTarget, includeTarget }; } if (isTemplates) { const templatesBase = path.join(__dirname, '../pages/templates'); @@ -88,14 +91,14 @@ function scaffold(userInput) { createFileIfNotExists(path.join(dir, '_category.json'), '{\n "label": "New Category"\n}\n'); createFileIfNotExists(path.join(dir, 'README.mdx'), '# New Template\n'); }); - return {templatesTarget}; + return { templatesTarget }; } if (isInclude) { const includeBase = path.join(__dirname, '../components/include'); const includeTarget = walkAndCreate(includeBase, relPath, (dir) => { createFileIfNotExists(path.join(dir, 'typescript.incl.md'), '# Typescript Include\n'); }); - return {includeTarget}; + return { includeTarget }; } } @@ -107,14 +110,18 @@ function isValidPath(p) { } if (!isValidPath(userPath)) { - console.error('Error: Invalid path. Use a relative path like "templates/new/two" or "include/new/two". Do not start with a slash or use "..".'); + console.error( + 'Error: Invalid path. Use a relative path like "templates/new/two" or "include/new/two". Do not start with a slash or use "..".' + ); process.exit(1); } const created = scaffold(userPath); if (created) { if (created.templatesTarget && created.includeTarget) { - console.log(`Scaffolded:\n Template: ${created.templatesTarget}\n Include: ${created.includeTarget}`); + console.log( + `Scaffolded:\n Template: ${created.templatesTarget}\n Include: ${created.includeTarget}` + ); } else if (created.templatesTarget) { console.log(`Scaffolded Template: ${created.templatesTarget}`); } else if (created.includeTarget) { diff --git a/teams.md/src/utils/normalizePath.ts b/teams.md/src/utils/normalizePath.ts index 40ba288ea5..19456cb0c7 100644 --- a/teams.md/src/utils/normalizePath.ts +++ b/teams.md/src/utils/normalizePath.ts @@ -2,5 +2,5 @@ * For cross-platform compatibility, normalize paths to use forward slash */ export default function normalizePath(path: string) { -return path.replace(/\\/g, '/'); -} \ No newline at end of file + return path.replace(/\\/g, '/'); +} diff --git a/teams.md/src/utils/pageAvailability.ts b/teams.md/src/utils/pageAvailability.ts index 50d209bf52..a8a23ac54d 100644 --- a/teams.md/src/utils/pageAvailability.ts +++ b/teams.md/src/utils/pageAvailability.ts @@ -9,7 +9,10 @@ let missingPagesCache: LanguageAvailabilityMap | null = null; * @param language - The target language to check * @returns Promise - true: page available; else false */ -export async function isPageAvailableForLanguage(pagePath: string, language: Language): Promise { +export async function isPageAvailableForLanguage( + pagePath: string, + language: Language +): Promise { if (!missingPagesCache) { try { const response = await fetch('/teams-sdk/missing-pages.json'); diff --git a/teams.md/static/scripts/clarity.js b/teams.md/static/scripts/clarity.js index 087ef74e67..2e906a8181 100644 --- a/teams.md/static/scripts/clarity.js +++ b/teams.md/static/scripts/clarity.js @@ -1,5 +1,12 @@ -(function(c,l,a,r,i,t,y){ - c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; - t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; - y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); -})(window, document, "clarity", "script", "rdeztc00ot"); \ No newline at end of file +(function (c, l, a, r, i, t, y) { + c[a] = + c[a] || + function () { + (c[a].q = c[a].q || []).push(arguments); + }; + t = l.createElement(r); + t.async = 1; + t.src = 'https://www.clarity.ms/tag/' + i; + y = l.getElementsByTagName(r)[0]; + y.parentNode.insertBefore(t, y); +})(window, document, 'clarity', 'script', 'rdeztc00ot');