rushchatbot 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/INTEGRATION.md +559 -0
  2. package/index.html +13 -0
  3. package/package/INTEGRATION.md +717 -0
  4. package/package/README.md +717 -0
  5. package/package/package-lock.json +3221 -0
  6. package/package/package.json +101 -0
  7. package/package/src/cli/index.ts +94 -0
  8. package/package/src/client/ChatWidget.tsx +163 -0
  9. package/package/src/client.ts +2 -0
  10. package/package/src/core/RushChatbot.ts +192 -0
  11. package/package/src/index.ts +5 -0
  12. package/package/src/server/guardrails.ts +107 -0
  13. package/package/src/server/index.ts +210 -0
  14. package/package/src/server/matcher.ts +140 -0
  15. package/package/src/shared/types.ts +58 -0
  16. package/package/src/shims-vue.d.ts +5 -0
  17. package/package/src/vanilla/index.ts +302 -0
  18. package/package/src/vanilla.ts +3 -0
  19. package/package/src/vue/ChatWidget.vue +198 -0
  20. package/package/src/vue.ts +3 -0
  21. package/package/src/vue2/ChatWidget.vue +204 -0
  22. package/package/src/vue2.ts +3 -0
  23. package/package/tsconfig.json +19 -0
  24. package/package/vite.config.ts +86 -0
  25. package/package.json +35 -0
  26. package/public/favicon.svg +1 -0
  27. package/public/icons.svg +24 -0
  28. package/server/data/analytics.json +795 -0
  29. package/server/data/chatbot.config.json +26 -0
  30. package/server/data/questions.json +238 -0
  31. package/server/data/questions.tl.json +182 -0
  32. package/server/index.ts +11 -0
  33. package/server/lib/guardrails.ts +107 -0
  34. package/server/lib/matcher.ts +140 -0
  35. package/server/routes/chat.ts +180 -0
  36. package/server/tsconfig.json +11 -0
  37. package/src/App.css +184 -0
  38. package/src/App.tsx +135 -0
  39. package/src/assets/hero.png +0 -0
  40. package/src/assets/react.svg +1 -0
  41. package/src/assets/vite.svg +1 -0
  42. package/src/components/ChatWidget.tsx +417 -0
  43. package/src/components/QuestionTree.tsx +57 -0
  44. package/src/hooks/useChatData.ts +49 -0
  45. package/src/index.css +111 -0
  46. package/src/main.tsx +10 -0
  47. package/src/types/index.ts +40 -0
  48. package/tsconfig.app.json +26 -0
  49. package/tsconfig.json +7 -0
  50. package/tsconfig.node.json +23 -0
  51. package/vite.config.ts +7 -0
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "rushchatbot",
3
+ "version": "1.6.7",
4
+ "description": "Embeddable hybrid chatbot with n8n fallback, license key protection, and configurable FAQ tree",
5
+ "keywords": [
6
+ "chatbot",
7
+ "n8n",
8
+ "react",
9
+ "express",
10
+ "faq",
11
+ "widget"
12
+ ],
13
+ "license": "UNLICENSED",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.js"
21
+ },
22
+ "./server": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.js"
26
+ },
27
+ "./client": {
28
+ "types": "./dist/client.d.ts",
29
+ "import": "./dist/client.es.js",
30
+ "require": "./dist/client.cjs.js"
31
+ },
32
+ "./vue": {
33
+ "types": "./dist/vue.d.ts",
34
+ "import": "./dist/vue.es.js",
35
+ "require": "./dist/vue.cjs.js"
36
+ },
37
+ "./vue2": {
38
+ "types": "./dist/vue2.d.ts",
39
+ "import": "./dist/vue2.es.js",
40
+ "require": "./dist/vue2.cjs.js"
41
+ },
42
+ "./vanilla": {
43
+ "types": "./dist/vanilla.d.ts",
44
+ "import": "./dist/vanilla.es.js"
45
+ }
46
+ },
47
+ "bin": {
48
+ "rushchatbot": "./dist/cli/index.js"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "README.md",
53
+ "INTEGRATION.md"
54
+ ],
55
+ "scripts": {
56
+ "build": "npm run build:server && npm run build:react && npm run build:vue && npm run build:vue2 && npm run build:vanilla",
57
+ "build:server": "./node_modules/.bin/tsc -p tsconfig.json",
58
+ "build:react": "./node_modules/.bin/vite build",
59
+ "build:vue": "./node_modules/.bin/vite build --mode vue",
60
+ "build:vue2": "./node_modules/.bin/vite build --mode vue2",
61
+ "build:vanilla": "./node_modules/.bin/vite build --mode vanilla",
62
+ "prepublishOnly": "npm run build"
63
+ },
64
+ "dependencies": {
65
+ "axios": "^1.7.2",
66
+ "bad-words": "^4.1.5",
67
+ "cors": "^2.8.5",
68
+ "express": "^4.19.2",
69
+ "fuse.js": "^7.0.0",
70
+ "marked": "^18.0.13"
71
+ },
72
+ "peerDependencies": {
73
+ "react": ">=18.0.0",
74
+ "react-dom": ">=18.0.0",
75
+ "vue": ">=3.0.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "react": {
79
+ "optional": true
80
+ },
81
+ "react-dom": {
82
+ "optional": true
83
+ },
84
+ "vue": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "devDependencies": {
89
+ "@types/bad-words": "^3.0.3",
90
+ "@types/cors": "^2.8.17",
91
+ "@types/express": "^4.17.21",
92
+ "@types/node": "^22.0.0",
93
+ "@types/react": "^19.1.6",
94
+ "@types/react-dom": "^19.1.6",
95
+ "@vitejs/plugin-react": "^4.3.4",
96
+ "@vitejs/plugin-vue": "^5.0.0",
97
+ "vue": "^3.4.0",
98
+ "typescript": "^5.5.3",
99
+ "vite": "^6.3.5"
100
+ }
101
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ const args = process.argv.slice(2)
6
+ const command = args[0]
7
+
8
+ const DEFAULT_CONFIG = {
9
+ branding: {
10
+ title: 'My ChatBot',
11
+ subtitle: 'How can we help you today?',
12
+ theme: {
13
+ primaryColor: '#4F46E5',
14
+ backgroundColor: '#FFFFFF',
15
+ userBubbleColor: '#4F46E5',
16
+ botBubbleColor: '#F3F4F6',
17
+ textColor: '#111827',
18
+ fontFamily: 'Inter, sans-serif',
19
+ },
20
+ },
21
+ n8n: {
22
+ webhookUrl: 'https://your-n8n.com/webhook/your-id',
23
+ timeoutMs: 55000,
24
+ },
25
+ chat: {
26
+ welcomeMessage: 'Hi! Choose a question below or type your own.',
27
+ fallbackMessage: "I'm looking into that for you...",
28
+ matchThreshold: 0.4,
29
+ showPreQuestionsOnStart: true,
30
+ language: 'en',
31
+ },
32
+ }
33
+
34
+ const DEFAULT_QUESTIONS = [
35
+ {
36
+ id: 'q1',
37
+ question: 'What services do you offer?',
38
+ answer: 'We offer a wide range of services. Please contact us for more details.',
39
+ subquestions: [],
40
+ },
41
+ {
42
+ id: 'q2',
43
+ question: 'How do I get in touch?',
44
+ answer: 'You can reach us via email or phone. Visit our Contact page for details.',
45
+ subquestions: [],
46
+ },
47
+ ]
48
+
49
+ function writeIfNotExists(filePath: string, content: unknown) {
50
+ if (fs.existsSync(filePath)) {
51
+ console.log(` [skip] ${filePath} already exists`)
52
+ return
53
+ }
54
+ fs.writeFileSync(filePath, JSON.stringify(content, null, 2))
55
+ console.log(` [created] ${filePath}`)
56
+ }
57
+
58
+ if (command === 'init') {
59
+ const targetDir = args[1] ? path.resolve(args[1]) : process.cwd()
60
+ fs.mkdirSync(targetDir, { recursive: true })
61
+
62
+ console.log(`\nInitializing RushChatbot config in: ${targetDir}\n`)
63
+ writeIfNotExists(path.join(targetDir, 'chatbot.config.json'), DEFAULT_CONFIG)
64
+ writeIfNotExists(path.join(targetDir, 'questions.json'), DEFAULT_QUESTIONS)
65
+ writeIfNotExists(path.join(targetDir, 'questions.tl.json'), DEFAULT_QUESTIONS)
66
+
67
+ console.log(`
68
+ Done! Next steps:
69
+ 1. Edit chatbot.config.json — set your n8n webhookUrl and branding
70
+ 2. Edit questions.json — add your FAQ tree
71
+ 3. In your server entry file:
72
+
73
+ import { createServer } from 'rushchatbot/server'
74
+ import path from 'path'
75
+
76
+ createServer({
77
+ configPath: path.resolve('./chatbot.config.json'),
78
+ questionsPath: path.resolve('./questions.json'),
79
+ questionsTagalogPath: path.resolve('./questions.tl.json'),
80
+ }).listen()
81
+
82
+ 4. In your React app:
83
+
84
+ import { ChatWidget } from 'rushchatbot/client'
85
+ <ChatWidget apiBase="http://localhost:3001/api/chat" />
86
+ `)
87
+ } else {
88
+ console.log(`
89
+ RushChatbot CLI
90
+
91
+ Commands:
92
+ rushchatbot init [dir] Scaffold config and questions files
93
+ `)
94
+ }
@@ -0,0 +1,163 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { RushChatbot, parseMarkdown } from '../core/RushChatbot'
3
+ import type { RushChatbotState } from '../core/RushChatbot'
4
+ import type { Question } from '../shared/types'
5
+
6
+ export interface ChatWidgetProps {
7
+ apiBase?: string
8
+ width?: number
9
+ height?: number
10
+ }
11
+
12
+ function MarkdownText({ text }: { text: string }) {
13
+ return (
14
+ <div
15
+ dangerouslySetInnerHTML={{ __html: parseMarkdown(text) }}
16
+ className='rushchatbot-md'
17
+ style={{ lineHeight: 1.6 }}
18
+ />
19
+ )
20
+ }
21
+
22
+ function ThinkingDots({ color }: { color: string }) {
23
+ return (
24
+ <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
25
+ {[0, 1, 2].map(i => (
26
+ <span key={i} style={{
27
+ width: 7, height: 7, borderRadius: '50%', background: color,
28
+ display: 'inline-block',
29
+ animation: `rushchatbot-bounce 1.2s ease-in-out ${i * 0.2}s infinite`,
30
+ }} />
31
+ ))}
32
+ <style>{`
33
+ @keyframes rushchatbot-bounce {
34
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
35
+ 40% { transform: translateY(-6px); opacity: 1; }
36
+ }
37
+ .rushchatbot-md p { margin: 0 0 6px 0; }
38
+ .rushchatbot-md p:last-child { margin-bottom: 0; }
39
+ .rushchatbot-md strong { font-weight: 600; }
40
+ .rushchatbot-md ul, .rushchatbot-md ol { margin: 4px 0 4px 16px; padding: 0; }
41
+ .rushchatbot-md li { margin-bottom: 2px; }
42
+ .rushchatbot-md a { color: inherit; text-decoration: underline; }
43
+ .rushchatbot-md h1, .rushchatbot-md h2, .rushchatbot-md h3 { margin: 4px 0 6px 0; font-weight: 700; line-height: 1.3; }
44
+ .rushchatbot-md h1 { font-size: 1.1em; }
45
+ .rushchatbot-md h2 { font-size: 1em; }
46
+ .rushchatbot-md h3 { font-size: 0.95em; }
47
+ `}</style>
48
+ </span>
49
+ )
50
+ }
51
+
52
+ export function ChatWidget({ apiBase, width = 380, height = 580 }: ChatWidgetProps) {
53
+ const chatRef = useRef<RushChatbot | null>(null)
54
+ const [state, setState] = useState<RushChatbotState | null>(null)
55
+ const bottomRef = useRef<HTMLDivElement>(null)
56
+
57
+ useEffect(() => {
58
+ const chat = new RushChatbot({ apiBase })
59
+ chatRef.current = chat
60
+ const unsub = chat.subscribe(setState)
61
+ chat.init()
62
+ return unsub
63
+ }, [apiBase])
64
+
65
+ useEffect(() => {
66
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
67
+ }, [state?.messages, state?.loading])
68
+
69
+ if (!state?.config) return null
70
+
71
+ const chat = chatRef.current!
72
+ const { theme, title, subtitle } = state.config.branding
73
+
74
+ const bubble = (isUser: boolean) => ({
75
+ background: isUser ? theme.userBubbleColor : theme.botBubbleColor,
76
+ color: isUser ? '#fff' : theme.textColor,
77
+ padding: '10px 14px', borderRadius: 12, maxWidth: '80%', fontSize: 14, lineHeight: 1.5,
78
+ })
79
+
80
+ return (
81
+ <div style={{
82
+ fontFamily: theme.fontFamily, color: theme.textColor, background: theme.backgroundColor,
83
+ width, height, borderRadius: 16, boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
84
+ display: 'flex', flexDirection: 'column', overflow: 'hidden',
85
+ }}>
86
+ <div style={{ background: theme.primaryColor, padding: '16px 20px', color: '#fff' }}>
87
+ <div style={{ fontWeight: 700, fontSize: 16 }}>{title}</div>
88
+ <div style={{ fontSize: 12, opacity: 0.85 }}>{subtitle}</div>
89
+ </div>
90
+
91
+ <div style={{ flex: 1, overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
92
+ {state.messages.map(msg => (
93
+ <div key={msg.id} style={{ display: 'flex', flexDirection: 'column', alignItems: msg.role === 'user' ? 'flex-end' : 'flex-start' }}>
94
+ <div style={bubble(msg.role === 'user')}>
95
+ {msg.subquestions && msg.subquestions.length > 0 ? (
96
+ <>
97
+ {msg.isFollowUp && <div style={{ marginBottom: 8 }}><MarkdownText text={msg.text} /></div>}
98
+ {msg.subquestions.map((sub: Question) => (
99
+ <button key={sub.id} onClick={() => chat.clickSub(sub, msg.subquestions!)} style={{
100
+ display: 'block', width: '100%', textAlign: 'left',
101
+ padding: '7px 10px', margin: '4px 0', borderRadius: 8,
102
+ border: `1px solid ${theme.primaryColor}`, background: '#fff',
103
+ color: theme.primaryColor, cursor: 'pointer', fontSize: 13,
104
+ }}>
105
+ {sub.question}
106
+ </button>
107
+ ))}
108
+ <button onClick={() => chat.backToMain()} style={{
109
+ display: 'block', width: '100%', textAlign: 'left',
110
+ padding: '7px 10px', margin: '4px 0', borderRadius: 8,
111
+ border: '1px solid #6B7280', background: '#fff',
112
+ color: '#6B7280', cursor: 'pointer', fontSize: 13,
113
+ }}>
114
+ ← Back to main menu
115
+ </button>
116
+ </>
117
+ ) : msg.role === 'bot' ? <MarkdownText text={msg.text} /> : msg.text}
118
+ </div>
119
+ </div>
120
+ ))}
121
+
122
+ {state.loading && (
123
+ <div style={{ alignSelf: 'flex-start', background: theme.botBubbleColor, padding: '10px 16px', borderRadius: 12, fontSize: 14 }}>
124
+ <ThinkingDots color={theme.primaryColor} />
125
+ </div>
126
+ )}
127
+
128
+ {state.showMain && state.questions.length > 0 && (
129
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 }}>
130
+ {state.questions.map((q: Question) => (
131
+ <button key={q.id} onClick={() => chat.clickMain(q)} style={{
132
+ textAlign: 'left', padding: '8px 12px', borderRadius: 8,
133
+ border: `1px solid ${theme.primaryColor}`, background: 'transparent',
134
+ color: theme.primaryColor, cursor: 'pointer', fontSize: 14,
135
+ }}>
136
+ {q.question}
137
+ </button>
138
+ ))}
139
+ </div>
140
+ )}
141
+
142
+ <div ref={bottomRef} />
143
+ </div>
144
+
145
+ <form onSubmit={e => { e.preventDefault(); chat.submitQuery() }} style={{ display: 'flex', borderTop: '1px solid #e5e7eb', padding: 12, gap: 8 }}>
146
+ <input
147
+ value={state.input}
148
+ onChange={e => chat.setInput(e.target.value)}
149
+ placeholder="Type a message..."
150
+ disabled={state.loading}
151
+ style={{ flex: 1, padding: '8px 12px', borderRadius: 8, border: '1px solid #e5e7eb', fontSize: 14, outline: 'none' }}
152
+ />
153
+ <button type="submit" disabled={state.loading} style={{
154
+ background: theme.primaryColor, color: '#fff', border: 'none',
155
+ borderRadius: 8, padding: '8px 16px',
156
+ cursor: state.loading ? 'not-allowed' : 'pointer', fontSize: 14,
157
+ }}>
158
+ Send
159
+ </button>
160
+ </form>
161
+ </div>
162
+ )
163
+ }
@@ -0,0 +1,2 @@
1
+ export { ChatWidget } from './client/ChatWidget'
2
+ export type { ChatWidgetProps } from './client/ChatWidget'
@@ -0,0 +1,192 @@
1
+ import { marked } from 'marked'
2
+ import type { Message, Question, WidgetSettings } from '../shared/types'
3
+
4
+ marked.setOptions({ breaks: true })
5
+
6
+ export interface RushChatbotConfig {
7
+ apiBase?: string
8
+ settings?: WidgetSettings
9
+ }
10
+
11
+ export interface RushChatbotState {
12
+ messages: Message[]
13
+ questions: Question[]
14
+ input: string
15
+ loading: boolean
16
+ showMain: boolean
17
+ config: { branding: any; chat: any } | null
18
+ settings: WidgetSettings
19
+ }
20
+
21
+ type Listener = (state: RushChatbotState) => void
22
+
23
+ function generateSessionId(): string {
24
+ return 'sess_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8)
25
+ }
26
+
27
+ function getSessionId(): string {
28
+ let id = sessionStorage.getItem('rushchatbot_session')
29
+ if (!id) { id = generateSessionId(); sessionStorage.setItem('rushchatbot_session', id) }
30
+ return id
31
+ }
32
+
33
+ export function parseMarkdown(text: string): string {
34
+ return marked.parse(text) as string
35
+ }
36
+
37
+ export type { WidgetSettings } from '../shared/types'
38
+
39
+ export class RushChatbot {
40
+ private base: string
41
+ private listeners: Listener[] = []
42
+ private state: RushChatbotState = {
43
+ messages: [],
44
+ questions: [],
45
+ input: '',
46
+ loading: false,
47
+ showMain: true,
48
+ config: null,
49
+ settings: {},
50
+ }
51
+
52
+ constructor(options: RushChatbotConfig = {}) {
53
+ this.base = options.apiBase ?? 'http://localhost:3001/api/chat'
54
+ if (options.settings) this.state.settings = options.settings
55
+ }
56
+
57
+ getState(): RushChatbotState {
58
+ return { ...this.state }
59
+ }
60
+
61
+ subscribe(listener: Listener): () => void {
62
+ this.listeners.push(listener)
63
+ return () => { this.listeners = this.listeners.filter(l => l !== listener) }
64
+ }
65
+
66
+ private setState(patch: Partial<RushChatbotState>) {
67
+ this.state = { ...this.state, ...patch }
68
+ this.listeners.forEach(l => l(this.getState()))
69
+ }
70
+
71
+ private addMessage(msg: Omit<Message, 'id'>) {
72
+ const messages = [...this.state.messages, { ...msg, id: Date.now().toString() + Math.random() }]
73
+ this.setState({ messages })
74
+ }
75
+
76
+ private async think(ms = 1000) {
77
+ this.setState({ loading: true })
78
+ await new Promise(r => setTimeout(r, ms))
79
+ this.setState({ loading: false })
80
+ }
81
+
82
+ private followUp(): string {
83
+ const phrases = [
84
+ 'Is there anything else I can help you with today? 😊',
85
+ 'Happy to help further! What else would you like to know?',
86
+ 'I hope that was helpful! Feel free to ask anything else.',
87
+ "You're in good hands! Is there something else on your mind?",
88
+ "I'm here for you — what else can I assist you with?",
89
+ ]
90
+ return phrases[Math.floor(Math.random() * phrases.length)]
91
+ }
92
+
93
+ async init() {
94
+ const [cfg, qs] = await Promise.all([
95
+ fetch(`${this.base}/config`).then(r => r.json()),
96
+ fetch(`${this.base}/questions`).then(r => r.json()),
97
+ ])
98
+ this.setState({
99
+ config: cfg,
100
+ questions: qs,
101
+ messages: [{ id: '0', role: 'bot', text: cfg.chat.welcomeMessage }],
102
+ showMain: cfg.chat.showPreQuestionsOnStart ?? true,
103
+ })
104
+ }
105
+
106
+ setInput(value: string) {
107
+ this.setState({ input: value })
108
+ }
109
+
110
+ async clickMain(q: Question) {
111
+ this.setState({ showMain: false })
112
+ this.addMessage({ role: 'user', text: q.question })
113
+ await fetch(`${this.base}/click`, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json', 'x-session-id': getSessionId() },
116
+ body: JSON.stringify({ questionId: q.id, sessionId: getSessionId() }),
117
+ })
118
+ await this.think()
119
+ if (q.subquestions?.length > 0) {
120
+ this.addMessage({ role: 'bot', text: `Here are some topics under **${q.question}**:`, source: 'config', subquestions: q.subquestions })
121
+ } else {
122
+ this.addMessage({ role: 'bot', text: q.answer, source: 'config' })
123
+ }
124
+ }
125
+
126
+ async clickSub(q: Question, siblings: Question[]) {
127
+ this.addMessage({ role: 'user', text: q.question })
128
+ await fetch(`${this.base}/click`, {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json', 'x-session-id': getSessionId() },
131
+ body: JSON.stringify({ questionId: q.id, sessionId: getSessionId() }),
132
+ })
133
+ await this.think()
134
+ this.addMessage({ role: 'bot', text: q.answer, source: 'config' })
135
+ await this.think(800)
136
+ this.addMessage({ role: 'bot', text: 'Would you have another inquiry?', subquestions: siblings, isFollowUp: true })
137
+ }
138
+
139
+ async backToMain() {
140
+ this.setState({ showMain: false })
141
+ this.addMessage({ role: 'bot', text: "Here's what I can help you with — feel free to pick a topic or type your own question! 😊" })
142
+ await this.think(800)
143
+ this.setState({ showMain: true })
144
+ }
145
+
146
+ endChat() {
147
+ this.setState({ showMain: false })
148
+ this.addMessage({ role: 'bot', text: 'Thank you for chatting with us! Have a wonderful day. Feel free to come back anytime you need help. 👋😊', isEndChat: true })
149
+ }
150
+
151
+ async startNewChat() {
152
+ const welcome = this.state.config?.chat.welcomeMessage ?? ''
153
+ this.setState({ messages: [{ id: '0', role: 'bot', text: welcome }], showMain: false })
154
+ await this.think(400)
155
+ this.setState({ showMain: true })
156
+ }
157
+
158
+ async submitQuery() {
159
+ const query = this.state.input.trim()
160
+ if (!query) return
161
+ this.setState({ input: '', showMain: false })
162
+ this.addMessage({ role: 'user', text: query })
163
+ this.setState({ loading: true })
164
+
165
+ try {
166
+ const sid = getSessionId()
167
+ const res = await fetch(`${this.base}/query?query=${encodeURIComponent(query)}&sessionId=${sid}`, {
168
+ headers: { 'x-session-id': sid },
169
+ }).then(r => r.json())
170
+
171
+ this.setState({ loading: false })
172
+
173
+ if (res.paragraphs?.length > 0) {
174
+ for (const p of res.paragraphs) {
175
+ this.addMessage({ role: 'bot', text: p, source: res.source })
176
+ await this.think(400)
177
+ }
178
+ } else {
179
+ this.addMessage({ role: 'bot', text: res.answer, source: res.source })
180
+ }
181
+ } catch {
182
+ this.setState({ loading: false })
183
+ this.addMessage({ role: 'bot', text: "Oops! It seems like I ran into a little hiccup. 😅 Please try again in a moment, or feel free to explore the topics below — I'm happy to help!", isFollowUp: true, subquestions: [] })
184
+ this.setState({ showMain: true })
185
+ return
186
+ }
187
+
188
+ await this.think(800)
189
+ this.addMessage({ role: 'bot', text: this.followUp(), isFollowUp: true, subquestions: [] })
190
+ this.setState({ showMain: true })
191
+ }
192
+ }
@@ -0,0 +1,5 @@
1
+ export { createServer } from './server/index'
2
+ export type { RushChatbotServerOptions } from './server/index'
3
+ export type { ChatbotConfig, Question, Message } from './shared/types'
4
+ export { RushChatbot, parseMarkdown } from './core/RushChatbot'
5
+ export type { RushChatbotConfig, RushChatbotState } from './core/RushChatbot'
@@ -0,0 +1,107 @@
1
+ import { Filter } from 'bad-words'
2
+
3
+ const filter = new Filter()
4
+
5
+ // Additional rude/aggressive phrases beyond profanity
6
+ const RUDE_PATTERNS = [
7
+ /\bstupid\b/i,
8
+ /\bidiot\b/i,
9
+ /\bmoron\b/i,
10
+ /\bdumb\b/i,
11
+ /\buseless\b/i,
12
+ /\bworthless\b/i,
13
+ /\bterrible\b/i,
14
+ /\bawful\b/i,
15
+ /\bhate\s+you\b/i,
16
+ /\bshut\s+up\b/i,
17
+ /\bgo\s+away\b/i,
18
+ /\byou\s+suck\b/i,
19
+ /\bwaste\s+of\s+time\b/i,
20
+ /\bscam\b/i,
21
+ /\bfraud\b/i,
22
+ ]
23
+
24
+ const GUARDRAIL_RESPONSES = [
25
+ "We truly value your time and want to make sure we're helping you in the best way possible. Let's keep our conversation respectful so we can assist you better. 😊 How can we help you today?",
26
+ "We understand that things can be frustrating sometimes, and we're here to help! Let's work together to find the answers you need. What can we assist you with?",
27
+ "Your experience matters to us, and we want to make sure you get the best support possible. Let's start fresh — what would you like to know?",
28
+ "We're committed to providing you with a positive and helpful experience. Let's keep things friendly so we can serve you better! What can we help you with today?",
29
+ "We're here to help and we want to make this a great experience for you! Let's take a step back and see how we can best assist you. 😊",
30
+ ]
31
+
32
+ const INVALID_INPUT_RESPONSES = [
33
+ "Hmm, it seems like that message didn't quite come through. Could you rephrase your question? We'd love to help! 😊",
34
+ "We want to make sure we understand you correctly! Could you try asking your question in a different way?",
35
+ "It looks like we didn't catch that. Could you provide a bit more detail so we can assist you better?",
36
+ "We're here to help, but we need a little more to go on! Could you try rephrasing that for us?",
37
+ ]
38
+
39
+ // Minimum valid word length and count
40
+ const MIN_WORD_LENGTH = 2
41
+ const MIN_VALID_WORDS = 1
42
+ const MAX_INPUT_LENGTH = 500
43
+
44
+ // Detect if input is gibberish — too many consecutive consonants or random chars
45
+ function isGibberish(word: string): boolean {
46
+ if (word.length < 2) return false
47
+ const consonantRun = /[bcdfghjklmnpqrstvwxyz]{5,}/i
48
+ const randomChars = /[^a-z0-9\s]{3,}/i
49
+ const repeatingChars = /(.)\1{4,}/i
50
+ return consonantRun.test(word) || randomChars.test(word) || repeatingChars.test(word)
51
+ }
52
+
53
+ export interface ValidationResult {
54
+ valid: boolean
55
+ response?: string
56
+ }
57
+
58
+ export function validateInput(text: string): ValidationResult {
59
+ const trimmed = text.trim()
60
+
61
+ // Empty or too short
62
+ if (!trimmed || trimmed.length < MIN_WORD_LENGTH) {
63
+ return { valid: false, response: "Please type a message so we can assist you! 😊" }
64
+ }
65
+
66
+ // Too long
67
+ if (trimmed.length > MAX_INPUT_LENGTH) {
68
+ return { valid: false, response: "Your message is a bit too long. Could you summarize your question so we can help you better?" }
69
+ }
70
+
71
+ // Only numbers or special characters
72
+ if (/^[^a-zA-Z]+$/.test(trimmed)) {
73
+ return { valid: false, response: INVALID_INPUT_RESPONSES[Math.floor(Math.random() * INVALID_INPUT_RESPONSES.length)] }
74
+ }
75
+
76
+ // Check for gibberish words
77
+ const words = trimmed.split(/\s+/).filter(w => w.length >= MIN_WORD_LENGTH)
78
+ const gibberishWords = words.filter(isGibberish)
79
+ if (words.length > 0 && gibberishWords.length / words.length > 0.6) {
80
+ return { valid: false, response: INVALID_INPUT_RESPONSES[Math.floor(Math.random() * INVALID_INPUT_RESPONSES.length)] }
81
+ }
82
+
83
+ // Not enough valid words
84
+ const validWords = words.filter(w => /[a-zA-Z]/.test(w))
85
+ if (validWords.length < MIN_VALID_WORDS) {
86
+ return { valid: false, response: INVALID_INPUT_RESPONSES[Math.floor(Math.random() * INVALID_INPUT_RESPONSES.length)] }
87
+ }
88
+
89
+ return { valid: true }
90
+ }
91
+
92
+ export interface GuardrailResult {
93
+ flagged: boolean
94
+ response?: string
95
+ }
96
+
97
+ export function checkGuardrails(text: string): GuardrailResult {
98
+ const hasProfanity = filter.isProfane(text)
99
+ const hasRudeness = RUDE_PATTERNS.some(pattern => pattern.test(text))
100
+
101
+ if (hasProfanity || hasRudeness) {
102
+ const response = GUARDRAIL_RESPONSES[Math.floor(Math.random() * GUARDRAIL_RESPONSES.length)]
103
+ return { flagged: true, response }
104
+ }
105
+
106
+ return { flagged: false }
107
+ }