Skip to content

Commit 557bf12

Browse files
authored
Merge pull request #17 from ClassConnect-org/dev
Dev
2 parents 62644e2 + e067c2e commit 557bf12

13 files changed

Lines changed: 485 additions & 50 deletions

File tree

src/@types/refine.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
interface Refine {
2+
question: string;
3+
answer: string;
4+
feedback: string;
5+
id: string;
6+
}
7+
8+
interface RefinePayload {
9+
question: string;
10+
answer: string;
11+
id: string;
12+
}
13+
14+
interface GetRefinesParams {
15+
page?: number;
16+
}
17+
18+
19+
export type { Refine, RefinePayload, GetRefinesParams };

src/components/AIChat.tsx

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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(/QUESTION:\s*'([^']*)'/);
30+
const passageMatch = raw.match(/PASSAGE:\s*'([\s\S]*?)'/);
31+
const passage = passageMatch ? passageMatch[1] : "";
32+
33+
// Usa extractBracedBlockFrom directamente sobre raw
34+
const metadata = extractBracedBlockFrom(raw, /METADATA:\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(/None/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;

src/components/AdminFormModal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const AdminFormModal: React.FC<Props> = ({ onClose, onSubmit }) => {
3131
</h3>
3232
<button
3333
onClick={onClose}
34-
className="text-gray-400 hover:text-gray-500"
34+
className="text-gray-400 hover:text-gray-500 cursor-pointer"
3535
>
3636
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
3737
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@@ -77,13 +77,13 @@ const AdminFormModal: React.FC<Props> = ({ onClose, onSubmit }) => {
7777
<button
7878
type="button"
7979
onClick={onClose}
80-
className="px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
80+
className="px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-pointer"
8181
>
8282
Cancel
8383
</button>
8484
<button
8585
type="submit"
86-
className="px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
86+
className="px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-pointer"
8787
>
8888
Create Admin
8989
</button>

src/components/Admins.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const AdminsManagementView: React.FC = () => {
4040
/>
4141
<button
4242
onClick={() => setShowModal(true)}
43-
className="px-4 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"
43+
className="px-4 py-1 bg-blue-500 text-white rounded text-sm hover:bg-blue-600 cursor-pointer"
4444
>
4545
New Admin
4646
</button>

0 commit comments

Comments
 (0)