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.
- package/INTEGRATION.md +559 -0
- package/index.html +13 -0
- package/package/INTEGRATION.md +717 -0
- package/package/README.md +717 -0
- package/package/package-lock.json +3221 -0
- package/package/package.json +101 -0
- package/package/src/cli/index.ts +94 -0
- package/package/src/client/ChatWidget.tsx +163 -0
- package/package/src/client.ts +2 -0
- package/package/src/core/RushChatbot.ts +192 -0
- package/package/src/index.ts +5 -0
- package/package/src/server/guardrails.ts +107 -0
- package/package/src/server/index.ts +210 -0
- package/package/src/server/matcher.ts +140 -0
- package/package/src/shared/types.ts +58 -0
- package/package/src/shims-vue.d.ts +5 -0
- package/package/src/vanilla/index.ts +302 -0
- package/package/src/vanilla.ts +3 -0
- package/package/src/vue/ChatWidget.vue +198 -0
- package/package/src/vue.ts +3 -0
- package/package/src/vue2/ChatWidget.vue +204 -0
- package/package/src/vue2.ts +3 -0
- package/package/tsconfig.json +19 -0
- package/package/vite.config.ts +86 -0
- package/package.json +35 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/server/data/analytics.json +795 -0
- package/server/data/chatbot.config.json +26 -0
- package/server/data/questions.json +238 -0
- package/server/data/questions.tl.json +182 -0
- package/server/index.ts +11 -0
- package/server/lib/guardrails.ts +107 -0
- package/server/lib/matcher.ts +140 -0
- package/server/routes/chat.ts +180 -0
- package/server/tsconfig.json +11 -0
- package/src/App.css +184 -0
- package/src/App.tsx +135 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/ChatWidget.tsx +417 -0
- package/src/components/QuestionTree.tsx +57 -0
- package/src/hooks/useChatData.ts +49 -0
- package/src/index.css +111 -0
- package/src/main.tsx +10 -0
- package/src/types/index.ts +40 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import cors from 'cors'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { findMatch, MatchResult, Question } from './matcher'
|
|
7
|
+
import { checkGuardrails, validateInput } from './guardrails'
|
|
8
|
+
import type { ChatbotConfig } from '../shared/types'
|
|
9
|
+
|
|
10
|
+
export interface RushChatbotServerOptions {
|
|
11
|
+
/** Absolute path to your chatbot.config.json */
|
|
12
|
+
configPath: string
|
|
13
|
+
/** Absolute path to your questions.json (English) */
|
|
14
|
+
questionsPath: string
|
|
15
|
+
/** Absolute path to your questions.tl.json (Tagalog, optional) */
|
|
16
|
+
questionsTagalogPath?: string
|
|
17
|
+
/** Port to listen on. Defaults to 3001 */
|
|
18
|
+
port?: number
|
|
19
|
+
/** CORS origin(s). Defaults to * */
|
|
20
|
+
corsOrigin?: string | string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type AnalyticsEntry = {
|
|
24
|
+
sessionId: string
|
|
25
|
+
questionId: string
|
|
26
|
+
timestamp: string
|
|
27
|
+
type: 'click' | 'match' | 'n8n' | 'error'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseN8nResponse(data: unknown): { paragraphs: string[] } | { answer: string } {
|
|
31
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
32
|
+
const first = data[0]
|
|
33
|
+
if (Array.isArray(first?.paragraphs) && first.paragraphs.length > 0)
|
|
34
|
+
return { paragraphs: first.paragraphs.map(String) }
|
|
35
|
+
if (typeof first === 'string') return { answer: first }
|
|
36
|
+
if (first?.output) return { answer: String(first.output) }
|
|
37
|
+
if (first?.text) return { answer: String(first.text) }
|
|
38
|
+
if (first?.answer) return { answer: String(first.answer) }
|
|
39
|
+
if (first?.message) return { answer: String(first.message) }
|
|
40
|
+
}
|
|
41
|
+
if (typeof data === 'object' && data !== null) {
|
|
42
|
+
const obj = data as Record<string, unknown>
|
|
43
|
+
if (Array.isArray(obj.paragraphs)) return { paragraphs: obj.paragraphs.map(String) }
|
|
44
|
+
if (obj.output) return { answer: String(obj.output) }
|
|
45
|
+
if (obj.answer) return { answer: String(obj.answer) }
|
|
46
|
+
if (obj.text) return { answer: String(obj.text) }
|
|
47
|
+
if (obj.message) return { answer: String(obj.message) }
|
|
48
|
+
}
|
|
49
|
+
if (typeof data === 'string') return { answer: data }
|
|
50
|
+
return { answer: JSON.stringify(data) }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createServer(options: RushChatbotServerOptions) {
|
|
54
|
+
const {
|
|
55
|
+
configPath,
|
|
56
|
+
questionsPath,
|
|
57
|
+
questionsTagalogPath,
|
|
58
|
+
port = 3001,
|
|
59
|
+
corsOrigin = '*',
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
// Validate paths exist
|
|
63
|
+
if (!fs.existsSync(configPath)) throw new Error(`[RushChatbot] configPath not found: ${configPath}`)
|
|
64
|
+
if (!fs.existsSync(questionsPath)) throw new Error(`[RushChatbot] questionsPath not found: ${questionsPath}`)
|
|
65
|
+
|
|
66
|
+
const analyticsPath = path.join(path.dirname(configPath), 'analytics.json')
|
|
67
|
+
|
|
68
|
+
function getConfig(): ChatbotConfig {
|
|
69
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getQuestions(lang: 'en' | 'tl'): Question[] {
|
|
73
|
+
if (lang === 'tl') {
|
|
74
|
+
if (!questionsTagalogPath || !fs.existsSync(questionsTagalogPath)) return []
|
|
75
|
+
return JSON.parse(fs.readFileSync(questionsTagalogPath, 'utf-8'))
|
|
76
|
+
}
|
|
77
|
+
return JSON.parse(fs.readFileSync(questionsPath, 'utf-8'))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getAnalytics(): AnalyticsEntry[] {
|
|
81
|
+
if (!fs.existsSync(analyticsPath)) return []
|
|
82
|
+
return JSON.parse(fs.readFileSync(analyticsPath, 'utf-8'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function saveAnalytics(entry: AnalyticsEntry) {
|
|
86
|
+
try {
|
|
87
|
+
const analytics = getAnalytics()
|
|
88
|
+
analytics.push(entry)
|
|
89
|
+
fs.writeFileSync(analyticsPath, JSON.stringify(analytics, null, 2))
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error('[RushChatbot analytics error]', e)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getSid(req: express.Request): string {
|
|
96
|
+
return String(req.body?.sessionId ?? req.query?.sessionId ?? req.headers['x-session-id'] ?? 'unknown')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const app = express()
|
|
100
|
+
app.use(cors({ origin: corsOrigin }))
|
|
101
|
+
app.use(express.json())
|
|
102
|
+
|
|
103
|
+
const router = express.Router()
|
|
104
|
+
|
|
105
|
+
router.post('/click', (req, res) => {
|
|
106
|
+
const { questionId } = req.body
|
|
107
|
+
const sid = getSid(req)
|
|
108
|
+
if (!questionId || typeof questionId !== 'string')
|
|
109
|
+
return res.status(400).json({ error: 'questionId is required' })
|
|
110
|
+
saveAnalytics({ sessionId: sid, questionId, timestamp: new Date().toISOString(), type: 'click' })
|
|
111
|
+
res.json({ ok: true })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
router.get('/query', async (req, res) => {
|
|
115
|
+
const cfg = getConfig()
|
|
116
|
+
const query = String(req.query.query ?? '').trim()
|
|
117
|
+
const sid = getSid(req)
|
|
118
|
+
if (!query) return res.status(400).json({ error: 'query param is required' })
|
|
119
|
+
try {
|
|
120
|
+
const { webhookUrl, timeoutMs = 55000 } = cfg.n8n
|
|
121
|
+
const { data } = await axios.post(
|
|
122
|
+
webhookUrl,
|
|
123
|
+
{ query, sessionId: sid },
|
|
124
|
+
{ timeout: timeoutMs, headers: { 'Content-Type': 'application/json', 'x-session-id': sid } }
|
|
125
|
+
)
|
|
126
|
+
return res.json({ ...parseN8nResponse(data), source: 'n8n' })
|
|
127
|
+
} catch (err: unknown) {
|
|
128
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
129
|
+
console.error('[RushChatbot query GET error]', message)
|
|
130
|
+
return res.status(500).json({ error: message })
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
router.post('/query', async (req, res) => {
|
|
135
|
+
const sid = getSid(req)
|
|
136
|
+
const cfg = getConfig()
|
|
137
|
+
try {
|
|
138
|
+
const { query } = req.body
|
|
139
|
+
if (!query || typeof query !== 'string' || !query.trim())
|
|
140
|
+
return res.status(400).json({ error: 'query is required' })
|
|
141
|
+
|
|
142
|
+
const validation = validateInput(query.trim())
|
|
143
|
+
if (!validation.valid) {
|
|
144
|
+
saveAnalytics({ sessionId: sid, questionId: 'invalid_input', timestamp: new Date().toISOString(), type: 'error' })
|
|
145
|
+
return res.json({ answer: validation.response, source: 'validation' })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const guardrail = checkGuardrails(query.trim())
|
|
149
|
+
if (guardrail.flagged) {
|
|
150
|
+
saveAnalytics({ sessionId: sid, questionId: 'guardrail', timestamp: new Date().toISOString(), type: 'error' })
|
|
151
|
+
return res.json({ answer: guardrail.response, source: 'guardrail' })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let result: MatchResult | null = findMatch(query.trim(), getQuestions('en'), cfg.chat.matchThreshold ?? 0.4)
|
|
155
|
+
if (!result) result = findMatch(query.trim(), getQuestions('tl'), cfg.chat.matchThreshold ?? 0.4)
|
|
156
|
+
|
|
157
|
+
if (result) {
|
|
158
|
+
saveAnalytics({ sessionId: sid, questionId: result.question.id, timestamp: new Date().toISOString(), type: 'match' })
|
|
159
|
+
return res.json({
|
|
160
|
+
answer: result.question.answer,
|
|
161
|
+
source: 'config',
|
|
162
|
+
matchedQuestion: result.question.question,
|
|
163
|
+
matchedKeywords: result.matchedKeywords,
|
|
164
|
+
score: Math.round(result.score * 100),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { webhookUrl, timeoutMs = 55000 } = cfg.n8n
|
|
169
|
+
if (!webhookUrl || webhookUrl.includes('your-n8n.com')) {
|
|
170
|
+
saveAnalytics({ sessionId: sid, questionId: 'n8n_not_configured', timestamp: new Date().toISOString(), type: 'error' })
|
|
171
|
+
return res.json({ answer: cfg.chat.fallbackMessage, source: 'error' })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
saveAnalytics({ sessionId: sid, questionId: 'n8n', timestamp: new Date().toISOString(), type: 'n8n' })
|
|
175
|
+
const { data } = await axios.post(
|
|
176
|
+
webhookUrl,
|
|
177
|
+
{ query: query.trim(), sessionId: sid },
|
|
178
|
+
{ timeout: timeoutMs, headers: { 'Content-Type': 'application/json', 'x-session-id': sid } }
|
|
179
|
+
)
|
|
180
|
+
return res.json({ ...parseN8nResponse(data), source: 'n8n' })
|
|
181
|
+
|
|
182
|
+
} catch (err: unknown) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
184
|
+
console.error('[RushChatbot query error]', message)
|
|
185
|
+
saveAnalytics({ sessionId: sid, questionId: 'error', timestamp: new Date().toISOString(), type: 'error' })
|
|
186
|
+
return res.status(500).json({ answer: cfg.chat.fallbackMessage, source: 'error' })
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
router.get('/questions', (_req, res) => {
|
|
191
|
+
const { chat } = getConfig()
|
|
192
|
+
res.json(getQuestions(chat.language === 'tl' ? 'tl' : 'en'))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
router.get('/config', (_req, res) => {
|
|
196
|
+
const { branding, chat } = getConfig()
|
|
197
|
+
res.json({ branding, chat })
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
router.get('/analytics', (_req, res) => {
|
|
201
|
+
res.json(getAnalytics())
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
app.use('/api/chat', router)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
app,
|
|
208
|
+
listen: () => app.listen(port, () => console.log(`[RushChatbot] Server running on http://localhost:${port}`)),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import Fuse from 'fuse.js'
|
|
2
|
+
|
|
3
|
+
export interface Question {
|
|
4
|
+
id: string
|
|
5
|
+
question: string
|
|
6
|
+
answer: string
|
|
7
|
+
subquestions: Question[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Common stop words to ignore during keyword extraction
|
|
11
|
+
const STOP_WORDS = new Set([
|
|
12
|
+
'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
13
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
14
|
+
'should', 'may', 'might', 'shall', 'can', 'need', 'dare', 'ought',
|
|
15
|
+
'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'she', 'it', 'they',
|
|
16
|
+
'what', 'where', 'when', 'who', 'how', 'why', 'which',
|
|
17
|
+
'in', 'on', 'at', 'to', 'for', 'of', 'with', 'about', 'by', 'from',
|
|
18
|
+
'and', 'or', 'but', 'if', 'so', 'yet', 'nor',
|
|
19
|
+
'this', 'that', 'these', 'those', 'there', 'here',
|
|
20
|
+
'tell', 'give', 'show', 'know', 'want', 'like', 'get', 'find', 'look'
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
// Synonym map — only domain-specific terms, not generic words
|
|
24
|
+
const SYNONYMS: Record<string, string[]> = {
|
|
25
|
+
website: ['site', 'web', 'url', 'online', 'portal', 'link'],
|
|
26
|
+
email: ['mail', 'e-mail'],
|
|
27
|
+
address: ['location', 'situated'],
|
|
28
|
+
fee: ['cost', 'price', 'charge', 'dues', 'rate', 'much'],
|
|
29
|
+
membership: ['member', 'join', 'enroll', 'apply'],
|
|
30
|
+
contact: ['reach', 'call', 'phone', 'inquire'],
|
|
31
|
+
benefit: ['advantage', 'perk', 'privilege'],
|
|
32
|
+
program: ['course', 'seminar', 'workshop', 'training'],
|
|
33
|
+
academy: ['school', 'learning', 'education'],
|
|
34
|
+
award: ['recognition', 'prize', 'honor', 'trophy'],
|
|
35
|
+
event: ['conference', 'summit', 'forum', 'gathering'],
|
|
36
|
+
publication: ['journal', 'digest', 'magazine'],
|
|
37
|
+
mission: ['goal', 'purpose', 'objective'],
|
|
38
|
+
founded: ['established', 'started', 'created', 'history', 'origin'],
|
|
39
|
+
hours: ['schedule', 'open', 'available'],
|
|
40
|
+
nominate: ['nomination', 'recommend', 'propose'],
|
|
41
|
+
contribute: ['write', 'author', 'publish'],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractKeywords(text: string): string[] {
|
|
45
|
+
return text
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.filter(w => w.length > 1 && !STOP_WORDS.has(w))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function expandWithSynonyms(keywords: string[]): string[] {
|
|
53
|
+
const expanded = new Set(keywords)
|
|
54
|
+
for (const [canonical, synonymList] of Object.entries(SYNONYMS)) {
|
|
55
|
+
for (const kw of keywords) {
|
|
56
|
+
if (synonymList.includes(kw) || kw === canonical) {
|
|
57
|
+
expanded.add(canonical)
|
|
58
|
+
synonymList.forEach(s => expanded.add(s))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(expanded)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function keywordScore(rawKeywords: string[], expandedKeywords: string[], question: Question): { score: number; matched: string[] } {
|
|
66
|
+
const target = `${question.question} ${question.answer}`.toLowerCase()
|
|
67
|
+
const targetWords = new Set(target.replace(/[^a-z0-9\s]/g, ' ').split(/\s+/))
|
|
68
|
+
|
|
69
|
+
// Check raw keywords first for direct matches
|
|
70
|
+
const rawMatched = rawKeywords.filter(kw => targetWords.has(kw))
|
|
71
|
+
|
|
72
|
+
// Check expanded keywords but only count them if the canonical term hits
|
|
73
|
+
const expandedMatched = expandedKeywords.filter(kw => targetWords.has(kw))
|
|
74
|
+
|
|
75
|
+
// Deduplicate and use the larger matched set
|
|
76
|
+
const matched = Array.from(new Set([...rawMatched, ...expandedMatched]))
|
|
77
|
+
|
|
78
|
+
// Score based on raw keywords so expansion doesn't inflate the score
|
|
79
|
+
const score = rawMatched.length > 0
|
|
80
|
+
? rawMatched.length / Math.max(rawKeywords.length, 1)
|
|
81
|
+
: expandedMatched.length > 0 ? 0.3 : 0 // small boost if only synonyms matched
|
|
82
|
+
|
|
83
|
+
return { score, matched }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function flattenQuestions(questions: Question[]): Question[] {
|
|
87
|
+
return questions.reduce<Question[]>((acc, q) => {
|
|
88
|
+
acc.push(q)
|
|
89
|
+
if (q.subquestions?.length) acc.push(...flattenQuestions(q.subquestions))
|
|
90
|
+
return acc
|
|
91
|
+
}, [])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface MatchResult {
|
|
95
|
+
question: Question
|
|
96
|
+
matchedKeywords: string[]
|
|
97
|
+
score: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function findMatch(
|
|
101
|
+
query: string,
|
|
102
|
+
questions: Question[],
|
|
103
|
+
threshold: number
|
|
104
|
+
): MatchResult | null {
|
|
105
|
+
const flat = flattenQuestions(questions)
|
|
106
|
+
|
|
107
|
+
const rawKeywords = extractKeywords(query)
|
|
108
|
+
const expandedKeywords = expandWithSynonyms(rawKeywords)
|
|
109
|
+
|
|
110
|
+
const fuse = new Fuse(flat, {
|
|
111
|
+
keys: [
|
|
112
|
+
{ name: 'question', weight: 0.7 },
|
|
113
|
+
{ name: 'answer', weight: 0.3 }
|
|
114
|
+
],
|
|
115
|
+
threshold: 0.2,
|
|
116
|
+
includeScore: true,
|
|
117
|
+
minMatchCharLength: 3,
|
|
118
|
+
ignoreLocation: true,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const fuseResults = fuse.search(query)
|
|
122
|
+
|
|
123
|
+
const candidates = flat.map(q => {
|
|
124
|
+
const fuseResult = fuseResults.find(r => r.item.id === q.id)
|
|
125
|
+
const fuseScore = fuseResult ? 1 - (fuseResult.score ?? 1) : 0
|
|
126
|
+
const { score: kwScore, matched } = keywordScore(rawKeywords, expandedKeywords, q)
|
|
127
|
+
|
|
128
|
+
// Require at least one keyword match AND a reasonable combined score
|
|
129
|
+
if (kwScore === 0) return { q, combined: 0, matchedKeywords: [] }
|
|
130
|
+
|
|
131
|
+
const combined = fuseScore * 0.4 + kwScore * 0.6
|
|
132
|
+
return { q, combined, matchedKeywords: matched }
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const best = candidates
|
|
136
|
+
.filter(c => c.combined > 0.3)
|
|
137
|
+
.sort((a, b) => b.combined - a.combined)[0]
|
|
138
|
+
|
|
139
|
+
return best ? { question: best.q, matchedKeywords: best.matchedKeywords, score: best.combined } : null
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface Question {
|
|
2
|
+
id: string
|
|
3
|
+
question: string
|
|
4
|
+
answer: string
|
|
5
|
+
subquestions: Question[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Theme {
|
|
9
|
+
primaryColor: string
|
|
10
|
+
backgroundColor: string
|
|
11
|
+
userBubbleColor: string
|
|
12
|
+
botBubbleColor: string
|
|
13
|
+
textColor: string
|
|
14
|
+
fontFamily: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChatbotConfig {
|
|
18
|
+
branding: {
|
|
19
|
+
title: string
|
|
20
|
+
subtitle: string
|
|
21
|
+
avatarUrl?: string
|
|
22
|
+
theme: Theme
|
|
23
|
+
}
|
|
24
|
+
n8n: {
|
|
25
|
+
webhookUrl: string
|
|
26
|
+
timeoutMs?: number
|
|
27
|
+
}
|
|
28
|
+
chat: {
|
|
29
|
+
welcomeMessage: string
|
|
30
|
+
fallbackMessage: string
|
|
31
|
+
matchThreshold?: number
|
|
32
|
+
showPreQuestionsOnStart?: boolean
|
|
33
|
+
language?: 'en' | 'tl'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Message {
|
|
38
|
+
id: string
|
|
39
|
+
role: 'user' | 'bot'
|
|
40
|
+
text: string
|
|
41
|
+
source?: 'config' | 'n8n' | 'error'
|
|
42
|
+
subquestions?: Question[]
|
|
43
|
+
isFollowUp?: boolean
|
|
44
|
+
isEndChat?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WidgetSettings {
|
|
48
|
+
primaryColor?: string
|
|
49
|
+
bgColor?: string
|
|
50
|
+
borderRadius?: number
|
|
51
|
+
showShadow?: boolean
|
|
52
|
+
placeholder?: string
|
|
53
|
+
sendLabel?: string
|
|
54
|
+
hideInput?: boolean
|
|
55
|
+
hideEndChat?: boolean
|
|
56
|
+
autoOpen?: boolean
|
|
57
|
+
position?: 'bottom-right' | 'bottom-left'
|
|
58
|
+
}
|