Skip to content
53 changes: 53 additions & 0 deletions cypress/e2e/copilot/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
1 change: 1 addition & 0 deletions libs/copilot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ declare global {
sendChainlitMessage: (message: IStep) => void;
getChainlitCopilotThreadId: () => string | null;
clearChainlitCopilotThreadId: (newThreadId?: string) => void;
setChainlitCopilotTheme: (theme: any) => void;
}
}

Expand Down
1 change: 1 addition & 0 deletions libs/copilot/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ declare global {
};
getChainlitCopilotThreadId: () => string | null;
clearChainlitCopilotThreadId: (newThreadId?: string) => void;
setChainlitCopilotTheme: (theme: any) => void;
}
}

Expand Down
7 changes: 6 additions & 1 deletion libs/copilot/src/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<DisplayMode>(() =>
Expand All @@ -43,15 +45,18 @@ 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.');
window.getChainlitCopilotThreadId = () => null;

window.clearChainlitCopilotThreadId = () =>
console.error('Widget not mounted.');
window.setChainlitCopilotTheme = () =>
console.error('Widget not mounted.');
};
}, []);
}, [setTheme]);

useEffect(() => {
localStorage.setItem(LS_DISPLAY_MODE_KEY, displayMode);
Expand Down