1
1
import { createKnowledge } from "@/db/dexie/knowledge"
2
2
import { Source } from "@/db/knowledge"
3
3
import { defaultEmbeddingModelForRag } from "@/services/ollama"
4
- import { convertToSource } from "@/utils/to-source"
4
+ import { convertTextToSource , convertToSource } from "@/utils/to-source"
5
5
import { useMutation } from "@tanstack/react-query"
6
- import { Modal , Form , Input , Upload , message , UploadFile } from "antd"
6
+ import { Modal , Form , Input , Upload , message , Tabs , Select } from "antd"
7
7
import { InboxIcon } from "lucide-react"
8
8
import { useTranslation } from "react-i18next"
9
9
import PubSub from "pubsub-js"
10
10
import { KNOWLEDGE_QUEUE } from "@/queue"
11
11
import { useStorage } from "@plasmohq/storage/hook"
12
12
import { unsupportedTypes } from "./utils/unsupported-types"
13
+ import React from "react"
13
14
14
15
type Props = {
15
16
open : boolean
@@ -20,18 +21,16 @@ export const AddKnowledge = ({ open, setOpen }: Props) => {
20
21
const { t } = useTranslation ( [ "knowledge" , "common" ] )
21
22
const [ form ] = Form . useForm ( )
22
23
const [ totalFilePerKB ] = useStorage ( "totalFilePerKB" , 5 )
24
+ const [ mode , setMode ] = React . useState < "upload" | "text" > ( "upload" )
23
25
24
- const onUploadHandler = async ( data : {
25
- title : string
26
- file : UploadFile [ ]
27
- } ) => {
26
+ const onUploadHandler = async ( data : any ) => {
28
27
const defaultEM = await defaultEmbeddingModelForRag ( )
29
28
30
29
if ( ! defaultEM ) {
31
30
throw new Error ( t ( "noEmbeddingModel" ) )
32
31
}
33
32
34
- const source : Source [ ] = [ ]
33
+ const source : Source [ ] = [ ]
35
34
36
35
const allowedTypes = [
37
36
"application/pdf" ,
@@ -41,19 +40,57 @@ export const AddKnowledge = ({ open, setOpen }: Props) => {
41
40
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
42
41
]
43
42
44
- for ( const file of data . file ) {
45
- let mime = file . type
46
- if ( ! allowedTypes . includes ( mime ) ) {
47
- mime = "text/plain"
43
+ if ( mode === "upload" ) {
44
+ for ( const file of data . file || [ ] ) {
45
+ let mime = file . type
46
+ if ( ! allowedTypes . includes ( mime ) ) {
47
+ mime = "text/plain"
48
+ }
49
+ const _src = await convertToSource ( { file, mime, sourceType : "file_upload" } )
50
+ source . push ( _src )
51
+ }
52
+ } else {
53
+ // Text mode validation
54
+ const rawText : string = ( data ?. textContent || "" ) . trim ( )
55
+ const textType : string = data ?. textType || "plain"
56
+ if ( ! rawText ) {
57
+ throw new Error ( t ( "form.textInput.required" ) )
58
+ }
59
+ // Prevent oversized content (e.g., > 500k chars)
60
+ if ( rawText . length > 500000 ) {
61
+ throw new Error ( t ( "form.textInput.tooLarge" ) )
62
+ }
63
+
64
+ const asMarkdown = textType === "markdown"
65
+ const filename = data ?. title
66
+ ? `${ data ?. title } .txt`
67
+ : `pasted_${ new Date ( ) . getTime ( ) } .txt`
68
+ const _src = await convertTextToSource ( {
69
+ text : rawText ,
70
+ filename,
71
+ mime : asMarkdown ? "text/markdown" : "text/plain" ,
72
+ asMarkdown,
73
+ sourceType : "text_input"
74
+ } )
75
+ source . push ( _src )
76
+ }
77
+
78
+ let title = data ?. title ?. trim ( )
79
+ if ( ! title || title . length === 0 ) {
80
+ if ( mode === "text" ) {
81
+ const text = ( data ?. textContent || "" ) . trim ( )
82
+ title = text . substring ( 0 , 50 ) || t ( "form.textInput.defaultTitle" )
83
+ } else if ( ( data ?. file || [ ] ) . length > 0 ) {
84
+ title = ( data . file [ 0 ] ?. name as string ) || t ( "form.textInput.defaultTitle" )
85
+ } else {
86
+ title = t ( "form.textInput.defaultTitle" )
48
87
}
49
- const data = await convertToSource ( { file, mime } )
50
- source . push ( data )
51
88
}
52
89
53
90
const knowledge = await createKnowledge ( {
54
91
embedding_model : defaultEM ,
55
92
source,
56
- title : data . title
93
+ title
57
94
} )
58
95
59
96
return knowledge . id
@@ -78,69 +115,94 @@ export const AddKnowledge = ({ open, setOpen }: Props) => {
78
115
open = { open }
79
116
footer = { null }
80
117
onCancel = { ( ) => setOpen ( false ) } >
118
+ < Tabs
119
+ activeKey = { mode }
120
+ onChange = { ( key ) => setMode ( key as any ) }
121
+ items = { [
122
+ { key : "upload" , label : t ( "form.tabs.upload" ) } ,
123
+ { key : "text" , label : t ( "form.tabs.text" ) }
124
+ ] }
125
+ />
81
126
< Form onFinish = { saveKnowledge } form = { form } layout = "vertical" >
82
- < Form . Item
83
- rules = { [
84
- {
85
- required : true ,
86
- message : t ( "form.title.required" )
87
- }
88
- ] }
89
- name = "title"
90
- label = { t ( "form.title.label" ) } >
91
- < Input size = "large" placeholder = { t ( "form.title.placeholder" ) } />
127
+ { /* Title is optional now */ }
128
+ < Form . Item name = "title" label = { t ( "form.title.label" ) } >
129
+ < Input size = "large" placeholder = { t ( "form.title.placeholderOptional" ) } />
92
130
</ Form . Item >
93
- < Form . Item
94
- name = "file"
95
- label = { t ( "form.uploadFile.label" ) }
96
- rules = { [
97
- {
98
- required : true ,
99
- message : t ( "form.uploadFile.required" )
100
- }
101
- ] }
102
- getValueFromEvent = { ( e ) => {
103
- if ( Array . isArray ( e ) ) {
104
- return e
105
- }
106
- return e ?. fileList
107
- } } >
108
- < Upload . Dragger
109
- multiple = { true }
110
- maxCount = { totalFilePerKB }
111
- beforeUpload = { ( file ) => {
112
- const allowedTypes = [
113
- "application/pdf" ,
114
- "text/csv" ,
115
- "text/plain" ,
116
- "text/markdown" ,
117
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
118
- ]
119
- . map ( ( type ) => type . toLowerCase ( ) )
120
- . join ( ", " )
121
-
122
- if ( unsupportedTypes . includes ( file . type . toLowerCase ( ) ) ) {
123
- message . error (
124
- t ( "form.uploadFile.uploadError" , { allowedTypes } )
125
- )
126
- return Upload . LIST_IGNORE
127
- }
128
131
129
- return false
132
+ { mode === "upload" ? (
133
+ < Form . Item
134
+ name = "file"
135
+ label = { t ( "form.uploadFile.label" ) }
136
+ rules = { [
137
+ {
138
+ required : true ,
139
+ message : t ( "form.uploadFile.required" )
140
+ }
141
+ ] }
142
+ getValueFromEvent = { ( e ) => {
143
+ if ( Array . isArray ( e ) ) {
144
+ return e
145
+ }
146
+ return e ?. fileList
130
147
} } >
131
- < div className = "p-3" >
132
- < p className = "flex justify-center ant-upload-drag-icon" >
133
- < InboxIcon className = "w-10 h-10 text-gray-400" />
134
- </ p >
135
- < p className = "ant-upload-text" >
136
- { t ( "form.uploadFile.uploadText" ) }
137
- </ p >
138
- { /* <p className="ant-upload-hint">
139
- {t("form.uploadFile.uploadHint")}
140
- </p> */ }
141
- </ div >
142
- </ Upload . Dragger >
143
- </ Form . Item >
148
+ < Upload . Dragger
149
+ multiple = { true }
150
+ maxCount = { totalFilePerKB }
151
+ beforeUpload = { ( file ) => {
152
+ const allowedTypes = [
153
+ "application/pdf" ,
154
+ "text/csv" ,
155
+ "text/plain" ,
156
+ "text/markdown" ,
157
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
158
+ ]
159
+ . map ( ( type ) => type . toLowerCase ( ) )
160
+ . join ( ", " )
161
+
162
+ if ( unsupportedTypes . includes ( file . type . toLowerCase ( ) ) ) {
163
+ message . error (
164
+ t ( "form.uploadFile.uploadError" , { allowedTypes } )
165
+ )
166
+ return Upload . LIST_IGNORE
167
+ }
168
+
169
+ return false
170
+ } } >
171
+ < div className = "p-3" >
172
+ < p className = "flex justify-center ant-upload-drag-icon" >
173
+ < InboxIcon className = "w-10 h-10 text-gray-400" />
174
+ </ p >
175
+ < p className = "ant-upload-text" >
176
+ { t ( "form.uploadFile.uploadText" ) }
177
+ </ p >
178
+ </ div >
179
+ </ Upload . Dragger >
180
+ </ Form . Item >
181
+ ) : (
182
+ < >
183
+ < Form . Item
184
+ name = "textType"
185
+ label = { t ( "form.textInput.typeLabel" ) }
186
+ initialValue = "plain" >
187
+ < Select
188
+ options = { [
189
+ { value : "plain" , label : t ( "form.textInput.type.plain" ) } ,
190
+ { value : "markdown" , label : t ( "form.textInput.type.markdown" ) } ,
191
+ { value : "code" , label : t ( "form.textInput.type.code" ) }
192
+ ] }
193
+ />
194
+ </ Form . Item >
195
+ < Form . Item
196
+ name = "textContent"
197
+ label = { t ( "form.textInput.contentLabel" ) }
198
+ rules = { [ { required : true , message : t ( "form.textInput.required" ) } ] } >
199
+ < Input . TextArea
200
+ autoSize = { { minRows : 8 , maxRows : 16 } }
201
+ placeholder = { t ( "form.textInput.placeholder" ) }
202
+ />
203
+ </ Form . Item >
204
+ </ >
205
+ ) }
144
206
145
207
< Form . Item >
146
208
< button
0 commit comments