Skip to content

Commit e4a707a

Browse files
authored
Merge pull request #70 from cortex-reply/fix/knowledge
feat: applies changes to knowledge addition, editing and viewing
2 parents 0748012 + 2fe500a commit e4a707a

22 files changed

Lines changed: 2333 additions & 512 deletions

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
"@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-migration": "^1.3.1",
101101
"@headlessui/react": "^2.2.0",
102102
"@heroicons/react": "^2.2.0",
103-
"@payloadcms/richtext-lexical": "^3.11.0",
103+
"@payloadcms/richtext-lexical": "^3.39.1",
104+
"@payloadcms/ui": "^3.56.0",
104105
"@radix-ui/react-avatar": "^1.1.2",
105106
"@radix-ui/react-checkbox": "^1.1.3",
106107
"@radix-ui/react-collapsible": "^1.1.2",
@@ -124,6 +125,7 @@
124125
"@radix-ui/react-use-controllable-state": "^1.2.2",
125126
"@tailwindcss/typography": "^0.5.15",
126127
"ai": "^5.0.42",
128+
"bson-objectid": "^2.0.4",
127129
"class-variance-authority": "^0.7.1",
128130
"clsx": "^2.1.1",
129131
"cmdk": "^1.0.4",
@@ -142,6 +144,7 @@
142144
"react-icons": "^5.4.0",
143145
"react-markdown": "^10.1.0",
144146
"react-syntax-highlighter": "^15.6.6",
147+
"remark-gfm": "^4.0.1",
145148
"streamdown": "^1.2.0",
146149
"tailwindcss-animate": "^1.0.7",
147150
"tailwindcss-animated": "^2.0.0",

pnpm-lock.yaml

Lines changed: 355 additions & 206 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import { Knowledge } from '../types'
2+
import {
3+
Button,
4+
Dialog,
5+
DialogContent,
6+
DialogHeader,
7+
DialogTitle,
8+
Input,
9+
Label,
10+
Select,
11+
SelectContent,
12+
SelectItem,
13+
SelectTrigger,
14+
SelectValue,
15+
} from '@/components/ui'
16+
import { Plus, Save, X } from 'lucide-react'
17+
import { KnowledgeContext } from '../types'
18+
import React, { useState } from 'react'
19+
20+
interface AddKnowledgeModalProps {
21+
isOpen: boolean
22+
onClose: () => void
23+
onAddKnowledge: (knowledge: Omit<Knowledge, 'id'>) => void
24+
knowledgeContexts: KnowledgeContext[]
25+
}
26+
27+
export const AddKnowledgeModal: React.FC<AddKnowledgeModalProps> = ({
28+
isOpen,
29+
onClose,
30+
onAddKnowledge,
31+
knowledgeContexts,
32+
}) => {
33+
const [formData, setFormData] = useState({
34+
name: '',
35+
description: '',
36+
metadata: [] as { key: string; value: string }[],
37+
})
38+
39+
const handleSubmit = (e: React.FormEvent) => {
40+
e.preventDefault()
41+
42+
if (!formData.name) {
43+
return
44+
}
45+
46+
onAddKnowledge({
47+
name: formData.name,
48+
description: formData.description,
49+
metadata: formData.metadata,
50+
source: 'payload',
51+
visibility: 'public',
52+
createdAt: new Date().toISOString(),
53+
updatedAt: new Date().toISOString(),
54+
// isActive: formData.isActive,
55+
// isSelected: false,
56+
})
57+
58+
// Reset form
59+
setFormData({
60+
name: '',
61+
description: '',
62+
metadata: [] as { key: string; value: string }[],
63+
// isActive: false,
64+
})
65+
66+
onClose()
67+
}
68+
69+
const handleClose = () => {
70+
setFormData({
71+
name: '',
72+
description: '',
73+
metadata: [] as { key: string; value: string }[],
74+
// isActive: false,
75+
})
76+
onClose()
77+
}
78+
79+
const getSuggestedMetadataKeys = () => {
80+
const keysFromContexts = new Set<string>()
81+
const keysFromDocuments = new Set<string>()
82+
83+
// Get keys from knowledge contexts (used for grouping)
84+
knowledgeContexts.forEach((context) => {
85+
context.menuConfig.groupBy.forEach((key) => keysFromContexts.add(key))
86+
})
87+
88+
return {
89+
contextKeys: Array.from(keysFromContexts).sort(),
90+
documentKeys: Array.from(keysFromDocuments).sort(),
91+
allKeys: Array.from(new Set([...keysFromContexts, ...keysFromDocuments])).sort(),
92+
}
93+
}
94+
const suggestedKeys = getSuggestedMetadataKeys()
95+
96+
// Get suggested values for a specific metadata key
97+
const getSuggestedValues = (key: string) => {
98+
const values = new Set<string>()
99+
100+
return Array.from(values).sort()
101+
}
102+
103+
const handleMetadataChange = (key: string, value: string) => {
104+
setFormData((prev) => {
105+
const prevItem = prev.metadata.find((item) => item.key === key)
106+
if (prevItem) {
107+
prevItem.value = value
108+
}
109+
return {
110+
...prev,
111+
metadata: [...prev.metadata.filter((item) => item.key !== key), { key, value }],
112+
}
113+
})
114+
}
115+
116+
const handleMetadataValueSelect = (key: string, value: string) => {
117+
if (value === '__custom__') {
118+
// Just clear the value and let user type in the select field
119+
handleMetadataChange(key, '')
120+
} else {
121+
handleMetadataChange(key, value)
122+
}
123+
}
124+
125+
const handleAddMetadataField = () => {
126+
handleMetadataChange(' ', '')
127+
}
128+
129+
const handleRemoveMetadataField = (key: string) => {
130+
setFormData((prev) => {
131+
const newMetadata = [...prev.metadata]
132+
const index = newMetadata.findIndex((item) => item.key === key)
133+
if (index !== -1) {
134+
newMetadata.splice(index, 1)
135+
}
136+
return { ...prev, metadata: newMetadata }
137+
})
138+
}
139+
140+
return (
141+
<Dialog open={isOpen} onOpenChange={handleClose}>
142+
<DialogContent className="sm:max-w-md">
143+
<DialogHeader>
144+
<DialogTitle>Add New Knowledge source</DialogTitle>
145+
</DialogHeader>
146+
147+
<form onSubmit={handleSubmit} className="space-y-4">
148+
<div className="space-y-2">
149+
<div>
150+
<Label htmlFor="name">Name *</Label>
151+
<Input
152+
id="name"
153+
value={formData.name}
154+
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
155+
required
156+
/>
157+
</div>
158+
<div>
159+
<Label htmlFor="name">Description</Label>
160+
<Input
161+
id="name"
162+
value={formData.description}
163+
onChange={(e) =>
164+
setFormData((prev) => ({
165+
...prev,
166+
description: e.target.value,
167+
}))
168+
}
169+
required
170+
/>
171+
</div>
172+
173+
<div className="space-y-2">
174+
<div className="flex items-center justify-between mb-3">
175+
<div>
176+
<Label htmlFor="metadata">Metadata</Label>
177+
{suggestedKeys.contextKeys.length > 0 && (
178+
<p className="text-xs text-muted-foreground">
179+
Suggested: {suggestedKeys.contextKeys.slice(0, 4).join(', ')}
180+
{suggestedKeys.contextKeys.length > 4 &&
181+
` +${suggestedKeys.contextKeys.length - 4}`}
182+
</p>
183+
)}
184+
</div>
185+
<Button
186+
type="button"
187+
variant="outline"
188+
size="sm"
189+
onClick={handleAddMetadataField}
190+
className="gap-1 h-8"
191+
>
192+
<Plus className="h-3 w-3" />
193+
Add
194+
</Button>
195+
</div>
196+
197+
{formData.metadata && formData.metadata.length > 0 ? (
198+
<div className="space-y-2">
199+
{formData.metadata.map(({ key, value }) => {
200+
const suggestedValues = getSuggestedValues(key)
201+
return (
202+
<div key={key} className="flex gap-2 items-center">
203+
<Input
204+
value={key}
205+
onChange={(e) => {
206+
const newKey = e.target.value || ' '
207+
208+
const oldValue = formData.metadata.find(
209+
(item) => item.key === key,
210+
)?.value
211+
setFormData((prev) => {
212+
const newMetadata = [...prev.metadata]
213+
const index = newMetadata.findIndex((item) => item.key === key)
214+
if (index !== -1) {
215+
newMetadata.splice(index, 1)
216+
}
217+
if (newKey) {
218+
newMetadata.push({
219+
key: newKey,
220+
value: oldValue || '',
221+
})
222+
}
223+
return { ...prev, metadata: newMetadata }
224+
})
225+
}}
226+
placeholder="Key"
227+
className="w-32 h-8 text-sm"
228+
list={`metadata-keys-${key}`}
229+
/>
230+
<datalist id={`metadata-keys-${key}`}>
231+
{suggestedKeys.allKeys.map((suggestedKey) => (
232+
<option key={suggestedKey} value={suggestedKey} />
233+
))}
234+
</datalist>
235+
236+
<span className="text-muted-foreground text-sm">=</span>
237+
238+
{suggestedValues.length > 0 ? (
239+
<Select
240+
value={String(value || '')}
241+
onValueChange={(newValue) => handleMetadataValueSelect(key, newValue)}
242+
>
243+
<SelectTrigger className="flex-1 h-8 text-sm">
244+
<SelectValue placeholder="Value" />
245+
</SelectTrigger>
246+
<SelectContent>
247+
{suggestedValues.map((suggestedValue) => (
248+
<SelectItem key={suggestedValue} value={suggestedValue}>
249+
{suggestedValue}
250+
</SelectItem>
251+
))}
252+
<SelectItem value="__custom__">
253+
<span className="text-muted-foreground text-xs">Custom...</span>
254+
</SelectItem>
255+
</SelectContent>
256+
</Select>
257+
) : (
258+
<Input
259+
value={String(value || '')}
260+
onChange={(e) => handleMetadataChange(key, e.target.value)}
261+
placeholder="Value"
262+
className="flex-1 h-8 text-sm"
263+
/>
264+
)}
265+
266+
<Button
267+
type="button"
268+
variant="ghost"
269+
size="sm"
270+
onClick={() => handleRemoveMetadataField(key)}
271+
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
272+
>
273+
<X className="h-3 w-3" />
274+
</Button>
275+
</div>
276+
)
277+
})}
278+
</div>
279+
) : (
280+
<div className="text-center py-3 border border-dashed border-border rounded-lg bg-muted/20">
281+
<p className="text-xs text-muted-foreground">
282+
No metadata • Click "Add" to create fields
283+
</p>
284+
</div>
285+
)}
286+
</div>
287+
288+
{/* <div className="flex items-center gap-2">
289+
<span className="text-sm font-medium text-foreground">Format:</span>
290+
<Badge variant="secondary" className="font-mono">
291+
{formData.metadata.map(({ key, value }) => `${key}: ${value}`).join(', ')}
292+
</Badge>
293+
</div> */}
294+
</div>
295+
296+
<div className="flex justify-end gap-3 pt-4">
297+
<Button variant="outline" size="sm" onClick={onClose} className="gap-2">
298+
<X className="h-4 w-4" />
299+
Cancel
300+
</Button>
301+
<Button type="submit">
302+
<Save className="h-4 w-4" />
303+
Create
304+
</Button>
305+
</div>
306+
</form>
307+
</DialogContent>
308+
</Dialog>
309+
)
310+
}

0 commit comments

Comments
 (0)