7 Commits

Author SHA1 Message Date
9fd816bfa1 Actualizar esta sección de seriación fix #195 2026-03-20 16:09:39 -06:00
658b2e245c Merge pull request 'Que no haga scroll fix #193' (#199) from issue/193-que-no-haga-scroll into main
All checks were successful
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m20s
Reviewed-on: #199
2026-03-19 20:20:45 +00:00
30562fead0 Merge branch 'main' into issue/193-que-no-haga-scroll 2026-03-19 20:20:30 +00:00
2b91004129 Que no haga scroll #193 2026-03-19 14:18:21 -06:00
96a045dc67 Añadir staticwebapp.config.json
All checks were successful
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m7s
2026-03-19 13:58:43 +00:00
a8229f12d5 Actualizar .gitea/workflows/deploy.yaml
All checks were successful
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m30s
2026-03-19 13:56:12 +00:00
dd4ac5374a Añadir .gitea/workflows/deploy.yaml
Some checks failed
Deploy to Azure Static Web Apps / build-and-deploy (push) Failing after 48s
2026-03-18 22:39:50 +00:00
6 changed files with 258 additions and 156 deletions

View 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"

View File

@@ -7,6 +7,13 @@ import type { AsignaturaDetail } from '@/data'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import {
Tooltip, Tooltip,
@@ -14,6 +21,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { usePlanAsignaturas } from '@/data'
import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects' import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects'
export interface BibliografiaEntry { export interface BibliografiaEntry {
@@ -59,8 +67,12 @@ export default function AsignaturaDetailPage() {
const { asignaturaId } = useParams({ const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId', from: '/planes/$planId/asignaturas/$asignaturaId',
}) })
const { planId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId',
})
const { data: asignaturaApi } = useSubject(asignaturaId) const { data: asignaturaApi } = useSubject(asignaturaId)
const { data: asignaturasApi, isLoading: loadingAsig } =
usePlanAsignaturas(planId)
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null) const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
const updateAsignatura = useUpdateAsignatura() const updateAsignatura = useUpdateAsignatura()
@@ -81,16 +93,54 @@ export default function AsignaturaDetailPage() {
}, },
}) })
} }
const asignaturaSeriada = useMemo(() => {
if (!asignaturaApi?.prerrequisito_asignatura_id || !asignaturasApi)
return null
return asignaturasApi.find(
(asig) => asig.id === asignaturaApi.prerrequisito_asignatura_id,
)
}, [asignaturaApi, asignaturasApi])
const requisitosFormateados = useMemo(() => {
if (!asignaturaSeriada) return []
return [
{
type: 'Pre-requisito',
code: asignaturaSeriada.codigo,
name: asignaturaSeriada.nombre,
id: asignaturaSeriada.id, // Guardamos el ID para el select
},
]
}, [asignaturaSeriada])
const handleUpdatePrerrequisito = (newId: string | null) => {
updateAsignatura.mutate({
asignaturaId,
patch: {
prerrequisito_asignatura_id: newId,
},
})
}
/* ---------- sincronizar API ---------- */ /* ---------- sincronizar API ---------- */
useEffect(() => { useEffect(() => {
if (asignaturaApi) setAsignatura(asignaturaApi) console.log(requisitosFormateados)
}, [asignaturaApi])
return <DatosGenerales onPersistDato={handlePersistDatoGeneral} /> if (asignaturaApi) setAsignatura(asignaturaApi)
}, [asignaturaApi, requisitosFormateados])
return (
<DatosGenerales
pre={requisitosFormateados}
availableSubjects={asignaturasApi}
onPersistDato={handlePersistDatoGeneral}
/>
)
} }
function DatosGenerales({ function DatosGenerales({
onPersistDato, onPersistDato,
pre,
availableSubjects,
}: { }: {
onPersistDato: (clave: string, value: string) => void onPersistDato: (clave: string, value: string) => void
}) { }) {
@@ -265,18 +315,19 @@ function DatosGenerales({
<InfoCard <InfoCard
title="Requisitos y Seriación" title="Requisitos y Seriación"
type="requirements" type="requirements"
initialContent={[ initialContent={pre}
{ // Pasamos las materias del plan para el Select (excluyendo la actual)
type: 'Pre-requisito', availableSubjects={
code: 'PA-301', availableSubjects?.filter((a) => a.id !== asignaturaId) || []
name: 'Programación Avanzada', }
onPersist={({ value }) => {
updateAsignatura.mutate({
asignaturaId,
patch: {
prerrequisito_asignatura_id: value, // value ya viene como ID o null desde handleSave
}, },
{ })
type: 'Co-requisito', }}
code: 'MAT-201',
name: 'Matemáticas Discretas',
},
]}
/> />
{/* Tarjeta de Evaluación */} {/* Tarjeta de Evaluación */}
@@ -316,6 +367,7 @@ interface InfoCardProps {
containerRef?: React.RefObject<HTMLDivElement | null> containerRef?: React.RefObject<HTMLDivElement | null>
forceEditToken?: number forceEditToken?: number
highlightToken?: number highlightToken?: number
availableSubjects?: any
} }
function InfoCard({ function InfoCard({
@@ -332,6 +384,7 @@ function InfoCard({
containerRef, containerRef,
forceEditToken, forceEditToken,
highlightToken, highlightToken,
availableSubjects,
}: InfoCardProps) { }: InfoCardProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [isHighlighted, setIsHighlighted] = useState(false) const [isHighlighted, setIsHighlighted] = useState(false)
@@ -349,7 +402,8 @@ function InfoCard({
useEffect(() => { useEffect(() => {
setData(initialContent) setData(initialContent)
setTempText(initialContent) setTempText(initialContent)
console.log(data)
console.log(initialContent)
if (type === 'evaluation') { if (type === 'evaluation') {
const raw = Array.isArray(initialContent) ? initialContent : [] const raw = Array.isArray(initialContent) ? initialContent : []
const rows: Array<CriterioEvaluacionRowDraft> = raw const rows: Array<CriterioEvaluacionRowDraft> = raw
@@ -392,6 +446,8 @@ function InfoCard({
const handleSave = () => { const handleSave = () => {
console.log('clave, valor:', clave, String(tempText ?? '')) console.log('clave, valor:', clave, String(tempText ?? ''))
console.log(clave)
console.log(tempText)
if (type === 'evaluation') { if (type === 'evaluation') {
const cleaned: Array<CriterioEvaluacionRow> = [] const cleaned: Array<CriterioEvaluacionRow> = []
@@ -422,6 +478,25 @@ function InfoCard({
void onPersist?.({ type, clave, value: cleaned }) void onPersist?.({ type, clave, value: cleaned })
return return
} }
if (type === 'requirements') {
console.log('entre aqui ')
// Si tempText es un array y tiene elementos, tomamos el ID del primero
// Si es "none" o está vacío, mandamos null (para limpiar la seriación)
const prerequisiteId =
Array.isArray(tempText) && tempText.length > 0 ? tempText[0].id : null
setData(tempText) // Actualiza la vista local
setIsEditing(false)
// Mandamos el ID específico a la base de datos
void onPersist?.({
type,
clave: 'prerrequisito_asignatura_id', // Forzamos la columna correcta
value: prerequisiteId,
})
return
}
setData(tempText) setData(tempText)
setIsEditing(false) setIsEditing(false)
@@ -541,7 +616,52 @@ function InfoCard({
<CardContent className="pt-4"> <CardContent className="pt-4">
{isEditing ? ( {isEditing ? (
<div className="space-y-3"> <div className="space-y-3">
{type === 'evaluation' ? ( {/* Condicionales de edición según el tipo */}
{type === 'requirements' ? (
<div className="space-y-3">
<label className="text-xs font-medium text-slate-500">
Materia de Seriación
</label>
<Select
value={tempText?.[0]?.id || 'none'}
onValueChange={(val) => {
const selected = availableSubjects?.find(
(s) => s.id === val,
)
if (val === 'none' || !selected) {
console.log('guardando')
setTempText([])
} else {
console.log('hola')
setTempText([
{
id: selected.id,
type: 'Pre-requisito',
code: selected.codigo,
name: selected.nombre,
},
])
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Selecciona una materia" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
Ninguna (Sin seriación)
</SelectItem>
{availableSubjects?.map((asig) => (
<SelectItem key={asig.id} value={asig.id}>
{asig.codigo} - {asig.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : type === 'evaluation' ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
{evalRows.map((row) => ( {evalRows.map((row) => (
@@ -563,85 +683,36 @@ function InfoCard({
) )
}} }}
/> />
<Input <Input
value={row.porcentaje} value={row.porcentaje}
placeholder="%" placeholder="%"
type="number" type="number"
min={1}
max={100}
step={1}
inputMode="numeric"
onChange={(e) => { onChange={(e) => {
const raw = e.target.value const raw = e.target.value
// Solo permitir '' o dígitos
if (raw !== '' && !/^\d+$/.test(raw)) return if (raw !== '' && !/^\d+$/.test(raw)) return
if (raw === '') {
setEvalRows((prev) =>
prev.map((r) =>
r.id === row.id
? {
id: r.id,
criterio: r.criterio,
porcentaje: '',
}
: r,
),
)
return
}
const n = Number(raw)
if (!Number.isFinite(n)) return
const porcentaje = Math.trunc(n)
if (porcentaje < 1 || porcentaje > 100) return
// No permitir suma > 100
setEvalRows((prev) => { setEvalRows((prev) => {
const next = prev.map((r) => const next = prev.map((r) =>
r.id === row.id r.id === row.id ? { ...r, porcentaje: raw } : r,
? { )
id: r.id, const total = next.reduce(
criterio: r.criterio, (acc, r) => acc + (Number(r.porcentaje) || 0),
porcentaje: raw, 0,
}
: r,
) )
const total = next.reduce((acc, r) => {
const v = String(r.porcentaje).trim()
if (!v) return acc
const nn = Number(v)
if (!Number.isFinite(nn)) return acc
const vv = Math.trunc(nn)
if (vv < 1 || vv > 100) return acc
return acc + vv
}, 0)
return total > 100 ? prev : next return total > 100 ? prev : next
}) })
}} }}
/> />
<div className="text-sm text-slate-600">%</div>
<div
className="flex w-[1ch] items-center justify-center text-sm text-slate-600"
aria-hidden
>
%
</div>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-red-600 hover:bg-red-50" className="h-8 w-8 text-red-600 hover:bg-red-50"
onClick={() => { onClick={() =>
setEvalRows((prev) => setEvalRows((prev) =>
prev.filter((r) => r.id !== row.id), prev.filter((r) => r.id !== row.id),
) )
}} }
aria-label="Quitar renglón"
title="Quitar"
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
@@ -651,22 +722,15 @@ function InfoCard({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span <span
className={ className={`text-sm ${evaluationTotal === 100 ? 'text-muted-foreground' : 'text-destructive font-semibold'}`}
'text-sm ' +
(evaluationTotal === 100
? 'text-muted-foreground'
: 'text-destructive font-semibold')
}
> >
Total: {evaluationTotal}/100 Total: {evaluationTotal}/100
</span> </span>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-emerald-700 hover:bg-emerald-50" className="text-emerald-700 hover:bg-emerald-50"
onClick={() => { onClick={() =>
// Agregar una fila vacía (siempre permitido)
setEvalRows((prev) => [ setEvalRows((prev) => [
...prev, ...prev,
{ {
@@ -675,7 +739,7 @@ function InfoCard({
porcentaje: '', porcentaje: '',
}, },
]) ])
}} }
> >
<Plus className="mr-2 h-4 w-4" /> Agregar renglón <Plus className="mr-2 h-4 w-4" /> Agregar renglón
</Button> </Button>
@@ -689,28 +753,15 @@ function InfoCard({
className="min-h-30 text-sm leading-relaxed" className="min-h-30 text-sm leading-relaxed"
/> />
)} )}
{/* Botones de acción comunes */}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setIsEditing(false) setIsEditing(false)
if (type === 'evaluation') { // Lógica de reset si es necesario...
const raw = Array.isArray(data) ? data : []
setEvalRows(
raw.map((r: CriterioEvaluacionRow) => ({
id: crypto.randomUUID(),
criterio:
typeof r.criterio === 'string' ? r.criterio : '',
porcentaje:
typeof r.porcentaje === 'number'
? String(Math.trunc(r.porcentaje))
: typeof r.porcentaje === 'string'
? String(Math.trunc(Number(r.porcentaje)))
: '',
})),
)
}
}} }}
> >
Cancelar Cancelar
@@ -726,6 +777,7 @@ function InfoCard({
</div> </div>
</div> </div>
) : ( ) : (
/* Modo Visualización */
<div className="text-sm leading-relaxed text-slate-600"> <div className="text-sm leading-relaxed text-slate-600">
{type === 'text' && {type === 'text' &&
(data ? ( (data ? (
@@ -734,9 +786,7 @@ function InfoCard({
<p className="text-slate-400 italic">Sin información.</p> <p className="text-slate-400 italic">Sin información.</p>
))} ))}
{type === 'requirements' && <RequirementsView items={data} />} {type === 'requirements' && <RequirementsView items={data} />}
{type === 'evaluation' && ( {type === 'evaluation' && <EvaluationView items={data} />}
<EvaluationView items={data as Array<CriterioEvaluacionRow>} />
)}
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -191,7 +191,7 @@ export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
.from('asignaturas') .from('asignaturas')
.select( .select(
` `
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion, id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,prerrequisito_asignatura_id,
planes_estudio( planes_estudio(
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))

View File

@@ -15,6 +15,7 @@ import {
Archive, Archive,
Loader2, Loader2,
Sparkles, Sparkles,
RotateCcw,
} from 'lucide-react' } from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
@@ -128,6 +129,7 @@ function RouteComponent() {
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null) const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
const isInitialLoad = useRef(true)
const [showArchived, setShowArchived] = useState(false) const [showArchived, setShowArchived] = useState(false)
const [editingChatId, setEditingChatId] = useState<string | null>(null) const [editingChatId, setEditingChatId] = useState<string | null>(null)
const editableRef = useRef<HTMLSpanElement>(null) const editableRef = useRef<HTMLSpanElement>(null)
@@ -204,20 +206,20 @@ function RouteComponent() {
return messages return messages
}) })
}, [mensajesDelChat, activeChatId, availableFields]) }, [mensajesDelChat, activeChatId, availableFields])
const scrollToBottom = () => { const scrollToBottom = (behavior = 'smooth') => {
if (scrollRef.current) { if (scrollRef.current) {
// Buscamos el viewport interno del ScrollArea de Radix
const scrollContainer = scrollRef.current.querySelector( const scrollContainer = scrollRef.current.querySelector(
'[data-radix-scroll-area-viewport]', '[data-radix-scroll-area-viewport]',
) )
if (scrollContainer) { if (scrollContainer) {
scrollContainer.scrollTo({ scrollContainer.scrollTo({
top: scrollContainer.scrollHeight, top: scrollContainer.scrollHeight,
behavior: 'smooth', behavior: behavior, // 'instant' para carga inicial, 'smooth' para mensajes nuevos
}) })
} }
} }
} }
const { activeChats, archivedChats } = useMemo(() => { const { activeChats, archivedChats } = useMemo(() => {
const allChats = lastConversation || [] const allChats = lastConversation || []
return { return {
@@ -229,22 +231,22 @@ function RouteComponent() {
}, [lastConversation]) }, [lastConversation])
useEffect(() => { useEffect(() => {
console.log(mensajesDelChat) if (chatMessages.length > 0) {
if (isInitialLoad.current) {
scrollToBottom() // Si es el primer render con mensajes, vamos al final al instante
}, [chatMessages, isLoading]) scrollToBottom('instant')
isInitialLoad.current = false
/* useEffect(() => { } else {
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input // Si ya estaba cargado y llegan nuevos, hacemos el smooth
const camposActualizados = selectedFields.filter((field) => scrollToBottom('smooth')
input.includes(field.label),
)
// Solo actualizamos el estado si hubo un cambio real (para evitar bucles infinitos)
if (camposActualizados.length !== selectedFields.length) {
setSelectedFields(camposActualizados)
} }
}, [input, selectedFields]) */ }
}, [chatMessages])
// 2. Resetear el flag cuando cambies de chat activo
useEffect(() => {
isInitialLoad.current = true
}, [activeChatId])
useEffect(() => { useEffect(() => {
if (isLoadingConv || isSending) return if (isLoadingConv || isSending) return
@@ -508,27 +510,38 @@ function RouteComponent() {
</div> </div>
<ScrollArea className="flex-1"> <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 ? ( {!showArchived ? (
activeChats.map((chat) => ( activeChats.map((chat) => (
<div <div
key={chat.id} key={chat.id}
onClick={() => setActiveChatId(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 activeChatId === chat.id
? 'bg-slate-100 font-medium text-slate-900' ? 'bg-slate-100 font-medium text-slate-900'
: 'text-slate-600 hover:bg-slate-50' : 'text-slate-600 hover:bg-slate-50'
}`} }`}
> >
{/* LADO IZQUIERDO: Icono + Texto con Tooltip */} {/* LADO IZQUIERDO: Icono + Texto */}
<div className="flex min-w-0 flex-1 items-center gap-3"> <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" /> <FileText size={16} className="shrink-0 opacity-40" />
<TooltipProvider delayDuration={400}> <TooltipProvider delayDuration={400}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild className="min-w-0 flex-1">
{/* Este contenedor es el que obliga al span a truncarse */} <div className="min-w-0 flex-1">
<div className="max-w-[calc(100%-48px)] min-w-0 flex-1">
<span <span
ref={ ref={
editingChatId === chat.id ? editableRef : null editingChatId === chat.id ? editableRef : null
@@ -574,8 +587,6 @@ function RouteComponent() {
</span> </span>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
{/* Tooltip: Solo aparece si no estás editando y el texto es largo */}
{editingChatId !== chat.id && ( {editingChatId !== chat.id && (
<TooltipContent <TooltipContent
side="right" side="right"
@@ -588,9 +599,9 @@ function RouteComponent() {
</TooltipProvider> </TooltipProvider>
</div> </div>
{/* LADO DERECHO: Acciones con shrink-0 para que no se muevan */} {/* LADO DERECHO: Acciones ABSOLUTAS */}
<div <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' activeChatId === chat.id ? 'bg-slate-100' : 'bg-slate-50'
}`} }`}
> >
@@ -614,7 +625,7 @@ function RouteComponent() {
</div> </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"> <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"> <p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
Archivados Archivados
@@ -622,18 +633,18 @@ function RouteComponent() {
{archivedChats.map((chat) => ( {archivedChats.map((chat) => (
<div <div
key={chat.id} 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" /> <Archive size={14} className="shrink-0 opacity-30" />
<span className="block min-w-0 flex-1 truncate"> <span className="block truncate">
{chat.nombre || {chat.nombre ||
`Archivado ${chat.creado_en.split('T')[0]}`} `Archivado ${chat.creado_en.split('T')[0]}`}
</span> </span>
</div> </div>
<button <button
onClick={(e) => unarchiveChat(e, chat.id)} 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} /> <RotateCcw size={14} />
</button> </button>

View File

@@ -166,30 +166,20 @@ function AsignaturaLayout() {
onSave={(val) => handleUpdateHeader('nombre', val)} onSave={(val) => handleUpdateHeader('nombre', val)}
/> />
</h1> </h1>
{
// console.log(headerData),
console.log(asignaturaApi.planes_estudio?.nombre)
}
<div className="flex flex-wrap gap-4 text-sm text-blue-200"> <div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GraduationCap className="h-4 w-4 shrink-0" /> <GraduationCap className="h-4 w-4 shrink-0" />
Pertenece al plan:{' '}
<span className="text-blue-100"> <span className="text-blue-100">
{(asignaturaApi.planes_estudio?.datos as DatosPlan) {(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
.nombre || ''}
</span>
</span>
<span className="flex items-center gap-1">
<span className="text-blue-100">
{(asignaturaApi.planes_estudio?.datos as DatosPlan)
.nombre ?? ''}
</span> </span>
</span> </span>
</div> </div>
<p className="text-sm text-blue-300">
Pertenece al plan:{' '}
<span className="cursor-pointer underline">
{asignaturaApi.planes_estudio?.nombre}
</span>
</p>
</div> </div>
<div className="flex flex-col items-end gap-2 text-right"> <div className="flex flex-col items-end gap-2 text-right">

14
staticwebapp.config.json Normal file
View File

@@ -0,0 +1,14 @@
{
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [
"/assets/*",
"/*.css",
"/*.js",
"/*.ico",
"/*.png",
"/*.jpg",
"/*.svg"
]
}
}