diff --git a/AI/prompts/contoso-audit-agent.md b/AI/prompts/contoso-audit-agent.md deleted file mode 100644 index 35fc044..0000000 --- a/AI/prompts/contoso-audit-agent.md +++ /dev/null @@ -1,145 +0,0 @@ - -Add the SharePoint Embedded copilot chat control on a expandable panel on the right side of the File List page. - ------------------------------------------------ -0. USER EXPERIENCE ------------------------------------------------ -- Expandable panel should show up only on the File List page by clicking an "AI Assistant" button -- Expandable panel should show inline with the other page content -- it should not overlap it and the other page content should compress to let it fit -- Panel visibility can be toggled and resized -- IMPORTANT: Make sure the height of the chat control fits into the visible device height - ------------------------------------------------ -1. REQUIRED CONFIGURATION ------------------------------------------------ -- must install the npm library directly using `npm install "https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz"` -- IMPORTANT: the chat library is NOT listed in the npm registry, you MUST install it with it's full url from https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz -- add the following to the central configuration file. - -SP_HOST=[url] - - ------------------------------------------------ -2. AUTHENTICAION ------------------------------------------------ -Create a new auth function called `requestSPOAccessToken` that will use MSAL to get another access token for the following scope: - -`${SP_HOST}/Container.Selected` - -IMPORTANT -- DO NOT try to get an access token that has both Graph and SPO scopes -- they must be kept separate -- ONLY use the SPO access token with the chat control. All other requests to Graph APIs need to use the Graph access token - ------------------------------------------------ -3. SHOWING THE CHAT CONTROL ------------------------------------------------ -Use the following example TypeScript code to load the SharePoint Embedded chat control as a React component - -```typescript -import React from 'react'; -import { - ChatEmbedded, - ChatEmbeddedAPI, - ChatLaunchConfig, - IChatEmbeddedApiAuthProvider -} from '@microsoft/sharepointembedded-copilotchat-react'; - -//... -async function requestSPOAccessToken() { - //... -} - -const authProvider: IChatEmbeddedApiAuthProvider = { - hostname: $SP_HOST, - getToken: requestSPOAccessToken, -}; - -function App() { - const [chatApi, setChatApi] = React.useState(null); - - const chatTheme: IThemeOptions = { - useDarkMode: false, - customTheme: { - themePrimary: '#4854EE', - themeSecondary: '#4854EE', - themeDark: '#4854EE', - themeDarker: '#4854EE', - themeTertiary: '#4854EE', - themeLight: '#dddeef', - themeDarkAlt: '#4854EE', - themeLighter: '#dddeef', - themeLighterAlt: '#dddeef', - themeDarkAltTransparent: '#4854EE', - themeLighterTransparent: '#dddeef', - themeLighterAltTransparent: '#dddeef', - themeMedium: '#4854EE', - neutralSecondary: '#4854EE', - neutralSecondaryAlt: '#4854EE', - neutralTertiary: '#4854EE', - neutralTertiaryAlt: '#4854EE', - neutralQuaternary: '#4854EE', - neutralQuaternaryAlt: '#4854EE', - neutralPrimaryAlt: '#4854EE', - neutralDark: '#4854EE', - themeBackground: 'white', - } - }; - - const prompts = { - headerText: `Chat with content in ${container.displayName}`, - promptSuggestionList: [ - { - suggestionText: 'Show me recent files', - iconRegular: { name: IconName.ChatBubblesQuestion, style: IconStyle.Regular }, - iconHover: { name: IconName.ChatBubblesQuestion, style: IconStyle.Filled }, - }, - { - suggestionText: "Make a table of marketing expenses over the past five years", - iconRegular: { name: IconName.DocumentCatchUp, style: IconStyle.Regular }, - iconHover: { name: IconName.DocumentCatchUp, style: IconStyle.Filled }, - }, - // Add two more suggested prompts - ] - } - - const chatConfig: ChatLaunchConfig = { - header: `Contoso Audit - ${container.displayName}`, - theme: chatTheme, - zeroQueryPrompts: prompts, - instruction: "You are a helpful assistant that auditors use to find and summarize information related to auditing cases. Make sure you include references to the documents data comes from when possible. ", - locale: "en", - }; - - React.useEffect(() => { - const openChat = async () => { - if (!chatApi) { - return; - } - - await chatApi.openChat(chatConfig); - }; - - openChat(); - }, [chatApi]); - - - return ( - //... - setChatApi(api)} - authProvider={authProvider} - containerId={container.id} - style={{ width: 'calc(100% - 4px)', height: 'calc(100vh - 8px)' }} - /> - //... - ); -} -``` ------------------------------------------------ -4. CUSTOMIZE THE CHAT CONTROL ------------------------------------------------ -Customize the chat control header, theme, prompts, and instruction to match the Contoso Audit application - -- override the default theme colors in the sample code to match the rest of the Contoso Audit app -- set the header, theme, prompts, and instruction to create a useful agent that can help users summarize and ask questions about the content within an audit case (container) - diff --git a/AI/prompts/contoso-legal-agent.md b/AI/prompts/contoso-legal-agent.md deleted file mode 100644 index 1f36c28..0000000 --- a/AI/prompts/contoso-legal-agent.md +++ /dev/null @@ -1,145 +0,0 @@ - -Add the SharePoint Embedded copilot chat control on a expandable panel on the right side of the File List page. - ------------------------------------------------ -0. USER EXPERIENCE ------------------------------------------------ -- Expandable panel should show up only on the File List page by clicking an "AI Assistant" button -- Expandable panel should show inline with the other page content -- it should not overlap it and the other page content should compress to let it fit -- Panel visibility can be toggled and resized -- IMPORTANT: Make sure the height of the chat control fits into the visible device height - ------------------------------------------------ -1. REQUIRED CONFIGURATION ------------------------------------------------ -- must install the npm library directly using `npm install "https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz"` -- IMPORTANT: the chat library is NOT listed in the npm registry, you MUST install it with it's full url from https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz -- add the following to the central configuration file. - -SP_HOST=[url] - - ------------------------------------------------ -2. AUTHENTICAION ------------------------------------------------ -Create a new auth function called `requestSPOAccessToken` that will use MSAL to get another access token for the following scope: - -`${SP_HOST}/Container.Selected` - -IMPORTANT -- DO NOT try to get an access token that has both Graph and SPO scopes -- they must be kept separate -- ONLY use the SPO access token with the chat control. All other requests to Graph APIs need to use the Graph access token - ------------------------------------------------ -3. SHOWING THE CHAT CONTROL ------------------------------------------------ -Use the following example TypeScript code to load the SharePoint Embedded chat control as a React component - -```typescript -import React from 'react'; -import { - ChatEmbedded, - ChatEmbeddedAPI, - ChatLaunchConfig, - IChatEmbeddedApiAuthProvider -} from '@microsoft/sharepointembedded-copilotchat-react'; - -//... -async function requestSPOAccessToken() { - //... -} - -const authProvider: IChatEmbeddedApiAuthProvider = { - hostname: $SP_HOST, - getToken: requestSPOAccessToken, -}; - -function App() { - const [chatApi, setChatApi] = React.useState(null); - - const chatTheme: IThemeOptions = { - useDarkMode: false, - customTheme: { - themePrimary: '#4854EE', - themeSecondary: '#4854EE', - themeDark: '#4854EE', - themeDarker: '#4854EE', - themeTertiary: '#4854EE', - themeLight: '#dddeef', - themeDarkAlt: '#4854EE', - themeLighter: '#dddeef', - themeLighterAlt: '#dddeef', - themeDarkAltTransparent: '#4854EE', - themeLighterTransparent: '#dddeef', - themeLighterAltTransparent: '#dddeef', - themeMedium: '#4854EE', - neutralSecondary: '#4854EE', - neutralSecondaryAlt: '#4854EE', - neutralTertiary: '#4854EE', - neutralTertiaryAlt: '#4854EE', - neutralQuaternary: '#4854EE', - neutralQuaternaryAlt: '#4854EE', - neutralPrimaryAlt: '#4854EE', - neutralDark: '#4854EE', - themeBackground: 'white', - } - }; - - const prompts = { - headerText: `Chat with content in ${container.displayName}`, - promptSuggestionList: [ - { - suggestionText: 'Show me recent files', - iconRegular: { name: IconName.ChatBubblesQuestion, style: IconStyle.Regular }, - iconHover: { name: IconName.ChatBubblesQuestion, style: IconStyle.Filled }, - }, - { - suggestionText: "Make a table of marketing expenses over the past five years", - iconRegular: { name: IconName.DocumentCatchUp, style: IconStyle.Regular }, - iconHover: { name: IconName.DocumentCatchUp, style: IconStyle.Filled }, - }, - // Add two more suggested prompts - ] - } - - const chatConfig: ChatLaunchConfig = { - header: `Contoso Audit - ${container.displayName}`, - theme: chatTheme, - zeroQueryPrompts: prompts, - instruction: "You are a helpful assistant that auditors use to find and summarize information related to auditing cases. Make sure you include references to the documents data comes from when possible. ", - locale: "en", - }; - - React.useEffect(() => { - const openChat = async () => { - if (!chatApi) { - return; - } - - await chatApi.openChat(chatConfig); - }; - - openChat(); - }, [chatApi]); - - - return ( - //... - setChatApi(api)} - authProvider={authProvider} - containerId={container.id} - style={{ width: 'calc(100% - 4px)', height: 'calc(100vh - 8px)' }} - /> - //... - ); -} -``` ------------------------------------------------ -4. CUSTOMIZE THE CHAT CONTROL ------------------------------------------------ -Customize the chat control header, theme, prompts, and instruction to match the Contoso Legal application - -- override the default theme colors in the sample code to match the rest of the Contoso Legal app -- set the header, theme, prompts, and instruction to create a useful agent that can help users summarize and ask questions about the content within an legal case (container) - diff --git a/Custom Apps/README.md b/Custom Apps/README.md index ca08dbe..952b9bc 100644 --- a/Custom Apps/README.md +++ b/Custom Apps/README.md @@ -7,7 +7,7 @@ Runnable sample applications demonstrating SharePoint Embedded integration patte | [boilerplate-react-azurefunction](./boilerplate-react-azurefunction) | React + Azure Functions | Reference boilerplate: SPA with an Azure Functions OBO proxy | | [boilerplate-aspnet-webservice](./boilerplate-aspnet-webservice) | C# / ASP.NET Core | Reference boilerplate: server-side MVC app with tenant onboarding | | [boilerplate-typescript-react](./boilerplate-typescript-react) | TypeScript + React + Azure Functions | TypeScript variant of the React boilerplate | -| [legal-docs](./legal-docs) | React + Fluent UI + Copilot SDK | Legal case management with AI-assisted document review | +| [legal-docs](./legal-docs) | React + Fluent UI | Legal case management sample | | [project-management](./project-management) | React + Vite + Tailwind + shadcn-ui | Project collaboration app | | [webhook](./webhook) | Node.js | Minimal Graph API change notification listener | diff --git a/Custom Apps/boilerplate-typescript-react/react-client/package-lock.json b/Custom Apps/boilerplate-typescript-react/react-client/package-lock.json index c1db030..45ce479 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/package-lock.json +++ b/Custom Apps/boilerplate-typescript-react/react-client/package-lock.json @@ -17,7 +17,6 @@ "@microsoft/mgt-msal2-provider": "^4.2.1", "@microsoft/mgt-react": "^4.2.1", "@microsoft/microsoft-graph-client": "^3.0.7", - "@microsoft/sharepointembedded-copilotchat-react": "https://download.microsoft.com/download/970802a5-2a7e-44ed-b17d-ad7dc99be312/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz", "@types/node": "^20.12.7", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", @@ -5091,16 +5090,6 @@ "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types-beta/-/microsoft-graph-types-beta-0.29.0-preview.tgz", "integrity": "sha512-83PCHDH7GxW8KM1J+MM6FPbaahIuj7VIgnY4UbMGQkkq2fBaHeKOmgjVbohvLVeIOOCTLmorPh6JtXH0VtFurA==" }, - "node_modules/@microsoft/sharepointembedded-copilotchat-react": { - "version": "1.0.9", - "resolved": "https://download.microsoft.com/download/970802a5-2a7e-44ed-b17d-ad7dc99be312/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz", - "integrity": "sha512-mwtB7OOvyox5Mzhpozz/45eq3kYV+DaVNTDxwXC4n5+HnAQabFia1GIWpTVXFAi3Ajp5LNORmWYHIoJBEY23rg==", - "peerDependencies": { - "@fluentui/react": ">=8.1.0", - "react": ">=17.0.2", - "react-dom": ">=17.0.2" - } - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", diff --git a/Custom Apps/boilerplate-typescript-react/react-client/package.json b/Custom Apps/boilerplate-typescript-react/react-client/package.json index 827d602..b3e0547 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/package.json +++ b/Custom Apps/boilerplate-typescript-react/react-client/package.json @@ -12,7 +12,6 @@ "@microsoft/mgt-msal2-provider": "^4.2.1", "@microsoft/mgt-react": "^4.2.1", "@microsoft/microsoft-graph-client": "^3.0.7", - "@microsoft/sharepointembedded-copilotchat-react": "https://download.microsoft.com/download/970802a5-2a7e-44ed-b17d-ad7dc99be312/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz", "@types/node": "^20.12.7", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/common/Scopes.ts b/Custom Apps/boilerplate-typescript-react/react-client/src/common/Scopes.ts index 20ae417..d892c2c 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/common/Scopes.ts +++ b/Custom Apps/boilerplate-typescript-react/react-client/src/common/Scopes.ts @@ -31,10 +31,3 @@ export const SAMPLE_API_SCOPES = SAMPLE_API_CONTAINER_MANAGE ? [SAMPLE_API_CONTAINER_MANAGE] : undefined; -// sharepoint scopes -export const SP_CONTAINER_SELECTED = `${Constants.SP_ROOT_SITE_URL}/Container.Selected`; - -// embedded chat scopes -export const CHAT_SCOPES = [ - SP_CONTAINER_SELECTED -]; diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/components/ChatSidebar.tsx b/Custom Apps/boilerplate-typescript-react/react-client/src/components/ChatSidebar.tsx deleted file mode 100644 index 2d64757..0000000 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/components/ChatSidebar.tsx +++ /dev/null @@ -1,20 +0,0 @@ - -import React from "react"; -import { IContainer } from '../../../common/schemas/ContainerSchemas'; - - - -interface ChatSidebarProps { - - container: IContainer; - -} - - -export const ChatSidebar: React.FunctionComponent = ({ container: _container }) => { - - - return (<> - - ); -} \ No newline at end of file diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/providers/ChatAuthProvider.ts b/Custom Apps/boilerplate-typescript-react/react-client/src/providers/ChatAuthProvider.ts deleted file mode 100644 index 9830033..0000000 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/providers/ChatAuthProvider.ts +++ /dev/null @@ -1,78 +0,0 @@ - -import { InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-browser'; -import * as Constants from '../common/Constants'; -import { GraphProvider } from './GraphProvider'; - -export class ChatAuthProvider { - - private static _instance?: ChatAuthProvider; - private _client: PublicClientApplication; - - public static async getInstance(): Promise { - if (!ChatAuthProvider._instance) { - const spHostname = await GraphProvider.instance.getSpUrl(); - ChatAuthProvider._instance = new ChatAuthProvider(spHostname); - await ChatAuthProvider._instance.initialize(); - } - return ChatAuthProvider._instance; - } - - private constructor(public readonly hostname: string) { - this._client = new PublicClientApplication({ - auth: { - clientId: Constants.AZURE_CLIENT_ID!, - authority: Constants.AUTH_AUTHORITY, - redirectUri: window.location.origin, - }, - cache: { - // https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/caching.md - /* - Cache Location | Cleared on | Shared between windows/tabs | Redirect flow supported - ----------------- ---------- ------------------------- ------------------------ - sessionStorage | window/tab close | No | Yes - localStorage | browser close | Yes | Yes - memoryStorage | page | refresh/navigation | No | No - */ - cacheLocation: 'localStorage', - storeAuthStateInCookie: true - }, - }); - } - - protected async initialize(): Promise { - await this._client.initialize(); - } - - public get scopes(): string[] { - return [ - `${this.hostname}/Container.Selected` - ]; - } - - public async login(): Promise { - await this._client.loginPopup({ - scopes: this.scopes, - prompt: 'select_account', - }); - } - - public async getToken(): Promise { - try { - if (!this._client.getActiveAccount()) { - throw new InteractionRequiredAuthError('no_account', 'No account is signed in'); - } - const response = await this._client.acquireTokenSilent({ - scopes: this.scopes - }); - return response.accessToken; - } catch (error) { - if (error instanceof InteractionRequiredAuthError) { - const response = await this._client.acquireTokenPopup({ - scopes: this.scopes - }); - return response.accessToken; - } - throw error; - } - } -} \ No newline at end of file diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/providers/ChatController.ts b/Custom Apps/boilerplate-typescript-react/react-client/src/providers/ChatController.ts deleted file mode 100644 index ef25265..0000000 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/providers/ChatController.ts +++ /dev/null @@ -1,103 +0,0 @@ - -import { - DataSourceType, - IDataSourcesProps, - IconName, - IconStyle, - IThemeOptions -} from '@microsoft/sharepointembedded-copilotchat-react'; -import { IContainer } from "../../../common/schemas/ContainerSchemas"; - -export class ChatController { - public static readonly instance = new ChatController(); - private constructor() { } - - public get dataSources(): IDataSourcesProps[] { - const sources: IDataSourcesProps[] = []; - - for (const container of this._selectedContainers) { - if (!container || !container.drive) { - continue; - } - sources.push({ - type: DataSourceType.DocumentLibrary, - value: { - name: container.displayName, - url: container.drive!.webUrl - } - }); - } - - return sources; - } - private _dataSourceSubscribers: ((dataSources: IDataSourcesProps[]) => void)[] = []; - public addDataSourceSubscriber(subscriber: (dataSources: IDataSourcesProps[]) => void) { - this._dataSourceSubscribers.push(subscriber); - } - public removeDataSourceSubscriber(subscriber: (dataSources: IDataSourcesProps[]) => void) { - this._dataSourceSubscribers = this._dataSourceSubscribers.filter(s => s !== subscriber); - } - - private _selectedContainers: IContainer[] = []; - public get selectedContainers(): IContainer[] { - return this._selectedContainers; - } - public set selectedContainers(value: IContainer[]) { console.log(value); - this._selectedContainers = value; - this._dataSourceSubscribers.forEach(subscriber => subscriber(this.dataSources)); - } - - - public readonly header = "SharePoint Embedded Chat"; - public readonly theme: IThemeOptions = { - useDarkMode: false, - customTheme: { - themePrimary: '#4854EE', - themeSecondary: '#4854EE', - themeDark: '#4854EE', - themeDarker: '#4854EE', - themeTertiary: '#4854EE', - themeLight: '#dddeef', - themeDarkAlt: '#4854EE', - themeLighter: '#dddeef', - themeLighterAlt: '#dddeef', - themeDarkAltTransparent: '#4854EE', - themeLighterTransparent: '#dddeef', - themeLighterAltTransparent: '#dddeef', - themeMedium: '#4854EE', - neutralSecondary: '#4854EE', - neutralSecondaryAlt: '#4854EE', - neutralTertiary: '#4854EE', - neutralTertiaryAlt: '#4854EE', - neutralQuaternary: '#4854EE', - neutralQuaternaryAlt: '#4854EE', - neutralPrimaryAlt: '#4854EE', - neutralDark: '#4854EE', - themeBackground: 'white', - } - }; - - public readonly zeroQueryPrompts = { - headerText: "SharePoint Embedded Chat: How can I help you today?", - promptSuggestionList: [ - { - suggestionText: 'Show me recent files', - iconRegular: { name: IconName.ChatBubblesQuestion, style: IconStyle.Regular }, - iconHover: { name: IconName.ChatBubblesQuestion, style: IconStyle.Filled }, - }, - { - suggestionText: 'What is SharePoint Embedded?', - iconRegular: { name: IconName.DocumentCatchUp, style: IconStyle.Regular }, - iconHover: { name: IconName.DocumentCatchUp, style: IconStyle.Filled }, - } - ] - }; - - public readonly suggestedPrompts = [ - "List and summarize recent files", - ]; - - public readonly pirateMetaPrompt = "Response must be in the tone of a pirate. Yarrr!"; - - public readonly locale = "en"; -} \ No newline at end of file diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.css b/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.css index 339fa60..ee4cb16 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.css +++ b/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.css @@ -170,56 +170,6 @@ body { border: 1px solid #CCC; } -.spe-app-content-sidebar { - flex: 1; - min-width: 400px; - width: 450px; - overflow: auto; - position: relative; - background-color: white; -} - -#sharepoint-embedded-chat { - background-color: #FFFFFF; - border-radius: 0px 0px 3px 3px; - width: 100%; - height: 92vh; - border: 0px; -} - -.sidebar-resizer { - min-width: 10px; - max-width: 10px; - width: 10px; - height: 100%; - cursor: col-resize; - background-color: black; - opacity: 0; - - z-index: 2; - position: absolute; - top: 0; - left: 0; -} - -.sidebar-resizer:hover { - opacity: 0.3; -} - -.sidebar-content { - height: 100%; - min-width: 100%; - max-width: 100%; - width: 100%; - overflow: auto; - - - position: absolute; - top: 0; - left: 0; -} - - .container-browser { display: flex; flex-direction: column; diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.tsx b/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.tsx index 281d576..889e54c 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.tsx +++ b/Custom Apps/boilerplate-typescript-react/react-client/src/routes/App.tsx @@ -24,13 +24,11 @@ import { ToolbarButton, webDarkTheme, webLightTheme, - Spinner } from "@fluentui/react-components"; import { Map20Regular, People20Regular, MoreVertical24Filled, - Chat32Regular, SignOut24Filled, } from '@fluentui/react-icons'; import './App.css'; @@ -38,7 +36,6 @@ import * as Constants from '../common/Constants'; import { ContainerSelector } from '../components/ContainerSelector'; import { IContainer } from '../../../common/schemas/ContainerSchemas'; import { CreateContainerButton } from '../components/CreateContainerButton'; -import { ChatSidebar } from '../components/ChatSidebar'; import { Outlet, useOutletContext } from "react-router-dom"; type ContextType = { @@ -76,14 +73,6 @@ function App() { const mainContentRef = React.useRef(null); const loginRef = React.useRef(null); - const [showSidebar, setShowSidebar] = useState(false); - const sidebarRef = React.useRef(null); - const sidebarResizerRef = React.useRef(null); - - const toggleSidebar = () => { - setShowSidebar(!showSidebar); - } - const signOut = () => { Providers.globalProvider.logout!(); } @@ -94,29 +83,6 @@ function App() { setSearchQuery(`${termQuery} AND ${baseSearchQuery}`); }, [baseSearchQuery]); - const onResizerMouseDown = (e: React.MouseEvent) => { - if (!sidebarRef.current) { - return; - } - const minSidebarWidth = 200; - const maxSidebarWidth = 600; - let prevX = e.clientX; - let sidebarBounds = sidebarRef.current!.getBoundingClientRect(); - const onMouseMove = (e: MouseEvent) => { - const newX = prevX - e.x; - const newWidth = Math.max(minSidebarWidth, Math.min(maxSidebarWidth, sidebarBounds.width + newX)); - sidebarRef.current!.style.minWidth = `${newWidth}px`; - } - - const onMouseUp = (e: MouseEvent) => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - } - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - } - return (
@@ -149,7 +115,6 @@ function App() {
- toggleSidebar()} icon={} /> @@ -191,30 +156,6 @@ function App() {
-
-
-
-
- {selectedContainer && ( - - )} - {!selectedContainer && (<> - - Select a container to view chat - - } /> - )} -
-
-
diff --git a/Custom Apps/boilerplate-typescript-react/react-client/src/routes/Containers.tsx b/Custom Apps/boilerplate-typescript-react/react-client/src/routes/Containers.tsx index 042d5b9..15f0293 100644 --- a/Custom Apps/boilerplate-typescript-react/react-client/src/routes/Containers.tsx +++ b/Custom Apps/boilerplate-typescript-react/react-client/src/routes/Containers.tsx @@ -4,7 +4,6 @@ import { Breadcrumb, BreadcrumbButton, BreadcrumbItem, Button, DataGrid, DataGri import { Delete20Filled } from "@fluentui/react-icons"; import { useState } from "react"; import { Spinner } from "@microsoft/mgt-react"; -import { ChatController } from "../providers/ChatController"; import { ContainersApiProvider } from "../providers/ContainersApiProvider"; import { IContainer } from "../../../common/schemas/ContainerSchemas"; @@ -27,9 +26,6 @@ export const Containers: React.FunctionComponent = () => { const onSelectionChange = (e: any, data: OnSelectionChangeData) => { const selectedIds = Array.from(data.selectedItems) as string[]; setSelectedItems(selectedIds); - - const selectedContainers = containers.filter((container) => selectedIds.includes(container.id)); - ChatController.instance.selectedContainers = selectedContainers; } const deleteSelectedContainers = async () => { diff --git a/Custom Apps/legal-docs/README.md b/Custom Apps/legal-docs/README.md index 5203196..7c4a7bc 100644 --- a/Custom Apps/legal-docs/README.md +++ b/Custom Apps/legal-docs/README.md @@ -5,7 +5,6 @@ This sample is a React + TypeScript web app that demonstrates a legal-workflow e - Azure AD authentication with MSAL - Legal case containers and folder navigation - Document-centric workspace views -- Copilot-style chat integration using the SharePoint Embedded Copilot Chat React SDK ## Project Overview @@ -18,7 +17,7 @@ The app provides a Contoso legal dashboard where users can: - Create new case containers - Browse case folders and documents -- Use assistant panels for summaries, tools, reports, and Copilot interactions +- Use assistant panels for summaries, tools, and reports ![alt text](./images/AIOverview.png) @@ -57,7 +56,6 @@ npm install - `clientId`: Azure AD application (client) ID - `tenantId`: Microsoft Entra tenant ID - `containerTypeId`: SharePoint Embedded container type ID -- `sharePointHostname`: SharePoint hostname for your tenant (for example: `https://contoso.sharepoint.com`) 3. Verify API permissions and scopes in your app registration: @@ -65,8 +63,6 @@ npm install - `Files.Read.All` - `Sites.Read.All` - `FileStorageContainer.Selected` -- SharePoint scope format used by the app: - - `{sharePointHostname}/Container.Selected` diff --git a/Custom Apps/legal-docs/lib/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz b/Custom Apps/legal-docs/lib/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz deleted file mode 100644 index 40da931..0000000 Binary files a/Custom Apps/legal-docs/lib/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz and /dev/null differ diff --git a/Custom Apps/legal-docs/package-lock.json b/Custom Apps/legal-docs/package-lock.json index 2787177..ebd4c01 100644 --- a/Custom Apps/legal-docs/package-lock.json +++ b/Custom Apps/legal-docs/package-lock.json @@ -12,7 +12,6 @@ "@azure/msal-react": "^3.0.23", "@fluentui/react": "^8.125.3", "@hookform/resolvers": "^3.10.0", - "@microsoft/sharepointembedded-copilotchat-react": "file:lib/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz", "@playwright/test": "^1.57.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -757,17 +756,6 @@ "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==", "license": "MIT" }, - "node_modules/@microsoft/sharepointembedded-copilotchat-react": { - "version": "1.0.9", - "resolved": "file:lib/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz", - "integrity": "sha512-mwtB7OOvyox5Mzhpozz/45eq3kYV+DaVNTDxwXC4n5+HnAQabFia1GIWpTVXFAi3Ajp5LNORmWYHIoJBEY23rg==", - "license": "MIT", - "peerDependencies": { - "@fluentui/react": ">=8.1.0", - "react": ">=17.0.2", - "react-dom": ">=17.0.2" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", diff --git a/Custom Apps/legal-docs/package.json b/Custom Apps/legal-docs/package.json index a9dac1d..d7e138a 100644 --- a/Custom Apps/legal-docs/package.json +++ b/Custom Apps/legal-docs/package.json @@ -15,7 +15,6 @@ "@azure/msal-react": "^3.0.23", "@fluentui/react": "^8.125.3", "@hookform/resolvers": "^3.10.0", - "@microsoft/sharepointembedded-copilotchat-react": "file:lib/microsoft-sharepointembedded-copilotchat-react-1.0.9.tgz", "@playwright/test": "^1.57.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/Custom Apps/legal-docs/src/components/CopilotChat.tsx b/Custom Apps/legal-docs/src/components/CopilotChat.tsx deleted file mode 100644 index 66d398c..0000000 --- a/Custom Apps/legal-docs/src/components/CopilotChat.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { useState, useEffect, useRef, useCallback, useMemo } from "react"; -import { X, Loader2, AlertCircle } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useAuth } from "@/context/AuthContext"; -import { useCopilotSite } from "@/hooks/useCopilotSite"; -import { CopilotAuthProvider, IChatEmbeddedApiAuthProvider } from "@/components/copilot/CopilotAuthProvider"; -import { APP_CONFIG, COPILOT_CONFIG } from "@/config/appConfig"; -import { Button } from "@/components/ui/button"; - -// Import SDK types -import type { - ChatEmbeddedAPI, - ChatLaunchConfig -} from "@microsoft/sharepointembedded-copilotchat-react"; -import { ChatEmbedded } from "@microsoft/sharepointembedded-copilotchat-react"; - -interface CopilotChatProps { - containerId: string; - isOpen: boolean; - onClose: () => void; -} - -type ChatStatus = 'initializing' | 'ready' | 'error' | 'timeout'; - -/** - * CopilotChat - Main chat modal component - * - * Features: - * - Creates CopilotAuthProvider instance with SharePoint hostname - * - Configures ChatLaunchConfig with header, instruction, locale, suggestedPrompts - * - Renders ChatEmbedded component from Microsoft SDK - * - Handles loading states, errors, and timeout detection - * - Modern slide-in panel UI with close button - */ -export default function CopilotChat({ containerId, isOpen, onClose }: CopilotChatProps) { - const { getAccessToken, isAuthenticated } = useAuth(); - const { containerName, isLoading: siteLoading, error: siteError } = useCopilotSite(containerId); - - const [status, setStatus] = useState('initializing'); - const [errorMessage, setErrorMessage] = useState(null); - const [mountKey, setMountKey] = useState(0); - - const chatApiRef = useRef(null); - const timeoutRef = useRef(null); - const containerRef = useRef(null); - - // Create auth provider instance - const authProvider = useMemo(() => - new CopilotAuthProvider(getAccessToken), - [getAccessToken] - ); - - // Chat launch configuration - must include zeroQueryPrompts for SDK to render UI - const chatConfig = useMemo(() => ({ - header: containerName || COPILOT_CONFIG.header, - instruction: COPILOT_CONFIG.instruction, - locale: COPILOT_CONFIG.locale, - zeroQueryPrompts: { - headerText: "How can I help you with this case?", - promptSuggestionList: COPILOT_CONFIG.suggestedPrompts.map(text => ({ - suggestionText: text - })), - }, - chatInputPlaceholder: COPILOT_CONFIG.chatInputPlaceholder, - }), [containerName]); - - // Reset state when container changes - useEffect(() => { - setStatus('initializing'); - setErrorMessage(null); - setMountKey(prev => prev + 1); - }, [containerId]); - - // Initialize chat when API is ready - const initializeCopilotChat = useCallback(async (api: ChatEmbeddedAPI) => { - console.log("🚀 Copilot API ready, initializing chat..."); - chatApiRef.current = api; - - // Clear any existing timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - try { - // Small delay to ensure DOM is ready - await new Promise(resolve => setTimeout(resolve, 500)); - - // Open the chat with configuration - await api.openChat(chatConfig); - - console.log("✅ Copilot chat opened successfully"); - setStatus('ready'); - } catch (error) { - console.error("❌ Failed to open Copilot chat:", error); - setStatus('error'); - setErrorMessage(error instanceof Error ? error.message : 'Failed to initialize chat'); - } - }, [chatConfig]); - - // Handle SDK API ready event - const handleApiReady = useCallback((api: ChatEmbeddedAPI) => { - console.log("📡 ChatEmbedded onApiReady fired"); - initializeCopilotChat(api); - }, [initializeCopilotChat]); - - // Set up timeout detection - useEffect(() => { - if (!isOpen || status !== 'initializing') return; - - timeoutRef.current = setTimeout(() => { - if (status === 'initializing') { - console.warn("⏰ Copilot initialization timeout"); - setStatus('timeout'); - setErrorMessage('Chat initialization timed out. This may be due to CSP restrictions.'); - } - }, 15000); // 15 second timeout - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, [isOpen, status]); - - // Retry handler - const handleRetry = useCallback(() => { - setStatus('initializing'); - setErrorMessage(null); - setMountKey(prev => prev + 1); - }, []); - - // Don't render if not open - if (!isOpen) return null; - - const showLoading = status === 'initializing' || siteLoading; - const showError = status === 'error' || status === 'timeout' || !!siteError; - - return ( -
- {/* Header */} -
-
-

AI Assistant

- {containerName && ( -

{containerName}

- )} -
- -
- - {/* Content */} -
- {/* Loading State */} - {showLoading && !showError && ( -
- -

- {siteLoading ? 'Loading container...' : 'Starting Copilot...'} -

-
- )} - - {/* Error State */} - {showError && ( -
- -
-

Unable to Load Chat

-

- {siteError || errorMessage || 'An error occurred while loading the chat.'} -

- {status === 'timeout' && ( -

- Ensure your published URL is whitelisted in SharePoint admin. -

- )} -
- -
- )} - - {/* SDK ChatEmbedded Component */} - {isAuthenticated && containerId && !siteError && ( -
- -
- )} -
-
- ); -} diff --git a/Custom Apps/legal-docs/src/components/CustomCopilotChat.tsx b/Custom Apps/legal-docs/src/components/CustomCopilotChat.tsx deleted file mode 100644 index ba7c3ab..0000000 --- a/Custom Apps/legal-docs/src/components/CustomCopilotChat.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { useState, useCallback, useMemo, useRef, useEffect } from "react"; -import { MessageCircle, X, Send, Loader2, Database, CheckCircle2 } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useAuth } from "@/context/AuthContext"; -import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Input } from "@/components/ui/input"; -import { - sendCopilotMessage, - createChatAuthProvider, - CopilotMessage, - DEFAULT_CHAT_CONFIG, - ChatLaunchConfig -} from "@/services/copilotChat"; - -interface CustomCopilotChatProps { - containerId: string; - containerName: string; - config?: ChatLaunchConfig; -} - -/** - * Custom Copilot Chat Component - * - * This component implements the SharePoint Embedded Copilot chat pattern - * following the SDK documentation at: - * https://learn.microsoft.com/en-us/sharepoint/dev/embedded/development/tutorials/spe-da-vscode - * - * It uses the SharePoint container as the document source, with authentication - * via Container.Selected scope as per the official SDK requirements. - */ -export default function CustomCopilotChat({ - containerId, - containerName, - config = DEFAULT_CHAT_CONFIG -}: CustomCopilotChatProps) { - const { getAccessToken } = useAuth(); - const [isOpen, setIsOpen] = useState(false); - const [messages, setMessages] = useState([]); - const [inputValue, setInputValue] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const scrollRef = useRef(null); - const prevContainerId = useRef(null); - - // Create auth provider following SDK pattern - uses Container.Selected scope - const authProvider = useMemo(() => - createChatAuthProvider(getAccessToken), - [getAccessToken] - ); - - // Merged config - const chatConfig = useMemo(() => ({ - ...DEFAULT_CHAT_CONFIG, - ...config, - header: config?.header || containerName, - }), [config, containerName]); - - // Reset messages and test connection when container changes - useEffect(() => { - if (prevContainerId.current !== containerId) { - prevContainerId.current = containerId; - setMessages([]); - setInputValue(""); - setIsConnected(false); - - // Test container connection on change - const testConnection = async () => { - try { - await authProvider.getToken(); - setIsConnected(true); - console.log("Connected to SharePoint container:", containerId); - } catch (error) { - console.error("Failed to connect to container:", error); - setIsConnected(false); - } - }; - - if (containerId) { - testConnection(); - } - } - }, [containerId, authProvider]); - - // Scroll to bottom when new messages arrive - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [messages]); - - // Handle sending a message - const handleSendMessage = useCallback(async (text: string) => { - if (!text.trim() || isLoading) return; - - const userMessage: CopilotMessage = { - role: "user", - content: text.trim(), - timestamp: new Date(), - }; - - setMessages(prev => [...prev, userMessage]); - setInputValue(""); - setIsLoading(true); - - try { - const response = await sendCopilotMessage( - authProvider, - containerId, - containerName, - text.trim(), - messages, - chatConfig - ); - - const assistantMessage: CopilotMessage = { - role: "assistant", - content: response, - timestamp: new Date(), - }; - - setMessages(prev => [...prev, assistantMessage]); - } catch (error) { - console.error("Chat error:", error); - const errorMessage: CopilotMessage = { - role: "assistant", - content: "I'm sorry, I encountered an error processing your request. Please try again.", - timestamp: new Date(), - }; - setMessages(prev => [...prev, errorMessage]); - } finally { - setIsLoading(false); - } - }, [authProvider, containerId, containerName, messages, chatConfig, isLoading]); - - // Handle form submit - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleSendMessage(inputValue); - }; - - // Handle suggestion click - const handleSuggestionClick = (suggestion: string) => { - handleSendMessage(suggestion); - }; - - // Toggle chat panel - const handleToggle = useCallback(() => { - setIsOpen(prev => !prev); - }, []); - - // Don't render if no container is selected - if (!containerId) return null; - - const zeroQueryPrompts = chatConfig.zeroQueryPrompts || DEFAULT_CHAT_CONFIG.zeroQueryPrompts; - - return ( - <> - {/* Chat Bubble Button */} - - - {/* Chat Flyout Panel */} -
- {/* Header */} -
-
-

{chatConfig.header}

-
- -

{containerName}

- {isConnected && ( - - - Connected - - )} -
-
-
- - {/* Messages Area */} - - {messages.length === 0 ? ( -
-

- {zeroQueryPrompts?.headerText} -

-
- {zeroQueryPrompts?.promptSuggestionList?.map((prompt, index) => ( - - ))} -
-
- ) : ( -
- {messages.map((message, index) => ( -
-
-

{message.content}

-
-
- ))} - {isLoading && ( -
-
- -
-
- )} -
- )} -
- - {/* Input Area */} -
-
- setInputValue(e.target.value)} - placeholder={chatConfig.chatInputPlaceholder || "Ask about this case..."} - disabled={isLoading} - className="flex-1" - /> - -
-
-
- - ); -} diff --git a/Custom Apps/legal-docs/src/components/FloatingCopilotIcon.tsx b/Custom Apps/legal-docs/src/components/FloatingCopilotIcon.tsx deleted file mode 100644 index c7e62fd..0000000 --- a/Custom Apps/legal-docs/src/components/FloatingCopilotIcon.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState, useCallback } from "react"; -import { MessageCircle } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useAuth } from "@/context/AuthContext"; -import CopilotChat from "@/components/CopilotChat"; -import { useToast } from "@/hooks/use-toast"; - -interface FloatingCopilotIconProps { - containerId: string | null; - containerName?: string; -} - -/** - * FloatingCopilotIcon - Floating action button for Copilot chat - * - * Features: - * - Fixed position at bottom-right corner - * - Checks authentication before opening chat - * - Validates a container is selected - * - Opens CopilotChat modal on click - */ -export default function FloatingCopilotIcon({ containerId, containerName }: FloatingCopilotIconProps) { - const { isAuthenticated } = useAuth(); - const { toast } = useToast(); - const [isOpen, setIsOpen] = useState(false); - - const handleClick = useCallback(() => { - if (!isAuthenticated) { - toast({ - title: "Authentication Required", - description: "Please sign in to use the AI Assistant.", - variant: "destructive", - }); - return; - } - - if (!containerId) { - toast({ - title: "No Case Selected", - description: "Please select a case to use the AI Assistant.", - variant: "destructive", - }); - return; - } - - setIsOpen(true); - }, [isAuthenticated, containerId, toast]); - - const handleClose = useCallback(() => { - setIsOpen(false); - }, []); - - // Only show if authenticated - if (!isAuthenticated) return null; - - return ( - <> - {/* Floating Button */} - - - {/* Backdrop */} - {isOpen && ( -
- )} - - {/* Chat Panel */} - {containerId && ( - - )} - - ); -} diff --git a/Custom Apps/legal-docs/src/components/FlyoutButtons.tsx b/Custom Apps/legal-docs/src/components/FlyoutButtons.tsx index de25232..ee46ece 100644 --- a/Custom Apps/legal-docs/src/components/FlyoutButtons.tsx +++ b/Custom Apps/legal-docs/src/components/FlyoutButtons.tsx @@ -1,22 +1,16 @@ import { cn } from "@/lib/utils"; import { ChevronLeft } from "lucide-react"; -export type PanelType = "caseSummary" | "tools" | "reports" | "copilot"; +export type PanelType = "caseSummary" | "tools" | "reports"; interface FlyoutButtonsProps { activePanel: PanelType | null; onPanelToggle: (panel: PanelType) => void; - showCopilot?: boolean; } -export default function FlyoutButtons({ activePanel, onPanelToggle, showCopilot }: FlyoutButtonsProps) { +export default function FlyoutButtons({ activePanel, onPanelToggle }: FlyoutButtonsProps) { const buttons: { id: PanelType; label: string; bgClass: string }[] = [ { id: "caseSummary", label: "Case Summary", bgClass: "bg-primary hover:bg-primary/90" }, - ...(showCopilot ? [{ - id: "copilot" as PanelType, - label: "AI Assistant", - bgClass: "bg-primary/80 hover:bg-primary/70" - }] : []), { id: "tools", label: "Tools", bgClass: "bg-primary/60 hover:bg-primary/50" }, { id: "reports", label: "Reports", bgClass: "bg-primary/40 hover:bg-primary/30" }, ]; diff --git a/Custom Apps/legal-docs/src/components/copilot/CopilotAuthProvider.ts b/Custom Apps/legal-docs/src/components/copilot/CopilotAuthProvider.ts deleted file mode 100644 index 09f7f65..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/CopilotAuthProvider.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { APP_CONFIG, SCOPES } from "@/config/appConfig"; - -/** - * Authentication provider interface for the SharePoint Embedded Copilot SDK. - * Matches the SDK's IChatEmbeddedApiAuthProvider interface. - */ -export interface IChatEmbeddedApiAuthProvider { - hostname: string; - getToken(): Promise; -} - -/** - * CopilotAuthProvider implements IChatEmbeddedApiAuthProvider for the SDK. - * - * The SDK requires: - * - hostname: SharePoint site URL (e.g., https://tenant.sharepoint.com) - * - getToken(): Returns access token with Container.Selected scope - */ -export class CopilotAuthProvider implements IChatEmbeddedApiAuthProvider { - public readonly hostname: string; - private getAccessToken: (scopes: string[]) => Promise; - private initialized: boolean = false; - - constructor(getAccessToken: (scopes: string[]) => Promise) { - this.hostname = APP_CONFIG.sharePointHostname; - this.getAccessToken = getAccessToken; - } - - /** - * Initialize the auth provider by testing token acquisition. - * Call this before using the ChatEmbedded component. - */ - async initialize(): Promise { - const token = await this.getToken(); - if (!token) { - throw new Error("Failed to initialize auth provider - could not acquire token"); - } - this.initialized = true; - console.log("CopilotAuthProvider: Initialized successfully"); - } - - /** - * Get access token with Container.Selected scope. - * Required by the SDK: {hostname}/Container.Selected - */ - async getToken(): Promise { - const token = await this.getAccessToken(SCOPES.sharePoint); - if (!token) { - throw new Error("Failed to acquire SharePoint Container.Selected token"); - } - console.log("CopilotAuthProvider: Acquired Container.Selected token"); - return token; - } - - get isInitialized(): boolean { - return this.initialized; - } -} diff --git a/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx b/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx deleted file mode 100644 index 32e0172..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/CopilotChatContainer.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { useCopilotSite } from '@/hooks/useCopilotSite'; -import CopilotDesktopView from './CopilotDesktopView'; -import { toast } from '@/hooks/use-toast'; -import { appConfig } from '@/config/appConfig'; -import { useAuth } from '@/context/AuthContext'; -import { - IChatEmbeddedApiAuthProvider, - ChatEmbeddedAPI, - ChatLaunchConfig -} from '@microsoft/sharepointembedded-copilotchat-react'; - -type ChatAuthProviderWithSiteUrl = IChatEmbeddedApiAuthProvider & { - siteUrl?: string; -}; - -interface CopilotChatContainerProps { - containerId: string; - containerName?: string; -} - -const CopilotChatContainer: React.FC = ({ - containerId, - containerName: propContainerName -}) => { - const [isOpen, setIsOpen] = useState(true); - const { getSharePointToken, isAuthenticated } = useAuth(); - const [chatApi, setChatApi] = useState(null); - const [chatKey, setChatKey] = useState(0); - - // Validate and normalize containerId - const normalizedContainerId = useMemo(() => { - if (!containerId || typeof containerId !== 'string') return ''; - return containerId.startsWith('b!') ? containerId : `b!${containerId}`; - }, [containerId]); - - const { - isLoading, - error, - webUrl: siteUrl, - containerName: hookSiteName, - sharePointHostname, - } = useCopilotSite(normalizedContainerId); - - // Use prop name or hook name - const siteName = propContainerName || hookSiteName || 'SharePoint Site'; - - // Ensure we have valid hostnames with proper normalization - const rawHostname = sharePointHostname || appConfig.sharePointHostname; - const safeSharePointHostname = appConfig.normalizeSharePointUrl(rawHostname); - - console.log('🏠 SharePoint hostname details:', { - original: rawHostname, - normalized: safeSharePointHostname, - fromConfig: appConfig.sharePointHostname, - fromHook: sharePointHostname - }); - - const handleError = useCallback((errorMessage: string) => { - console.error('Copilot chat error:', errorMessage); - - setTimeout(() => { - const chatContainer = document.querySelector('[data-testid="copilot-chat-wrapper"]'); - const hasIframe = chatContainer?.querySelector('iframe'); - - if (!hasIframe) { - toast({ - title: "Copilot error", - description: `${errorMessage} The system will attempt to recover automatically.`, - variant: "destructive", - }); - } else { - console.log('🔄 Copilot chat recovered automatically, skipping error notification'); - } - }, 2000); - }, []); - - // Create auth provider for Copilot chat - const authProvider = useMemo((): IChatEmbeddedApiAuthProvider => { - const containerWebUrl = siteUrl || safeSharePointHostname; - - console.log('🔧 Creating auth provider with URLs:', { - hostname: safeSharePointHostname, - siteUrl: containerWebUrl, - }); - - const provider: ChatAuthProviderWithSiteUrl = { - hostname: safeSharePointHostname, - getToken: async () => { - try { - if (!isAuthenticated) { - console.error('User not authenticated, cannot get token'); - return ''; - } - - const token = await getSharePointToken(); - console.log('🔑 SharePoint auth token retrieved:', token ? 'successfully' : 'failed'); - - if (!token) { - handleError('Failed to get authentication token for SharePoint.'); - return ''; - } - - return token; - } catch (err) { - console.error('❌ Error getting token for Copilot chat:', err); - handleError('Failed to authenticate with SharePoint. Please try again.'); - return ''; - } - } - }; - - // The SDK requires siteUrl on the auth provider for proper site context - provider.siteUrl = containerWebUrl; - - return provider; - }, [safeSharePointHostname, siteUrl, getSharePointToken, handleError, isAuthenticated]); - - // Chat configuration - const chatConfig = useMemo((): ChatLaunchConfig => ({ - header: `Copilot Chat - ${siteName}`, - instruction: "You are a helpful AI assistant. Help users find information, answer questions, and work with their SharePoint files and documents.", - locale: "en", - suggestedPrompts: ["What are my files?", "Help me find documents", "Show me recent changes"] - }), [siteName]); - - const handleResetChat = useCallback(() => { - console.log('🔄 Resetting Copilot chat container'); - setChatKey(prev => prev + 1); - setChatApi(null); - setIsOpen(false); - setTimeout(() => { - setIsOpen(true); - }, 500); - }, []); - - const handleApiReady = useCallback((api: ChatEmbeddedAPI) => { - if (!api) { - console.error('❌ Chat API is undefined'); - handleError('Chat API initialization failed'); - return; - } - - console.log('✅ Copilot chat API is ready'); - setChatApi(api); - }, [handleError]); - - // Early return after all hooks are called - if (!normalizedContainerId) { - console.error('CopilotChatContainer: Invalid containerId provided:', containerId); - return null; - } - - return ( - - ); -}; - -export default CopilotChatContainer; diff --git a/Custom Apps/legal-docs/src/components/copilot/CopilotDesktopView.tsx b/Custom Apps/legal-docs/src/components/copilot/CopilotDesktopView.tsx deleted file mode 100644 index 3954d94..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/CopilotDesktopView.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { Button } from '@/components/ui/button'; -import { RefreshCw, AlertTriangle, ExternalLink } from 'lucide-react'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { ChatEmbedded, ChatEmbeddedAPI, IChatEmbeddedApiAuthProvider, ChatLaunchConfig } from '@microsoft/sharepointembedded-copilotchat-react'; - -interface CopilotDesktopViewProps { - isOpen: boolean; - setIsOpen: (value: boolean) => void; - siteName: string; - siteUrl: string | null; - isLoading: boolean; - error: string | null; - containerId: string; - onError: (errorMessage: string) => void; - chatConfig: ChatLaunchConfig; - authProvider: IChatEmbeddedApiAuthProvider; - onApiReady: (api: ChatEmbeddedAPI) => void; - chatKey: number; - onResetChat?: () => void; - isAuthenticated?: boolean; - chatApi: ChatEmbeddedAPI | null; -} - -const CopilotDesktopView: React.FC = ({ - isOpen, - setIsOpen, - siteName, - siteUrl, - isLoading, - error, - containerId, - onError, - chatConfig, - authProvider, - onApiReady, - chatKey, - onResetChat, - isAuthenticated = true, - chatApi -}) => { - const chatInitializedRef = useRef(false); - const containerRef = useRef(null); - const [componentReady, setComponentReady] = useState(false); - const [cspError, setCspError] = useState(false); - const [debugInfo, setDebugInfo] = useState<{hostname: string, origin: string} | null>(null); - - // Enhanced CSP error detection to catch all variations - const isCSPError = useCallback((errorMessage: string) => { - return errorMessage.includes('Content Security Policy') || - errorMessage.includes('frame-ancestors') || - errorMessage.includes('Refused to frame') || - errorMessage.includes('Refused to display') || - errorMessage.includes('because an ancestor violates'); - }, []); - - // Debug initial component state - useEffect(() => { - console.log('🟢 CopilotDesktopView mounted with props:', { - isOpen, - containerId, - siteName, - isLoading, - error, - isAuthenticated, - authHostname: authProvider.hostname - }); - }, [isOpen, containerId, siteName, isLoading, error, isAuthenticated, authProvider.hostname]); - - // Capture debug information for CSP troubleshooting - useEffect(() => { - setDebugInfo({ - hostname: authProvider.hostname, - origin: window.location.origin - }); - }, [authProvider.hostname]); - - // Handle CSP errors specifically with enhanced detection - useEffect(() => { - const handleCSPError = (event: ErrorEvent) => { - const errorMessage = event.message || event.error?.message || ''; - console.error('🚨 CSP Error detected:', errorMessage); - - if (isCSPError(errorMessage)) { - setCspError(true); - onError(`SharePoint Content Security Policy Error: ${errorMessage}`); - event.preventDefault(); - } - }; - - const handleUnhandledRejection = (event: PromiseRejectionEvent) => { - const reason = event.reason?.message || event.reason || ''; - console.error('🚨 CSP Promise rejection:', reason); - - if (typeof reason === 'string' && isCSPError(reason)) { - setCspError(true); - onError(`SharePoint CSP Rejection: ${reason}`); - event.preventDefault(); - } - }; - - // Also listen for console errors that might indicate CSP issues - const originalConsoleError = console.error; - console.error = (...args) => { - const message = args.join(' '); - if (isCSPError(message)) { - setCspError(true); - onError(`Console CSP Error: ${message}`); - } - originalConsoleError.apply(console, args); - }; - - window.addEventListener('error', handleCSPError); - window.addEventListener('unhandledrejection', handleUnhandledRejection); - - return () => { - window.removeEventListener('error', handleCSPError); - window.removeEventListener('unhandledrejection', handleUnhandledRejection); - console.error = originalConsoleError; - }; - }, [onError, isCSPError]); - - // Debug container contents periodically - const debugContainerContents = useCallback(() => { - if (containerRef.current) { - const container = containerRef.current; - console.log('🔍 Container debugging:', { - hasChildren: container.children.length > 0, - childCount: container.children.length, - innerHTML: container.innerHTML.substring(0, 500) + '...', - iframes: container.querySelectorAll('iframe').length, - inputs: container.querySelectorAll('input').length, - textareas: container.querySelectorAll('textarea').length, - buttons: container.querySelectorAll('button').length, - divs: container.querySelectorAll('div').length - }); - - // Look for any chat-related elements - const chatElements = container.querySelectorAll('[class*="chat"], [id*="chat"], [data-*="chat"]'); - console.log('🔍 Chat-related elements found:', chatElements.length); - - // Check for any hidden elements - const hiddenElements = container.querySelectorAll('[style*="display: none"], [style*="visibility: hidden"], [hidden]'); - console.log('🔍 Hidden elements found:', hiddenElements.length); - - // Force show any hidden input elements as a last resort - const allInputs = container.querySelectorAll('input, textarea'); - allInputs.forEach((input, index) => { - const styles = window.getComputedStyle(input); - console.log(`🔍 Input ${index}:`, { - display: styles.display, - visibility: styles.visibility, - opacity: styles.opacity, - zIndex: styles.zIndex, - position: styles.position - }); - }); - } - }, []); - - // Handle API ready with better initialization - const handleApiReady = useCallback((api: ChatEmbeddedAPI) => { - console.log('🚀 Copilot API ready, initializing chat...'); - onApiReady(api); - setComponentReady(true); - setCspError(false); // Reset CSP error if API is ready - }, [onApiReady]); - - // Open chat following Microsoft documentation pattern - const initializeCopilotChat = useCallback(async (api: ChatEmbeddedAPI) => { - try { - console.log('📋 Opening copilot chat with config:', { - header: chatConfig.header, - locale: chatConfig.locale, - containerId: containerId, - hasInstruction: !!chatConfig.instruction, - authHostname: authProvider.hostname, - currentOrigin: window.location.origin - }); - - // Wait for component to be fully mounted (following MS docs pattern) - await new Promise(resolve => setTimeout(resolve, 100)); - - // Use the exact pattern from Microsoft documentation - await api.openChat(chatConfig); - console.log('✅ Copilot chat opened successfully'); - - // Debug container contents immediately after opening - setTimeout(() => { - debugContainerContents(); - }, 1000); - - // Continue debugging every few seconds to track changes - const debugInterval = setInterval(() => { - debugContainerContents(); - }, 3000); - - // Stop debugging after 15 seconds - setTimeout(() => { - clearInterval(debugInterval); - }, 15000); - - // Monitor for CSP errors and UI issues after chat opens - setTimeout(() => { - if (containerRef.current) { - const iframes = containerRef.current.querySelectorAll('iframe'); - console.log('📊 Container analysis after chat open:', { - childElementCount: containerRef.current.childElementCount, - iframeCount: iframes.length, - authHostname: authProvider.hostname, - origin: window.location.origin - }); - - // Check if iframe failed to load due to CSP - if (iframes.length > 0) { - iframes.forEach((iframe, index) => { - iframe.addEventListener('error', () => { - console.error(`❌ Iframe ${index} failed to load - likely CSP issue`); - setCspError(true); - }); - - // Also monitor iframe content loading - iframe.addEventListener('load', () => { - console.log(`✅ Iframe ${index} loaded successfully`); - // Debug iframe contents after a delay - setTimeout(() => { - debugContainerContents(); - }, 2000); - }); - }); - } - } - }, 2000); - - } catch (err) { - console.error('❌ Error opening copilot chat:', err); - - // Check if it's a CSP-related error - const errorMessage = err instanceof Error ? err.message : String(err); - if (isCSPError(errorMessage)) { - setCspError(true); - onError(`SharePoint CSP Error: ${errorMessage}`); - } else { - // For "Failed to fetch site URL" errors, this is often transient - the reset mechanism will retry - console.log('🔄 Chat initialization failed, reset mechanism will attempt recovery...'); - onError('Failed to initialize chat'); - } - setComponentReady(false); - } - }, [chatConfig, containerId, authProvider.hostname, debugContainerContents, isCSPError, onError]); - - // Effect to handle chat initialization and reset - useEffect(() => { - console.log('🔍 Copilot initialization state:', { - isOpen, - hasChatApi: !!chatApi, - componentReady, - chatInitialized: chatInitializedRef.current, - cspError - }); - - // Initialize chat when dialog opens and API is ready - if (isOpen && chatApi && componentReady && !chatInitializedRef.current && !cspError) { - console.log('🎯 Initializing copilot chat...'); - chatInitializedRef.current = true; - initializeCopilotChat(chatApi); - } - - if (!isOpen) { - chatInitializedRef.current = false; - setComponentReady(false); - setCspError(false); - } - }, [isOpen, chatApi, componentReady, cspError, initializeCopilotChat]); - - // Reset chat when requested - const handleResetChat = useCallback(() => { - if (onResetChat) { - console.log('🔄 Resetting copilot chat component'); - chatInitializedRef.current = false; - setComponentReady(false); - setCspError(false); - onResetChat(); - } - }, [onResetChat]); - - // Early return if not authenticated - AFTER all hooks - if (!isAuthenticated) { - console.log('CopilotDesktopView: Not rendering because not authenticated'); - return null; - } - - return ( -
- {isOpen && ( - <> -
-
- SharePoint Embedded Copilot - Connected to: {siteName || 'SharePoint Site'} -
- {onResetChat && isAuthenticated && ( - - )} -
- -
- {cspError ? ( -
- - - - SharePoint Content Security Policy Restriction -

- Refused to frame '{debugInfo?.hostname}' because an ancestor violates CSP directive "frame-ancestors" -

-
- What this means: -
    -
  • SharePoint is configured to only allow framing from specific domains
  • -
  • Your app (origin: {debugInfo?.origin}) is not in the allowed list
  • -
  • This is a SharePoint server-side security feature that cannot be bypassed
  • -
-
-
- Required SharePoint Admin Actions: -
    -
  • Update CSP headers to include: {debugInfo?.origin}
  • -
  • Or configure wildcard domain patterns if applicable
  • -
  • Or use SharePoint's native Copilot interface instead
  • -
-
-
-

- Alternative: Access Copilot directly in SharePoint at: -
- - {debugInfo?.hostname} - -

-
-
-
- {onResetChat && ( - - )} -
- ) : isLoading ? ( -
-
- Loading... -
- ) : error ? ( -
- - - {error || "Unable to load the chat. Please try again."} - - - {onResetChat && ( - - )} -
- ) : ( -
- -
- )} -
- - )} -
- ); -}; - -export default CopilotDesktopView; diff --git a/Custom Apps/legal-docs/src/components/copilot/CopilotErrorBoundary.tsx b/Custom Apps/legal-docs/src/components/copilot/CopilotErrorBoundary.tsx deleted file mode 100644 index f2f1db0..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/CopilotErrorBoundary.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component, ErrorInfo, ReactNode } from "react"; -import { AlertTriangle, RefreshCw, ExternalLink } from "lucide-react"; -import { Button } from "@/components/ui/button"; - -interface Props { - children: ReactNode; - onRetry?: () => void; - onClose?: () => void; -} - -interface State { - hasError: boolean; - error: Error | null; - errorInfo: ErrorInfo | null; -} - -/** - * Error boundary specifically for the Copilot SDK. - * Catches internal SDK errors and provides a graceful fallback UI. - */ -export class CopilotErrorBoundary extends Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false, error: null, errorInfo: null }; - } - - static getDerivedStateFromError(error: Error): Partial { - return { hasError: true, error }; - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error("CopilotErrorBoundary caught error:", error); - console.error("Component stack:", errorInfo.componentStack); - this.setState({ errorInfo }); - } - - handleRetry = () => { - this.setState({ hasError: false, error: null, errorInfo: null }); - this.props.onRetry?.(); - }; - - render() { - if (this.state.hasError) { - const errorMessage = this.state.error?.message || "Unknown error"; - const isNameError = errorMessage.includes("Cannot read properties of undefined (reading 'name')"); - - return ( -
-
- -
-

Copilot SDK Error

- - {isNameError ? ( -
-

The Copilot SDK encountered an internal error.

-

- This typically occurs when the container metadata is incomplete or the tenant configuration needs adjustment. -

-
- ) : ( -

- {errorMessage} -

- )} - -
- - -
- -
-

Troubleshooting steps:

-
    -
  • Verify container has documents uploaded
  • -
  • Check CopilotEmbeddedChatHosts configuration
  • -
  • Ensure DiscoverabilityDisabled is false
  • -
- - View SDK documentation - - -
-
- ); - } - - return this.props.children; - } -} diff --git a/Custom Apps/legal-docs/src/components/copilot/InlineCopilotChat.tsx b/Custom Apps/legal-docs/src/components/copilot/InlineCopilotChat.tsx deleted file mode 100644 index a5faa1b..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/InlineCopilotChat.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { useState, useCallback, useMemo, useRef, useEffect } from "react"; -import { Send, Loader2, Database, CheckCircle2 } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { useAuth } from "@/context/AuthContext"; -import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Input } from "@/components/ui/input"; -import { - sendCopilotMessage, - createChatAuthProvider, - CopilotMessage, - DEFAULT_CHAT_CONFIG, - ChatLaunchConfig -} from "@/services/copilotChat"; - -interface InlineCopilotChatProps { - containerId: string; - containerName: string; - config?: ChatLaunchConfig; -} - -/** - * Inline Copilot Chat Component - * - * A simplified chat component designed to be embedded inline (not as a floating bubble). - * Uses the Graph API fallback for document-grounded responses. - */ -export default function InlineCopilotChat({ - containerId, - containerName, - config = DEFAULT_CHAT_CONFIG -}: InlineCopilotChatProps) { - const { getAccessToken } = useAuth(); - const [messages, setMessages] = useState([]); - const [inputValue, setInputValue] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const scrollRef = useRef(null); - const prevContainerId = useRef(null); - - // Create auth provider following SDK pattern - uses Container.Selected scope - const authProvider = useMemo(() => - createChatAuthProvider(getAccessToken), - [getAccessToken] - ); - - // Merged config - const chatConfig = useMemo(() => ({ - ...DEFAULT_CHAT_CONFIG, - ...config, - header: config?.header || containerName, - }), [config, containerName]); - - // Reset messages and test connection when container changes - useEffect(() => { - if (prevContainerId.current !== containerId) { - prevContainerId.current = containerId; - setMessages([]); - setInputValue(""); - setIsConnected(false); - - // Test container connection on change - const testConnection = async () => { - try { - await authProvider.getToken(); - setIsConnected(true); - console.log("Connected to SharePoint container:", containerId); - } catch (error) { - console.error("Failed to connect to container:", error); - setIsConnected(false); - } - }; - - if (containerId) { - testConnection(); - } - } - }, [containerId, authProvider]); - - // Scroll to bottom when new messages arrive - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [messages]); - - // Handle sending a message - const handleSendMessage = useCallback(async (text: string) => { - if (!text.trim() || isLoading) return; - - const userMessage: CopilotMessage = { - role: "user", - content: text.trim(), - timestamp: new Date(), - }; - - setMessages(prev => [...prev, userMessage]); - setInputValue(""); - setIsLoading(true); - - try { - const response = await sendCopilotMessage( - authProvider, - containerId, - containerName, - text.trim(), - messages, - chatConfig - ); - - const assistantMessage: CopilotMessage = { - role: "assistant", - content: response, - timestamp: new Date(), - }; - - setMessages(prev => [...prev, assistantMessage]); - } catch (error) { - console.error("Chat error:", error); - const errorMessage: CopilotMessage = { - role: "assistant", - content: "I'm sorry, I encountered an error processing your request. Please try again.", - timestamp: new Date(), - }; - setMessages(prev => [...prev, errorMessage]); - } finally { - setIsLoading(false); - } - }, [authProvider, containerId, containerName, messages, chatConfig, isLoading]); - - // Handle form submit - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleSendMessage(inputValue); - }; - - // Handle suggestion click - const handleSuggestionClick = (suggestion: string) => { - handleSendMessage(suggestion); - }; - - // Don't render if no container is selected - if (!containerId) return null; - - const zeroQueryPrompts = chatConfig.zeroQueryPrompts || DEFAULT_CHAT_CONFIG.zeroQueryPrompts; - - return ( -
- {/* Header */} -
-
-

{chatConfig.header}

-
- -

{containerName}

- {isConnected && ( - - - Connected - - )} -
-
-
- - {/* Messages Area */} - - {messages.length === 0 ? ( -
-

- {zeroQueryPrompts?.headerText} -

-
- {zeroQueryPrompts?.promptSuggestionList?.map((prompt, index) => ( - - ))} -
-
- ) : ( -
- {messages.map((message, index) => ( -
-
-

{message.content}

-
-
- ))} - {isLoading && ( -
-
- -
-
- )} -
- )} -
- - {/* Input Area */} -
-
- setInputValue(e.target.value)} - placeholder={chatConfig.chatInputPlaceholder || "Ask about this case..."} - disabled={isLoading} - className="flex-1" - /> - -
-
-
- ); -} diff --git a/Custom Apps/legal-docs/src/components/copilot/SDKCopilotChat.tsx b/Custom Apps/legal-docs/src/components/copilot/SDKCopilotChat.tsx deleted file mode 100644 index dece0d6..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/SDKCopilotChat.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useState, useEffect, useMemo, useCallback } from "react"; -import { X, MessageSquare, Loader2, AlertTriangle, RefreshCw } from "lucide-react"; -import { useAuth } from "@/context/AuthContext"; -import { Button } from "@/components/ui/button"; -import { CopilotAuthProvider } from "@/components/copilot/CopilotAuthProvider"; -import { CopilotErrorBoundary } from "./CopilotErrorBoundary"; -import { ChatEmbedded, ChatEmbeddedAPI, ChatLaunchConfig } from "@microsoft/sharepointembedded-copilotchat-react"; - -interface SDKCopilotChatProps { - containerId: string; - containerName: string; - isOpen: boolean; - onClose: () => void; -} - -/** - * SharePoint Embedded Copilot Chat using the official SDK. - * - * Prerequisites: - * 1. Install SDK: npm install @microsoft/sharepointembedded-copilotchat-react - * 2. Configure CopilotEmbeddedChatHosts via PowerShell - * 3. Set DiscoverabilityDisabled to false - * - * Reference: https://learn.microsoft.com/en-us/sharepoint/dev/embedded/development/declarative-agent/spe-da-adv - */ -export default function SDKCopilotChat({ - containerId, - containerName, - isOpen, - onClose, -}: SDKCopilotChatProps) { - const { getAccessToken } = useAuth(); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [chatApi, setChatApi] = useState(null); - const [sdkAvailable, setSdkAvailable] = useState(false); - - // Create auth provider with Container.Selected scope - const authProvider = useMemo( - () => new CopilotAuthProvider(getAccessToken), - [getAccessToken] - ); - - // SDK is now statically imported, mark as available - useEffect(() => { - setSdkAvailable(true); - console.log("SDKCopilotChat: SDK is available"); - }, []); - - // Initialize auth provider when opened - useEffect(() => { - if (!isOpen || !containerId || !sdkAvailable) return; - - const initAuth = async () => { - setIsLoading(true); - setError(null); - - try { - await authProvider.initialize(); - console.log("SDKCopilotChat: Auth provider initialized"); - setIsLoading(false); - } catch (err) { - console.error("SDKCopilotChat: Auth initialization failed", err); - setError("Authentication failed. Please try again."); - setIsLoading(false); - } - }; - - // Set timeout for initialization - const timeout = setTimeout(() => { - if (isLoading) { - setError( - "The SharePoint Embedded Copilot chat is not responding.\n\n" + - "Possible causes:\n" + - "• CopilotEmbeddedChatHosts not configured\n" + - "• DiscoverabilityDisabled is true\n" + - "• Copilot not enabled for your tenant" - ); - setIsLoading(false); - } - }, 15000); - - initAuth(); - - return () => clearTimeout(timeout); - }, [isOpen, containerId, sdkAvailable, authProvider]); - - // Open chat when API is ready - useEffect(() => { - if (!chatApi) return; - - const openChat = async () => { - try { - await chatApi.openChat({ - header: `Case Assistant - ${containerName}`, - zeroQueryPrompts: { - headerText: "How can I help you with this case?", - promptSuggestionList: [ - { suggestionText: "Summarize the key facts of this case" }, - { suggestionText: "Who are the parties involved?" }, - { suggestionText: "What are the important dates?" }, - { suggestionText: "List the key documents" }, - ], - }, - instruction: - "You are a legal case assistant. Provide clear, professional responses based on the case documents.", - locale: "en", - }); - console.log("SDKCopilotChat: Chat opened successfully"); - } catch (err) { - console.error("SDKCopilotChat: Failed to open chat", err); - setError("Failed to open chat interface"); - } - }; - - openChat(); - }, [chatApi, containerName]); - - const handleApiReady = useCallback((api: ChatEmbeddedAPI) => { - console.log("SDKCopilotChat: API ready"); - setChatApi(api); - setIsLoading(false); - setError(null); - }, []); - - const handleRetry = useCallback(() => { - setError(null); - setIsLoading(true); - setChatApi(null); - }, []); - - if (!isOpen) return null; - - return ( -
-
- {/* Header */} -
-
-
- -
-
-

Copilot Assistant

-

{containerName}

-
-
- -
- - {/* Content */} -
- {error ? ( -
-
- -
-

Copilot Unavailable

-

- {error} -

-
- - -
-
- ) : isLoading ? ( -
- -

Initializing Copilot...

-

Connecting to Microsoft services

-
- ) : sdkAvailable ? ( - -
- -
-
- ) : null} -
- - {/* Footer status */} - {!error && !isLoading && ( -
- - Copilot Active -
- )} -
-
- ); -} diff --git a/Custom Apps/legal-docs/src/components/copilot/index.ts b/Custom Apps/legal-docs/src/components/copilot/index.ts deleted file mode 100644 index aa25dcf..0000000 --- a/Custom Apps/legal-docs/src/components/copilot/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copilot Chat Components -// Use CopilotChatContainer as the main entry point for Copilot chat -// It handles auth, config, and renders CopilotDesktopView - -export { default as CopilotChatContainer } from "./CopilotChatContainer"; -export { default as CustomCopilotChat } from "../CustomCopilotChat"; -export { default as SDKCopilotChat } from "./SDKCopilotChat"; -export { default as CopilotDesktopView } from "./CopilotDesktopView"; -export { CopilotAuthProvider } from "./CopilotAuthProvider"; -export { CopilotErrorBoundary } from "./CopilotErrorBoundary"; -export type { IChatEmbeddedApiAuthProvider } from "./CopilotAuthProvider"; diff --git a/Custom Apps/legal-docs/src/components/panels/CopilotPanel.tsx b/Custom Apps/legal-docs/src/components/panels/CopilotPanel.tsx deleted file mode 100644 index 8311296..0000000 --- a/Custom Apps/legal-docs/src/components/panels/CopilotPanel.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { CopilotChatContainer } from "@/components/copilot"; -import { CopilotErrorBoundary } from "@/components/copilot"; - -interface CopilotPanelProps { - containerId: string; - containerName: string; -} - -/** - * CopilotPanel - Wrapper component for the SharePoint Embedded Copilot chat - * Uses CopilotChatContainer which handles authentication, configuration, and rendering - */ -export default function CopilotPanel({ containerId, containerName }: CopilotPanelProps) { - return ( -
- - - -
- ); -} diff --git a/Custom Apps/legal-docs/src/config/appConfig.ts b/Custom Apps/legal-docs/src/config/appConfig.ts index 1b5ac1c..0cdcaac 100644 --- a/Custom Apps/legal-docs/src/config/appConfig.ts +++ b/Custom Apps/legal-docs/src/config/appConfig.ts @@ -1,17 +1,5 @@ // SharePoint Embedded & Azure AD Configuration -// Central configuration file for the Copilot Chat implementation - -// Helper to normalize SharePoint URLs (ensure https:// prefix) -const normalizeSharePointUrl = (url: string): string => { - if (!url) return ''; - // Remove trailing slashes - let normalized = url.replace(/\/+$/, ''); - // Ensure https:// prefix - if (!normalized.startsWith('https://') && !normalized.startsWith('http://')) { - normalized = `https://${normalized}`; - } - return normalized; -}; +// Central configuration file export const appConfig = { // Azure AD App Registration @@ -20,10 +8,6 @@ export const appConfig = { // SharePoint Embedded containerTypeId: "", - sharePointHostname: "https://.sharepoint.com", - - // Utility function for URL normalization - normalizeSharePointUrl, }; // Keep APP_CONFIG as alias for backward compatibility @@ -56,22 +40,6 @@ export const SCOPES = { "https://graph.microsoft.com/Files.Read.All", "https://graph.microsoft.com/Sites.Read.All", ] as string[], - // SharePoint Container.Selected scope for SDK authentication - sharePoint: [`${APP_CONFIG.sharePointHostname}/Container.Selected`] as string[], // Container management scope containerManagement: ["https://graph.microsoft.com/FileStorageContainer.Selected"] as string[], }; - -// Copilot Chat UI Configuration -export const COPILOT_CONFIG = { - header: "Case Assistant", - instruction: "You are a helpful legal case assistant. Answer questions about the documents in this case clearly and professionally.", - locale: "en-US", - suggestedPrompts: [ - "Summarize the key facts of this case", - "Who are the parties involved?", - "What are the important dates?", - "List the key documents", - ] as string[], - chatInputPlaceholder: "Ask about this case...", -}; diff --git a/Custom Apps/legal-docs/src/config/sharepoint.ts b/Custom Apps/legal-docs/src/config/sharepoint.ts index fdbd923..73c1f73 100644 --- a/Custom Apps/legal-docs/src/config/sharepoint.ts +++ b/Custom Apps/legal-docs/src/config/sharepoint.ts @@ -5,9 +5,6 @@ export const SHAREPOINT_CONFIG = { CLIENT_ID: "", TENANT_ID: "", CONTAINER_TYPE_ID: "", - // SharePoint hostname for Copilot API authentication (must include https://) - // Use tenant name format: https://{tenant}.sharepoint.com - SHAREPOINT_HOSTNAME: "https://.sharepoint.com", } as const; // MSAL Configuration @@ -28,37 +25,9 @@ export const MSAL_CONFIG = { export const GRAPH_ENDPOINT = "https://graph.microsoft.com/v1.0"; export const GRAPH_BETA_ENDPOINT = "https://graph.microsoft.com/beta"; -// Scopes for Copilot - using SharePoint Container.Selected as per SDK documentation -// The SDK requires this scope pattern: {hostname}/Container.Selected -export const COPILOT_SCOPES = [`${SHAREPOINT_CONFIG.SHAREPOINT_HOSTNAME}/Container.Selected`]; - -// Graph API scopes for search-based Copilot functionality -// Used when calling Graph API endpoints directly (current implementation) +// Graph API scopes export const GRAPH_SEARCH_SCOPES = [ "https://graph.microsoft.com/Files.Read.All", "https://graph.microsoft.com/Sites.Read.All", ]; -// SharePoint-specific scopes for container access -export const SHAREPOINT_CONTAINER_SCOPES = [`${SHAREPOINT_CONFIG.SHAREPOINT_HOSTNAME}/Container.Selected`]; - -// Copilot Chat Auth Provider Interface (matches SDK's IChatEmbeddedApiAuthProvider) -export interface IChatEmbeddedApiAuthProvider { - hostname: string; - getToken(): Promise; -} - -// Copilot Chat Launch Configuration (matches SDK's ChatLaunchConfig) -export interface ChatLaunchConfig { - header?: string; - zeroQueryPrompts?: { - headerText: string; - promptSuggestionList?: Array<{ - suggestionText: string; - }>; - }; - suggestedPrompts?: string[]; - instruction?: string; - locale?: string; - chatInputPlaceholder?: string; -} diff --git a/Custom Apps/legal-docs/src/context/AuthContext.tsx b/Custom Apps/legal-docs/src/context/AuthContext.tsx index d52029e..a00b45e 100644 --- a/Custom Apps/legal-docs/src/context/AuthContext.tsx +++ b/Custom Apps/legal-docs/src/context/AuthContext.tsx @@ -5,7 +5,7 @@ import { AuthenticationResult, InteractionRequiredAuthError } from "@azure/msal-browser"; -import { MSAL_CONFIG, APP_CONFIG, SCOPES } from "@/config/appConfig"; +import { MSAL_CONFIG } from "@/config/appConfig"; interface AuthContextType { isInitialized: boolean; @@ -14,7 +14,6 @@ interface AuthContextType { login: () => Promise; logout: () => Promise; getAccessToken: (scopes: string[]) => Promise; - getSharePointToken: () => Promise; isLoggingIn: boolean; } @@ -135,14 +134,6 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [user]); - /** - * Get SharePoint-scoped token for Copilot SDK - * Uses the {hostname}/Container.Selected scope pattern - */ - const getSharePointToken = useCallback(async (): Promise => { - return getAccessToken(SCOPES.sharePoint); - }, [getAccessToken]); - return ( diff --git a/Custom Apps/legal-docs/src/hooks/useCopilotSite.ts b/Custom Apps/legal-docs/src/hooks/useCopilotSite.ts deleted file mode 100644 index 2854a11..0000000 --- a/Custom Apps/legal-docs/src/hooks/useCopilotSite.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useAuth } from '@/context/AuthContext'; -import { APP_CONFIG, ENDPOINTS, SCOPES } from '@/config/appConfig'; - -interface CopilotSiteState { - isLoading: boolean; - error: string | null; - containerId: string | null; - containerName: string | null; - webUrl: string | null; - sharePointHostname: string; -} - -/** - * Hook to fetch SharePoint container/site information for Copilot. - * - * - Normalizes container ID (adds b! prefix if missing) - * - Fetches container name and webUrl via Graph API - * - Extracts SharePoint hostname for authentication - */ -export function useCopilotSite(rawContainerId: string | null): CopilotSiteState { - const { getAccessToken, isAuthenticated } = useAuth(); - const [state, setState] = useState({ - isLoading: false, - error: null, - containerId: null, - containerName: null, - webUrl: null, - sharePointHostname: APP_CONFIG.sharePointHostname, - }); - - useEffect(() => { - let cancelled = false; - - const fetchContainerInfo = async () => { - if (!rawContainerId || !isAuthenticated) { - setState(prev => ({ - ...prev, - isLoading: false, - error: !rawContainerId ? null : 'Not authenticated', - containerId: null, - containerName: null, - webUrl: null, - })); - return; - } - - setState(prev => ({ ...prev, isLoading: true, error: null })); - - try { - // Use the raw container ID as-is (with b! prefix) for the drives endpoint - const normalizedId = rawContainerId; - - // Get token with container management scopes - const token = await getAccessToken(SCOPES.containerManagement); - - if (!token) { - if (!cancelled) { - setState(prev => ({ - ...prev, - isLoading: false, - error: 'Failed to acquire access token', - })); - } - return; - } - - // Use the /drives/{id} endpoint which accepts b!-prefixed IDs - // and returns both displayName and webUrl - const driveResponse = await fetch( - `${ENDPOINTS.graph}/drives/${normalizedId}`, - { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - } - ); - - if (!driveResponse.ok) { - const errorText = await driveResponse.text(); - console.error('Drive fetch error:', driveResponse.status, errorText); - if (!cancelled) { - setState(prev => ({ - ...prev, - isLoading: false, - error: `Container not accessible: ${driveResponse.status}`, - })); - } - return; - } - - const driveData = await driveResponse.json(); - console.log('📦 Drive metadata:', { - id: driveData.id, - name: driveData.name, - webUrl: driveData.webUrl, - }); - - if (!cancelled) { - setState({ - isLoading: false, - error: null, - containerId: normalizedId, - containerName: driveData.name || driveData.description || 'SharePoint Container', - webUrl: driveData.webUrl || null, - sharePointHostname: APP_CONFIG.sharePointHostname, - }); - } - } catch (err) { - console.error('Error fetching container info:', err); - if (!cancelled) { - setState(prev => ({ - ...prev, - isLoading: false, - error: err instanceof Error ? err.message : 'Failed to fetch container info', - })); - } - } - }; - - fetchContainerInfo(); - - return () => { - cancelled = true; - }; - }, [rawContainerId, isAuthenticated, getAccessToken]); - - return state; -} diff --git a/Custom Apps/legal-docs/src/lib/sharepointembedded-copilotchat-react/index.tsx b/Custom Apps/legal-docs/src/lib/sharepointembedded-copilotchat-react/index.tsx deleted file mode 100644 index 5939d65..0000000 --- a/Custom Apps/legal-docs/src/lib/sharepointembedded-copilotchat-react/index.tsx +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Local shim for @microsoft/sharepointembedded-copilotchat-react SDK - * - * This module provides the same interface as the official Microsoft SDK. - * The ChatEmbedded component renders an iframe that loads Microsoft's - * SharePoint Embedded Copilot experience. - * - * The actual SDK is distributed as a private preview tgz which cannot - * be installed via npm/bun in this environment. - */ - -import React, { useEffect, useRef, useCallback } from 'react'; - -// ==================== Type Definitions ==================== - -export interface IChatEmbeddedApiAuthProvider { - hostname: string; - getToken(): Promise; -} - -export interface ChatEmbeddedAPI { - openChat(config: ChatLaunchConfig): Promise; -} - -export interface PromptSuggestion { - suggestionText: string; -} - -export interface ZeroQueryPrompts { - headerText?: string; - promptSuggestionList?: PromptSuggestion[]; -} - -export interface ChatLaunchConfig { - header?: string; - instruction?: string; - locale?: string; - suggestedPrompts?: string[]; - zeroQueryPrompts?: ZeroQueryPrompts; - chatInputPlaceholder?: string; -} - -// ==================== ChatEmbedded Component ==================== - -interface ChatEmbeddedProps { - authProvider: IChatEmbeddedApiAuthProvider; - containerId: string; - onApiReady?: (api: ChatEmbeddedAPI) => void; - style?: React.CSSProperties; -} - -/** - * ChatEmbedded component - renders the SharePoint Embedded Copilot chat iframe. - * - * This component loads the Copilot experience from SharePoint's servers - * using the provided auth provider and container ID. - */ -export const ChatEmbedded: React.FC = ({ - authProvider, - containerId, - onApiReady, - style, -}) => { - const containerRef = useRef(null); - const iframeRef = useRef(null); - const initializedRef = useRef(false); - const chatConfigRef = useRef(null); - - // Build the Copilot iframe URL - const getCopilotUrl = useCallback(() => { - const hostname = authProvider.hostname.replace(/\/$/, ''); - // The SDK uses this endpoint pattern for the embedded Copilot chat - return `${hostname}/_layouts/15/copilotchat.aspx?containerId=${encodeURIComponent(containerId)}&embedded=true`; - }, [authProvider.hostname, containerId]); - - // Handle messages from the Copilot iframe - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - // Only accept messages from our SharePoint hostname - const hostname = authProvider.hostname.replace(/\/$/, ''); - try { - const expectedOrigin = new URL(hostname).origin; - if (event.origin !== expectedOrigin) { - return; - } - } catch { - // Fallback: simple string comparison - if (!event.origin.includes(hostname.replace(/^https?:\/\//, ''))) { - return; - } - } - - // Handle both string (JSON) and object message formats - let data = event.data; - if (typeof data === 'string') { - try { - data = JSON.parse(data); - } catch { - console.log('ChatEmbedded: Ignoring non-JSON string message from iframe'); - return; - } - } - if (!data || typeof data !== 'object') return; - - // Handle token requests from the iframe - if (data.type === 'getToken' || data.messageType === 'getToken') { - authProvider.getToken().then(token => { - iframeRef.current?.contentWindow?.postMessage(JSON.stringify({ - type: 'tokenResponse', - messageType: 'tokenResponse', - token, - }), hostname); - }).catch(err => { - console.error('ChatEmbedded: Failed to get token for iframe:', err); - }); - } - - // Handle ready state - if (data.type === 'ready' || data.messageType === 'ready') { - console.log('ChatEmbedded: iframe ready'); - // If we have a pending config, send it - if (chatConfigRef.current) { - iframeRef.current?.contentWindow?.postMessage(JSON.stringify({ - type: 'openChat', - messageType: 'openChat', - config: chatConfigRef.current, - }), hostname); - } - } - }; - - window.addEventListener('message', handleMessage); - return () => window.removeEventListener('message', handleMessage); - }, [authProvider]); - - // Create and manage the iframe - useEffect(() => { - if (initializedRef.current || !containerRef.current) return; - initializedRef.current = true; - - const copilotUrl = getCopilotUrl(); - console.log('ChatEmbedded: Loading Copilot iframe from:', copilotUrl); - - // Create iframe - const iframe = document.createElement('iframe'); - iframe.src = copilotUrl; - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.border = 'none'; - iframe.setAttribute('allow', 'clipboard-write'); - iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox'); - - iframeRef.current = iframe; - containerRef.current.appendChild(iframe); - - // Create the API object - const api: ChatEmbeddedAPI = { - openChat: async (config: ChatLaunchConfig) => { - chatConfigRef.current = config; - const hostname = authProvider.hostname.replace(/\/$/, ''); - - // Get auth token first - try { - const token = await authProvider.getToken(); - - // Send config and token to iframe - iframe.contentWindow?.postMessage(JSON.stringify({ - type: 'openChat', - messageType: 'openChat', - config, - token, - }), hostname); - - console.log('ChatEmbedded: openChat config sent to iframe'); - } catch (err) { - console.error('ChatEmbedded: Failed to get token for openChat:', err); - throw err; - } - }, - }; - - // Notify parent that API is ready once iframe loads - iframe.addEventListener('load', () => { - console.log('ChatEmbedded: iframe loaded, firing onApiReady'); - onApiReady?.(api); - }); - - // Also fire onApiReady after a timeout in case load event doesn't fire - const timeout = setTimeout(() => { - if (!iframe.contentWindow) return; - console.log('ChatEmbedded: Timeout reached, firing onApiReady'); - onApiReady?.(api); - }, 5000); - - return () => { - clearTimeout(timeout); - }; - }, [getCopilotUrl, authProvider, onApiReady]); - - // Reset when containerId changes - useEffect(() => { - return () => { - initializedRef.current = false; - if (iframeRef.current && containerRef.current) { - try { - containerRef.current.removeChild(iframeRef.current); - } catch { - // ignore - } - iframeRef.current = null; - } - }; - }, [containerId]); - - return ( -
- ); -}; - -export default ChatEmbedded; diff --git a/Custom Apps/legal-docs/src/pages/Dashboard.tsx b/Custom Apps/legal-docs/src/pages/Dashboard.tsx index 01fd9be..10b3db9 100644 --- a/Custom Apps/legal-docs/src/pages/Dashboard.tsx +++ b/Custom Apps/legal-docs/src/pages/Dashboard.tsx @@ -26,7 +26,6 @@ import FlyoutButtons from "@/components/FlyoutButtons"; import CaseSummaryPanel from "@/components/panels/CaseSummaryPanel"; import ToolsPanel from "@/components/panels/ToolsPanel"; import ReportsPanel from "@/components/panels/ReportsPanel"; -import CopilotPanel from "@/components/panels/CopilotPanel"; import { PanelType } from "@/components/FlyoutButtons"; // Button column width @@ -282,7 +281,6 @@ export default function Dashboard() { {/* Flyout Panels */} @@ -318,23 +316,6 @@ export default function Dashboard() { > - - {/* Copilot Panel */} - {selectedContainer && ( - handlePanelClose("copilot")} - isPinned={pinnedPanels.has("copilot")} - onPinToggle={() => handlePinToggle("copilot")} - onWidthChange={handlePanelWidthChange} - > - - - )}
); diff --git a/Custom Apps/legal-docs/src/services/copilotChat.ts b/Custom Apps/legal-docs/src/services/copilotChat.ts deleted file mode 100644 index c6bdcab..0000000 --- a/Custom Apps/legal-docs/src/services/copilotChat.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { - SHAREPOINT_CONFIG, - GRAPH_ENDPOINT, - GRAPH_BETA_ENDPOINT, - COPILOT_SCOPES, - GRAPH_SEARCH_SCOPES, - SHAREPOINT_CONTAINER_SCOPES, - IChatEmbeddedApiAuthProvider, - ChatLaunchConfig -} from "@/config/sharepoint"; - -export type { ChatLaunchConfig }; - -export interface CopilotMessage { - role: "user" | "assistant"; - content: string; - timestamp: Date; -} - -// Response type with optional citations -export interface CopilotResponse { - content: string; - citations?: Array<{ - documentName: string; - webUrl: string; - snippet?: string; - }>; -} - -interface DriveSearchItem { - name?: string; - size?: number; -} - -interface SearchHitResource { - name?: string; -} - -interface SearchHitExtract { - text?: string; -} - -interface SearchHit { - extracts?: SearchHitExtract[]; - summary?: string; - resource?: SearchHitResource; -} - -// Default launch configuration following SDK patterns -export const DEFAULT_CHAT_CONFIG: ChatLaunchConfig = { - header: "Case Assistant", - zeroQueryPrompts: { - headerText: "How can I help you with this case?", - promptSuggestionList: [ - { suggestionText: "Summarize the key facts of this case" }, - { suggestionText: "Who are the parties involved?" }, - { suggestionText: "What are the important dates?" }, - { suggestionText: "List the key documents" }, - ], - }, - suggestedPrompts: [ - "What are the main legal issues?", - "Summarize the evidence", - "What is the current status?", - ], - instruction: "You are a legal case assistant. Provide clear, professional responses based on the case documents.", - locale: "en", -}; - -// Clean up text from Copilot API responses -function cleanCopilotText(text: string): string { - let cleaned = text; - // Remove page markers - cleaned = cleaned.replace(//g, '').replace(/<\/page_\d+>/g, ''); - // Remove escaped markdown characters - cleaned = cleaned.replace(/\\_/g, '_').replace(/\\-/g, '-'); - cleaned = cleaned.replace(/\\\[/g, '[').replace(/\\\]/g, ']'); - cleaned = cleaned.replace(/\\\(/g, '(').replace(/\\\)/g, ')'); - cleaned = cleaned.replace(/\\\*/g, '*'); - // Remove standalone asterisks used as separators - cleaned = cleaned.replace(/(\s*\*\s*){2,}/g, ' '); - // Remove single asterisks at word boundaries - cleaned = cleaned.replace(/\*+/g, ''); - // Remove backslashes before common characters - cleaned = cleaned.replace(/\\([^\\])/g, '$1'); - // Clean up whitespace - cleaned = cleaned.replace(/\r\n/g, ' ').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); - return cleaned; -} - -/** - * Create auth provider for Copilot chat. - * - * IMPORTANT: The Graph API endpoints require Graph tokens (audience: graph.microsoft.com), - * while the official SharePoint Embedded SDK requires Container.Selected scope. - * - * Since we're calling Graph API endpoints directly (not using the SDK), - * we must use Graph scopes for the token audience to be correct. - */ -export function createChatAuthProvider( - getToken: (scopes: string[]) => Promise -): IChatEmbeddedApiAuthProvider { - return { - hostname: SHAREPOINT_CONFIG.SHAREPOINT_HOSTNAME, - getToken: async () => { - // Use Graph scopes since we're calling Graph API endpoints directly - // The beta/copilot/retrieval endpoint requires Graph token audience - const token = await getToken(GRAPH_SEARCH_SCOPES); - if (!token) { - throw new Error("Failed to acquire token for Copilot chat"); - } - console.log("Acquired Graph token for Copilot chat"); - return token; - }, - }; -} - -/** - * Send a message to Copilot using the beta Copilot retrieval API. - * Falls back to Graph Search if the beta API is not available. - */ -export async function sendCopilotMessage( - authProvider: IChatEmbeddedApiAuthProvider, - containerId: string, - containerName: string, - userMessage: string, - conversationHistory: CopilotMessage[], - config: ChatLaunchConfig = DEFAULT_CHAT_CONFIG -): Promise { - const accessToken = await authProvider.getToken(); - - // Build conversation context for the Copilot API - const contextMessages = conversationHistory - .slice(-6) - .map((m) => ({ - role: m.role, - content: m.content, - })); - - const systemInstruction = config.instruction || DEFAULT_CHAT_CONFIG.instruction; - - // Try the beta Copilot retrieval API first (may not be available for all tenants) - const copilotResponse = await callCopilotRetrievalAPI( - accessToken, - containerId, - containerName, - userMessage, - contextMessages, - systemInstruction - ); - - if (copilotResponse) { - return copilotResponse; - } - - // Fallback to drive search when Copilot API is unavailable - console.log("Copilot API unavailable, using drive search fallback"); - return await searchBasedResponse(accessToken, containerId, containerName, userMessage, config); -} - -/** - * Call the Microsoft Graph beta Copilot retrieval API. - * This provides AI-generated responses grounded in container documents. - */ -async function callCopilotRetrievalAPI( - accessToken: string, - containerId: string, - containerName: string, - userMessage: string, - contextMessages: Array<{ role: string; content: string }>, - systemInstruction: string -): Promise { - const copilotUrl = `${GRAPH_BETA_ENDPOINT}/copilot/retrieval`; - - const requestBody = { - requests: [ - { - entityTypes: ["driveItem"], - contentSources: [`/drives/${containerId}`], - query: { - queryString: userMessage, - }, - groundingOptions: { - systemPrompt: systemInstruction, - conversationContext: contextMessages, - }, - }, - ], - }; - - const response = await fetch(copilotUrl, { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - // 400/401/403 are expected when beta Copilot API is not enabled for the tenant - // Only log as warning, not error, to avoid console noise - if (response.status === 400 || response.status === 401 || response.status === 403) { - console.warn("Copilot retrieval API not available (expected for tenants without Copilot preview)"); - } else { - const errorText = await response.text(); - console.error("Copilot retrieval API error:", response.status, errorText); - } - return null; - } - - const data = await response.json(); - - // Extract response content from the Copilot API response - const responseContent = data.value?.response || data.value?.[0]?.response; - - if (responseContent) { - return cleanCopilotText(responseContent); - } - - // Check for any other response format - if (data.value?.content) { - return cleanCopilotText(data.value.content); - } - - return null; -} - -/** - * Search-based response using Graph Search API. - * Scoped to the specific container (drive) to only search documents in the selected case. - */ -async function searchBasedResponse( - accessToken: string, - containerId: string, - containerName: string, - userMessage: string, - config: ChatLaunchConfig -): Promise { - // Use drive-specific search endpoint directly for SharePoint Embedded containers - // This is more reliable than the /search/query endpoint for container-scoped searches - return await searchWithDriveFilter(accessToken, containerId, containerName, userMessage); -} - -/** - * Alternative search approach: directly query the container's drive for content. - */ -async function searchWithDriveFilter( - accessToken: string, - containerId: string, - containerName: string, - userMessage: string -): Promise { - // Sanitize the query - remove special characters that cause URL issues - const sanitizedQuery = userMessage - .replace(/[?&=#%]/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - - // Use the drive's children endpoint with filter for broader content listing - // Then use search endpoint with properly encoded query - const driveSearchUrl = `${GRAPH_ENDPOINT}/drives/${containerId}/root/search(q='${encodeURIComponent(sanitizedQuery)}')`; - - console.log("Searching container:", containerId, "for:", sanitizedQuery); - - try { - const response = await fetch(driveSearchUrl, { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (!response.ok) { - console.error("Drive search failed:", response.status); - // Fallback: list all files in the container - return await listContainerFiles(accessToken, containerId, containerName, userMessage); - } - - const data = await response.json(); - const items = (data.value || []) as DriveSearchItem[]; - - if (items.length === 0) { - return await listContainerFiles(accessToken, containerId, containerName, userMessage); - } - - // Format results from drive search - const responses: string[] = items.slice(0, 5).map((item) => { - const name = item.name || 'Document'; - const size = item.size ? `(${Math.round(item.size / 1024)} KB)` : ''; - return `• **${name}** ${size}`; - }); - - return `Found ${items.length} document(s) in "${containerName}" matching "${sanitizedQuery}":\n\n${responses.join("\n")}\n\nWould you like more details about any of these documents?`; - } catch (error) { - console.error("Drive search error:", error); - return await listContainerFiles(accessToken, containerId, containerName, userMessage); - } -} - -/** - * Fallback: List files in the container when search fails. - */ -async function listContainerFiles( - accessToken: string, - containerId: string, - containerName: string, - userMessage: string -): Promise { - try { - const listUrl = `${GRAPH_ENDPOINT}/drives/${containerId}/root/children?$top=10`; - const response = await fetch(listUrl, { - method: "GET", - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (!response.ok) { - return getNoResultsMessage(containerName, userMessage); - } - - const data = await response.json(); - const items = (data.value || []) as DriveSearchItem[]; - - if (items.length === 0) { - return `The "${containerName}" case doesn't have any documents yet. Upload some files to get started!`; - } - - const fileList = items.slice(0, 5).map((item) => `• ${item.name}`).join("\n"); - return `Here are the documents in "${containerName}":\n\n${fileList}\n\nAsk me about any of these documents, or try a more specific search term.`; - } catch (error) { - console.error("List files error:", error); - return getNoResultsMessage(containerName, userMessage); - } -} - -/** - * Format search results into a readable response. - */ -function formatSearchResults(hits: SearchHit[], containerName: string): string { - const responses: string[] = []; - - for (const hit of hits.slice(0, 5)) { - if (hit.extracts && hit.extracts.length > 0) { - const extractText = cleanCopilotText(hit.extracts[0].text); - if (extractText) { - responses.push(`**${hit.resource?.name || 'Document'}:**\n${extractText}`); - } - } else if (hit.summary) { - responses.push(`**${hit.resource?.name || 'Document'}:**\n${cleanCopilotText(hit.summary)}`); - } - } - - if (responses.length > 0) { - return `Based on documents in the ${containerName} case:\n\n${responses.join("\n\n")}`; - } - - return getNoResultsMessage(containerName, "your query"); -} - -/** - * Generate a helpful message when no results are found. - */ -function getNoResultsMessage(containerName: string, userMessage: string): string { - return `I couldn't find specific information about "${userMessage}" in the ${containerName} case documents. Try: -• Rephrasing your question with different keywords -• Asking about specific document names or topics -• Checking if the documents have been uploaded to this case`; -} diff --git a/Custom Apps/legal-docs/tsconfig.app.json b/Custom Apps/legal-docs/tsconfig.app.json index eb60267..a981075 100644 --- a/Custom Apps/legal-docs/tsconfig.app.json +++ b/Custom Apps/legal-docs/tsconfig.app.json @@ -19,9 +19,6 @@ "paths": { "@/*": [ "./src/*" - ], - "@microsoft/sharepointembedded-copilotchat-react": [ - "./src/lib/sharepointembedded-copilotchat-react/index.tsx" ] }, "skipLibCheck": true, diff --git a/Custom Apps/legal-docs/vite.config.ts b/Custom Apps/legal-docs/vite.config.ts index 6d8a8ac..da25c6d 100644 --- a/Custom Apps/legal-docs/vite.config.ts +++ b/Custom Apps/legal-docs/vite.config.ts @@ -13,28 +13,6 @@ export default defineConfig(({ mode }) => ({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - // Keep Vite aligned with tsconfig so production builds resolve the local SDK shim. - "@microsoft/sharepointembedded-copilotchat-react": path.resolve(__dirname, "./src/lib/sharepointembedded-copilotchat-react/index.tsx"), - - // Force all React imports to use the same instance - "react": path.resolve(__dirname, "./node_modules/react"), - "react-dom": path.resolve(__dirname, "./node_modules/react-dom"), - }, - // Deduplicate React to fix "Cannot read properties of null (reading 'useState')" - // This ensures the SharePoint SDK uses the same React instance as the app - dedupe: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"], - }, - optimizeDeps: { - // Force pre-bundling of these to ensure single instance - include: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"], - // Exclude the SDK from optimization to let it use our React - - }, - build: { - commonjsOptions: { - // Handle CommonJS modules that might bundle their own React - include: [/node_modules/], - transformMixedEsModules: true, }, }, })); diff --git a/Custom Apps/project-management/README.md b/Custom Apps/project-management/README.md index af1d23a..40796cb 100644 --- a/Custom Apps/project-management/README.md +++ b/Custom Apps/project-management/README.md @@ -44,10 +44,7 @@ Start using the application by updating the configuration with your SharePoint E tenantId: "", // Replace with your tenant ID containerTypeId: "", // Replace with your container type ID -# Line 11: Update your domain to authenticate properly - sharePointHostname: "https://.sharepoint.com", - -# Line 16: Update the Client ID +# In the msalConfig.auth block: Update the Client ID clientId: "", // Same as above ``` diff --git a/Custom Apps/project-management/package-lock.json b/Custom Apps/project-management/package-lock.json index 8d9c0eb..11faf38 100644 --- a/Custom Apps/project-management/package-lock.json +++ b/Custom Apps/project-management/package-lock.json @@ -13,7 +13,6 @@ "@fluentui/react": "^8.112.9", "@fluentui/react-icons": "^2.0.220", "@hookform/resolvers": "^3.9.0", - "@microsoft/sharepointembedded-copilotchat-react": "https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", @@ -1160,17 +1159,6 @@ "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==", "license": "MIT" }, - "node_modules/@microsoft/sharepointembedded-copilotchat-react": { - "version": "1.0.8", - "resolved": "https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz", - "integrity": "sha512-fLJiKAprqZ6VQ5OJyUvLVMf05DjNVrNw3wxw4wDydkt9n8CYikRQnqR8OnkquM0UIcBA633ru52+8C8WImW7Gw==", - "license": "MIT", - "peerDependencies": { - "@fluentui/react": ">=8.1.0", - "react": ">=17.0.2", - "react-dom": ">=17.0.2" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/Custom Apps/project-management/package.json b/Custom Apps/project-management/package.json index bf98140..f88d0eb 100644 --- a/Custom Apps/project-management/package.json +++ b/Custom Apps/project-management/package.json @@ -16,7 +16,6 @@ "@fluentui/react": "^8.112.9", "@fluentui/react-icons": "^2.0.220", "@hookform/resolvers": "^3.9.0", - "@microsoft/sharepointembedded-copilotchat-react": "https://download.microsoft.com/download/e2d6b1ec-7168-4787-b8de-4a9862f10744/microsoft-sharepointembedded-copilotchat-react-1.0.8.tgz", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", diff --git a/Custom Apps/project-management/src/components/CopilotChat.tsx b/Custom Apps/project-management/src/components/CopilotChat.tsx deleted file mode 100644 index f313e2d..0000000 --- a/Custom Apps/project-management/src/components/CopilotChat.tsx +++ /dev/null @@ -1,88 +0,0 @@ - -import React, { useEffect, useState } from 'react'; -import CopilotChatContainer from './copilot/CopilotChatContainer'; -import { toast } from '@/hooks/use-toast'; -import { useAuth } from '../context/AuthContext'; -import { useIsMobile } from '@/hooks/use-mobile'; - -interface CopilotChatProps { - containerId: string; - className?: string; -} - -const CopilotChat: React.FC = ({ containerId, className }) => { - const { isAuthenticated } = useAuth(); - const isMobile = useIsMobile(); - const [errorShown, setErrorShown] = useState(false); - - useEffect(() => { - if (!containerId || typeof containerId !== 'string') { - const errorMsg = 'CopilotChat: No valid containerId provided'; - console.error(errorMsg); - - // Only show toast once to avoid spamming - if (!errorShown) { - setErrorShown(true); - toast({ - title: "Copilot Error", - description: "No valid container ID provided. Copilot chat cannot load.", - variant: "destructive", - }); - } - return; - } - - console.log('CopilotChat initialized with containerId:', containerId); - setErrorShown(false); - }, [containerId, errorShown]); - - // Add error boundary via window.addEventListener - useEffect(() => { - const handleError = (event: ErrorEvent) => { - if (event.error && event.error.message && event.error.message.includes('Cannot read properties of undefined')) { - console.error('Caught SharePoint Embedded error:', event.error); - if (!errorShown) { - setErrorShown(true); - toast({ - title: "Copilot Error", - description: "An error occurred while loading the chat component. Please try refreshing.", - variant: "destructive", - }); - } - event.preventDefault(); // Prevent default browser error handling - } - }; - - window.addEventListener('error', handleError); - return () => { - window.removeEventListener('error', handleError); - }; - }, [errorShown]); - - // Early return if no containerId is provided - if (!containerId || typeof containerId !== 'string') { - return ( -
- Error: No valid containerId provided for Copilot Chat -
- ); - } - - // Don't render on mobile devices - if (isMobile) { - return null; - } - - // Don't render if not authenticated - if (!isAuthenticated) { - return null; - } - - return ( -
- -
- ); -}; - -export default CopilotChat; diff --git a/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx b/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx deleted file mode 100644 index 0589f71..0000000 --- a/Custom Apps/project-management/src/components/copilot/CopilotChatContainer.tsx +++ /dev/null @@ -1,169 +0,0 @@ - -import React, { useState, useCallback, useMemo } from 'react'; -import { useCopilotSite } from '@/hooks/useCopilotSite'; -import CopilotDesktopView from './CopilotDesktopView'; -import { toast } from '@/hooks/use-toast'; -import { appConfig } from '@/config/appConfig'; -import { useAuth } from '@/context/AuthContext'; -import { - IChatEmbeddedApiAuthProvider, - ChatEmbeddedAPI, - ChatLaunchConfig -} from '@microsoft/sharepointembedded-copilotchat-react'; - -interface CopilotChatContainerProps { - containerId: string; -} - -const CopilotChatContainer: React.FC = ({ containerId }) => { - const [isOpen, setIsOpen] = useState(true); // Keep the chat component open within its container - const { getSharePointToken, isAuthenticated } = useAuth(); - const [chatApi, setChatApi] = useState(null); - const [chatKey, setChatKey] = useState(0); - - // Validate and normalize containerId - const normalizedContainerId = useMemo(() => { - if (!containerId || typeof containerId !== 'string') { - return ''; - } - - return containerId.startsWith('b!') ? containerId : `b!${containerId}`; - }, [containerId]); - - const { - isLoading, - error, - siteName, - sharePointHostname, - } = useCopilotSite(normalizedContainerId); - - // Ensure we have valid hostnames and site names - const safeSharePointHostname = sharePointHostname || appConfig.sharePointHostname; - const safeSiteName = siteName || 'SharePoint Site'; - - const handleError = useCallback((errorMessage: string) => { - console.error('Copilot chat error:', errorMessage); - toast({ - title: "Copilot error", - description: errorMessage, - variant: "destructive", - }); - }, []); - - // Create auth provider for Copilot chat with better error handling - const authProvider = useMemo((): IChatEmbeddedApiAuthProvider => { - return { - hostname: safeSharePointHostname, - getToken: async () => { - try { - if (!isAuthenticated) { - console.error('User not authenticated, cannot get token'); - return ''; - } - - console.log('Getting SharePoint token for hostname:', safeSharePointHostname); - const token = await getSharePointToken(safeSharePointHostname); - console.log('SharePoint auth token retrieved:', token ? 'successfully' : 'failed'); - - if (!token) { - handleError('Failed to get authentication token for SharePoint.'); - return ''; - } - - return token; - } catch (err) { - console.error('Error getting token for Copilot chat:', err); - handleError('Failed to authenticate with SharePoint. Please try again.'); - return ''; - } - } - }; - }, [safeSharePointHostname, getSharePointToken, handleError, isAuthenticated]); - - // Create chat theme config - const chatTheme = useMemo(() => ({ - useDarkMode: false, - customTheme: { - themePrimary: '#4854EE', - themeSecondary: '#4854EE', - themeDark: '#4854EE', - themeDarker: '#4854EE', - themeTertiary: '#4854EE', - themeLight: '#dddeef', - themeDarkAlt: '#4854EE', - themeLighter: '#dddeef', - themeLighterAlt: '#dddeef', - themeDarkAltTransparent: '#4854EE', - themeLighterTransparent: '#dddeef', - themeLighterAltTransparent: '#dddeef', - themeMedium: '#4854EE', - neutralSecondary: '#4854EE', - neutralSecondaryAlt: '#4854EE', - neutralTertiary: '#4854EE', - neutralTertiaryAlt: '#4854EE', - neutralQuaternary: '#4854EE', - neutralQuaternaryAlt: '#4854EE', - neutralPrimaryAlt: '#4854EE', - neutralDark: '#4854EE', - themeBackground: 'white', - } - }), []); - - // Create chat configuration with instruction to ensure prompt visibility - const chatConfig = useMemo((): ChatLaunchConfig => ({ - header: `SharePoint Embedded - ${safeSiteName}`, - theme: chatTheme, - instruction: "You are a helpful assistant that helps users find and summarize information related to their files and documents.", - locale: "en-US", - // Removed the unsupported properties - }), [safeSiteName, chatTheme]); - - // Reset chat when there's an issue - const handleResetChat = useCallback(() => { - console.log('Resetting Copilot chat'); - setChatKey(prev => prev + 1); - setChatApi(null); - setIsOpen(false); - setTimeout(() => { - setIsOpen(true); - }, 500); - }, []); - - // Handles API ready event from ChatEmbedded component - const handleApiReady = useCallback((api: ChatEmbeddedAPI) => { - if (!api) { - console.error('Chat API is undefined'); - handleError('Chat API initialization failed'); - return; - } - - console.log('Copilot chat API is ready'); - setChatApi(api); - }, [handleError]); - - if (!normalizedContainerId) { - console.error('CopilotChatContainer: Invalid containerId provided:', containerId); - return null; - } - - return ( - - ); -}; - -export default CopilotChatContainer; diff --git a/Custom Apps/project-management/src/components/copilot/CopilotDesktopView.tsx b/Custom Apps/project-management/src/components/copilot/CopilotDesktopView.tsx deleted file mode 100644 index 3dac3c3..0000000 --- a/Custom Apps/project-management/src/components/copilot/CopilotDesktopView.tsx +++ /dev/null @@ -1,167 +0,0 @@ - -import React, { useEffect } from 'react'; -import { Button } from '@/components/ui/button'; -import { RefreshCw } from 'lucide-react'; -import { ChatEmbedded, ChatEmbeddedAPI, IChatEmbeddedApiAuthProvider, ChatLaunchConfig } from '@microsoft/sharepointembedded-copilotchat-react'; - -interface CopilotDesktopViewProps { - isOpen: boolean; - setIsOpen: (value: boolean) => void; - siteName: string; - isLoading: boolean; - error: string | null; - containerId: string; - onError: (errorMessage: string) => void; - chatConfig: ChatLaunchConfig; - authProvider: IChatEmbeddedApiAuthProvider; - onApiReady: (api: ChatEmbeddedAPI) => void; - chatKey: number; - onResetChat?: () => void; - isAuthenticated?: boolean; - chatApi: ChatEmbeddedAPI | null; -} - -const CopilotDesktopView: React.FC = ({ - isOpen, - setIsOpen, - siteName, - isLoading, - error, - containerId, - onError, - chatConfig, - authProvider, - onApiReady, - chatKey, - onResetChat, - isAuthenticated = true, - chatApi -}) => { - // Open the chat when the component is opened and we have a valid chat API - useEffect(() => { - if (!isAuthenticated || !isOpen || !chatApi) { - return; - } - - console.log('Component opened, attempting to open chat...', containerId); - - const openChatOnOpen = async () => { - try { - // Ensure we have required config fields to avoid the undefined error - if (!chatConfig) { - console.error('Chat config is undefined or missing required fields'); - onError('Invalid chat configuration'); - return; - } - - // Add a small delay to ensure the container is ready - setTimeout(async () => { - try { - console.log('Opening chat with config:', JSON.stringify({ - header: chatConfig.header, - locale: chatConfig.locale, - hasTheme: !!chatConfig.theme, - containerId - // Removed references to unsupported properties - })); - - await chatApi.openChat(chatConfig); - console.log('Chat opened successfully'); - } catch (innerErr) { - console.error('Error in delayed chat open:', innerErr); - onError('Failed to load chat interface. Try resetting the chat.'); - } - }, 300); - } catch (err) { - console.error('Error opening chat:', err); - onError('Failed to load chat interface. Try resetting the chat.'); - } - }; - - openChatOnOpen(); - }, [isOpen, chatApi, chatConfig, onError, containerId]); - - if (!isAuthenticated) { - console.log('CopilotDesktopView: Not rendering because not authenticated'); - return null; - } - - // Reset chat when requested - const handleResetChat = () => { - if (onResetChat) { - console.log('Force refreshing chat component'); - onResetChat(); - } - }; - - return ( -
- {isOpen && ( - <> -
-
-

SharePoint Embedded Copilot

-

Connected to: {siteName || 'SharePoint Site'}

-
- {onResetChat && ( - - )} -
- -
- {isLoading ? ( -
-
- Loading... -
- ) : error ? ( -
-

- {error || "Unable to load the chat. Please try again."} -

- {onResetChat && ( - - )} -
- ) : ( -
- -
- )} -
- - )} -
- ); -}; - -export default CopilotDesktopView; diff --git a/Custom Apps/project-management/src/components/copilot/CopilotMobileView.tsx b/Custom Apps/project-management/src/components/copilot/CopilotMobileView.tsx deleted file mode 100644 index ef959b2..0000000 --- a/Custom Apps/project-management/src/components/copilot/CopilotMobileView.tsx +++ /dev/null @@ -1,82 +0,0 @@ - -import React from 'react'; -import { Button } from '@/components/ui/button'; -import { - CustomDrawer, - CustomDrawerContent, - CustomDrawerTrigger, - CustomDrawerTitle -} from '@/components/ui/custom-drawer'; -import { MessageSquare, ExternalLink, Monitor } from 'lucide-react'; - -interface CopilotMobileViewProps { - isOpen: boolean; - setIsOpen: (value: boolean) => void; - siteName: string | null; - isLoading: boolean; - error: string | null; - openExternalChat: (() => void) | null; -} - -const CopilotMobileView: React.FC = ({ - isOpen, - setIsOpen, - siteName, - isLoading, - error, - openExternalChat, -}) => { - return ( - - - - - -
- - SharePoint Embedded Copilot - -
-
- {siteName &&

Connected to: {siteName}

} -
-
- {isLoading ? ( -
-
-
- ) : error ? ( -
-

Could not load Copilot Chat: {error}

-
- ) : ( -
-
- -

Desktop View Recommended

-

For the best experience with Copilot Chat, please use a desktop device or switch to desktop view on your browser.

-
- {openExternalChat && ( - - )} -
- )} -
-
-
- ); -}; - -export default CopilotMobileView; diff --git a/Custom Apps/project-management/src/config/appConfig.ts b/Custom Apps/project-management/src/config/appConfig.ts index ee78de8..4b4c819 100644 --- a/Custom Apps/project-management/src/config/appConfig.ts +++ b/Custom Apps/project-management/src/config/appConfig.ts @@ -7,9 +7,6 @@ export const appConfig = { containerTypeId: "", // Replace with your container type ID appName: "Project Management using SharePoint Embedded", - // Add the SharePoint hostname explicitly - sharePointHostname: "https://.sharepoint.com", - // MSAL configuration msalConfig: { auth: { @@ -30,33 +27,4 @@ export const appConfig = { containers: "/containers", drives: "/drives", }, - - // Copilot theme configuration - copilotTheme: { - useDarkMode: false, - customTheme: { - themePrimary: '#6941C6', - themeSecondary: '#7F56D9', - themeDark: '#5E37BF', - themeDarker: '#4924A1', - themeTertiary: '#9E77ED', - themeLight: '#E9D7FE', - themeDarkAlt: '#7F56D9', - themeLighter: '#F4EBFF', - themeLighterAlt: '#FAF5FF', - themeDarkAltTransparent: 'rgba(111, 66, 193, 0.9)', - themeLighterTransparent: 'rgba(233, 215, 254, 0.9)', - themeLighterAltTransparent: 'rgba(250, 245, 255, 0.9)', - themeMedium: '#9E77ED', - neutralSecondary: '#6941C6', - neutralSecondaryAlt: '#7F56D9', - neutralTertiary: '#9E77ED', - neutralTertiaryAlt: '#B692F6', - neutralQuaternary: '#D6BBFB', - neutralQuaternaryAlt: '#E9D7FE', - neutralPrimaryAlt: '#4924A1', - neutralDark: '#5E37BF', - themeBackground: 'white', - } - } }; diff --git a/Custom Apps/project-management/src/context/AuthContext.tsx b/Custom Apps/project-management/src/context/AuthContext.tsx index 2cae3d7..4e22d13 100644 --- a/Custom Apps/project-management/src/context/AuthContext.tsx +++ b/Custom Apps/project-management/src/context/AuthContext.tsx @@ -14,7 +14,6 @@ interface AuthContextType { login: () => Promise; logout: () => void; getAccessToken: (resource?: string) => Promise; - getSharePointToken: (hostname: string) => Promise; } const AuthContext = createContext(undefined); @@ -148,20 +147,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => } } }; - - // Get a token specifically for SharePoint - const getSharePointToken = async (hostname: string): Promise => { - try { - // Extract the hostname without protocol - const domain = hostname.replace(/^https?:\/\//, ''); - - // Use the SharePoint Online scope format with the specific domain - return await getAccessToken(`https://${domain}`); - } catch (error) { - console.error('Failed to get SharePoint token:', error); - return null; - } - }; return ( = ({ children }) => user, login, logout, - getAccessToken, - getSharePointToken + getAccessToken }}> {children} diff --git a/Custom Apps/project-management/src/hooks/useCopilotSite.ts b/Custom Apps/project-management/src/hooks/useCopilotSite.ts deleted file mode 100644 index 7a211c0..0000000 --- a/Custom Apps/project-management/src/hooks/useCopilotSite.ts +++ /dev/null @@ -1,142 +0,0 @@ - -import { useState, useEffect, useMemo } from 'react'; -import { useAuth } from '../context/AuthContext'; -import { sharePointService } from '../services/sharePointService'; -import { appConfig } from '../config/appConfig'; - -export const useCopilotSite = (containerId: string) => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [siteUrl, setSiteUrl] = useState(null); - const [siteName, setSiteName] = useState(null); - const { getAccessToken, isAuthenticated } = useAuth(); - const [fetchAttempted, setFetchAttempted] = useState(false); - - // Skip all processing if not authenticated - const shouldProcess = isAuthenticated && !!containerId; - - // Normalize container ID to handle different formats - const normalizedContainerId = useMemo(() => { - if (!containerId || !shouldProcess) { - console.log('No containerId provided to useCopilotSite or user not authenticated'); - return ''; - } - - // If it already starts with b!, keep it as is - if (containerId.startsWith('b!')) { - return containerId; - } - - // Otherwise, add the b! prefix - return `b!${containerId}`; - }, [containerId, shouldProcess]); - - useEffect(() => { - // Set default values immediately for safety - if (!siteUrl) { - setSiteUrl(appConfig.sharePointHostname.replace(/\/+$/, '')); - } - - if (!siteName) { - setSiteName('SharePoint Site'); - } - - // Early return if conditions aren't met - if (!shouldProcess || !normalizedContainerId) { - console.log('Skipping site info fetch - not authenticated or no valid containerId'); - return; - } - - // Avoid refetching unnecessarily - if (fetchAttempted) { - console.log('Already attempted to fetch site info, skipping'); - return; - } - - const fetchSiteInfo = async () => { - // Skip if already loading - if (isLoading) return; - - try { - setIsLoading(true); - setError(null); - const token = await getAccessToken(); - if (!token) { - console.error('Authentication token not available'); - setError('Authentication token not available'); - return; - } - - console.log('Fetching container details for:', normalizedContainerId); - const containerDetails = await sharePointService.getContainerDetails(token, normalizedContainerId); - - // Mark as attempted regardless of outcome - setFetchAttempted(true); - - // Set fallback values and handle missing data - if (!containerDetails) { - console.error('Container details are undefined'); - setError('Container details are undefined'); - return; - } - - // Handle name specifically - ensure it's never undefined - const name = containerDetails.name || 'SharePoint Site'; - setSiteName(name); - console.log('Container name retrieved:', name); - - if (!containerDetails.webUrl) { - console.error('Container webUrl is undefined'); - setError('Container webUrl is undefined'); - return; - } - - // Store the site URL without any trailing slashes - const normalizedUrl = containerDetails.webUrl.replace(/\/+$/, ''); - setSiteUrl(normalizedUrl); - console.log('Container webUrl retrieved:', normalizedUrl); - } catch (err) { - console.error('Error fetching site info:', err); - setError('Failed to load site information'); - } finally { - setIsLoading(false); - } - }; - - fetchSiteInfo(); - }, [normalizedContainerId, getAccessToken, isAuthenticated, shouldProcess, fetchAttempted]); - - // Get the base SharePoint hostname (without any paths or trailing slashes) - // This is used for authentication and CSP compatibility - const sharePointHostname = useMemo(() => { - try { - // If no site URL yet, use the default from config - if (!siteUrl) { - const defaultHostname = appConfig.sharePointHostname.replace(/\/+$/, ''); - console.log('Using default SharePoint hostname:', defaultHostname); - return defaultHostname; - } - - // Parse only the hostname part from the URL with protocol - const url = new URL(siteUrl); - const hostname = `${url.protocol}//${url.hostname}`; - console.log('Extracted SharePoint hostname from URL:', hostname); - return hostname; - } catch (e) { - console.error('Error parsing site URL:', e); - // Return default from config as fallback - const fallback = appConfig.sharePointHostname.replace(/\/+$/, ''); - console.log('Using fallback SharePoint hostname after error:', fallback); - return fallback; - } - }, [siteUrl]); - - return { - isLoading, - error, - siteUrl, - siteName, - sharePointHostname, - fetchAttempted - }; -}; diff --git a/Custom Apps/project-management/src/pages/Files.tsx b/Custom Apps/project-management/src/pages/Files.tsx index 6f203ae..1d66a4a 100644 --- a/Custom Apps/project-management/src/pages/Files.tsx +++ b/Custom Apps/project-management/src/pages/Files.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { AlertCircle, FolderPlus, Upload, MessageSquare, FilePlus } from 'lucide-react'; +import { AlertCircle, FolderPlus, Upload, FilePlus } from 'lucide-react'; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { @@ -24,14 +24,12 @@ import FolderNavigation from '@/components/files/FolderNavigation'; import FilePreviewDialog from '@/components/files/FilePreviewDialog'; import FileUploadProgress from '@/components/files/FileUploadProgress'; import CreateOfficeFileDialog from '@/components/files/CreateOfficeFileDialog'; -import CopilotChat from '@/components/CopilotChat'; import { useFiles } from '@/hooks/useFiles'; import { useContainerDetails } from '@/hooks/useContainerDetails'; import { useFilePreview } from '@/hooks/useFilePreview'; import { useAuth } from '@/context/AuthContext'; import { sharePointService } from '@/services/sharePointService'; import { toast } from '@/hooks/use-toast'; -import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"; const Files = () => { const { containerId } = useParams<{ containerId: string }>(); @@ -42,8 +40,6 @@ const Files = () => { const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false); const [isCreatingFolder, setIsCreatingFolder] = useState(false); const [isOfficeFileDialogOpen, setIsOfficeFileDialogOpen] = useState(false); - const [isCopilotOpen, setIsCopilotOpen] = useState(false); - const [copilotSize, setCopilotSize] = useState(30); const { files, @@ -208,18 +204,6 @@ const Files = () => { }); }; - // Handle resize event from the resizable panel - const handleResize = (sizes: number[]) => { - if (sizes.length > 0) { - setCopilotSize(100 - sizes[0]); - } - }; - - // Toggle the Copilot panel - const toggleCopilot = () => { - setIsCopilotOpen(!isCopilotOpen); - }; - // Get project name with proper loading and error handling const getProjectDisplayName = () => { if (detailsLoading) { @@ -300,18 +284,6 @@ const Files = () => { /> - - {/* Copilot Chat Button */} - {containerId && ( - - )} @@ -344,58 +316,16 @@ const Files = () => { /> )} - {/* Main content with resizable panels */} -
- - {/* File List panel - always visible */} - -
- handleFolderClick(item.id, item.name)} - onFileClick={handleViewFile} - onViewFile={handleViewFile} - onDeleteFile={handleDeleteFile} - containerId={containerId ? normalizeContainerId(containerId) : ''} - /> -
-
- - {/* Copilot panel - only visible when opened */} - {isCopilotOpen && ( - <> - - - -
-
-
-

SharePoint AI Copilot

-

- {containerDetails?.name || 'AI Assistant'} -

-
- -
- - {/* CopilotChat with full height to ensure prompt is visible */} - {containerId && ( -
- -
- )} -
-
- - )} -
+
+ handleFolderClick(item.id, item.name)} + onFileClick={handleViewFile} + onViewFile={handleViewFile} + onDeleteFile={handleDeleteFile} + containerId={containerId ? normalizeContainerId(containerId) : ''} + />