fix: version 4.0.3 -- Indicador de proceso en consultas a la IA

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-23 17:01:56 -05:00
parent 3ee17d1c5a
commit b38c631348
9 changed files with 45 additions and 3 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog - project.linkedcorp.net # Changelog - project.linkedcorp.net
## [4.0.3] - 2026-10-23
### Fixed
- Las consultas a la IA ("Nuevo con IA" y "Plan con IA") muestran un indicador de proceso con
spinner, mensaje y segundos transcurridos (componente compartido `BusyPanel`); antes parecía
que el botón no hacía nada mientras la IA respondía.
## [4.0.2] - 2026-10-23 ## [4.0.2] - 2026-10-23
### Changed ### Changed
- Detección de proyectos duplicados en "Nuevo con IA" ahora la decide la IA, dentro de la misma - Detección de proyectos duplicados en "Nuevo con IA" ahora la decide la IA, dentro de la misma
+1 -1
View File
@@ -1 +1 @@
4.0.2 4.0.3
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "project-linkedcorp-backend", "name": "project-linkedcorp-backend",
"version": "4.0.2", "version": "4.0.3",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/server.js", "main": "src/server.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "project-linkedcorp-frontend", "name": "project-linkedcorp-frontend",
"private": true, "private": true,
"version": "4.0.2", "version": "4.0.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+20
View File
@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
// Indicador de proceso largo (ej. consultas a la IA): spinner, mensaje y segundos transcurridos,
// para que el usuario sepa que el sistema esta trabajando y no colgado.
export function BusyPanel({ message, hint }) {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<div className="busy-panel" role="status" aria-live="polite">
<div className="busy-spinner" aria-hidden="true" />
<div>
<div className="busy-message">{message} <span className="text-muted">({seconds}s)</span></div>
{hint && <div className="text-muted busy-hint">{hint}</div>}
</div>
</div>
);
}
+6
View File
@@ -26,6 +26,9 @@ export const messages = {
ai_plan_generating: 'Generando...', ai_plan_generating: 'Generando...',
ai_plan_review: 'Desmarca lo que no quieras. Las actividades se crean encadenadas desde hoy, con prioridad media; luego puedes editarlas.', ai_plan_review: 'Desmarca lo que no quieras. Las actividades se crean encadenadas desde hoy, con prioridad media; luego puedes editarlas.',
ai_plan_days: 'días', ai_plan_days: 'días',
ai_busy_generating: 'La IA está preparando la propuesta...',
ai_busy_hint: 'Suele tardar entre 5 y 30 segundos. No cierres esta ventana.',
ai_busy_creating: 'Creando actividades y tareas...',
ai_plan_day: 'día', ai_plan_day: 'día',
ai_plan_apply: 'Crear seleccionadas', ai_plan_apply: 'Crear seleccionadas',
ai_plan_creating: 'Creando...', ai_plan_creating: 'Creando...',
@@ -588,6 +591,9 @@ export const messages = {
ai_plan_generating: 'Generating...', ai_plan_generating: 'Generating...',
ai_plan_review: 'Uncheck what you do not want. Activities are created back to back from today with medium priority; you can edit them later.', ai_plan_review: 'Uncheck what you do not want. Activities are created back to back from today with medium priority; you can edit them later.',
ai_plan_days: 'days', ai_plan_days: 'days',
ai_busy_generating: 'AI is preparing the proposal...',
ai_busy_hint: 'It usually takes 5 to 30 seconds. Do not close this window.',
ai_busy_creating: 'Creating activities and tasks...',
ai_plan_day: 'day', ai_plan_day: 'day',
ai_plan_apply: 'Create selected', ai_plan_apply: 'Create selected',
ai_plan_creating: 'Creating...', ai_plan_creating: 'Creating...',
@@ -4,6 +4,7 @@ import { DetailPopup } from '../../components/DetailPopup.jsx';
import { Button } from '../../components/Button.jsx'; import { Button } from '../../components/Button.jsx';
import { Field } from '../../components/Field.jsx'; import { Field } from '../../components/Field.jsx';
import { InfoBar } from '../../components/InfoBar.jsx'; import { InfoBar } from '../../components/InfoBar.jsx';
import { BusyPanel } from '../../components/BusyPanel.jsx';
import { useUi } from '../../context/ui-context.jsx'; import { useUi } from '../../context/ui-context.jsx';
import { generateProjectPlan, applyProjectPlan } from '../../services/ai.js'; import { generateProjectPlan, applyProjectPlan } from '../../services/ai.js';
@@ -54,6 +55,7 @@ export function AiPlanDialog({ open, projectId, onClose, onCreated }) {
return ( return (
<DetailPopup open={open} title={t('ai_plan_title')} onClose={busy ? () => {} : close} width={720} height={620}> <DetailPopup open={open} title={t('ai_plan_title')} onClose={busy ? () => {} : close} width={720} height={620}>
{error && <InfoBar tone="error">{error}</InfoBar>} {error && <InfoBar tone="error">{error}</InfoBar>}
{busy && <BusyPanel message={plan ? t('ai_busy_creating') : t('ai_busy_generating')} hint={plan ? undefined : t('ai_busy_hint')} />}
{!plan ? ( {!plan ? (
<> <>
<p className="text-muted" style={{ marginTop: 0 }}>{t('ai_plan_hint')}</p> <p className="text-muted" style={{ marginTop: 0 }}>{t('ai_plan_hint')}</p>
@@ -5,6 +5,7 @@ import { Button } from '../../components/Button.jsx';
import { Field, FieldRow } from '../../components/Field.jsx'; import { Field, FieldRow } from '../../components/Field.jsx';
import { ComboBox } from '../../components/ComboBox.jsx'; import { ComboBox } from '../../components/ComboBox.jsx';
import { InfoBar } from '../../components/InfoBar.jsx'; import { InfoBar } from '../../components/InfoBar.jsx';
import { BusyPanel } from '../../components/BusyPanel.jsx';
import { useUi } from '../../context/ui-context.jsx'; import { useUi } from '../../context/ui-context.jsx';
import { useAuth } from '../../context/auth-context.jsx'; import { useAuth } from '../../context/auth-context.jsx';
import { proposeNewProject, createNewProject } from '../../services/ai.js'; import { proposeNewProject, createNewProject } from '../../services/ai.js';
@@ -71,6 +72,7 @@ export function NewProjectAiDialog({ open, deptOptions, minLength = 20, onClose,
return ( return (
<DetailPopup open={open} title={t('ai_new_project_title')} onClose={close} width={760} height={680}> <DetailPopup open={open} title={t('ai_new_project_title')} onClose={close} width={760} height={680}>
{error && <InfoBar tone="error">{error}</InfoBar>} {error && <InfoBar tone="error">{error}</InfoBar>}
{busy && <BusyPanel message={proposal ? t('ai_busy_creating') : t('ai_busy_generating')} hint={proposal ? undefined : t('ai_busy_hint')} />}
{!proposal ? ( {!proposal ? (
<> <>
<Field label={t('ai_new_project_objective')} hint={`${objective.trim().length}/${minLength} ${t('ai_new_project_min_chars')}`}> <Field label={t('ai_new_project_objective')} hint={`${objective.trim().length}/${minLength} ${t('ai_new_project_min_chars')}`}>
+6
View File
@@ -575,3 +575,9 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
.text-muted { color: rgb(var(--muted)); } .text-muted { color: rgb(var(--muted)); }
.mt-0 { margin-top: 0; } .mt-0 { margin-top: 0; }
.empty-state { text-align: center; padding: 40px 16px; color: rgb(var(--muted)); } .empty-state { text-align: center; padding: 40px 16px; color: rgb(var(--muted)); }
/* BusyPanel: proceso largo en curso (consultas a la IA) */
.busy-panel { display: flex; align-items: center; gap: 14px; padding: 16px; margin: 12px 0; border-radius: 8px; background: rgb(var(--accent) / .08); border: 1px solid rgb(var(--accent) / .3); }
.busy-spinner { width: 28px; height: 28px; flex: none; border-radius: 50%; border: 3px solid rgb(var(--accent) / .25); border-top-color: rgb(var(--accent)); animation: update-spin .9s linear infinite; }
.busy-message { font-weight: 600; }
.busy-hint { font-size: .78rem; margin-top: 2px; }