@@ -119,6 +119,81 @@ const CreateTestset: React.FC<Props> = ({setCurrent, onCancel}) => {
119119 const [ previewData , setPreviewData ] = useState < GenericObject [ ] > ( [ ] )
120120 const setRefreshTrigger = useSetAtom ( testsetsRefreshTriggerAtom )
121121
122+ /**
123+ * Parse CSV text properly handling quoted fields with embedded newlines and commas
124+ */
125+ const parseCSVRows = ( text : string ) : string [ ] [ ] => {
126+ const rows : string [ ] [ ] = [ ]
127+ let currentRow : string [ ] = [ ]
128+ let currentField = ""
129+ let inQuotes = false
130+ let i = 0
131+
132+ while ( i < text . length ) {
133+ const char = text [ i ]
134+
135+ if ( inQuotes ) {
136+ if ( char === '"' ) {
137+ // Check for escaped quote ("")
138+ if ( i + 1 < text . length && text [ i + 1 ] === '"' ) {
139+ currentField += '"'
140+ i += 2
141+ continue
142+ }
143+ // End of quoted field
144+ inQuotes = false
145+ i ++
146+ continue
147+ }
148+ // Inside quotes - add character as-is (including newlines)
149+ currentField += char
150+ i ++
151+ } else {
152+ if ( char === '"' ) {
153+ // Start of quoted field
154+ inQuotes = true
155+ i ++
156+ } else if ( char === "," ) {
157+ // Field separator
158+ currentRow . push ( currentField . trim ( ) )
159+ currentField = ""
160+ i ++
161+ } else if ( char === "\n" || ( char === "\r" && text [ i + 1 ] === "\n" ) ) {
162+ // Row separator
163+ currentRow . push ( currentField . trim ( ) )
164+ if ( currentRow . some ( ( field ) => field !== "" ) ) {
165+ rows . push ( currentRow )
166+ }
167+ currentRow = [ ]
168+ currentField = ""
169+ i += char === "\r" ? 2 : 1
170+ } else if ( char === "\r" ) {
171+ // Handle standalone \r as row separator
172+ currentRow . push ( currentField . trim ( ) )
173+ if ( currentRow . some ( ( field ) => field !== "" ) ) {
174+ rows . push ( currentRow )
175+ }
176+ currentRow = [ ]
177+ currentField = ""
178+ i ++
179+ } else {
180+ currentField += char
181+ i ++
182+ }
183+ }
184+ }
185+
186+ // Handle last field and row
187+ if ( currentField || currentRow . length > 0 ) {
188+ currentRow . push ( currentField . trim ( ) )
189+ if ( currentRow . some ( ( field ) => field !== "" ) ) {
190+ rows . push ( currentRow )
191+ }
192+ }
193+
194+ return rows
195+ }
196+
122197 const parseFileForPreview = async (
123198 file : File ,
124199 fileType : "CSV" | "JSON" ,
@@ -132,12 +207,12 @@ const CreateTestset: React.FC<Props> = ({setCurrent, onCancel}) => {
132207 return parsed . slice ( 0 , maxPreviewRows )
133208 }
134209 } else {
135- const lines = text . split ( "\n" ) . filter ( ( line ) => line . trim ( ) )
136- if ( lines . length > 0 ) {
137- const headers = lines [ 0 ] . split ( "," ) . map ( ( h ) => h . trim ( ) )
210+ const csvRows = parseCSVRows ( text )
211+ if ( csvRows . length > 0 ) {
212+ const headers = csvRows [ 0 ]
138213 const rows : GenericObject [ ] = [ ]
139- for ( let i = 1 ; i < Math . min ( lines . length , maxPreviewRows + 1 ) ; i ++ ) {
140- const values = lines [ i ] . split ( "," ) . map ( ( v ) => v . trim ( ) )
214+ for ( let i = 1 ; i < Math . min ( csvRows . length , maxPreviewRows + 1 ) ; i ++ ) {
215+ const values = csvRows [ i ]
141216 const row : GenericObject = { }
142217 headers . forEach ( ( header , idx ) => {
143218 row [ header ] = values [ idx ] || ""
0 commit comments