diff --git a/cypress/e2e/copilot/spec.cy.ts b/cypress/e2e/copilot/spec.cy.ts index 2deb0a5214..b6e8bc8003 100644 --- a/cypress/e2e/copilot/spec.cy.ts +++ b/cypress/e2e/copilot/spec.cy.ts @@ -302,4 +302,57 @@ describe('Copilot', { includeShadowDom: true }, () => { }); }); }); + + describe('Theme', () => { + beforeEach(() => { + cy.window().then((win) => { + win.localStorage.removeItem('vite-ui-theme'); + }); + }); + + it('should be able to change theme programmatically', () => { + mountCopilotWidget(); + cy.window().should('have.property', 'setChainlitCopilotTheme'); + + cy.step('Change to dark theme'); + cy.window().then((win) => { + win.setChainlitCopilotTheme('dark'); + }); + + cy.get('#chainlit-copilot') + .shadow() + .find('#cl-shadow-root') + .should('have.class', 'dark'); + cy.window().then((win) => { + expect(win.localStorage.getItem('vite-ui-theme')).to.equal('dark'); + }); + + cy.step('Change to light theme'); + cy.window().then((win) => { + win.setChainlitCopilotTheme('light'); + }); + + cy.get('#chainlit-copilot') + .shadow() + .find('#cl-shadow-root') + .should('have.class', 'light'); + cy.window().then((win) => { + expect(win.localStorage.getItem('vite-ui-theme')).to.equal('light'); + }); + }); + + it('should respect persisted theme on mount', () => { + cy.step('Pre-set dark theme in localStorage'); + cy.window().then((win) => { + win.localStorage.setItem('vite-ui-theme', 'dark'); + }); + + mountCopilotWidget(); + + cy.get('#chainlit-copilot') + .shadow() + .find('#cl-shadow-root') + .should('have.class', 'dark'); + }); + }); }); diff --git a/libs/copilot/index.tsx b/libs/copilot/index.tsx index 9dc840612e..10040ac779 100644 --- a/libs/copilot/index.tsx +++ b/libs/copilot/index.tsx @@ -33,6 +33,7 @@ declare global { sendChainlitMessage: (message: IStep) => void; getChainlitCopilotThreadId: () => string | null; clearChainlitCopilotThreadId: (newThreadId?: string) => void; + setChainlitCopilotTheme: (theme: any) => void; } } diff --git a/libs/copilot/src/app.tsx b/libs/copilot/src/app.tsx index 256ecabd1e..9dbc9e9bd7 100644 --- a/libs/copilot/src/app.tsx +++ b/libs/copilot/src/app.tsx @@ -28,6 +28,7 @@ declare global { }; getChainlitCopilotThreadId: () => string | null; clearChainlitCopilotThreadId: (newThreadId?: string) => void; + setChainlitCopilotTheme: (theme: any) => void; } } diff --git a/libs/copilot/src/widget.tsx b/libs/copilot/src/widget.tsx index e2485f316a..61df71c884 100644 --- a/libs/copilot/src/widget.tsx +++ b/libs/copilot/src/widget.tsx @@ -13,6 +13,7 @@ import { useConfig } from '@chainlit/react-client'; import Header from './components/Header'; +import { useTheme } from './ThemeProvider'; import ChatWrapper from './chat'; import { useSidebarResize } from './hooks'; import { LS_DISPLAY_MODE_KEY, resolveDisplayMode } from './resolveDisplayMode'; @@ -28,6 +29,7 @@ interface Props { } const Widget = ({ config, error }: Props) => { + const { setTheme } = useTheme(); const [expanded, setExpanded] = useState(config?.expanded || false); const [isOpen, setIsOpen] = useState(config?.opened || false); const [displayMode, setDisplayMode] = useState(() => @@ -43,6 +45,7 @@ const Widget = ({ config, error }: Props) => { window.toggleChainlitCopilot = () => setIsOpen((prev) => !prev); window.getChainlitCopilotThreadId = getChainlitCopilotThreadId; window.clearChainlitCopilotThreadId = clearChainlitCopilotThreadId; + window.setChainlitCopilotTheme = setTheme; return () => { window.toggleChainlitCopilot = () => console.error('Widget not mounted.'); @@ -50,8 +53,10 @@ const Widget = ({ config, error }: Props) => { window.clearChainlitCopilotThreadId = () => console.error('Widget not mounted.'); + window.setChainlitCopilotTheme = () => + console.error('Widget not mounted.'); }; - }, []); + }, [setTheme]); useEffect(() => { localStorage.setItem(LS_DISPLAY_MODE_KEY, displayMode);