292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { useState } from 'react'
|
|
import {
|
|
Plus,
|
|
Edit2,
|
|
Trash2,
|
|
Brain,
|
|
Save,
|
|
X,
|
|
Activity,
|
|
BookOpen
|
|
} from 'lucide-react'
|
|
import { api } from '../lib/api'
|
|
|
|
interface Module {
|
|
name: string
|
|
description?: string
|
|
program_patterns: string[]
|
|
keywords: string[]
|
|
rules: string[]
|
|
restrictions: string[]
|
|
}
|
|
|
|
export default function Modules() {
|
|
const queryClient = useQueryClient()
|
|
const [editingModule, setEditingModule] = useState<Module | null>(null)
|
|
const [isNew, setIsNew] = useState(false)
|
|
|
|
const { data: modules, isLoading } = useQuery({
|
|
queryKey: ['modules'],
|
|
queryFn: () => api.get('/api/config/modules').then(r => r.data),
|
|
})
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: (module: Module) =>
|
|
isNew
|
|
? api.post('/api/config/modules', module)
|
|
: api.put(`/api/config/modules/${module.name}`, module),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['modules'] })
|
|
setEditingModule(null)
|
|
setIsNew(false)
|
|
},
|
|
})
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (name: string) => api.delete(`/api/config/modules/${name}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['modules'] })
|
|
},
|
|
})
|
|
|
|
const modulesList: Module[] = modules || []
|
|
|
|
const handleNewModule = () => {
|
|
setEditingModule({
|
|
name: '',
|
|
description: '',
|
|
program_patterns: [],
|
|
keywords: [],
|
|
rules: [],
|
|
restrictions: [],
|
|
})
|
|
setIsNew(true)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-gray-500">
|
|
Módulos de regras de negócio para contextualização da IA
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleNewModule}
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Novo Módulo
|
|
</button>
|
|
</div>
|
|
|
|
{/* Modules List */}
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<Activity className="h-8 w-8 animate-spin text-blue-500" />
|
|
</div>
|
|
) : modulesList.length === 0 && !editingModule ? (
|
|
<div className="bg-white rounded-lg shadow p-12 text-center">
|
|
<Brain className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
|
<h3 className="text-lg font-medium text-gray-900">Nenhum módulo configurado</h3>
|
|
<p className="text-gray-500 mt-2 max-w-md mx-auto">
|
|
Módulos definem regras de negócio específicas que a IA usa para entender o contexto do sistema
|
|
</p>
|
|
<button
|
|
onClick={handleNewModule}
|
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
>
|
|
Criar primeiro módulo
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{modulesList.map(module => (
|
|
<div key={module.name} className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Brain className="h-8 w-8 text-purple-500" />
|
|
<div>
|
|
<h3 className="font-semibold text-lg">{module.name}</h3>
|
|
<p className="text-gray-500">{module.description || 'Sem descrição'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => { setEditingModule(module); setIsNew(false); }}
|
|
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
|
|
>
|
|
<Edit2 className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (confirm(`Remover módulo ${module.name}?`)) {
|
|
deleteMutation.mutate(module.name)
|
|
}
|
|
}}
|
|
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-500">Padrões de programa:</span>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{module.program_patterns.map(p => (
|
|
<span key={p} className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded text-xs">
|
|
{p}
|
|
</span>
|
|
))}
|
|
{module.program_patterns.length === 0 && <span className="text-gray-400">-</span>}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Keywords:</span>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{module.keywords.slice(0, 5).map(k => (
|
|
<span key={k} className="px-2 py-0.5 bg-purple-100 text-purple-800 rounded text-xs">
|
|
{k}
|
|
</span>
|
|
))}
|
|
{module.keywords.length > 5 && (
|
|
<span className="text-gray-400 text-xs">+{module.keywords.length - 5}</span>
|
|
)}
|
|
{module.keywords.length === 0 && <span className="text-gray-400">-</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit/Create Modal */}
|
|
{editingModule && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
|
<h2 className="text-xl font-semibold">
|
|
{isNew ? 'Novo Módulo' : `Editar: ${editingModule.name}`}
|
|
</h2>
|
|
<button onClick={() => setEditingModule(null)} className="p-2 hover:bg-gray-100 rounded-lg">
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Nome *</label>
|
|
<input
|
|
type="text"
|
|
value={editingModule.name}
|
|
onChange={e => setEditingModule({ ...editingModule, name: e.target.value })}
|
|
disabled={!isNew}
|
|
placeholder="ACQ-Auth"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Descrição</label>
|
|
<textarea
|
|
value={editingModule.description || ''}
|
|
onChange={e => setEditingModule({ ...editingModule, description: e.target.value })}
|
|
placeholder="Módulo de autorização de transações..."
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Padrões de Programa (um por linha)
|
|
</label>
|
|
<textarea
|
|
value={editingModule.program_patterns.join('\n')}
|
|
onChange={e => setEditingModule({
|
|
...editingModule,
|
|
program_patterns: e.target.value.split('\n').filter(Boolean)
|
|
})}
|
|
placeholder="AUTH* ACQAUTH* *AUTOR*"
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Keywords (uma por linha)
|
|
</label>
|
|
<textarea
|
|
value={editingModule.keywords.join('\n')}
|
|
onChange={e => setEditingModule({
|
|
...editingModule,
|
|
keywords: e.target.value.split('\n').filter(Boolean)
|
|
})}
|
|
placeholder="autorização ISO8583 network timeout"
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Regras de Negócio (uma por linha)
|
|
</label>
|
|
<textarea
|
|
value={editingModule.rules.join('\n')}
|
|
onChange={e => setEditingModule({
|
|
...editingModule,
|
|
rules: e.target.value.split('\n').filter(Boolean)
|
|
})}
|
|
placeholder="Sempre validar CVV antes de enviar para rede Timeout de rede = 30 segundos"
|
|
rows={4}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Restrições (uma por linha)
|
|
</label>
|
|
<textarea
|
|
value={editingModule.restrictions.join('\n')}
|
|
onChange={e => setEditingModule({
|
|
...editingModule,
|
|
restrictions: e.target.value.split('\n').filter(Boolean)
|
|
})}
|
|
placeholder="Não alterar formato de mensagem ISO sem aprovação Não modificar copybook ISOMSGO1"
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 border-t border-gray-200 flex gap-3">
|
|
<button
|
|
onClick={() => setEditingModule(null)}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={() => saveMutation.mutate(editingModule)}
|
|
disabled={!editingModule.name || saveMutation.isPending}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
<Save className="h-4 w-4" />
|
|
{saveMutation.isPending ? 'Salvando...' : 'Salvar'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|