@digilogiclabs/create-saas-app 1.17.1 → 1.18.0
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/CHANGELOG.md +51 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +6 -2
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +13 -7
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/README.md +655 -0
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/app/(tabs)/ai.tsx +683 -0
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/docs/MOBILE-SETUP.md +787 -0
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/hooks/useRAGSystem.ts +346 -0
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/lib/rag/config.ts +180 -0
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/package.json +113 -0
- package/dist/templates/mobile/ui-auth-payments-ai-rag/template/scripts/setup-rag.js +599 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/README.md +434 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/components/rag/KnowledgeManager.tsx +642 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/components/rag/RAGAnalytics.tsx +466 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/components/rag/RAGChatInterface.tsx +393 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/docs/GETTING-STARTED.md +457 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/hooks/useRAGSystem.ts +478 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/lib/rag/config.ts +250 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/package.json +74 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/scripts/setup-rag.js +622 -0
- package/dist/templates/web/ui-auth-payments-ai-rag/template/src/app/ai/page.tsx +396 -0
- package/package.json +1 -1
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/README.md +655 -0
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/app/(tabs)/ai.tsx +683 -0
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/docs/MOBILE-SETUP.md +787 -0
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/hooks/useRAGSystem.ts +346 -0
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/lib/rag/config.ts +180 -0
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/package.json +113 -0
- package/src/templates/mobile/ui-auth-payments-ai-rag/template/scripts/setup-rag.js +599 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/README.md +434 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/components/rag/KnowledgeManager.tsx +642 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/components/rag/RAGAnalytics.tsx +466 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/components/rag/RAGChatInterface.tsx +393 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/docs/GETTING-STARTED.md +457 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/hooks/useRAGSystem.ts +478 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/lib/rag/config.ts +250 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/package.json +74 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/scripts/setup-rag.js +622 -0
- package/src/templates/web/ui-auth-payments-ai-rag/template/src/app/ai/page.tsx +396 -0
package/dist/templates/web/ui-auth-payments-ai-rag/template/components/rag/KnowledgeManager.tsx
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef } from 'react'
|
|
4
|
+
import { Button, Card } from '@digilogiclabs/saas-factory-ui'
|
|
5
|
+
import {
|
|
6
|
+
Plus,
|
|
7
|
+
Search,
|
|
8
|
+
Filter,
|
|
9
|
+
Upload,
|
|
10
|
+
Download,
|
|
11
|
+
Trash2,
|
|
12
|
+
Edit,
|
|
13
|
+
FileText,
|
|
14
|
+
Calendar,
|
|
15
|
+
Tag,
|
|
16
|
+
Eye,
|
|
17
|
+
MoreHorizontal,
|
|
18
|
+
Loader2,
|
|
19
|
+
CheckCircle,
|
|
20
|
+
AlertCircle,
|
|
21
|
+
Info
|
|
22
|
+
} from 'lucide-react'
|
|
23
|
+
import { toast } from 'sonner'
|
|
24
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
25
|
+
import type { UseRAGSystemReturn } from '../../hooks/useRAGSystem'
|
|
26
|
+
import type { RAGDocument, RAGSearchResult } from '@digilogiclabs/saas-factory-ai-types'
|
|
27
|
+
|
|
28
|
+
interface KnowledgeManagerProps {
|
|
29
|
+
ragSystem: UseRAGSystemReturn<any>
|
|
30
|
+
enableBulkOperations?: boolean
|
|
31
|
+
enableExport?: boolean
|
|
32
|
+
onSuccess?: (message: string) => void
|
|
33
|
+
onError?: (error: string) => void
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface KnowledgeDocument extends RAGDocument<any> {
|
|
38
|
+
id: string
|
|
39
|
+
selected?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function KnowledgeManager({
|
|
43
|
+
ragSystem,
|
|
44
|
+
enableBulkOperations = true,
|
|
45
|
+
enableExport = true,
|
|
46
|
+
onSuccess,
|
|
47
|
+
onError,
|
|
48
|
+
className = ''
|
|
49
|
+
}: KnowledgeManagerProps) {
|
|
50
|
+
const [documents, setDocuments] = useState<KnowledgeDocument[]>([])
|
|
51
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
52
|
+
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
|
53
|
+
const [isAddingDocument, setIsAddingDocument] = useState(false)
|
|
54
|
+
const [editingDocument, setEditingDocument] = useState<KnowledgeDocument | null>(null)
|
|
55
|
+
const [viewingDocument, setViewingDocument] = useState<KnowledgeDocument | null>(null)
|
|
56
|
+
const [selectedDocuments, setSelectedDocuments] = useState<Set<string>>(new Set())
|
|
57
|
+
const [bulkAction, setBulkAction] = useState<'none' | 'delete' | 'export'>('none')
|
|
58
|
+
|
|
59
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
60
|
+
|
|
61
|
+
// Load documents
|
|
62
|
+
const loadDocuments = async () => {
|
|
63
|
+
try {
|
|
64
|
+
const results = await ragSystem.searchKnowledge(searchQuery || '', {
|
|
65
|
+
category: selectedCategory === 'all' ? undefined : selectedCategory,
|
|
66
|
+
limit: 50,
|
|
67
|
+
includeMetadata: true
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
setDocuments(results.map(result => ({
|
|
71
|
+
...result,
|
|
72
|
+
id: result.id || `doc-${Date.now()}-${Math.random()}`,
|
|
73
|
+
selected: selectedDocuments.has(result.id || '')
|
|
74
|
+
})))
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Failed to load documents:', error)
|
|
77
|
+
onError?.('Failed to load documents')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Search documents
|
|
82
|
+
const handleSearch = async () => {
|
|
83
|
+
await loadDocuments()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add new document
|
|
87
|
+
const handleAddDocument = async (documentData: Partial<RAGDocument<any>>) => {
|
|
88
|
+
try {
|
|
89
|
+
const id = await ragSystem.addKnowledge({
|
|
90
|
+
content: documentData.content || '',
|
|
91
|
+
category: documentData.category || 'general',
|
|
92
|
+
fields: documentData.fields || {},
|
|
93
|
+
metadata: documentData.metadata || {},
|
|
94
|
+
source: documentData.source || { type: 'manual', createdBy: 'user' }
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (id) {
|
|
98
|
+
onSuccess?.('Document added successfully')
|
|
99
|
+
setIsAddingDocument(false)
|
|
100
|
+
await loadDocuments()
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Failed to add document:', error)
|
|
104
|
+
onError?.('Failed to add document')
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Update document
|
|
109
|
+
const handleUpdateDocument = async (id: string, updates: Partial<RAGDocument<any>>) => {
|
|
110
|
+
try {
|
|
111
|
+
const success = await ragSystem.updateKnowledge(id, updates)
|
|
112
|
+
|
|
113
|
+
if (success) {
|
|
114
|
+
onSuccess?.('Document updated successfully')
|
|
115
|
+
setEditingDocument(null)
|
|
116
|
+
await loadDocuments()
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Failed to update document:', error)
|
|
120
|
+
onError?.('Failed to update document')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Delete document
|
|
125
|
+
const handleDeleteDocument = async (id: string) => {
|
|
126
|
+
try {
|
|
127
|
+
const success = await ragSystem.deleteKnowledge(id)
|
|
128
|
+
|
|
129
|
+
if (success) {
|
|
130
|
+
onSuccess?.('Document deleted successfully')
|
|
131
|
+
await loadDocuments()
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Failed to delete document:', error)
|
|
135
|
+
onError?.('Failed to delete document')
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Bulk operations
|
|
140
|
+
const handleBulkAction = async () => {
|
|
141
|
+
if (selectedDocuments.size === 0) return
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
switch (bulkAction) {
|
|
145
|
+
case 'delete':
|
|
146
|
+
for (const id of selectedDocuments) {
|
|
147
|
+
await ragSystem.deleteKnowledge(id)
|
|
148
|
+
}
|
|
149
|
+
onSuccess?.(`Deleted ${selectedDocuments.size} documents`)
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
case 'export':
|
|
153
|
+
const exportData = documents
|
|
154
|
+
.filter(doc => selectedDocuments.has(doc.id))
|
|
155
|
+
.map(doc => ({
|
|
156
|
+
content: doc.content,
|
|
157
|
+
category: doc.category,
|
|
158
|
+
fields: doc.fields,
|
|
159
|
+
metadata: doc.metadata
|
|
160
|
+
}))
|
|
161
|
+
|
|
162
|
+
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
|
163
|
+
type: 'application/json'
|
|
164
|
+
})
|
|
165
|
+
const url = URL.createObjectURL(blob)
|
|
166
|
+
const a = document.createElement('a')
|
|
167
|
+
a.href = url
|
|
168
|
+
a.download = `knowledge-export-${new Date().toISOString().split('T')[0]}.json`
|
|
169
|
+
a.click()
|
|
170
|
+
URL.revokeObjectURL(url)
|
|
171
|
+
|
|
172
|
+
onSuccess?.(`Exported ${selectedDocuments.size} documents`)
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setSelectedDocuments(new Set())
|
|
177
|
+
setBulkAction('none')
|
|
178
|
+
await loadDocuments()
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Bulk operation failed:', error)
|
|
182
|
+
onError?.('Bulk operation failed')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// File upload
|
|
187
|
+
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
188
|
+
const files = event.target.files
|
|
189
|
+
if (!files) return
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
for (const file of Array.from(files)) {
|
|
193
|
+
const content = await file.text()
|
|
194
|
+
|
|
195
|
+
await handleAddDocument({
|
|
196
|
+
content,
|
|
197
|
+
category: 'uploaded',
|
|
198
|
+
metadata: {
|
|
199
|
+
filename: file.name,
|
|
200
|
+
fileType: file.type,
|
|
201
|
+
fileSize: file.size,
|
|
202
|
+
uploadedAt: new Date().toISOString()
|
|
203
|
+
},
|
|
204
|
+
source: { type: 'file', filename: file.name }
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('File upload failed:', error)
|
|
209
|
+
onError?.('File upload failed')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Document selection
|
|
214
|
+
const toggleDocumentSelection = (id: string) => {
|
|
215
|
+
const newSelection = new Set(selectedDocuments)
|
|
216
|
+
if (newSelection.has(id)) {
|
|
217
|
+
newSelection.delete(id)
|
|
218
|
+
} else {
|
|
219
|
+
newSelection.add(id)
|
|
220
|
+
}
|
|
221
|
+
setSelectedDocuments(newSelection)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const selectAllDocuments = () => {
|
|
225
|
+
if (selectedDocuments.size === documents.length) {
|
|
226
|
+
setSelectedDocuments(new Set())
|
|
227
|
+
} else {
|
|
228
|
+
setSelectedDocuments(new Set(documents.map(doc => doc.id)))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const categories = ['all', 'general', 'uploaded', 'manual', 'imported']
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className={`space-y-6 ${className}`}>
|
|
236
|
+
{/* Header */}
|
|
237
|
+
<div className="flex items-center justify-between">
|
|
238
|
+
<div>
|
|
239
|
+
<h2 className="text-2xl font-bold">Knowledge Base Manager</h2>
|
|
240
|
+
<p className="text-gray-600 dark:text-gray-300">
|
|
241
|
+
{ragSystem.totalDocuments} documents in your knowledge base
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="flex items-center gap-2">
|
|
246
|
+
<Button
|
|
247
|
+
variant="outline"
|
|
248
|
+
onClick={() => fileInputRef.current?.click()}
|
|
249
|
+
disabled={ragSystem.isLoading}
|
|
250
|
+
>
|
|
251
|
+
<Upload className="w-4 h-4 mr-2" />
|
|
252
|
+
Upload
|
|
253
|
+
</Button>
|
|
254
|
+
|
|
255
|
+
<Button
|
|
256
|
+
onClick={() => setIsAddingDocument(true)}
|
|
257
|
+
disabled={ragSystem.isLoading}
|
|
258
|
+
>
|
|
259
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
260
|
+
Add Document
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Search and Filters */}
|
|
266
|
+
<Card className="p-4">
|
|
267
|
+
<div className="flex flex-col sm:flex-row gap-4">
|
|
268
|
+
<div className="flex-1 relative">
|
|
269
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
270
|
+
<input
|
|
271
|
+
type="text"
|
|
272
|
+
placeholder="Search documents..."
|
|
273
|
+
value={searchQuery}
|
|
274
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
275
|
+
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
|
276
|
+
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<select
|
|
281
|
+
value={selectedCategory}
|
|
282
|
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
283
|
+
className="px-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
284
|
+
>
|
|
285
|
+
{categories.map(category => (
|
|
286
|
+
<option key={category} value={category}>
|
|
287
|
+
{category === 'all' ? 'All Categories' : category.charAt(0).toUpperCase() + category.slice(1)}
|
|
288
|
+
</option>
|
|
289
|
+
))}
|
|
290
|
+
</select>
|
|
291
|
+
|
|
292
|
+
<Button onClick={handleSearch} disabled={ragSystem.isLoading}>
|
|
293
|
+
{ragSystem.isLoading ? (
|
|
294
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
295
|
+
) : (
|
|
296
|
+
<Search className="w-4 h-4" />
|
|
297
|
+
)}
|
|
298
|
+
</Button>
|
|
299
|
+
</div>
|
|
300
|
+
</Card>
|
|
301
|
+
|
|
302
|
+
{/* Bulk Actions */}
|
|
303
|
+
{enableBulkOperations && selectedDocuments.size > 0 && (
|
|
304
|
+
<Card className="p-4 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
|
305
|
+
<div className="flex items-center justify-between">
|
|
306
|
+
<span className="font-medium">
|
|
307
|
+
{selectedDocuments.size} document{selectedDocuments.size !== 1 ? 's' : ''} selected
|
|
308
|
+
</span>
|
|
309
|
+
|
|
310
|
+
<div className="flex items-center gap-2">
|
|
311
|
+
<select
|
|
312
|
+
value={bulkAction}
|
|
313
|
+
onChange={(e) => setBulkAction(e.target.value as any)}
|
|
314
|
+
className="px-3 py-1 border border-gray-200 dark:border-gray-700 rounded"
|
|
315
|
+
>
|
|
316
|
+
<option value="none">Choose action...</option>
|
|
317
|
+
<option value="delete">Delete</option>
|
|
318
|
+
{enableExport && <option value="export">Export</option>}
|
|
319
|
+
</select>
|
|
320
|
+
|
|
321
|
+
<Button
|
|
322
|
+
onClick={handleBulkAction}
|
|
323
|
+
disabled={bulkAction === 'none' || ragSystem.isLoading}
|
|
324
|
+
variant="outline"
|
|
325
|
+
size="sm"
|
|
326
|
+
>
|
|
327
|
+
Apply
|
|
328
|
+
</Button>
|
|
329
|
+
|
|
330
|
+
<Button
|
|
331
|
+
onClick={() => setSelectedDocuments(new Set())}
|
|
332
|
+
variant="ghost"
|
|
333
|
+
size="sm"
|
|
334
|
+
>
|
|
335
|
+
Clear Selection
|
|
336
|
+
</Button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</Card>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
{/* Documents List */}
|
|
343
|
+
<Card className="p-0">
|
|
344
|
+
{documents.length === 0 ? (
|
|
345
|
+
<div className="text-center py-12">
|
|
346
|
+
<FileText className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
|
347
|
+
<h3 className="text-lg font-medium mb-2">No documents found</h3>
|
|
348
|
+
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
349
|
+
{searchQuery ? 'Try adjusting your search terms' : 'Start by adding your first document'}
|
|
350
|
+
</p>
|
|
351
|
+
<Button onClick={() => setIsAddingDocument(true)}>
|
|
352
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
353
|
+
Add Document
|
|
354
|
+
</Button>
|
|
355
|
+
</div>
|
|
356
|
+
) : (
|
|
357
|
+
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
358
|
+
{/* Header */}
|
|
359
|
+
{enableBulkOperations && (
|
|
360
|
+
<div className="px-6 py-3 bg-gray-50 dark:bg-gray-800/50">
|
|
361
|
+
<label className="flex items-center gap-3">
|
|
362
|
+
<input
|
|
363
|
+
type="checkbox"
|
|
364
|
+
checked={selectedDocuments.size === documents.length && documents.length > 0}
|
|
365
|
+
onChange={selectAllDocuments}
|
|
366
|
+
className="rounded border-gray-300"
|
|
367
|
+
/>
|
|
368
|
+
<span className="text-sm font-medium">Select All</span>
|
|
369
|
+
</label>
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{/* Document Items */}
|
|
374
|
+
<AnimatePresence>
|
|
375
|
+
{documents.map((document) => (
|
|
376
|
+
<motion.div
|
|
377
|
+
key={document.id}
|
|
378
|
+
initial={{ opacity: 0, y: 20 }}
|
|
379
|
+
animate={{ opacity: 1, y: 0 }}
|
|
380
|
+
exit={{ opacity: 0, y: -20 }}
|
|
381
|
+
className="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
|
382
|
+
>
|
|
383
|
+
<div className="flex items-start gap-4">
|
|
384
|
+
{enableBulkOperations && (
|
|
385
|
+
<input
|
|
386
|
+
type="checkbox"
|
|
387
|
+
checked={selectedDocuments.has(document.id)}
|
|
388
|
+
onChange={() => toggleDocumentSelection(document.id)}
|
|
389
|
+
className="mt-1 rounded border-gray-300"
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
<div className="flex-1 min-w-0">
|
|
394
|
+
<div className="flex items-start justify-between">
|
|
395
|
+
<div className="flex-1">
|
|
396
|
+
<h4 className="font-medium line-clamp-1">
|
|
397
|
+
{document.fields?.title || `Document ${document.id.slice(-8)}`}
|
|
398
|
+
</h4>
|
|
399
|
+
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2 mt-1">
|
|
400
|
+
{document.content}
|
|
401
|
+
</p>
|
|
402
|
+
|
|
403
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
|
404
|
+
<span className="flex items-center gap-1">
|
|
405
|
+
<Tag className="w-3 h-3" />
|
|
406
|
+
{document.category}
|
|
407
|
+
</span>
|
|
408
|
+
<span className="flex items-center gap-1">
|
|
409
|
+
<Calendar className="w-3 h-3" />
|
|
410
|
+
{new Date(document.createdAt || Date.now()).toLocaleDateString()}
|
|
411
|
+
</span>
|
|
412
|
+
{document.metadata?.filename && (
|
|
413
|
+
<span className="flex items-center gap-1">
|
|
414
|
+
<FileText className="w-3 h-3" />
|
|
415
|
+
{document.metadata.filename}
|
|
416
|
+
</span>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div className="flex items-center gap-1 ml-4">
|
|
422
|
+
<Button
|
|
423
|
+
variant="ghost"
|
|
424
|
+
size="sm"
|
|
425
|
+
onClick={() => setViewingDocument(document)}
|
|
426
|
+
>
|
|
427
|
+
<Eye className="w-4 h-4" />
|
|
428
|
+
</Button>
|
|
429
|
+
|
|
430
|
+
<Button
|
|
431
|
+
variant="ghost"
|
|
432
|
+
size="sm"
|
|
433
|
+
onClick={() => setEditingDocument(document)}
|
|
434
|
+
>
|
|
435
|
+
<Edit className="w-4 h-4" />
|
|
436
|
+
</Button>
|
|
437
|
+
|
|
438
|
+
<Button
|
|
439
|
+
variant="ghost"
|
|
440
|
+
size="sm"
|
|
441
|
+
onClick={() => handleDeleteDocument(document.id)}
|
|
442
|
+
className="text-red-500 hover:text-red-700"
|
|
443
|
+
>
|
|
444
|
+
<Trash2 className="w-4 h-4" />
|
|
445
|
+
</Button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</motion.div>
|
|
451
|
+
))}
|
|
452
|
+
</AnimatePresence>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
</Card>
|
|
456
|
+
|
|
457
|
+
{/* Hidden file input */}
|
|
458
|
+
<input
|
|
459
|
+
ref={fileInputRef}
|
|
460
|
+
type="file"
|
|
461
|
+
multiple
|
|
462
|
+
accept=".txt,.md,.json,.csv"
|
|
463
|
+
onChange={handleFileUpload}
|
|
464
|
+
className="hidden"
|
|
465
|
+
/>
|
|
466
|
+
|
|
467
|
+
{/* Add Document Modal */}
|
|
468
|
+
{isAddingDocument && (
|
|
469
|
+
<DocumentModal
|
|
470
|
+
onSave={handleAddDocument}
|
|
471
|
+
onCancel={() => setIsAddingDocument(false)}
|
|
472
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
|
|
475
|
+
{/* Edit Document Modal */}
|
|
476
|
+
{editingDocument && (
|
|
477
|
+
<DocumentModal
|
|
478
|
+
document={editingDocument}
|
|
479
|
+
onSave={(data) => handleUpdateDocument(editingDocument.id, data)}
|
|
480
|
+
onCancel={() => setEditingDocument(null)}
|
|
481
|
+
/>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{/* View Document Modal */}
|
|
485
|
+
{viewingDocument && (
|
|
486
|
+
<DocumentViewModal
|
|
487
|
+
document={viewingDocument}
|
|
488
|
+
onClose={() => setViewingDocument(null)}
|
|
489
|
+
/>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Document Modal Component
|
|
496
|
+
function DocumentModal({
|
|
497
|
+
document,
|
|
498
|
+
onSave,
|
|
499
|
+
onCancel
|
|
500
|
+
}: {
|
|
501
|
+
document?: KnowledgeDocument
|
|
502
|
+
onSave: (data: Partial<RAGDocument<any>>) => void
|
|
503
|
+
onCancel: () => void
|
|
504
|
+
}) {
|
|
505
|
+
const [formData, setFormData] = useState({
|
|
506
|
+
content: document?.content || '',
|
|
507
|
+
category: document?.category || 'general',
|
|
508
|
+
title: document?.fields?.title || '',
|
|
509
|
+
description: document?.fields?.description || ''
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
513
|
+
e.preventDefault()
|
|
514
|
+
onSave({
|
|
515
|
+
content: formData.content,
|
|
516
|
+
category: formData.category,
|
|
517
|
+
fields: {
|
|
518
|
+
title: formData.title,
|
|
519
|
+
description: formData.description
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
526
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
527
|
+
<h3 className="text-lg font-semibold mb-4">
|
|
528
|
+
{document ? 'Edit Document' : 'Add New Document'}
|
|
529
|
+
</h3>
|
|
530
|
+
|
|
531
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
532
|
+
<div>
|
|
533
|
+
<label className="block text-sm font-medium mb-2">Title</label>
|
|
534
|
+
<input
|
|
535
|
+
type="text"
|
|
536
|
+
value={formData.title}
|
|
537
|
+
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
|
538
|
+
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
539
|
+
placeholder="Document title..."
|
|
540
|
+
/>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div>
|
|
544
|
+
<label className="block text-sm font-medium mb-2">Category</label>
|
|
545
|
+
<select
|
|
546
|
+
value={formData.category}
|
|
547
|
+
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))}
|
|
548
|
+
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
549
|
+
>
|
|
550
|
+
<option value="general">General</option>
|
|
551
|
+
<option value="manual">Manual</option>
|
|
552
|
+
<option value="faq">FAQ</option>
|
|
553
|
+
<option value="guide">Guide</option>
|
|
554
|
+
<option value="reference">Reference</option>
|
|
555
|
+
</select>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<div>
|
|
559
|
+
<label className="block text-sm font-medium mb-2">Description</label>
|
|
560
|
+
<input
|
|
561
|
+
type="text"
|
|
562
|
+
value={formData.description}
|
|
563
|
+
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
|
564
|
+
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
565
|
+
placeholder="Brief description..."
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div>
|
|
570
|
+
<label className="block text-sm font-medium mb-2">Content</label>
|
|
571
|
+
<textarea
|
|
572
|
+
value={formData.content}
|
|
573
|
+
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
|
|
574
|
+
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent h-32"
|
|
575
|
+
placeholder="Document content..."
|
|
576
|
+
required
|
|
577
|
+
/>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div className="flex justify-end gap-3">
|
|
581
|
+
<Button type="button" variant="outline" onClick={onCancel}>
|
|
582
|
+
Cancel
|
|
583
|
+
</Button>
|
|
584
|
+
<Button type="submit">
|
|
585
|
+
{document ? 'Update' : 'Add'} Document
|
|
586
|
+
</Button>
|
|
587
|
+
</div>
|
|
588
|
+
</form>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Document View Modal Component
|
|
595
|
+
function DocumentViewModal({
|
|
596
|
+
document,
|
|
597
|
+
onClose
|
|
598
|
+
}: {
|
|
599
|
+
document: KnowledgeDocument
|
|
600
|
+
onClose: () => void
|
|
601
|
+
}) {
|
|
602
|
+
return (
|
|
603
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
604
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
605
|
+
<div className="flex items-start justify-between mb-4">
|
|
606
|
+
<div>
|
|
607
|
+
<h3 className="text-lg font-semibold">
|
|
608
|
+
{document.fields?.title || `Document ${document.id.slice(-8)}`}
|
|
609
|
+
</h3>
|
|
610
|
+
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
611
|
+
{document.category} • {new Date(document.createdAt || Date.now()).toLocaleDateString()}
|
|
612
|
+
</p>
|
|
613
|
+
</div>
|
|
614
|
+
<Button variant="ghost" onClick={onClose}>
|
|
615
|
+
✕
|
|
616
|
+
</Button>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
{document.fields?.description && (
|
|
620
|
+
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
|
621
|
+
{document.fields.description}
|
|
622
|
+
</p>
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
<div className="prose dark:prose-invert max-w-none">
|
|
626
|
+
<div className="whitespace-pre-wrap">
|
|
627
|
+
{document.content}
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
{document.metadata && Object.keys(document.metadata).length > 0 && (
|
|
632
|
+
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
633
|
+
<h4 className="font-medium mb-2">Metadata</h4>
|
|
634
|
+
<pre className="text-xs bg-gray-50 dark:bg-gray-900 p-3 rounded overflow-x-auto">
|
|
635
|
+
{JSON.stringify(document.metadata, null, 2)}
|
|
636
|
+
</pre>
|
|
637
|
+
</div>
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
)
|
|
642
|
+
}
|