chore: import upstream snapshot with attribution
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../stories/**/*.mdx',
|
||||
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions'
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {}
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag'
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import '../src/index.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@chainlit/app/src/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@chainlit/app/src/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { type IStep } from '@chainlit/react-client';
|
||||
|
||||
// @ts-expect-error inline css
|
||||
import sonnercss from './sonner.css?inline';
|
||||
// @ts-expect-error inline css
|
||||
import tailwindcss from './src/index.css?inline';
|
||||
// @ts-expect-error inline css
|
||||
import hljscss from 'highlight.js/styles/monokai-sublime.css?inline';
|
||||
|
||||
import AppWrapper from './src/appWrapper';
|
||||
import {
|
||||
clearChainlitCopilotThreadId,
|
||||
getChainlitCopilotThreadId
|
||||
} from './src/state';
|
||||
import { IWidgetConfig } from './src/types';
|
||||
|
||||
const id = 'chainlit-copilot';
|
||||
let root: ReactDOM.Root | null = null;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
cl_shadowRootElement: HTMLDivElement;
|
||||
theme?: {
|
||||
light: Record<string, string>;
|
||||
dark: Record<string, string>;
|
||||
};
|
||||
mountChainlitWidget: (config: IWidgetConfig) => void;
|
||||
unmountChainlitWidget: () => void;
|
||||
toggleChainlitCopilot: () => void;
|
||||
sendChainlitMessage: (message: IStep) => void;
|
||||
getChainlitCopilotThreadId: () => string | null;
|
||||
clearChainlitCopilotThreadId: (newThreadId?: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
window.mountChainlitWidget = (config: IWidgetConfig) => {
|
||||
const container = document.createElement('div');
|
||||
container.id = id;
|
||||
document.body.appendChild(container);
|
||||
|
||||
const shadowContainer = container.attachShadow({ mode: 'open' });
|
||||
const shadowRootElement = document.createElement('div');
|
||||
shadowRootElement.id = 'cl-shadow-root';
|
||||
shadowContainer.appendChild(shadowRootElement);
|
||||
|
||||
window.cl_shadowRootElement = shadowRootElement;
|
||||
|
||||
root = ReactDOM.createRoot(shadowRootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<style type="text/css">{tailwindcss.toString()}</style>
|
||||
<style type="text/css">{sonnercss.toString()}</style>
|
||||
<style type="text/css">{hljscss.toString()}</style>
|
||||
<AppWrapper widgetConfig={config} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
};
|
||||
|
||||
window.unmountChainlitWidget = () => {
|
||||
root?.unmount();
|
||||
};
|
||||
|
||||
window.sendChainlitMessage = () => {
|
||||
console.info('Copilot is not active. Please check if the widget is mounted.');
|
||||
};
|
||||
|
||||
window.getChainlitCopilotThreadId = getChainlitCopilotThreadId;
|
||||
window.clearChainlitCopilotThreadId = clearChainlitCopilotThreadId;
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@chainlit/copilot",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "echo 'SKIPPED: copilot type-check disabled due to cross-project path alias mismatch. See docs/research/copilot-type-checking.md'",
|
||||
"test": "echo no tests yet",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chainlit/app": "workspace:^",
|
||||
"@chainlit/react-client": "workspace:^",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^23.7.16",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^14.0.0",
|
||||
"recoil": "^0.7.7",
|
||||
"sonner": "^1.2.3",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.4.7",
|
||||
"@storybook/addon-interactions": "^8.4.7",
|
||||
"@storybook/addon-links": "^8.4.7",
|
||||
"@storybook/addon-onboarding": "^1.0.11",
|
||||
"@storybook/blocks": "^8.4.7",
|
||||
"@storybook/react": "^8.4.7",
|
||||
"@storybook/react-vite": "^8.4.7",
|
||||
"@storybook/test": "^8.4.7",
|
||||
"@swc/core": "^1.3.86",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/node": "^20.5.7",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-file-icon": "^1.0.2",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"storybook": "^8.4.7",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.4.14",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite@>=4.4.0 <4.4.12": ">=4.4.12",
|
||||
"vite@>=4.0.0 <=4.5.1": ">=4.5.2",
|
||||
"express@<4.19.2": ">=4.19.2",
|
||||
"vite@>=4.0.0 <=4.5.2": ">=4.5.3",
|
||||
"tar@<6.2.1": ">=6.2.1",
|
||||
"braces@<3.0.3": ">=3.0.3",
|
||||
"ejs@<3.1.10": ">=3.1.10",
|
||||
"ws@>=8.0.0 <8.17.1": ">=8.17.1",
|
||||
"micromatch@<4.0.8": ">=4.0.8",
|
||||
"body-parser@<1.20.3": ">=1.20.3",
|
||||
"send@<0.19.0": ">=0.19.0",
|
||||
"serve-static@<1.16.0": ">=1.16.0",
|
||||
"express@<4.20.0": ">=4.20.0",
|
||||
"path-to-regexp@<0.1.10": ">=0.1.10",
|
||||
"vite@>=4.0.0 <4.5.4": ">=4.5.4",
|
||||
"vite@>=4.0.0 <=4.5.3": ">=4.5.4",
|
||||
"rollup@>=3.0.0 <3.29.5": ">=3.29.5",
|
||||
"cookie@<0.7.0": ">=0.7.0",
|
||||
"cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+4887
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,690 @@
|
||||
:where(html[dir='ltr']),
|
||||
:where([data-sonner-toaster][dir='ltr']) {
|
||||
--toast-icon-margin-start: -3px;
|
||||
--toast-icon-margin-end: 4px;
|
||||
--toast-svg-margin-start: -1px;
|
||||
--toast-svg-margin-end: 0px;
|
||||
--toast-button-margin-start: auto;
|
||||
--toast-button-margin-end: 0;
|
||||
--toast-close-button-start: 0;
|
||||
--toast-close-button-end: unset;
|
||||
--toast-close-button-transform: translate(-35%, -35%);
|
||||
}
|
||||
|
||||
:where(html[dir='rtl']),
|
||||
:where([data-sonner-toaster][dir='rtl']) {
|
||||
--toast-icon-margin-start: 4px;
|
||||
--toast-icon-margin-end: -3px;
|
||||
--toast-svg-margin-start: 0px;
|
||||
--toast-svg-margin-end: -1px;
|
||||
--toast-button-margin-start: 0;
|
||||
--toast-button-margin-end: auto;
|
||||
--toast-close-button-start: unset;
|
||||
--toast-close-button-end: 0;
|
||||
--toast-close-button-transform: translate(35%, -35%);
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster]) {
|
||||
position: fixed;
|
||||
width: var(--width);
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
Segoe UI,
|
||||
Roboto,
|
||||
Helvetica Neue,
|
||||
Arial,
|
||||
Noto Sans,
|
||||
sans-serif,
|
||||
Apple Color Emoji,
|
||||
Segoe UI Emoji,
|
||||
Segoe UI Symbol,
|
||||
Noto Color Emoji;
|
||||
--gray1: hsl(0, 0%, 99%);
|
||||
--gray2: hsl(0, 0%, 97.3%);
|
||||
--gray3: hsl(0, 0%, 95.1%);
|
||||
--gray4: hsl(0, 0%, 93%);
|
||||
--gray5: hsl(0, 0%, 90.9%);
|
||||
--gray6: hsl(0, 0%, 88.7%);
|
||||
--gray7: hsl(0, 0%, 85.8%);
|
||||
--gray8: hsl(0, 0%, 78%);
|
||||
--gray9: hsl(0, 0%, 56.1%);
|
||||
--gray10: hsl(0, 0%, 52.3%);
|
||||
--gray11: hsl(0, 0%, 43.5%);
|
||||
--gray12: hsl(0, 0%, 9%);
|
||||
--border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
z-index: 999999999;
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-x-position='right']) {
|
||||
right: max(var(--offset), env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-x-position='left']) {
|
||||
left: max(var(--offset), env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-x-position='center']) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-y-position='top']) {
|
||||
top: max(var(--offset), env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-y-position='bottom']) {
|
||||
bottom: max(var(--offset), env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) {
|
||||
--y: translateY(100%);
|
||||
--lift-amount: calc(var(--lift) * var(--gap));
|
||||
z-index: var(--z-index);
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transform: var(--y);
|
||||
filter: blur(0);
|
||||
/* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */
|
||||
touch-action: none;
|
||||
transition:
|
||||
transform 400ms,
|
||||
opacity 400ms,
|
||||
height 400ms,
|
||||
box-shadow 200ms;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-styled='true']) {
|
||||
padding: 16px;
|
||||
background: var(--normal-bg);
|
||||
border: 1px solid var(--normal-border);
|
||||
color: var(--normal-text);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||
width: var(--width);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]:focus-visible) {
|
||||
box-shadow:
|
||||
0px 4px 12px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-y-position='top']) {
|
||||
top: 0;
|
||||
--y: translateY(-100%);
|
||||
--lift: 1;
|
||||
--lift-amount: calc(1 * var(--gap));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-y-position='bottom']) {
|
||||
bottom: 0;
|
||||
--y: translateY(100%);
|
||||
--lift: -1;
|
||||
--lift-amount: calc(var(--lift) * var(--gap));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-description]) {
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-title]) {
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-icon]) {
|
||||
display: flex;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--toast-icon-margin-start);
|
||||
margin-right: var(--toast-icon-margin-end);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transform-origin: center;
|
||||
animation: sonner-fade-in 300ms ease forwards;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-icon]) > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-icon]) svg {
|
||||
margin-left: var(--toast-svg-margin-start);
|
||||
margin-right: var(--toast-svg-margin-end);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-content]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-styled='true'] [data-button] {
|
||||
border-radius: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
color: var(--normal-bg);
|
||||
background: var(--normal-text);
|
||||
margin-left: var(--toast-button-margin-start);
|
||||
margin-right: var(--toast-button-margin-end);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
opacity 400ms,
|
||||
box-shadow 200ms;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-button]):focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-button]):first-of-type {
|
||||
margin-left: var(--toast-button-margin-start);
|
||||
margin-right: var(--toast-button-margin-end);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-cancel]) {
|
||||
color: var(--normal-text);
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-close-button]) {
|
||||
position: absolute;
|
||||
left: var(--toast-close-button-start);
|
||||
right: var(--toast-close-button-end);
|
||||
top: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
background: var(--gray1);
|
||||
color: var(--gray12);
|
||||
border: 1px solid var(--gray4);
|
||||
transform: var(--toast-close-button-transform);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition:
|
||||
opacity 100ms,
|
||||
background 200ms,
|
||||
border-color 200ms;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-close-button]):focus-visible {
|
||||
box-shadow:
|
||||
0px 4px 12px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-disabled='true']) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]):hover :where([data-close-button]):hover {
|
||||
background: var(--gray2);
|
||||
border-color: var(--gray5);
|
||||
}
|
||||
|
||||
/* Leave a ghost div to avoid setting hover to false when swiping out */
|
||||
:where([data-sonner-toast][data-swiping='true'])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
:where(
|
||||
[data-sonner-toast][data-y-position='top'][data-swiping='true']
|
||||
)::before {
|
||||
/* y 50% needed to distribute height additional height evenly */
|
||||
bottom: 50%;
|
||||
transform: scaleY(3) translateY(50%);
|
||||
}
|
||||
|
||||
:where(
|
||||
[data-sonner-toast][data-y-position='bottom'][data-swiping='true']
|
||||
)::before {
|
||||
/* y -50% needed to distribute height additional height evenly */
|
||||
top: 50%;
|
||||
transform: scaleY(3) translateY(-50%);
|
||||
}
|
||||
|
||||
/* Leave a ghost div to avoid setting hover to false when transitioning out */
|
||||
:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: scaleY(2);
|
||||
}
|
||||
|
||||
/* Needed to avoid setting hover to false when inbetween toasts */
|
||||
:where([data-sonner-toast])::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: calc(var(--gap) + 1px);
|
||||
bottom: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-mounted='true']) {
|
||||
--y: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-expanded='false'][data-front='false']) {
|
||||
--scale: var(--toasts-before) * 0.05 + 1;
|
||||
--y: translateY(calc(var(--lift-amount) * var(--toasts-before)))
|
||||
scale(calc(-1 * var(--scale)));
|
||||
height: var(--front-toast-height);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) > * {
|
||||
transition: opacity 400ms;
|
||||
}
|
||||
|
||||
:where(
|
||||
[data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']
|
||||
)
|
||||
> * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-visible='false']) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) {
|
||||
--y: translateY(calc(var(--lift) * var(--offset)));
|
||||
height: var(--initial-height);
|
||||
}
|
||||
|
||||
:where(
|
||||
[data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']
|
||||
) {
|
||||
--y: translateY(calc(var(--lift) * -100%));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:where(
|
||||
[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']
|
||||
) {
|
||||
--y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:where(
|
||||
[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']
|
||||
) {
|
||||
--y: translateY(40%);
|
||||
opacity: 0;
|
||||
transition:
|
||||
transform 500ms,
|
||||
opacity 200ms;
|
||||
}
|
||||
|
||||
/* Bump up the height to make sure hover state doesn't get set to false */
|
||||
:where([data-sonner-toast][data-removed='true'][data-front='false'])::before {
|
||||
height: calc(var(--initial-height) + 20%);
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-swiping='true'] {
|
||||
transform: var(--y) translateY(var(--swipe-amount, 0px));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
|
||||
[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
|
||||
animation: swipe-out 200ms ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes swipe-out {
|
||||
from {
|
||||
transform: translateY(
|
||||
calc(var(--lift) * var(--offset) + var(--swipe-amount))
|
||||
);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(
|
||||
calc(
|
||||
var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%
|
||||
)
|
||||
);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
[data-sonner-toaster] {
|
||||
position: fixed;
|
||||
--mobile-offset: 16px;
|
||||
right: var(--mobile-offset);
|
||||
left: var(--mobile-offset);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-sonner-toaster] [data-sonner-toast] {
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: calc(100% - var(--mobile-offset) * 2);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-x-position='left'] {
|
||||
left: var(--mobile-offset);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-y-position='bottom'] {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-y-position='top'] {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-x-position='center'] {
|
||||
left: var(--mobile-offset);
|
||||
right: var(--mobile-offset);
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='light'] {
|
||||
--normal-bg: #fff;
|
||||
--normal-border: var(--gray4);
|
||||
--normal-text: var(--gray12);
|
||||
|
||||
--success-bg: hsl(143, 85%, 96%);
|
||||
--success-border: hsl(145, 92%, 91%);
|
||||
--success-text: hsl(140, 100%, 27%);
|
||||
|
||||
--info-bg: hsl(208, 100%, 97%);
|
||||
--info-border: hsl(221, 91%, 91%);
|
||||
--info-text: hsl(210, 92%, 45%);
|
||||
|
||||
--warning-bg: hsl(49, 100%, 97%);
|
||||
--warning-border: hsl(49, 91%, 91%);
|
||||
--warning-text: hsl(31, 92%, 45%);
|
||||
|
||||
--error-bg: hsl(359, 100%, 97%);
|
||||
--error-border: hsl(359, 100%, 94%);
|
||||
--error-text: hsl(360, 100%, 45%);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='light']
|
||||
[data-sonner-toast][data-invert='true'] {
|
||||
--normal-bg: #000;
|
||||
--normal-border: hsl(0, 0%, 20%);
|
||||
--normal-text: var(--gray1);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='dark']
|
||||
[data-sonner-toast][data-invert='true'] {
|
||||
--normal-bg: #fff;
|
||||
--normal-border: var(--gray3);
|
||||
--normal-text: var(--gray12);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='dark'] {
|
||||
--normal-bg: #000;
|
||||
--normal-border: hsl(0, 0%, 20%);
|
||||
--normal-text: var(--gray1);
|
||||
|
||||
--success-bg: hsl(150, 100%, 6%);
|
||||
--success-border: hsl(147, 100%, 12%);
|
||||
--success-text: hsl(150, 86%, 65%);
|
||||
|
||||
--info-bg: hsl(215, 100%, 6%);
|
||||
--info-border: hsl(223, 100%, 12%);
|
||||
--info-text: hsl(216, 87%, 65%);
|
||||
|
||||
--warning-bg: hsl(64, 100%, 6%);
|
||||
--warning-border: hsl(60, 100%, 12%);
|
||||
--warning-text: hsl(46, 87%, 65%);
|
||||
|
||||
--error-bg: hsl(358, 76%, 10%);
|
||||
--error-border: hsl(357, 89%, 16%);
|
||||
--error-text: hsl(358, 100%, 81%);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
|
||||
background: var(--success-bg);
|
||||
border-color: var(--success-border);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='success']
|
||||
[data-close-button] {
|
||||
background: var(--success-bg);
|
||||
border-color: var(--success-border);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
|
||||
background: var(--info-bg);
|
||||
border-color: var(--info-border);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='info']
|
||||
[data-close-button] {
|
||||
background: var(--info-bg);
|
||||
border-color: var(--info-border);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
|
||||
background: var(--warning-bg);
|
||||
border-color: var(--warning-border);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='warning']
|
||||
[data-close-button] {
|
||||
background: var(--warning-bg);
|
||||
border-color: var(--warning-border);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
|
||||
background: var(--error-bg);
|
||||
border-color: var(--error-border);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='error']
|
||||
[data-close-button] {
|
||||
background: var(--error-bg);
|
||||
border-color: var(--error-border);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.sonner-loading-wrapper {
|
||||
--size: 16px;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sonner-loading-wrapper[data-visible='false'] {
|
||||
transform-origin: center;
|
||||
animation: sonner-fade-out 0.2s ease forwards;
|
||||
}
|
||||
|
||||
.sonner-spinner {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
.sonner-loading-bar {
|
||||
animation: sonner-spin 1.2s linear infinite;
|
||||
background: var(--gray11);
|
||||
border-radius: 6px;
|
||||
height: 8%;
|
||||
left: -10%;
|
||||
position: absolute;
|
||||
top: -3.9%;
|
||||
width: 24%;
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(1) {
|
||||
animation-delay: -1.2s;
|
||||
transform: rotate(0.0001deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(2) {
|
||||
animation-delay: -1.1s;
|
||||
transform: rotate(30deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(3) {
|
||||
animation-delay: -1s;
|
||||
transform: rotate(60deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(4) {
|
||||
animation-delay: -0.9s;
|
||||
transform: rotate(90deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(5) {
|
||||
animation-delay: -0.8s;
|
||||
transform: rotate(120deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(6) {
|
||||
animation-delay: -0.7s;
|
||||
transform: rotate(150deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(7) {
|
||||
animation-delay: -0.6s;
|
||||
transform: rotate(180deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(8) {
|
||||
animation-delay: -0.5s;
|
||||
transform: rotate(210deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(9) {
|
||||
animation-delay: -0.4s;
|
||||
transform: rotate(240deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(10) {
|
||||
animation-delay: -0.3s;
|
||||
transform: rotate(270deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(11) {
|
||||
animation-delay: -0.2s;
|
||||
transform: rotate(300deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(12) {
|
||||
animation-delay: -0.1s;
|
||||
transform: rotate(330deg) translate(146%);
|
||||
}
|
||||
|
||||
@keyframes sonner-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sonner-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sonner-spin {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
[data-sonner-toast],
|
||||
[data-sonner-toast] > *,
|
||||
.sonner-loading-bar {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sonner-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center;
|
||||
transition:
|
||||
opacity 200ms,
|
||||
transform 200ms;
|
||||
}
|
||||
|
||||
.sonner-loader[data-visible='false'] {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translate(-50%, -50%);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: 'system',
|
||||
setTheme: () => null
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
function applyThemeVariables(variant: 'dark' | 'light') {
|
||||
if (!window.theme) return;
|
||||
|
||||
const variables = window.theme[variant];
|
||||
if (!variables) return;
|
||||
|
||||
const shadowContainer = window.cl_shadowRootElement;
|
||||
if (!shadowContainer) return;
|
||||
|
||||
// Apply new theme variables
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
shadowContainer.style.setProperty(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'vite-ui-theme',
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const shadowContainer = window.cl_shadowRootElement;
|
||||
if (!shadowContainer) return;
|
||||
|
||||
// Remove existing theme classes
|
||||
shadowContainer.classList.remove('light', 'dark');
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
shadowContainer.classList.add(systemTheme);
|
||||
applyThemeVariables(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
shadowContainer.classList.add(theme);
|
||||
applyThemeVariables(theme);
|
||||
}, [theme]);
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
if (theme !== 'system') return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = () => {
|
||||
const shadowContainer = window.cl_shadowRootElement;
|
||||
if (!shadowContainer) return;
|
||||
|
||||
const newTheme = mediaQuery.matches ? 'dark' : 'light';
|
||||
shadowContainer.classList.remove('light', 'dark');
|
||||
shadowContainer.classList.add(newTheme);
|
||||
applyThemeVariables(newTheme);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
|
||||
const variant = context.theme === 'system' ? systemTheme : context.theme;
|
||||
|
||||
return { ...context, variant };
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { ChainlitAPI, ClientError } from '@chainlit/react-client';
|
||||
|
||||
export function makeApiClient(
|
||||
chainlitServer: string,
|
||||
additionalQueryParams: Record<string, string>
|
||||
) {
|
||||
const httpEndpoint = chainlitServer;
|
||||
|
||||
const on401 = () => {
|
||||
toast.error('Unauthorized');
|
||||
};
|
||||
|
||||
const onError = (error: ClientError) => {
|
||||
toast.error(error.toString());
|
||||
};
|
||||
|
||||
return new ChainlitAPI(
|
||||
httpEndpoint,
|
||||
'copilot',
|
||||
additionalQueryParams,
|
||||
on401,
|
||||
onError
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { Toaster } from 'sonner';
|
||||
import { IWidgetConfig } from 'types';
|
||||
import Widget from 'widget';
|
||||
|
||||
import { useTranslation } from '@chainlit/app/src/components/i18n/Translator';
|
||||
import { ChainlitContext, useAuth } from '@chainlit/react-client';
|
||||
|
||||
import { useCopilotInteract } from './hooks/useCopilotInteract';
|
||||
|
||||
import { ThemeProvider } from './ThemeProvider';
|
||||
import {
|
||||
COPILOT_THREAD_CHANGED_EVENT_KEY,
|
||||
CopilotThreadChangedEventParams
|
||||
} from './state';
|
||||
|
||||
interface Props {
|
||||
widgetConfig: IWidgetConfig;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
cl_shadowRootElement: HTMLDivElement;
|
||||
toggleChainlitCopilot: () => void;
|
||||
theme?: {
|
||||
light: Record<string, string>;
|
||||
dark: Record<string, string>;
|
||||
};
|
||||
getChainlitCopilotThreadId: () => string | null;
|
||||
clearChainlitCopilotThreadId: (newThreadId?: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export default function App({ widgetConfig }: Props) {
|
||||
const { isAuthenticated, data } = useAuth();
|
||||
const apiClient = useContext(ChainlitContext);
|
||||
const { i18n } = useTranslation();
|
||||
const { startNewChat } = useCopilotInteract();
|
||||
const languageInUse = widgetConfig.language || navigator.language || 'en-US';
|
||||
const [authError, setAuthError] = useState<string>();
|
||||
const [fetchError, setFetchError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
apiClient
|
||||
.get(`/project/translations?language=${languageInUse}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
i18n.addResourceBundle(languageInUse, 'translation', data.translation);
|
||||
i18n.changeLanguage(languageInUse);
|
||||
})
|
||||
.catch((err) => {
|
||||
setFetchError(String(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const defaultTheme = widgetConfig.theme || data?.default_theme;
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchError) return;
|
||||
if (!isAuthenticated) {
|
||||
if (!widgetConfig.accessToken) {
|
||||
setAuthError('No authentication token provided.');
|
||||
} else {
|
||||
apiClient
|
||||
.jwtAuth(widgetConfig.accessToken)
|
||||
.catch((err) => setAuthError(String(err)));
|
||||
}
|
||||
} else {
|
||||
setAuthError(undefined);
|
||||
}
|
||||
}, [isAuthenticated, apiClient, fetchError, setAuthError]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventListener = (e: Event) => {
|
||||
const customEvent = e as CustomEvent<CopilotThreadChangedEventParams>;
|
||||
startNewChat(customEvent?.detail?.newThreadId);
|
||||
};
|
||||
|
||||
window.addEventListener(COPILOT_THREAD_CHANGED_EVENT_KEY, eventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
COPILOT_THREAD_CHANGED_EVENT_KEY,
|
||||
eventListener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider storageKey="vite-ui-theme" defaultTheme={defaultTheme}>
|
||||
<Toaster className="toast" position="bottom-center" />
|
||||
<Widget config={widgetConfig} error={fetchError || authError} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { makeApiClient } from 'api';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { IWidgetConfig } from 'types';
|
||||
|
||||
import { i18nSetupLocalization } from '@chainlit/app/src/i18n';
|
||||
import { ChainlitContext } from '@chainlit/react-client';
|
||||
|
||||
import App from './app';
|
||||
|
||||
i18nSetupLocalization();
|
||||
interface Props {
|
||||
widgetConfig: IWidgetConfig;
|
||||
}
|
||||
|
||||
export default function AppWrapper({ widgetConfig }: Props) {
|
||||
const additionalQueryParams = widgetConfig?.additionalQueryParamsForAPI;
|
||||
const apiClient = makeApiClient(
|
||||
widgetConfig.chainlitServer,
|
||||
additionalQueryParams || {}
|
||||
);
|
||||
const [customThemeLoaded, setCustomThemeLoaded] = useState(false);
|
||||
|
||||
function completeInitialization() {
|
||||
if (widgetConfig.customCssUrl) {
|
||||
const linkEl = document.createElement('link');
|
||||
linkEl.rel = 'stylesheet';
|
||||
linkEl.href = widgetConfig.customCssUrl;
|
||||
window.cl_shadowRootElement.getRootNode().appendChild(linkEl);
|
||||
}
|
||||
setCustomThemeLoaded(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let fontLoaded = false;
|
||||
|
||||
apiClient
|
||||
.get('/public/theme.json')
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const customTheme = await res.json();
|
||||
if (customTheme.custom_fonts?.length) {
|
||||
fontLoaded = true;
|
||||
customTheme.custom_fonts.forEach((href: string) => {
|
||||
const linkEl = document.createElement('link');
|
||||
linkEl.rel = 'stylesheet';
|
||||
linkEl.href = href;
|
||||
window.cl_shadowRootElement.getRootNode().appendChild(linkEl);
|
||||
});
|
||||
}
|
||||
if (customTheme.variables) {
|
||||
window.theme = customTheme.variables;
|
||||
}
|
||||
} finally {
|
||||
// If no custom font, default to Inter
|
||||
if (!fontLoaded) {
|
||||
const linkEl = document.createElement('link');
|
||||
linkEl.rel = 'stylesheet';
|
||||
linkEl.href =
|
||||
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap';
|
||||
window.cl_shadowRootElement.getRootNode().appendChild(linkEl);
|
||||
}
|
||||
completeInitialization();
|
||||
}
|
||||
})
|
||||
.catch(() => completeInitialization());
|
||||
}, []);
|
||||
|
||||
if (!customThemeLoaded) return null;
|
||||
|
||||
return (
|
||||
<ChainlitContext.Provider value={apiClient}>
|
||||
<RecoilRoot>
|
||||
<App widgetConfig={widgetConfig} />
|
||||
</RecoilRoot>
|
||||
</ChainlitContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { toast } from 'sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Alert from '@chainlit/app/src/components/Alert';
|
||||
import ChatSettingsModal from '@chainlit/app/src/components/ChatSettings';
|
||||
import { ErrorBoundary } from '@chainlit/app/src/components/ErrorBoundary';
|
||||
import { TaskList } from '@chainlit/app/src/components/Tasklist';
|
||||
import ChatFooter from '@chainlit/app/src/components/chat/Footer';
|
||||
import MessagesContainer from '@chainlit/app/src/components/chat/MessagesContainer';
|
||||
import ScrollContainer from '@chainlit/app/src/components/chat/ScrollContainer';
|
||||
import Translator from '@chainlit/app/src/components/i18n/Translator';
|
||||
import { useLayoutMaxWidth } from '@chainlit/app/src/hooks/useLayoutMaxWidth';
|
||||
import { useUpload } from '@chainlit/app/src/hooks/useUpload';
|
||||
import { IAttachment, attachmentsState } from '@chainlit/app/src/state/chat';
|
||||
import {
|
||||
useChatData,
|
||||
useChatInteract,
|
||||
useConfig
|
||||
} from '@chainlit/react-client';
|
||||
|
||||
import WelcomeScreen from '@/components/WelcomeScreen';
|
||||
import ElementSideView from 'components/ElementSideView';
|
||||
|
||||
const Chat = () => {
|
||||
const { config } = useConfig();
|
||||
const layoutMaxWidth = useLayoutMaxWidth();
|
||||
const setAttachments = useSetRecoilState(attachmentsState);
|
||||
const autoScrollRef = useRef(true);
|
||||
const { error, disabled, callFn } = useChatData();
|
||||
const { uploadFile } = useChatInteract();
|
||||
const uploadFileRef = useRef(uploadFile);
|
||||
|
||||
const fileSpec = useMemo(
|
||||
() => ({
|
||||
max_size_mb:
|
||||
config?.features?.spontaneous_file_upload?.max_size_mb || 500,
|
||||
max_files: config?.features?.spontaneous_file_upload?.max_files || 20,
|
||||
accept: config?.features?.spontaneous_file_upload?.accept || ['*/*']
|
||||
}),
|
||||
[config]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (callFn) {
|
||||
const event = new CustomEvent('chainlit-call-fn', {
|
||||
detail: callFn
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}, [callFn]);
|
||||
|
||||
useEffect(() => {
|
||||
uploadFileRef.current = uploadFile;
|
||||
}, [uploadFile]);
|
||||
|
||||
const onFileUpload = useCallback(
|
||||
(payloads: File[]) => {
|
||||
const attachements: IAttachment[] = payloads.map((file) => {
|
||||
const id = uuidv4();
|
||||
|
||||
const { xhr, promise } = uploadFileRef.current(file, (progress) => {
|
||||
setAttachments((prev) =>
|
||||
prev.map((attachment) => {
|
||||
if (attachment.id === id) {
|
||||
return {
|
||||
...attachment,
|
||||
uploadProgress: progress
|
||||
};
|
||||
}
|
||||
return attachment;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
promise
|
||||
.then((res) => {
|
||||
setAttachments((prev) =>
|
||||
prev.map((attachment) => {
|
||||
if (attachment.id === id) {
|
||||
return {
|
||||
...attachment,
|
||||
// Update with the server ID
|
||||
serverId: res.id,
|
||||
uploaded: true,
|
||||
uploadProgress: 100,
|
||||
cancel: undefined
|
||||
};
|
||||
}
|
||||
return attachment;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Failed to upload ${file.name}: ${error.message}`);
|
||||
setAttachments((prev) =>
|
||||
prev.filter((attachment) => attachment.id !== id)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
uploadProgress: 0,
|
||||
cancel: () => {
|
||||
toast.info(`Cancelled upload of ${file.name}`);
|
||||
xhr.abort();
|
||||
setAttachments((prev) =>
|
||||
prev.filter((attachment) => attachment.id !== id)
|
||||
);
|
||||
},
|
||||
remove: () => {
|
||||
setAttachments((prev) =>
|
||||
prev.filter((attachment) => attachment.id !== id)
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
setAttachments((prev) => prev.concat(attachements));
|
||||
},
|
||||
[uploadFile]
|
||||
);
|
||||
|
||||
const onFileUploadError = useCallback(
|
||||
(error: string) => toast.error(error),
|
||||
[toast]
|
||||
);
|
||||
|
||||
const upload = useUpload({
|
||||
spec: fileSpec,
|
||||
onResolved: onFileUpload,
|
||||
onError: onFileUploadError,
|
||||
options: { noClick: true }
|
||||
});
|
||||
|
||||
const enableAttachments =
|
||||
!disabled && config?.features?.spontaneous_file_upload?.enabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(enableAttachments
|
||||
? upload?.getRootProps({ className: 'dropzone' })
|
||||
: {})}
|
||||
// Disable the onFocus and onBlur events in react-dropzone to avoid interfering with child trigger events
|
||||
onBlur={undefined}
|
||||
onFocus={undefined}
|
||||
className="flex w-full h-full flex-col overflow-y-auto"
|
||||
>
|
||||
{upload ? (
|
||||
<input id="#upload-drop-input" {...upload.getInputProps()} />
|
||||
) : null}
|
||||
<div className="flex-grow flex flex-col overflow-y-auto">
|
||||
{error ? (
|
||||
<div className="w-full mx-auto my-2">
|
||||
<Alert className="mx-2" id="session-error" variant="error">
|
||||
<Translator path="common.status.error.serverConnection" />
|
||||
</Alert>
|
||||
</div>
|
||||
) : null}
|
||||
<ChatSettingsModal />
|
||||
<ErrorBoundary>
|
||||
<ScrollContainer
|
||||
autoScrollUserMessage={config?.features?.user_message_autoscroll}
|
||||
autoScrollAssistantMessage={
|
||||
config?.features?.assistant_message_autoscroll
|
||||
}
|
||||
autoScrollRef={autoScrollRef}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col mx-auto w-full flex-grow px-4 pt-4"
|
||||
style={{
|
||||
maxWidth: layoutMaxWidth
|
||||
}}
|
||||
>
|
||||
<TaskList isMobile={true} isCopilot />
|
||||
<WelcomeScreen />
|
||||
<MessagesContainer />
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
<div
|
||||
className="flex flex-col mx-auto w-full px-4 pb-4"
|
||||
style={{
|
||||
maxWidth: layoutMaxWidth
|
||||
}}
|
||||
>
|
||||
<ChatFooter
|
||||
showIfEmptyThread
|
||||
fileSpec={fileSpec}
|
||||
onFileUpload={onFileUpload}
|
||||
onFileUploadError={onFileUploadError}
|
||||
autoScrollRef={autoScrollRef}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<ElementSideView />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
threadIdToResumeState,
|
||||
useChatInteract,
|
||||
useChatSession
|
||||
} from '@chainlit/react-client';
|
||||
|
||||
import { copilotThreadIdState } from '../state';
|
||||
import ChatBody from './body';
|
||||
|
||||
export default function ChatWrapper() {
|
||||
const { connect, session, idToResume } = useChatSession();
|
||||
const { sendMessage } = useChatInteract();
|
||||
const copilotThreadId = useRecoilValue(copilotThreadIdState);
|
||||
const setThreadIdToResume = useSetRecoilState(threadIdToResumeState);
|
||||
const hasConnected = useRef<boolean>(false);
|
||||
const lastConnectedThreadId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!copilotThreadId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setThreadIdToResume(copilotThreadId);
|
||||
}, [copilotThreadId, setThreadIdToResume]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
copilotThreadId &&
|
||||
lastConnectedThreadId.current &&
|
||||
copilotThreadId !== lastConnectedThreadId.current &&
|
||||
hasConnected.current
|
||||
) {
|
||||
if (session?.socket?.connected) {
|
||||
session.socket.disconnect();
|
||||
}
|
||||
hasConnected.current = false;
|
||||
lastConnectedThreadId.current = null;
|
||||
}
|
||||
}, [copilotThreadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!copilotThreadId || !idToResume || copilotThreadId !== idToResume) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasConnected.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasConnected.current = true;
|
||||
lastConnectedThreadId.current = copilotThreadId;
|
||||
connect({
|
||||
// @ts-expect-error window typing
|
||||
transports: window.transports,
|
||||
userEnv: {}
|
||||
});
|
||||
}, [copilotThreadId, idToResume, connect]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error is not a valid prop
|
||||
window.sendChainlitMessage = sendMessage;
|
||||
}, [sendMessage]);
|
||||
|
||||
return <ChatBody />;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { Element } from '@chainlit/app/src/components/Elements';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@chainlit/app/src/components/ui/dialog';
|
||||
import { sideViewState } from '@chainlit/react-client';
|
||||
|
||||
export default function ElementSideView() {
|
||||
const [sideView, setSideView] = useRecoilState(sideViewState);
|
||||
|
||||
if (!sideView || sideView.title === 'canvas') return null;
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && setSideView(undefined)}>
|
||||
<DialogContent className="max-w-[80%]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{sideView.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div
|
||||
className="mt-4 overflow-y-auto overscroll-contain min-h-[50vh] max-h-[80vh] flex flex-col gap-4"
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{sideView.elements.map((e) => (
|
||||
<Element key={e.id} element={e} />
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { ChevronsRight, Maximize, Minimize, PanelRight } from 'lucide-react';
|
||||
|
||||
import AudioPresence from '@chainlit/app/src/components/AudioPresence';
|
||||
import { Logo } from '@chainlit/app/src/components/Logo';
|
||||
import ChatProfiles from '@chainlit/app/src/components/header/ChatProfiles';
|
||||
import NewChatButton from '@chainlit/app/src/components/header/NewChat';
|
||||
import { Button } from '@chainlit/app/src/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@chainlit/app/src/components/ui/dropdown-menu';
|
||||
import { IChainlitConfig, useAudio } from '@chainlit/react-client';
|
||||
|
||||
import { useCopilotInteract } from '../hooks';
|
||||
import { DisplayMode } from '../types';
|
||||
|
||||
interface IProjectConfig {
|
||||
config?: IChainlitConfig;
|
||||
error?: Error;
|
||||
isLoading: boolean;
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
expanded: boolean;
|
||||
setExpanded: (expanded: boolean) => void;
|
||||
projectConfig: IProjectConfig;
|
||||
displayMode?: DisplayMode;
|
||||
setDisplayMode?: (mode: DisplayMode) => void;
|
||||
setIsOpen?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
expanded,
|
||||
setExpanded,
|
||||
projectConfig,
|
||||
displayMode,
|
||||
setDisplayMode,
|
||||
setIsOpen
|
||||
}: Props): JSX.Element => {
|
||||
const { config } = projectConfig;
|
||||
const { audioConnection } = useAudio();
|
||||
const { startNewChat } = useCopilotInteract();
|
||||
|
||||
const hasChatProfiles = !!config?.chatProfiles.length;
|
||||
|
||||
return (
|
||||
<div className="flex align-center justify-between p-4 pb-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{hasChatProfiles ? <ChatProfiles /> : <Logo className="w-[100px]" />}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{audioConnection === 'on' ? (
|
||||
<AudioPresence
|
||||
type="server"
|
||||
height={20}
|
||||
width={40}
|
||||
barCount={4}
|
||||
barSpacing={2}
|
||||
/>
|
||||
) : null}
|
||||
<NewChatButton
|
||||
className="text-muted-foreground mt-[1.5px]"
|
||||
onConfirm={startNewChat}
|
||||
/>
|
||||
{setDisplayMode && (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button id="display-mode-button" size="icon" variant="ghost">
|
||||
<PanelRight className="!size-5 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
container={window.cl_shadowRootElement}
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
value={displayMode}
|
||||
onValueChange={(v) => setDisplayMode(v as DisplayMode)}
|
||||
>
|
||||
<DropdownMenuRadioItem value="floating">
|
||||
Floating
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="sidebar">
|
||||
Sidebar
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{displayMode === 'sidebar' && setIsOpen ? (
|
||||
<Button
|
||||
id="close-sidebar-button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<ChevronsRight className="!size-5 text-muted-foreground" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<Minimize className="!size-5 text-muted-foreground" />
|
||||
) : (
|
||||
<Maximize className="!size-5 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Starters from '@chainlit/app/src/components/chat/Starters';
|
||||
import { cn, hasMessage } from '@chainlit/app/src/lib/utils';
|
||||
import { useChatMessages } from '@chainlit/react-client';
|
||||
|
||||
export default function WelcomeScreen() {
|
||||
const { messages } = useChatMessages();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
}, []);
|
||||
|
||||
if (hasMessage(messages)) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col pb-4 flex-grow welcome-screen transition-opacity duration-500 opacity-0 delay-100',
|
||||
isVisible && 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<Starters className="items-end mt-auto" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCopilotInteract } from './useCopilotInteract';
|
||||
export { useSidebarResize } from './useSidebarResize';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { useChatInteract } from '@chainlit/react-client';
|
||||
|
||||
import { copilotThreadIdState } from '../state';
|
||||
|
||||
export const useCopilotInteract = () => {
|
||||
const chatInteract = useChatInteract();
|
||||
const setCopilotThreadId = useSetRecoilState(copilotThreadIdState);
|
||||
|
||||
const startNewChat = useCallback(
|
||||
(newThreadId?: string) => {
|
||||
chatInteract.clear();
|
||||
setCopilotThreadId(newThreadId || uuidv4());
|
||||
},
|
||||
[chatInteract, setCopilotThreadId]
|
||||
);
|
||||
|
||||
return {
|
||||
...chatInteract,
|
||||
clear: startNewChat,
|
||||
startNewChat
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DisplayMode } from '../types';
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 300;
|
||||
const SIDEBAR_DEFAULT_WIDTH = 400;
|
||||
const SIDEBAR_MAX_WIDTH_RATIO = 0.5;
|
||||
const LS_WIDTH_KEY = 'chainlit-copilot-sidebarWidth';
|
||||
|
||||
interface UseSidebarResizeOptions {
|
||||
displayMode: DisplayMode;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface UseSidebarResizeReturn {
|
||||
sidebarWidth: number;
|
||||
handleMouseDown: () => void;
|
||||
}
|
||||
|
||||
export function useSidebarResize({
|
||||
displayMode,
|
||||
isOpen
|
||||
}: UseSidebarResizeOptions): UseSidebarResizeReturn {
|
||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||
const stored = localStorage.getItem(LS_WIDTH_KEY);
|
||||
return stored ? Number(stored) : SIDEBAR_DEFAULT_WIDTH;
|
||||
});
|
||||
const isDragging = useRef(false);
|
||||
const originalMarginRef = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
if (displayMode === 'sidebar') {
|
||||
localStorage.setItem(LS_WIDTH_KEY, String(sidebarWidth));
|
||||
}
|
||||
}, [sidebarWidth, displayMode]);
|
||||
|
||||
const stopDragging = useCallback(() => {
|
||||
if (!isDragging.current) return;
|
||||
isDragging.current = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.transition = 'margin-right 0.3s ease-in-out';
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
isDragging.current = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.transition = '';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (displayMode !== 'sidebar' || !isOpen) return;
|
||||
|
||||
function onMouseMove(e: MouseEvent): void {
|
||||
if (!isDragging.current) return;
|
||||
const maxWidth = window.innerWidth * SIDEBAR_MAX_WIDTH_RATIO;
|
||||
const newWidth = Math.min(
|
||||
maxWidth,
|
||||
Math.max(SIDEBAR_MIN_WIDTH, window.innerWidth - e.clientX)
|
||||
);
|
||||
setSidebarWidth(newWidth);
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', stopDragging);
|
||||
window.addEventListener('blur', stopDragging);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', stopDragging);
|
||||
window.removeEventListener('blur', stopDragging);
|
||||
if (isDragging.current) {
|
||||
isDragging.current = false;
|
||||
document.body.style.userSelect = '';
|
||||
}
|
||||
};
|
||||
}, [stopDragging, displayMode, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (displayMode === 'sidebar' && isOpen) {
|
||||
originalMarginRef.current = document.body.style.marginRight;
|
||||
document.body.style.transition = 'margin-right 0.3s ease-in-out';
|
||||
return () => {
|
||||
document.body.style.marginRight = originalMarginRef.current;
|
||||
document.body.style.transition = '';
|
||||
};
|
||||
}
|
||||
}, [displayMode, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (displayMode === 'sidebar' && isOpen) {
|
||||
document.body.style.marginRight = `${sidebarWidth}px`;
|
||||
}
|
||||
}, [sidebarWidth, displayMode, isOpen]);
|
||||
|
||||
return { sidebarWidth, handleMouseDown };
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
#cl-shadow-root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: hsl(var(--foreground));
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#cl-shadow-root code {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
#shadow-root-container {
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-mono:
|
||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 5%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 5%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 5%;
|
||||
--primary: 340 92% 52%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 0 0% 90%;
|
||||
--muted-foreground: 0 0% 36%;
|
||||
--accent: 0 0% 95%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 0 0% 90%;
|
||||
--input: 0 0% 90%;
|
||||
--ring: 340 92% 52%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.75rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 13%;
|
||||
--foreground: 0 0% 93%;
|
||||
--card: 0 0% 18%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 0 0% 18%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 340 92% 52%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 0 0% 19%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 0 1% 26%;
|
||||
--muted-foreground: 0 0% 71%;
|
||||
--accent: 0 0% 26%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 0 1% 26%;
|
||||
--input: 0 1% 26%;
|
||||
--ring: 340 92% 52%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--radius: 0.75rem;
|
||||
--sidebar-background: 0 0% 9%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 0 0% 13%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-shimmer {
|
||||
0% {
|
||||
background-position: -100% top;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 250% top;
|
||||
}
|
||||
}
|
||||
|
||||
#cl-shadow-root .loading-shimmer {
|
||||
background-position: -100% top;
|
||||
text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: loading-shimmer;
|
||||
background: hsl(var(--muted))
|
||||
gradient(
|
||||
linear,
|
||||
100% 0,
|
||||
0 0,
|
||||
from(hsl(var(--muted))),
|
||||
color-stop(0.5, hsl(var(--foreground))),
|
||||
to(hsl(var(--muted)))
|
||||
);
|
||||
background: hsl(var(--muted)) -webkit-gradient(
|
||||
linear,
|
||||
100% 0,
|
||||
0 0,
|
||||
from(hsl(var(--muted))),
|
||||
color-stop(0.5, hsl(var(--foreground))),
|
||||
to(hsl(var(--muted)))
|
||||
);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 50% 200%;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { DisplayMode } from './types';
|
||||
|
||||
export const LS_DISPLAY_MODE_KEY = 'chainlit-copilot-displayMode';
|
||||
|
||||
export function resolveDisplayMode(
|
||||
configDisplayMode: DisplayMode | undefined
|
||||
): DisplayMode {
|
||||
return (
|
||||
configDisplayMode ||
|
||||
(localStorage.getItem(LS_DISPLAY_MODE_KEY) as DisplayMode) ||
|
||||
'floating'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { atom } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const COPILOT_THREAD_ID_KEY = 'chainlit-copilot-thread-id';
|
||||
export const COPILOT_THREAD_CHANGED_EVENT_KEY =
|
||||
'chainlit-copilot-thread-changed';
|
||||
export class CopilotThreadChangedEventParams {
|
||||
newThreadId?: string;
|
||||
}
|
||||
|
||||
export const copilotThreadIdState = atom<string>({
|
||||
key: 'CopilotThreadId',
|
||||
default: '',
|
||||
effects: [
|
||||
({ setSelf, onSet }) => {
|
||||
const savedValue = localStorage.getItem(COPILOT_THREAD_ID_KEY);
|
||||
|
||||
if (savedValue != null) {
|
||||
try {
|
||||
const parsedValue = JSON.parse(savedValue);
|
||||
setSelf(parsedValue);
|
||||
} catch (_error) {
|
||||
const newThreadId = uuidv4();
|
||||
localStorage.setItem(
|
||||
COPILOT_THREAD_ID_KEY,
|
||||
JSON.stringify(newThreadId)
|
||||
);
|
||||
setSelf(newThreadId);
|
||||
}
|
||||
} else {
|
||||
const newThreadId = uuidv4();
|
||||
localStorage.setItem(
|
||||
COPILOT_THREAD_ID_KEY,
|
||||
JSON.stringify(newThreadId)
|
||||
);
|
||||
setSelf(newThreadId);
|
||||
}
|
||||
|
||||
onSet((newValue, _, isReset) => {
|
||||
if (isReset) {
|
||||
localStorage.removeItem(COPILOT_THREAD_ID_KEY);
|
||||
} else {
|
||||
localStorage.setItem(COPILOT_THREAD_ID_KEY, JSON.stringify(newValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const getChainlitCopilotThreadId = () => {
|
||||
const threadId = localStorage.getItem(COPILOT_THREAD_ID_KEY);
|
||||
return threadId ? JSON.parse(threadId) : null;
|
||||
};
|
||||
|
||||
export const clearChainlitCopilotThreadId = (newThreadId?: string) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<CopilotThreadChangedEventParams>(
|
||||
COPILOT_THREAD_CHANGED_EVENT_KEY,
|
||||
{
|
||||
detail: { newThreadId }
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
export type DisplayMode = 'floating' | 'sidebar';
|
||||
|
||||
export interface IWidgetConfig {
|
||||
chainlitServer: string;
|
||||
showCot?: boolean;
|
||||
accessToken?: string;
|
||||
theme?: 'light' | 'dark';
|
||||
button?: {
|
||||
containerId?: string;
|
||||
imageUrl?: string;
|
||||
className?: string;
|
||||
};
|
||||
customCssUrl?: string;
|
||||
additionalQueryParamsForAPI?: Record<string, string>;
|
||||
expanded?: boolean;
|
||||
language?: string;
|
||||
opened?: boolean;
|
||||
displayMode?: DisplayMode;
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MessageCircle, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Alert from '@chainlit/app/src/components/Alert';
|
||||
import { Button } from '@chainlit/app/src/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from '@chainlit/app/src/components/ui/popover';
|
||||
import { useConfig } from '@chainlit/react-client';
|
||||
|
||||
import Header from './components/Header';
|
||||
|
||||
import ChatWrapper from './chat';
|
||||
import { useSidebarResize } from './hooks';
|
||||
import { LS_DISPLAY_MODE_KEY, resolveDisplayMode } from './resolveDisplayMode';
|
||||
import {
|
||||
clearChainlitCopilotThreadId,
|
||||
getChainlitCopilotThreadId
|
||||
} from './state';
|
||||
import { DisplayMode, IWidgetConfig } from './types';
|
||||
|
||||
interface Props {
|
||||
config: IWidgetConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Widget = ({ config, error }: Props) => {
|
||||
const [expanded, setExpanded] = useState(config?.expanded || false);
|
||||
const [isOpen, setIsOpen] = useState(config?.opened || false);
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>(() =>
|
||||
resolveDisplayMode(config?.displayMode)
|
||||
);
|
||||
const projectConfig = useConfig();
|
||||
const { sidebarWidth, handleMouseDown } = useSidebarResize({
|
||||
displayMode,
|
||||
isOpen
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.toggleChainlitCopilot = () => setIsOpen((prev) => !prev);
|
||||
window.getChainlitCopilotThreadId = getChainlitCopilotThreadId;
|
||||
window.clearChainlitCopilotThreadId = clearChainlitCopilotThreadId;
|
||||
|
||||
return () => {
|
||||
window.toggleChainlitCopilot = () => console.error('Widget not mounted.');
|
||||
window.getChainlitCopilotThreadId = () => null;
|
||||
|
||||
window.clearChainlitCopilotThreadId = () =>
|
||||
console.error('Widget not mounted.');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(LS_DISPLAY_MODE_KEY, displayMode);
|
||||
}, [displayMode]);
|
||||
|
||||
const customClassName = config?.button?.className || '';
|
||||
|
||||
const chatContent = error ? (
|
||||
<Alert variant="error">{error}</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Header
|
||||
expanded={expanded}
|
||||
setExpanded={setExpanded}
|
||||
projectConfig={projectConfig}
|
||||
displayMode={displayMode}
|
||||
setDisplayMode={setDisplayMode}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
<div className="flex flex-grow overflow-y-auto">
|
||||
<ChatWrapper />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (displayMode === 'sidebar') {
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<Button
|
||||
id="chainlit-copilot-button"
|
||||
aria-expanded="false"
|
||||
className={cn(
|
||||
'fixed h-16 w-16 rounded-full bottom-8 right-8 z-[20]',
|
||||
'transition-transform duration-300 ease-in-out',
|
||||
customClassName
|
||||
)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
{config?.button?.imageUrl ? (
|
||||
<img
|
||||
width="100%"
|
||||
src={config.button.imageUrl}
|
||||
alt="Chat bubble icon"
|
||||
/>
|
||||
) : (
|
||||
<MessageCircle className="!size-7" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 right-0 h-full z-[50] bg-background border-l shadow-lg flex flex-col"
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
<div
|
||||
data-testid="sidebar-drag-handle"
|
||||
onMouseDown={handleMouseDown}
|
||||
className="absolute top-0 left-0 w-1 h-full cursor-col-resize hover:bg-primary/20 active:bg-primary/30 z-10"
|
||||
/>
|
||||
<div id="chainlit-copilot-chat" className="flex flex-col h-full w-full">
|
||||
{chatContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="chainlit-copilot-button"
|
||||
className={cn(
|
||||
'fixed h-16 w-16 rounded-full bottom-8 right-8 z-[20]',
|
||||
'transition-transform duration-300 ease-in-out',
|
||||
customClassName
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
{config?.button?.imageUrl ? (
|
||||
<img
|
||||
width="100%"
|
||||
src={config.button.imageUrl}
|
||||
alt="Chat bubble icon"
|
||||
className={cn(
|
||||
'transition-opacity',
|
||||
isOpen ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<MessageCircle
|
||||
className={cn(
|
||||
'!size-7 transition-opacity',
|
||||
isOpen ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<X
|
||||
className={cn(
|
||||
'absolute !size-7 transition-all',
|
||||
isOpen ? 'rotate-0 scale-100' : 'rotate-90 scale-0'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={12}
|
||||
className={cn(
|
||||
'flex flex-col p-0',
|
||||
'transition-all duration-300 ease-in-out bg-background',
|
||||
expanded ? 'w-[80vw]' : 'w-[min(400px,80vw)]',
|
||||
'h-[min(730px,calc(100vh-150px))]',
|
||||
'overflow-hidden rounded-xl',
|
||||
'shadow-lg',
|
||||
'z-50',
|
||||
'animate-in fade-in-0 zoom-in-95',
|
||||
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
||||
expanded
|
||||
? 'copilot-container-expanded'
|
||||
: 'copilot-container-collapsed'
|
||||
)}
|
||||
>
|
||||
<div id="chainlit-copilot-chat" className="flex flex-col h-full w-full">
|
||||
{chatContent}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default Widget;
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import AppWrapper from 'appWrapper';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||
const meta = {
|
||||
title: 'Example/App',
|
||||
component: AppWrapper,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'centered'
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {}
|
||||
} satisfies Meta<typeof AppWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
widgetConfig: {
|
||||
chainlitServer: 'http://localhost:8000'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
widgetConfig: {
|
||||
chainlitServer: 'http://localhost:8000',
|
||||
theme: 'dark',
|
||||
button: {
|
||||
imageUrl:
|
||||
'https://steelbluemedia.com/wp-content/uploads/2019/06/new-google-favicon-512.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./src/**/*.{ts,tsx,js,jsx}',
|
||||
'../../frontend/src/**/*.{ts,tsx,js,jsx}'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
1: 'hsl(var(--chart-1))',
|
||||
2: 'hsl(var(--chart-2))',
|
||||
3: 'hsl(var(--chart-3))',
|
||||
4: 'hsl(var(--chart-4))',
|
||||
5: 'hsl(var(--chart-5))'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
plugins: [require('tailwindcss-animate')]
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["./src", "./stories"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths(), svgr()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
copilot: path.resolve(__dirname, 'index.tsx')
|
||||
},
|
||||
output: [
|
||||
{
|
||||
name: 'copilot',
|
||||
dir: 'dist',
|
||||
format: 'iife',
|
||||
entryFileNames: 'index.js',
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// To prevent conflicts with packages in @chainlit/app, we need to specify the resolution paths for these dependencies.
|
||||
react: path.resolve(__dirname, './node_modules/react'),
|
||||
'@chainlit': path.resolve(__dirname, './node_modules/@chainlit'),
|
||||
postcss: path.resolve(__dirname, './node_modules/postcss'),
|
||||
tailwindcss: path.resolve(__dirname, './node_modules/tailwindcss'),
|
||||
i18next: path.resolve(__dirname, './node_modules/i18next'),
|
||||
sonner: path.resolve(__dirname, './node_modules/sonner'),
|
||||
'highlight.js': path.resolve(__dirname, './node_modules/highlight.js'),
|
||||
'react-i18next': path.resolve(__dirname, './node_modules/react-i18next'),
|
||||
'usehooks-ts': path.resolve(__dirname, './node_modules/usehooks-ts'),
|
||||
lodash: path.resolve(__dirname, './node_modules/lodash'),
|
||||
recoil: path.resolve(__dirname, './node_modules/recoil')
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user