Compare commits
9 Commits
32f0c4c4d4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 658b2e245c | |||
| 30562fead0 | |||
| 2b91004129 | |||
| 96a045dc67 | |||
| a8229f12d5 | |||
| dd4ac5374a | |||
| 670e0b1d14 | |||
| 93fe247a19 | |||
| 32ebfde9ed |
37
.gitea/workflows/deploy.yaml
Normal file
37
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Deploy to Azure Static Web Apps
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
|
||||
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
|
||||
run: bunx --bun vite build
|
||||
|
||||
# No hace falta instalar el CLI globalmente, usamos bunx
|
||||
- name: Deploy to Azure Static Web Apps
|
||||
env:
|
||||
AZURE_SWA_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOYMENT_TOKEN }}
|
||||
run: |
|
||||
bunx @azure/static-web-apps-cli deploy ./dist \
|
||||
--env production \
|
||||
--deployment-token "$AZURE_SWA_DEPLOYMENT_TOKEN"
|
||||
@@ -207,7 +207,7 @@ export async function plan_asignaturas_list(
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.select(
|
||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,prerrequisito_asignatura_id',
|
||||
)
|
||||
.eq('plan_estudio_id', planId)
|
||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Archive,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
RotateCcw,
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
|
||||
@@ -128,6 +129,7 @@ function RouteComponent() {
|
||||
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
||||
const queryClient = useQueryClient()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const isInitialLoad = useRef(true)
|
||||
const [showArchived, setShowArchived] = useState(false)
|
||||
const [editingChatId, setEditingChatId] = useState<string | null>(null)
|
||||
const editableRef = useRef<HTMLSpanElement>(null)
|
||||
@@ -204,20 +206,20 @@ function RouteComponent() {
|
||||
return messages
|
||||
})
|
||||
}, [mensajesDelChat, activeChatId, availableFields])
|
||||
const scrollToBottom = () => {
|
||||
const scrollToBottom = (behavior = 'smooth') => {
|
||||
if (scrollRef.current) {
|
||||
// Buscamos el viewport interno del ScrollArea de Radix
|
||||
const scrollContainer = scrollRef.current.querySelector(
|
||||
'[data-radix-scroll-area-viewport]',
|
||||
)
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: scrollContainer.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
behavior: behavior, // 'instant' para carga inicial, 'smooth' para mensajes nuevos
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { activeChats, archivedChats } = useMemo(() => {
|
||||
const allChats = lastConversation || []
|
||||
return {
|
||||
@@ -229,22 +231,22 @@ function RouteComponent() {
|
||||
}, [lastConversation])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(mensajesDelChat)
|
||||
|
||||
scrollToBottom()
|
||||
}, [chatMessages, isLoading])
|
||||
|
||||
/* useEffect(() => {
|
||||
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input
|
||||
const camposActualizados = selectedFields.filter((field) =>
|
||||
input.includes(field.label),
|
||||
)
|
||||
|
||||
// Solo actualizamos el estado si hubo un cambio real (para evitar bucles infinitos)
|
||||
if (camposActualizados.length !== selectedFields.length) {
|
||||
setSelectedFields(camposActualizados)
|
||||
if (chatMessages.length > 0) {
|
||||
if (isInitialLoad.current) {
|
||||
// Si es el primer render con mensajes, vamos al final al instante
|
||||
scrollToBottom('instant')
|
||||
isInitialLoad.current = false
|
||||
} else {
|
||||
// Si ya estaba cargado y llegan nuevos, hacemos el smooth
|
||||
scrollToBottom('smooth')
|
||||
}
|
||||
}, [input, selectedFields]) */
|
||||
}
|
||||
}, [chatMessages])
|
||||
|
||||
// 2. Resetear el flag cuando cambies de chat activo
|
||||
useEffect(() => {
|
||||
isInitialLoad.current = true
|
||||
}, [activeChatId])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingConv || isSending) return
|
||||
@@ -508,27 +510,38 @@ function RouteComponent() {
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 pr-2">
|
||||
{' '}
|
||||
{/* Agregamos un pr-2 para que el scrollbar no tape botones */}
|
||||
{!showArchived ? (
|
||||
activeChats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
onClick={() => setActiveChatId(chat.id)}
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||
className={`group relative flex w-full items-center overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||
activeChatId === chat.id
|
||||
? 'bg-slate-100 font-medium text-slate-900'
|
||||
: 'text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{/* LADO IZQUIERDO: Icono + Texto con Tooltip */}
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
{/* LADO IZQUIERDO: Icono + Texto */}
|
||||
<div
|
||||
className="flex min-w-0 flex-1 items-center gap-3 transition-all duration-200"
|
||||
style={{
|
||||
// Aplicamos la máscara solo cuando el mouse está encima para que se note el desvanecimiento
|
||||
// donde aparecen los botones
|
||||
maskImage:
|
||||
'linear-gradient(to right, black 70%, transparent 95%)',
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(to right, black 70%, transparent 95%)',
|
||||
}}
|
||||
>
|
||||
{/* pr-12 reserva espacio para los botones absolutos */}
|
||||
<FileText size={16} className="shrink-0 opacity-40" />
|
||||
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{/* Este contenedor es el que obliga al span a truncarse */}
|
||||
<div className="max-w-[calc(100%-48px)] min-w-0 flex-1">
|
||||
<TooltipTrigger asChild className="min-w-0 flex-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<span
|
||||
ref={
|
||||
editingChatId === chat.id ? editableRef : null
|
||||
@@ -574,8 +587,6 @@ function RouteComponent() {
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{/* Tooltip: Solo aparece si no estás editando y el texto es largo */}
|
||||
{editingChatId !== chat.id && (
|
||||
<TooltipContent
|
||||
side="right"
|
||||
@@ -588,9 +599,9 @@ function RouteComponent() {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{/* LADO DERECHO: Acciones con shrink-0 para que no se muevan */}
|
||||
{/* LADO DERECHO: Acciones ABSOLUTAS */}
|
||||
<div
|
||||
className={`flex shrink-0 items-center gap-1 pl-2 opacity-0 transition-opacity group-hover:opacity-100 ${
|
||||
className={`absolute top-1/2 right-2 z-20 flex -translate-y-1/2 items-center gap-1 rounded-md px-1 opacity-0 transition-opacity group-hover:opacity-100 ${
|
||||
activeChatId === chat.id ? 'bg-slate-100' : 'bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
@@ -614,7 +625,7 @@ function RouteComponent() {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
/* Sección de archivados */
|
||||
/* Sección de archivados (Simplificada para mantener consistencia) */
|
||||
<div className="animate-in fade-in slide-in-from-left-2 px-1">
|
||||
<p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
|
||||
Archivados
|
||||
@@ -622,18 +633,18 @@ function RouteComponent() {
|
||||
{archivedChats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className="group relative mb-1 flex w-full items-center justify-between overflow-hidden rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
|
||||
className="group relative mb-1 flex w-full items-center overflow-hidden rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3 pr-10">
|
||||
<Archive size={14} className="shrink-0 opacity-30" />
|
||||
<span className="block min-w-0 flex-1 truncate">
|
||||
<span className="block truncate">
|
||||
{chat.nombre ||
|
||||
`Archivado ${chat.creado_en.split('T')[0]}`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => unarchiveChat(e, chat.id)}
|
||||
className="ml-2 shrink-0 rounded bg-slate-50/80 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-teal-600"
|
||||
className="absolute top-1/2 right-2 shrink-0 -translate-y-1/2 rounded bg-slate-100 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-teal-600"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
|
||||
@@ -76,7 +76,7 @@ const mapAsignaturasToAsignaturas = (
|
||||
// Mapeo directo de los nuevos campos de la API
|
||||
hd: asig.horas_academicas ?? 0,
|
||||
hi: asig.horas_independientes ?? 0,
|
||||
prerrequisitos: [],
|
||||
prerrequisito_asignatura_id: asig.prerrequisito_asignatura_id ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -336,6 +336,7 @@ function MapaCurricularPage() {
|
||||
horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes']
|
||||
numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo']
|
||||
linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id']
|
||||
prerrequisito_asignatura_id?: string | null
|
||||
}
|
||||
const patch: Partial<AsignaturaPatch> = {
|
||||
nombre: editingData.nombre,
|
||||
@@ -345,6 +346,7 @@ function MapaCurricularPage() {
|
||||
horas_independientes: editingData.hi,
|
||||
numero_ciclo: editingData.ciclo,
|
||||
linea_plan_id: editingData.lineaCurricularId,
|
||||
prerrequisito_asignatura_id: editingData.prerrequisito_asignatura_id,
|
||||
tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA)
|
||||
}
|
||||
|
||||
@@ -490,7 +492,7 @@ function MapaCurricularPage() {
|
||||
e: React.FocusEvent<HTMLSpanElement>,
|
||||
id: string,
|
||||
) => {
|
||||
const nuevoNombre = e.currentTarget.textContent?.trim() || ''
|
||||
const nuevoNombre = e.currentTarget.textContent.trim() || ''
|
||||
|
||||
// Buscamos la línea original para comparar
|
||||
const lineaOriginal = lineas.find((l) => l.id === id)
|
||||
@@ -935,65 +937,55 @@ function MapaCurricularPage() {
|
||||
{/* Fila 4: Seriación (Prerrequisitos) */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||
Seriación (Prerrequisitos)
|
||||
Seriación (Prerrequisito)
|
||||
</label>
|
||||
<Select
|
||||
value={seriacionValue}
|
||||
// Cambiamos a manejo de valor único basado en el ID de la columna
|
||||
value={editingData.prerrequisito_asignatura_id || undefined}
|
||||
onValueChange={(val) => {
|
||||
if (val === 'none') {
|
||||
setSeriacionValue('')
|
||||
return
|
||||
}
|
||||
if (!editingData.prerrequisitos.includes(val)) {
|
||||
console.log(editingData)
|
||||
|
||||
setEditingData({
|
||||
...editingData,
|
||||
prerrequisitos: [...editingData.prerrequisitos, val],
|
||||
prerrequisito_asignatura_id: val === 'none' ? null : val,
|
||||
})
|
||||
}
|
||||
setSeriacionValue('')
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full bg-white">
|
||||
<SelectValue placeholder="Seleccionar asignatura..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">-- Sin Seriación --</SelectItem>
|
||||
|
||||
{asignaturas
|
||||
.filter((m) => m.id !== editingData.id)
|
||||
.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id}>
|
||||
{m.nombre} ({m.clave})
|
||||
.filter((asig) => {
|
||||
// 1. No es la misma materia
|
||||
const noEsMisma = asig.id !== editingData.id
|
||||
// 2. El ciclo debe ser estrictamente MENOR
|
||||
const esCicloMenor =
|
||||
asig.ciclo !== null &&
|
||||
editingData.ciclo !== null &&
|
||||
asig.ciclo < editingData.ciclo
|
||||
|
||||
return noEsMisma && esCicloMenor
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.ciclo || 0) - (b.ciclo || 0) ||
|
||||
a.nombre.localeCompare(b.nombre),
|
||||
)
|
||||
.map((asig) => (
|
||||
<SelectItem key={asig.id} value={asig.id}>
|
||||
<span className="font-bold text-teal-600">
|
||||
[C{asig.ciclo}]
|
||||
</span>{' '}
|
||||
{asig.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Visualización de los prerrequisitos seleccionados */}
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{editingData.prerrequisitos.map((pre) => (
|
||||
<Badge
|
||||
key={pre}
|
||||
variant="secondary"
|
||||
className="bg-slate-100 text-slate-600"
|
||||
>
|
||||
{pre}
|
||||
<button
|
||||
className="ml-1 hover:text-red-500"
|
||||
onClick={() => {
|
||||
setEditingData({
|
||||
...editingData,
|
||||
prerrequisitos: editingData.prerrequisitos.filter(
|
||||
(p) => p !== pre,
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{/* Visualización del Prerrequisito con el Nombre */}
|
||||
</div>
|
||||
|
||||
{/* Fila 5: Tipo */}
|
||||
|
||||
@@ -166,30 +166,20 @@ function AsignaturaLayout() {
|
||||
onSave={(val) => handleUpdateHeader('nombre', val)}
|
||||
/>
|
||||
</h1>
|
||||
{
|
||||
// console.log(headerData),
|
||||
|
||||
console.log(asignaturaApi.planes_estudio?.nombre)
|
||||
}
|
||||
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
||||
<span className="flex items-center gap-1">
|
||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||
Pertenece al plan:{' '}
|
||||
<span className="text-blue-100">
|
||||
{(asignaturaApi.planes_estudio?.datos as DatosPlan)
|
||||
.nombre || ''}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-blue-100">
|
||||
{(asignaturaApi.planes_estudio?.datos as DatosPlan)
|
||||
.nombre ?? ''}
|
||||
{(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-blue-300">
|
||||
Pertenece al plan:{' '}
|
||||
<span className="cursor-pointer underline">
|
||||
{asignaturaApi.planes_estudio?.nombre}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-2 text-right">
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface Asignatura {
|
||||
orden?: number
|
||||
hd: number // <--- Añadir
|
||||
hi: number // <--- Añadir
|
||||
prerrequisitos: Array<string>
|
||||
prerrequisito_asignatura_id: string | null
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
|
||||
@@ -156,6 +156,7 @@ export type Database = {
|
||||
plan_estudio_id: string
|
||||
tipo: Database['public']['Enums']['tipo_asignatura']
|
||||
tipo_origen: Database['public']['Enums']['tipo_origen'] | null
|
||||
prerrequisito_asignatura_id?: string
|
||||
}
|
||||
Insert: {
|
||||
actualizado_en?: string
|
||||
|
||||
14
staticwebapp.config.json
Normal file
14
staticwebapp.config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"navigationFallback": {
|
||||
"rewrite": "/index.html",
|
||||
"exclude": [
|
||||
"/assets/*",
|
||||
"/*.css",
|
||||
"/*.js",
|
||||
"/*.ico",
|
||||
"/*.png",
|
||||
"/*.jpg",
|
||||
"/*.svg"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user