1+ import React , { useEffect , useState } from "react" ;
2+ import type { Refine } from "../@types/refine" ;
3+ import { getRefine , postRefine , deleteChat } from "../services/ai_chat.ts" ;
4+ import type { JSX } from "react" ;
5+
6+ function extractBracedBlockFrom ( str : string , startPattern : RegExp ) : string | null {
7+ const match = str . match ( startPattern ) ;
8+ if ( ! match ) return null ;
9+ let startIdx = match . index ! + match [ 0 ] . length ;
10+
11+ // Avanza hasta la primera llave de apertura
12+ while ( str [ startIdx ] !== '{' && startIdx < str . length ) startIdx ++ ;
13+ if ( str [ startIdx ] !== '{' ) return null ;
14+
15+ let open = 1 ;
16+ let i = startIdx + 1 ;
17+ for ( ; i < str . length ; i ++ ) {
18+ if ( str [ i ] === '{' ) open ++ ;
19+ if ( str [ i ] === '}' ) open -- ;
20+ if ( open === 0 ) break ;
21+ }
22+
23+ if ( open !== 0 ) return null ;
24+
25+ return str . slice ( startIdx , i + 1 ) ;
26+ }
27+
28+ function parseRefineQuestion ( raw : string ) {
29+ const questionMatch = raw . match ( / Q U E S T I O N : \s * ' ( [ ^ ' ] * ) ' / ) ;
30+ const passageMatch = raw . match ( / P A S S A G E : \s * ' ( [ \s \S ] * ?) ' / ) ;
31+ const passage = passageMatch ? passageMatch [ 1 ] : "" ;
32+
33+ // Usa extractBracedBlockFrom directamente sobre raw
34+ const metadata = extractBracedBlockFrom ( raw , / M E T A D A T A : \s * ' / ) ?? "" ;
35+
36+ return {
37+ question : questionMatch ? questionMatch [ 1 ] : "" ,
38+ passage,
39+ metadata,
40+ } ;
41+ }
42+
43+ function renderObjectAsTable ( obj : any ) : JSX . Element {
44+ return (
45+ < table className = "min-w-full rounded-md overflow-hidden mb-2 text-base" >
46+ < tbody >
47+ { Object . entries ( obj ) . map ( ( [ key , value ] ) => (
48+ < tr key = { key } className = "border-t border-gray-200 last:border-b" >
49+ < td className = "px-3 py-2 font-medium capitalize text-gray-700 bg-gray-50 w-1/4 border-r border-gray-200 align-top" >
50+ { key }
51+ </ td >
52+ < td className = "px-3 py-2 bg-white align-top" >
53+ { Array . isArray ( value )
54+ ? value . map ( ( v , i ) =>
55+ typeof v === "object" && v !== null
56+ ? (
57+ < div
58+ key = { i }
59+ className = { i < value . length - 1 ? "pb-2 mb-2" : "" }
60+ >
61+ { renderObjectAsTable ( v ) }
62+ </ div >
63+ )
64+ : (
65+ < span
66+ key = { i }
67+ className = { i < value . length - 1 ? "pb-1 mb-1 inline-block" : "" }
68+ >
69+ { String ( v ) } { i < value . length - 1 ? ', ' : '' }
70+ </ span >
71+ )
72+ )
73+ : typeof value === "object" && value !== null
74+ ? renderObjectAsTable ( value )
75+ : String ( value )
76+ }
77+ </ td >
78+ </ tr >
79+ ) ) }
80+ </ tbody >
81+ </ table >
82+ ) ;
83+ }
84+
85+ function formatMetadata ( metadata : string ) {
86+ try {
87+ const fixed = metadata
88+ . replace ( / N o n e / g, 'null' )
89+ . replace ( / ' ( [ ^ ' ] * ?) ' \s * : / g, ( _ , key ) => `"${ key } ":` )
90+ . replace ( / : \s * ' ( [ ^ ' ] * ?) ' / g, ( _ , val ) => `: "${ val } "` ) ;
91+ const obj = JSON . parse ( fixed ) ;
92+
93+ // Extrae los campos id, name, email si existen en la metadata raíz o en profile_info
94+ const filtered =
95+ "profile_info" in obj
96+ ? {
97+ id : obj . profile_info . id ?? obj . id ?? "" ,
98+ name : obj . profile_info . name ?? obj . name ?? "" ,
99+ email : obj . profile_info . email ?? obj . email ?? "" ,
100+ }
101+ : {
102+ id : obj . id ?? "" ,
103+ name : obj . name ?? "" ,
104+ email : obj . email ?? "" ,
105+ } ;
106+
107+ return (
108+ < div className = "text-base text-gray-800 space-y-4" >
109+ { renderObjectAsTable ( filtered ) }
110+ </ div >
111+ ) ;
112+ } catch ( err ) {
113+ console . error ( "Metadata parse error:" , err ) ;
114+ return < span > { metadata } </ span > ;
115+ }
116+ }
117+
118+ const AIChatManagment : React . FC = ( ) => {
119+ const [ refines , setRefines ] = useState < Refine [ ] > ( [ ] ) ;
120+ const [ expandedRefineId , setExpandedRefineId ] = useState < string | null > ( null ) ;
121+ const [ editingRefineId , setEditingRefineId ] = useState < string | null > ( null ) ;
122+ const [ editedAnswer , setEditedAnswer ] = useState < string > ( "" ) ;
123+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
124+ const [ hasNextPage , setHasNextPage ] = useState ( false ) ;
125+
126+ const fetchRefines = async ( ) => {
127+ // Pide la página actual
128+ const refinesPage = await getRefine ( { page : currentPage } ) ;
129+ setRefines ( refinesPage ) ;
130+
131+ // Pide la siguiente página para saber si hay más
132+ const nextPage = await getRefine ( { page : currentPage + 1 } ) ;
133+ setHasNextPage ( nextPage . length > 0 ) ;
134+ } ;
135+
136+ useEffect ( ( ) => {
137+ fetchRefines ( ) ;
138+ } , [ currentPage ] ) ;
139+
140+ const handleEdit = ( refine : Refine ) => {
141+ setEditingRefineId ( refine . id ) ;
142+ setEditedAnswer ( refine . answer ) ;
143+ } ;
144+
145+ const handleCancel = ( ) => {
146+ setEditingRefineId ( null ) ;
147+ setEditedAnswer ( "" ) ;
148+ } ;
149+
150+ const handlePost = async ( refine : Refine ) => {
151+ await postRefine ( [ {
152+ question : refine . question ,
153+ answer : refine . answer ,
154+ id : refine . id ,
155+ } ] ) ;
156+ fetchRefines ( ) ;
157+ } ;
158+
159+ const handleSave = ( refine : Refine ) => {
160+ setRefines ( ( prev ) =>
161+ prev . map ( ( r ) =>
162+ r . id === refine . id ? { ...r , answer : editedAnswer } : r
163+ )
164+ ) ;
165+ setEditingRefineId ( null ) ;
166+ setEditedAnswer ( "" ) ;
167+ } ;
168+
169+ const handleDelete = async ( id : string ) => {
170+ if ( window . confirm ( "Are you sure you want to delete this rule?" ) ) {
171+ await deleteChat ( id ) ;
172+ fetchRefines ( ) ;
173+ if ( expandedRefineId === id ) {
174+ setExpandedRefineId ( null ) ;
175+ }
176+ }
177+ } ;
178+
179+ const toggleExpandRule = ( id : string ) => {
180+ setExpandedRefineId ( expandedRefineId === id ? null : id ) ;
181+ } ;
182+
183+ return (
184+ < div className = "space-y-4" >
185+ < div className = "space-y-3" >
186+ { refines . map ( ( refine ) => {
187+ const { question, metadata } = parseRefineQuestion ( refine . question ) ;
188+ return (
189+ < div key = { refine . id } className = "border border-gray-200 rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow" >
190+ < div
191+ className = "p-4 bg-white hover:bg-gray-50 cursor-pointer flex justify-between items-center"
192+ onClick = { ( ) => toggleExpandRule ( refine . id ) }
193+ >
194+ < div >
195+ < span className = "text-gray-500 text-sm mr-2" > User Message:</ span >
196+ < span className = "font-medium text-gray-900" > { question } </ span >
197+ </ div >
198+ < svg
199+ className = { `w-5 h-5 text-gray-500 transition-transform ${
200+ expandedRefineId === refine . id ? "transform rotate-180" : ""
201+ } `}
202+ fill = "none"
203+ viewBox = "0 0 24 24"
204+ stroke = "currentColor"
205+ >
206+ < path
207+ strokeLinecap = "round"
208+ strokeLinejoin = "round"
209+ strokeWidth = { 2 }
210+ d = "M19 9l-7 7-7-7"
211+ />
212+ </ svg >
213+ </ div >
214+ { expandedRefineId === refine . id && (
215+ < div className = "p-4 border-t border-gray-200 bg-gray-50 space-y-4" >
216+ { metadata && (
217+ < div >
218+ < h4 className = "font-semibold text-gray-700 mb-2" > User</ h4 >
219+ { formatMetadata ( metadata ) }
220+ </ div >
221+ ) }
222+ { refine . feedback && (
223+ < div >
224+ < h4 className = "font-semibold text-gray-700 mb-2" > Feedback</ h4 >
225+ < div className = "mb-2 p-3 bg-white border border-gray-200 rounded-md text-base leading-relaxed" > { refine . feedback } </ div >
226+ </ div >
227+ ) }
228+ < div >
229+ < h4 className = "font-semibold text-gray-700 mb-2" > Respuesta IA</ h4 >
230+ { editingRefineId === refine . id ? (
231+ < textarea
232+ className = "w-full p-2 border border-gray-300 rounded text-base"
233+ value = { editedAnswer }
234+ onChange = { ( e ) => setEditedAnswer ( e . target . value ) }
235+ rows = { 4 }
236+ />
237+ ) : (
238+ < div className = "p-3 bg-white border border-gray-200 rounded-md text-base leading-relaxed" > { refine . answer } </ div >
239+ ) }
240+ </ div >
241+
242+ < div className = "flex space-x-2 pt-2" >
243+ < button
244+ onClick = { ( e ) => {
245+ e . stopPropagation ( ) ;
246+ handlePost ( refine )
247+ } }
248+ className = "px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm transition-colors shadow-sm cursor-pointer"
249+ >
250+ Accept
251+ </ button >
252+
253+ { editingRefineId === refine . id ? (
254+ < >
255+ < button
256+ onClick = { ( e ) => {
257+ e . stopPropagation ( ) ;
258+ handleSave ( refine ) ;
259+ } }
260+ className = "px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm transition-colors shadow-sm cursor-pointer"
261+ >
262+ Save
263+ </ button >
264+ < button
265+ onClick = { ( e ) => {
266+ e . stopPropagation ( ) ;
267+ handleCancel ( ) ;
268+ } }
269+ className = "px-3 py-1 bg-gray-400 hover:bg-gray-500 text-white rounded-md text-sm transition-colors shadow-sm cursor-pointer"
270+ >
271+ Cancel
272+ </ button >
273+ </ >
274+ ) : (
275+ < button
276+ onClick = { ( e ) => {
277+ e . stopPropagation ( ) ;
278+ handleEdit ( refine ) ;
279+ } }
280+ className = "px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm transition-colors shadow-sm cursor-pointer"
281+ >
282+ Modify
283+ </ button >
284+ ) }
285+
286+ < button
287+ onClick = { ( e ) => {
288+ e . stopPropagation ( ) ;
289+ handleDelete ( refine . id ) ;
290+ } }
291+ className = "px-3 py-1 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm transition-colors shadow-sm cursor-pointer"
292+ >
293+ Delete
294+ </ button >
295+ </ div >
296+ </ div >
297+ ) }
298+ </ div >
299+ ) ;
300+ } ) }
301+ </ div >
302+ { /* Paginación */ }
303+ < div className = "flex items-center justify-between border-t border-gray-200" >
304+ < div className = "flex items-center justify-end pt-4" >
305+ < button
306+ onClick = { ( ) => setCurrentPage ( p => Math . max ( 1 , p - 1 ) ) }
307+ disabled = { currentPage === 1 }
308+ className = {
309+ "px-3 py-1 border border-gray-300 rounded text-sm disabled:opacity-50 hover:bg-gray-50" +
310+ ( ! hasNextPage ? "" : " cursor-pointer" )
311+ }
312+ >
313+ Previous
314+ </ button >
315+ < button
316+ onClick = { ( ) => setCurrentPage ( p => p + 1 ) }
317+ disabled = { ! hasNextPage }
318+ className = {
319+ "px-3 py-1 border border-gray-300 rounded text-sm disabled:opacity-50 hover:bg-gray-50" +
320+ ( ! hasNextPage ? "" : " cursor-pointer" )
321+ }
322+ >
323+ Next
324+ </ button >
325+ </ div >
326+ </ div >
327+ </ div >
328+ ) ;
329+ } ;
330+
331+ export default AIChatManagment ;
0 commit comments