A fully modular, reusable chat package that can be integrated into any React application.
Install from the repository using a branch (replace main with your branch name):
# npm
npm install git+https://github.com/devslane/chat-react-module.git#main
# yarn
yarn add git+https://github.com/devslane/chat-react-module.git#main
# pnpm
pnpm add git+https://github.com/devslane/chat-react-module.git#mainOr add to package.json:
{
"dependencies": {
"@secondchair/chat-module": "github:devslane/chat-react-module#main"
}
}Ensure your bundler (Vite, Webpack, etc.) resolves TypeScript from node_modules or that you alias the package to source. The package exposes ./src/index.ts as the main entry.
The easiest way to integrate the chat module:
import { Chat } from "@secondchair/chat-module";
import "@secondchair/chat-module/index.css";
function App() {
return (
<Chat
sessionId={123}
onNavigateToSession={(newSessionId) => {
router.push(`/chat/${newSessionId}`);
}}
/>
);
}That's it! The Chat component includes everything you need.
<Chat
sessionId={sessionId}
onNavigateToSession={(id) => router.push(`/chat/${id}`)}
/><Chat
sessionId={sessionId}
contextId={projectId} // Generic context - can be projectId, userId, etc.
contextType="project"
contextName="My Project"
onNavigateToSession={(id, contextId) =>
router.push(`/projects/${contextId}/chat/${id}`)
}
/><Chat
sessionId={sessionId}
config={{
chatPanel: {
enableToneSelector: false, // Disable tone selector
enableStateSelector: false, // Disable state selector
enableDocumentSelection: false, // Disable document selection
enableFileAttachments: false, // Disable file attachments
},
canvas: {
enableSave: false, // Disable save button
enableExport: true, // Keep export enabled
exportFormats: {
pdf: true,
word: false,
pleading: false,
clipboard: true,
},
},
}}
onNavigateToSession={(id) => router.push(`/chat/${id}`)}
/><Chat
sessionId={sessionId}
adapters={{
// Authentication
auth: {
getCurrentUser: () => currentUser,
isAuthenticated: () => !!currentUser,
},
// Toast notifications
toast: {
success: (msg) => toast.success(msg),
error: (msg) => toast.error(msg),
info: (msg) => toast.info(msg),
warning: (msg) => toast.warning(msg),
},
// Document management
documents: {
getDocumentCollections: () => [
{
type: "project",
label: "Project Files",
documents: projectDocuments,
},
],
onDocumentsSelected: (type, ids) => {
console.log(`Selected ${ids.length} ${type} documents`);
},
},
// Navigation
navigation: {
navigateToSession: (sessionId, contextId) => {
router.push(`/chat/${sessionId}`);
},
},
}}
onNavigateToSession={(id) => router.push(`/chat/${id}`)}
/><Chat
sessionId={sessionId}
onMessageSent={(message) => {
analytics.track("message_sent", { messageId: message.id });
}}
onSessionCreated={(sessionId) => {
analytics.track("session_created", { sessionId });
}}
onCanvasOpen={(messageId) => {
analytics.track("canvas_opened", { messageId });
}}
onCanvasSave={(messageId, content) => {
analytics.track("canvas_saved", { messageId });
}}
onNavigateToSession={(id) => router.push(`/chat/${id}`)}
/>For full control over the layout, use the provider and individual components:
import {
ChatModuleProvider,
ChatPanel,
Canvas,
useCanvasManager,
} from "@secondchair/chat-module";
function MyCustomChat() {
return (
<ChatModuleProvider
config={{ chatPanel: { enableCanvas: true } }}
adapters={{
toast: { success: toast.success, error: toast.error },
}}
context={{ contextId: projectId, contextType: "project" }}
>
<CustomLayout />
</ChatModuleProvider>
);
}
function CustomLayout() {
const canvas = useCanvasManager();
return (
<div className="flex h-screen">
<div className="flex-1">
<ChatPanel
chatSession={session}
isCanvasOpen={canvas.isOpen}
onOpenCanvas={(msg) => canvas.openCanvas(msg.id)}
/>
</div>
{canvas.isOpen && canvas.messageId && (
<div className="w-1/2">
<Canvas
isOpen={canvas.isOpen}
onClose={canvas.closeCanvas}
messageId={canvas.messageId}
selectedMessageIndex={canvas.selectedMessageIndex}
lastIndex={canvas.totalMessages - 1}
handleChangeCanvasMessage={(dir) =>
dir > 0 ? canvas.goToNext() : canvas.goToPrevious()
}
isStreaming={canvas.isStreaming}
/>
</div>
)}
</div>
);
}| Hook | Purpose |
|---|---|
useChatModule() |
Access all module context (includes config) |
useFeatureEnabled(section, feature) |
Check if a feature is enabled |
useMessageSubmission(options) |
Handle message sending |
useDocuments(options) |
Manage document selection |
useCanvasManager(options) |
Manage canvas state |
useCurrentUser() |
Get current user from adapter |
useChatToast() |
Access toast notifications |
canvas: {
enableTextSelectionPopup: true, // Text selection popup
enableNavigation: true, // Message navigation
enableExport: true, // Export/download
enableSave: true, // Save functionality
saveConfig: {
showSaveButton: true,
showSaveAndClose: true,
enableAutoSave: false,
autoSaveDelay: 2000,
},
exportFormats: {
pdf: true,
word: true,
pleading: true,
clipboard: true,
showDownloadMenu: true,
},
toolbarFeatures: {
undoRedo: true,
fontSize: true,
heading: true,
textFormatting: true,
colors: true,
lists: true,
alignment: true,
insertElements: true,
collapsible: true,
},
}chatPanel: {
enableDocumentSelection: true, // Document selection
enableFileAttachments: true, // File uploads
enableChatOptions: true, // Chat options bar
enableToneSelector: true, // Tone selector
enableStateSelector: true, // State selector
enableWelcomeSection: true, // Welcome section
enableMessageEditing: true, // Edit messages
enableCanvas: true, // Canvas integration
enableDocumentPreview: true, // Document preview
enableSecureDocumentLinks: true, // Secure document links
}chat: {
enableStreaming: true, // Streaming support
enablePagination: true, // Message pagination
enableMessageReview: true, // Message review/status
enableComparisonMode: true, // Comparison mode
maxDocumentSelection: 10, // Max documents
maxClientDocumentSelection: 5, // Max client documents
}interface ChatModuleAdapters {
auth?: {
getCurrentUser: () => ChatUser | null;
isAuthenticated?: () => boolean;
};
documents?: {
getDocumentCollections: () => DocumentCollection[];
onDocumentsSelected?: (type: string, ids: (number | string)[]) => void;
getSecureDocumentUrl?: (id: number | string) => Promise<string>;
uploadDocument?: (file: File) => Promise<ChatDocument>;
};
navigation?: {
navigateToSession: (sessionId: number, contextId?: number | string) => void;
navigateToNewChat?: (contextId?: number | string) => void;
};
api?: {
sendMessage: (payload: SendMessagePayload) => Promise<ChatMessage | void>;
sendStreamingMessage?: (payload: SendMessagePayload) => void;
fetchMessages?: (sessionId: number, page?: number) => Promise<ChatMessage[]>;
};
toast?: {
success: (message: string) => void;
error: (message: string) => void;
info?: (message: string) => void;
warning?: (message: string) => void;
};
storage?: {
getItem: (key: string) => string | null;
setItem: (key: string, value: string) => void;
removeItem: (key: string) => void;
};
}chat-react-module/
βββ src/
β βββ components/
β β βββ Chat.tsx # All-in-one component
β β βββ chat/
β β βββ chat-panel/ # Chat panel components
β β βββ canvas/ # Canvas components
β βββ context/
β β βββ ChatModuleProvider.tsx # Main provider
β βββ hooks/
β β βββ useMessageSubmission.ts
β β βββ useDocuments.ts
β β βββ useCanvasManager.ts
β βββ services/ # API services
β βββ types/ # TypeScript types
β βββ index.ts # Main entry
βββ assets/ # Icons and static assets
βββ README.md
If you're migrating from the old clientId pattern:
// Before (with clientId)
<ChatPanel clientId={clientId} sessionId={sessionId} />
// After (generic context)
<Chat
sessionId={sessionId}
contextId={clientId} // Same value, new name
contextType="client" // Identifies what kind of context
onNavigateToSession={(id) => router.push(`/client/${clientId}/chat/${id}`)}
/>- All internal imports use
@chatModule/*or relative paths. - Keep components composable and independent.
- Run
npm run typecheckbefore committing.