From 4b83a24e4c6e6cb45d77d910b996ba0cc90b83fe Mon Sep 17 00:00:00 2001 From: Milton Gonzalez Date: Fri, 23 Oct 2026 16:15:53 -0500 Subject: [PATCH] feat: version 4.0.0 -- Asistente Nuevo proyecto con IA Co-Authored-By: Claude Opus 5.5 --- API.md | 2 + CHANGELOG.md | 16 ++ ESTADO.md | 5 +- VERSION | 2 +- backend/package.json | 2 +- backend/src/modules/ai/controller.js | 8 + backend/src/modules/ai/routes.js | 4 +- backend/src/modules/ai/service.js | 246 ++++++++++++++---- frontend/package.json | 2 +- frontend/src/i18n/messages.js | 28 ++ frontend/src/pages/Proyectos/AiPlanDialog.jsx | 4 +- .../pages/Proyectos/NewProjectAiDialog.jsx | 128 +++++++++ .../src/pages/Proyectos/ProyectosPage.jsx | 19 +- frontend/src/services/ai.js | 2 + 14 files changed, 402 insertions(+), 66 deletions(-) create mode 100644 frontend/src/pages/Proyectos/NewProjectAiDialog.jsx diff --git a/API.md b/API.md index 3e82180..5a6b02b 100644 --- a/API.md +++ b/API.md @@ -182,6 +182,8 @@ el registro. En empresa y SMTP la respuesta `GET` incluye `edit_version`. - `GET /api/ai/status` → `{ enabled }` (cualquier autenticado). - `POST /api/ai/projects/:projectId/plan` `{ description?, lang? }` → `{ activities: [{ name, description, durationDays, tasks: [] }] }`. Requiere módulo Proyectos y escritura en el proyecto; 30/hora. - `POST /api/ai/projects/:projectId/plan/apply` `{ activities }` → `{ created }` (crea actividades y tareas). +- `POST /api/ai/new-project/propose` `{ objective, startDate, lang? }` → `{ name, objective, description, projectTypeId, projectTypeName, estimatedEndDate, activities }`; `400` si el objetivo es corto o la fecha inválida (sin llamar a la IA), `422` si no es un proyecto. 30/hora. +- `POST /api/ai/new-project` `{ objective, startDate, name, description?, projectTypeId?, requestingDepartmentId, activities }` → `{ id, code }`. Roles que crean proyectos. - `GET/PUT /api/settings/ai` `{ apiKey?, model, editVersion }` y `POST /api/settings/ai/test` — solo superadmin; la clave nunca se devuelve. ### Log de errores (`/api/error-logs`) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d1ae6..58e3ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog - project.linkedcorp.net +## [4.0.0] - 2026-10-23 +### Added +- Asistente "Nuevo con IA" en Proyectos (roles que crean proyectos, con la IA configurada): + 1) el usuario describe el objetivo (mín. 20 caracteres), elige departamento solicitante y fecha + de inicio — validado antes de llamar a la IA; 2) una sola llamada propone nombre, objetivo + redactado, descripción (alcance), tipo, fin estimado y actividades con tareas y días; 3) el + usuario revisa, edita el nombre, desmarca y crea. El proyecto queda Planificado, con quien lo + crea como responsable y único miembro; actividades encadenadas y tareas con fecha dentro de + su actividad, todas con responsable. +- Si el texto no describe un proyecto, la IA lo rechaza ("Solo puedo ayudarte a crear + proyectos") con un consumo mínimo. Ninguna actividad se crea sin tareas. +### Fixed +- "Plan con IA" → "Crear seleccionadas" fallaba (orden por columna inexistente en prioridades) y + el diálogo no mostraba bien las tareas; ahora muestran su duración. Al aplicar, las + actividades arrancan hoy o en el inicio del proyecto si aún no empezó. + ## [3.2.0] - 2026-10-23 ### Added - Integración IA (API de Anthropic): Configuración → "Inteligencia artificial (IA)", solo diff --git a/ESTADO.md b/ESTADO.md index 5810cc9..02cd0cf 100644 --- a/ESTADO.md +++ b/ESTADO.md @@ -174,9 +174,8 @@ y recargar. Pendiente verificar el flujo completo en un despliegue autorizado; n - Reportes exportables (PDF/Excel/CSV) y vista ejecutiva separada. - Permisos por accion dentro de cada modulo (hoy: acceso por modulo y usuario desde v3.1.0 + roles fijos codificados). -- IA (v3.2.0 hecho: config + "Plan con IA" en Actividades). Siguiente: asistente "Nuevo proyecto con - IA" por pasos (objetivo -> datos fijos/elegidos -> una llamada -> revision), fechas del proyecto, - miembros/responsables y tareas con fechas. Despues: resumen semanal del Panel. +- IA (v4.0.0 hecho: config, "Plan con IA" y asistente "Nuevo con IA"). Siguiente posible: resumen + semanal del Panel; responsables por actividad entre los miembros del proyecto. - Notificaciones por email/WhatsApp (hoy: solo in-app). - Job programado para detectar actividades vencidas/por vencer (hoy: solo se notifica al asignar). - DatePicker: sigue usando `` nativo (simplificacion deliberada del MVP). diff --git a/VERSION b/VERSION index 944880f..fcdb2e1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +4.0.0 diff --git a/backend/package.json b/backend/package.json index 32261e0..207dd1a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "project-linkedcorp-backend", - "version": "3.2.0", + "version": "4.0.0", "private": true, "type": "module", "main": "src/server.js", diff --git a/backend/src/modules/ai/controller.js b/backend/src/modules/ai/controller.js index 1ca6055..7a1c380 100644 --- a/backend/src/modules/ai/controller.js +++ b/backend/src/modules/ai/controller.js @@ -10,3 +10,11 @@ export const postProjectPlan = asyncHandler(async (req, res) => { export const postApplyProjectPlan = asyncHandler(async (req, res) => { ok(res, await service.applyProjectPlan(Number(req.params.projectId), req.body?.activities, req.currentUser), 201); }); + +export const postProposeProject = asyncHandler(async (req, res) => { + ok(res, await service.proposeNewProject(req.body || {}, req.currentUser)); +}); + +export const postCreateProject = asyncHandler(async (req, res) => { + ok(res, await service.createNewProject(req.body || {}, req.currentUser), 201); +}); diff --git a/backend/src/modules/ai/routes.js b/backend/src/modules/ai/routes.js index 6f43350..ae72852 100644 --- a/backend/src/modules/ai/routes.js +++ b/backend/src/modules/ai/routes.js @@ -2,7 +2,7 @@ import { Router } from 'express'; import rateLimit from 'express-rate-limit'; import { requireAuth } from '../../middleware/auth.js'; import { requireModule } from '../../middleware/modules.js'; -import { getStatus, postProjectPlan, postApplyProjectPlan } from './controller.js'; +import { getStatus, postProjectPlan, postApplyProjectPlan, postProposeProject, postCreateProject } from './controller.js'; const router = Router(); @@ -12,6 +12,8 @@ const aiLimiter = rateLimit({ windowMs: 60 * 60 * 1000, limit: 30, standardHeade router.use(requireAuth); router.get('/status', getStatus); router.post('/projects/:projectId/plan', requireModule('projects'), aiLimiter, postProjectPlan); +router.post('/new-project/propose', requireModule('projects'), aiLimiter, postProposeProject); +router.post('/new-project', requireModule('projects'), postCreateProject); router.post('/projects/:projectId/plan/apply', requireModule('projects'), postApplyProjectPlan); export default router; diff --git a/backend/src/modules/ai/service.js b/backend/src/modules/ai/service.js index f5d8a70..28d9205 100644 --- a/backend/src/modules/ai/service.js +++ b/backend/src/modules/ai/service.js @@ -3,36 +3,43 @@ import { HttpError } from '../../utils/http.js'; import { askStructured } from '../../utils/ai.js'; import { logAudit } from '../../utils/audit.js'; import { getAiConfig } from '../settings/service.js'; -import { getProject, canWriteProject } from '../projects/service.js'; +import { getProject, canWriteProject, createProject } from '../projects/service.js'; import { createActivity } from '../activities/service.js'; import { createTask } from '../tasks/service.js'; const MAX_ACTIVITIES = 15; const MAX_TASKS = 10; +export const MIN_OBJECTIVE_LENGTH = 20; +const MAX_OBJECTIVE_LENGTH = 2000; -const PLAN_SCHEMA = { - type: 'object', - properties: { - activities: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - description: { type: 'string' }, - durationDays: { type: 'integer', minimum: 1 }, - tasks: { type: 'array', items: { type: 'string' } }, +const ACTIVITIES_SCHEMA = { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + durationDays: { type: 'integer', minimum: 1 }, + tasks: { + type: 'array', + minItems: 2, + items: { + type: 'object', + properties: { name: { type: 'string' }, durationDays: { type: 'integer', minimum: 1 } }, + required: ['name', 'durationDays'], }, - required: ['name', 'durationDays', 'tasks'], }, }, + required: ['name', 'durationDays', 'tasks'], }, - required: ['activities'], }; +const PLANNER_SYSTEM = (language) => `Eres un jefe de proyectos de TI de una empresa. Solo planificas proyectos: propones actividades realistas en orden de ejecucion, cada una con duracion en dias habiles y de 2 a ${MAX_TASKS} tareas concretas y accionables (las tareas son lo mas importante: ninguna actividad sin tareas). Responde en ${language}.`; +const languageFor = (lang) => (String(lang || '').startsWith('en') ? 'English' : 'español neutro'); + export async function getStatus() { const { configured } = await getAiConfig(); - return { enabled: configured }; + return { enabled: configured, minObjectiveLength: MIN_OBJECTIVE_LENGTH }; } async function getWritableProject(projectId, user) { @@ -41,68 +48,195 @@ async function getWritableProject(projectId, user) { return project; } -// Normaliza lo que devuelve la IA: recorta, limita cantidades y descarta entradas vacias. +const clip = (v, max) => String(v || '').trim().slice(0, max); +const days = (v) => Math.min(365, Math.max(1, Number.parseInt(v, 10) || 1)); + +// Normaliza lo que devuelve la IA (o lo que reenvia el cliente al aplicar): recorta, limita +// cantidades y descarta actividades sin tareas. Acepta tareas como texto o { name, durationDays }. function cleanPlan(activities) { - return (Array.isArray(activities) ? activities : []).slice(0, MAX_ACTIVITIES).map((a) => ({ - name: String(a?.name || '').trim().slice(0, 200), - description: String(a?.description || '').trim().slice(0, 1000), - durationDays: Math.min(365, Math.max(1, Number.parseInt(a?.durationDays, 10) || 1)), - tasks: (Array.isArray(a?.tasks) ? a.tasks : []).map((t) => String(t || '').trim().slice(0, 200)).filter(Boolean).slice(0, MAX_TASKS), - })).filter((a) => a.name); + return (Array.isArray(activities) ? activities : []).slice(0, MAX_ACTIVITIES).map((a) => { + const tasks = (Array.isArray(a?.tasks) ? a.tasks : []) + .map((t) => (typeof t === 'string' ? { name: t, durationDays: 1 } : t)) + .map((t) => ({ name: clip(t?.name, 200), durationDays: days(t?.durationDays) })) + .filter((t) => t.name) + .slice(0, MAX_TASKS); + return { name: clip(a?.name, 200), description: clip(a?.description, 1000), durationDays: days(a?.durationDays), tasks }; + }).filter((a) => a.name && a.tasks.length); } +function addDays(date, n) { + const d = new Date(date); + d.setDate(d.getDate() + n); + return d; +} +const isoDate = (d) => d.toISOString().slice(0, 10); + +// Fecha de fin estimada = inicio + suma de duraciones (actividades encadenadas). +const planEndDate = (plan, start) => addDays(start, Math.max(1, plan.reduce((s, a) => s + a.durationDays, 0)) - 1); + +// Crea actividades encadenadas desde `start` y sus tareas repartidas dentro de cada actividad, +// con la prioridad media y el primer estado abierto. Reusa los servicios normales (permisos, +// auditoria, notificaciones y recalculo de avance incluidos). +async function createPlanActivities(projectId, plan, start, user, responsibleUserId = null) { + const [priorities] = await pool.query('SELECT id FROM priorities ORDER BY level, id'); + const [statuses] = await pool.query('SELECT id FROM activity_statuses WHERE is_closed = 0 ORDER BY sort_order, id LIMIT 1'); + if (!priorities.length || !statuses.length) throw new HttpError(400, 'Faltan prioridades o estados de actividad en Configuracion'); + const priorityId = priorities[Math.floor((priorities.length - 1) / 2)].id; + let cursor = new Date(start); + for (const a of plan) { + const due = addDays(cursor, a.durationDays - 1); + const activity = await createActivity(projectId, { + name: a.name, description: a.description, priorityId, statusId: statuses[0].id, + responsibleUserId, startDate: isoDate(cursor), dueDate: isoDate(due), + }, user); + let taskCursor = new Date(cursor); + for (const task of a.tasks) { + let taskDue = addDays(taskCursor, task.durationDays - 1); + if (taskDue > due) taskDue = due; + await createTask(activity.id, { name: task.name, responsibleUserId, dueDate: isoDate(taskDue) }, user); + taskCursor = taskDue < due ? addDays(taskDue, 1) : due; + } + cursor = addDays(due, 1); + } + return plan.length; +} + +// --- Plan para un proyecto existente (pestaña Actividades) --- + export async function generateProjectPlan(projectId, description, lang, user) { const project = await getWritableProject(projectId, user); - const extra = String(description || '').trim().slice(0, 2000); + const extra = clip(description, MAX_OBJECTIVE_LENGTH); const [existing] = await pool.query('SELECT name FROM activities WHERE project_id = ? AND deleted_at IS NULL', [projectId]); - const language = String(lang || '').startsWith('en') ? 'English' : 'español neutro'; const result = await askStructured({ - system: `Eres un jefe de proyectos de TI. Propones planes de trabajo realistas y concretos para un area de TI corporativa. Responde en ${language}.`, + system: PLANNER_SYSTEM(languageFor(lang)), prompt: [ `Proyecto: ${project.name}`, project.description ? `Descripcion: ${project.description}` : '', project.objective ? `Objetivo: ${project.objective}` : '', extra ? `Indicaciones del usuario: ${extra}` : '', existing.length ? `Actividades que YA existen (no las repitas): ${existing.map((r) => r.name).join('; ')}` : '', - `Propon entre 3 y ${MAX_ACTIVITIES} actividades en orden de ejecucion, cada una con una duracion estimada en dias habiles y de 2 a ${MAX_TASKS} tareas concretas.`, + `Propon entre 3 y ${MAX_ACTIVITIES} actividades nuevas.`, ].filter(Boolean).join('\n'), toolName: 'plan_de_proyecto', - schema: PLAN_SCHEMA, + schema: { type: 'object', properties: { activities: ACTIVITIES_SCHEMA }, required: ['activities'] }, maxTokens: 4000, }); await logAudit({ userId: user.id, action: 'ai_plan', entityType: 'project', entityId: projectId }); return { activities: cleanPlan(result.activities) }; } -function addDays(date, days) { - const d = new Date(date); - d.setDate(d.getDate() + days); - return d; -} -const isoDate = (d) => d.toISOString().slice(0, 10); - -// Crea las actividades/tareas que el usuario acepto, encadenadas desde hoy, con la prioridad -// media y el primer estado abierto del catalogo. Reusa los servicios normales (permisos, -// auditoria, notificaciones y recalculo de avance incluidos). export async function applyProjectPlan(projectId, activities, user) { - await getWritableProject(projectId, user); + const project = await getWritableProject(projectId, user); const plan = cleanPlan(activities); if (!plan.length) throw new HttpError(400, 'No hay actividades para crear'); - const [priorities] = await pool.query('SELECT id FROM priorities ORDER BY sort_order, id'); - const [statuses] = await pool.query('SELECT id FROM activity_statuses WHERE is_closed = 0 ORDER BY sort_order, id LIMIT 1'); - if (!priorities.length || !statuses.length) throw new HttpError(400, 'Faltan prioridades o estados de actividad en Configuracion'); - const priorityId = priorities[Math.floor((priorities.length - 1) / 2)].id; - let start = new Date(); - let created = 0; - for (const a of plan) { - const due = addDays(start, a.durationDays - 1); - const activity = await createActivity(projectId, { - name: a.name, description: a.description, priorityId, statusId: statuses[0].id, - startDate: isoDate(start), dueDate: isoDate(due), - }, user); - for (const name of a.tasks) await createTask(activity.id, { name }, user); - start = addDays(due, 1); - created += 1; - } - return { created }; + // Desde hoy, o desde el inicio del proyecto si todavia no empezo. + const today = new Date(); + const projectStart = project.start_date ? new Date(project.start_date) : null; + const start = projectStart && projectStart > today ? projectStart : today; + return { created: await createPlanActivities(projectId, plan, start, user) }; +} + +// --- Asistente "Nuevo proyecto con IA" --- +// Solo el objetivo va a la IA (una llamada). Estado, responsable, miembros y fecha los fija la +// app; el departamento se elige de la lista real. Nada se crea hasta que el usuario confirma. + +const PROJECT_CREATOR_ROLES = ['superadmin', 'admin', 'it_manager', 'project_lead']; + +function requireCreator(user) { + if (!PROJECT_CREATOR_ROLES.includes(user.roleCode)) throw new HttpError(403, 'No tienes permisos para crear proyectos'); +} + +function parseStartDate(value) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ''))) throw new HttpError(400, 'Fecha de inicio invalida'); + const d = new Date(`${value}T12:00:00`); + if (Number.isNaN(d.getTime())) throw new HttpError(400, 'Fecha de inicio invalida'); + return d; +} + +function parseObjective(value) { + const objective = clip(value, MAX_OBJECTIVE_LENGTH); + if (objective.length < MIN_OBJECTIVE_LENGTH) throw new HttpError(400, `Describe el objetivo con al menos ${MIN_OBJECTIVE_LENGTH} caracteres`); + return objective; +} + +export async function proposeNewProject({ objective, startDate, lang }, user) { + requireCreator(user); + const goal = parseObjective(objective); + const start = parseStartDate(startDate); + const [types] = await pool.query('SELECT id, name FROM project_types ORDER BY name'); + const typeNames = types.map((t) => t.name); + const result = await askStructured({ + system: `${PLANNER_SYSTEM(languageFor(lang))} Si el texto del usuario NO describe un proyecto o iniciativa de trabajo (saludos, preguntas generales, chistes, temas ajenos), devuelve isProject=false y no inventes nada.`, + prompt: [ + `Objetivo del proyecto descrito por el usuario: """${goal}"""`, + `Fecha de inicio: ${isoDate(start)}`, + typeNames.length ? `Tipos de proyecto disponibles (elige uno exacto): ${typeNames.join('; ')}` : '', + `Si es un proyecto: propone un nombre corto (max 80 caracteres); un objetivo (una sola oracion que empiece con verbo en infinitivo y diga QUE se quiere lograr y para que, max 250 caracteres); una descripcion (alcance y contexto: que incluye y como se hara, 2-3 oraciones); y entre 3 y ${MAX_ACTIVITIES} actividades.`, + ].filter(Boolean).join('\n'), + toolName: 'propuesta_de_proyecto', + schema: { + type: 'object', + properties: { + isProject: { type: 'boolean' }, + name: { type: 'string' }, + objective: { type: 'string' }, + description: { type: 'string' }, + projectType: typeNames.length ? { type: 'string', enum: typeNames } : { type: 'string' }, + activities: ACTIVITIES_SCHEMA, + }, + required: ['isProject'], + }, + maxTokens: 4000, + }); + await logAudit({ userId: user.id, action: 'ai_new_project', entityType: 'project', entityId: null }); + if (!result.isProject) throw new HttpError(422, 'Solo puedo ayudarte a crear proyectos. Describe el objetivo de un proyecto de trabajo.'); + const plan = cleanPlan(result.activities); + if (!plan.length) throw new HttpError(422, 'No se pudo generar un plan con tareas para ese objetivo. Intenta describirlo con mas detalle.'); + const type = types.find((t) => t.name === result.projectType); + return { + name: clip(result.name, 80) || goal.slice(0, 80), + // El texto del usuario suele ser corto: la IA lo redacta como objetivo y deja el alcance en la descripcion. + objective: clip(result.objective, 500) || goal, + description: clip(result.description, 1000), + projectTypeId: type?.id || null, + projectTypeName: type?.name || null, + estimatedEndDate: isoDate(planEndDate(plan, start)), + activities: plan, + }; +} + +export async function createNewProject(payload, user) { + requireCreator(user); + const objective = parseObjective(payload.objective); + const start = parseStartDate(payload.startDate); + const name = clip(payload.name, 200); + if (!name) throw new HttpError(400, 'El nombre es obligatorio'); + const plan = cleanPlan(payload.activities); + if (!plan.length) throw new HttpError(400, 'Selecciona al menos una actividad con tareas'); + const departmentId = Number(payload.requestingDepartmentId); + const [[department]] = await pool.query('SELECT id FROM departments WHERE id = ?', [departmentId]); + if (!department) throw new HttpError(400, 'Departamento solicitante invalido'); + const [[type]] = payload.projectTypeId ? await pool.query('SELECT id FROM project_types WHERE id = ?', [Number(payload.projectTypeId)]) : [[null]]; + const [priorities] = await pool.query('SELECT id FROM priorities ORDER BY level, id'); + // Estado "Planificado" si existe; si no, el primer estado abierto. + const [statuses] = await pool.query( + "SELECT id FROM project_statuses WHERE is_closed = 0 ORDER BY (name LIKE 'Planific%' OR name LIKE 'Plann%') DESC, sort_order, id LIMIT 1" + ); + if (!priorities.length || !statuses.length) throw new HttpError(400, 'Faltan prioridades o estados de proyecto en Configuracion'); + + const project = await createProject({ + name, + description: clip(payload.description, 1000), + objective, + projectTypeId: type?.id || null, + priorityId: priorities[Math.floor((priorities.length - 1) / 2)].id, + statusId: statuses[0].id, + ownerUserId: user.id, + requestingDepartmentId: department.id, + startDate: isoDate(start), + estimatedEndDate: isoDate(planEndDate(plan, start)), + }, user); + await createPlanActivities(project.id, plan, start, user, user.id); + await logAudit({ userId: user.id, action: 'ai_create_project', entityType: 'project', entityId: project.id }); + return { id: project.id, code: project.code }; } diff --git a/frontend/package.json b/frontend/package.json index 22c53e9..26c7d3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "project-linkedcorp-frontend", "private": true, - "version": "3.2.0", + "version": "4.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js index 27f88fa..92701ec 100644 --- a/frontend/src/i18n/messages.js +++ b/frontend/src/i18n/messages.js @@ -26,10 +26,24 @@ export const messages = { 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_days: 'días', + ai_plan_day: 'día', ai_plan_apply: 'Crear seleccionadas', ai_plan_creating: 'Creando...', ai_plan_back: 'Volver', ai_plan_created: 'Actividades creadas', + ai_new_project_button: 'Nuevo con IA', + ai_new_project_title: 'Nuevo proyecto con IA', + ai_new_project_objective: '¿Qué proyecto quieres emprender? Describe su objetivo', + ai_new_project_min_chars: 'caracteres mínimos', + ai_new_project_placeholder: 'Ej.: implementar una mesa de ayuda para que los usuarios reporten incidencias de TI con seguimiento y tiempos de respuesta', + ai_new_project_start: 'Fecha de inicio', + ai_new_project_fixed: 'El proyecto se crea en estado Planificado, contigo como responsable y único miembro. La IA propone el nombre, las actividades y sus tareas; podrás revisarlos antes de crear.', + ai_new_project_type: 'Tipo', + ai_new_project_goal: 'Objetivo', + ai_new_project_desc: 'Descripción', + ai_new_project_end: 'Fin estimado', + ai_new_project_review: 'Desmarca las actividades que no quieras. Puedes cambiar el nombre aquí o después en el proyecto.', + ai_new_project_create: 'Crear proyecto', nav_projects: 'Proyectos', nav_organization: 'Organizacion', nav_config: 'Configuracion', @@ -573,10 +587,24 @@ export const messages = { 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_days: 'days', + ai_plan_day: 'day', ai_plan_apply: 'Create selected', ai_plan_creating: 'Creating...', ai_plan_back: 'Back', ai_plan_created: 'Activities created', + ai_new_project_button: 'New with AI', + ai_new_project_title: 'New project with AI', + ai_new_project_objective: 'What project do you want to start? Describe its goal', + ai_new_project_min_chars: 'minimum characters', + ai_new_project_placeholder: 'E.g.: implement a help desk so users can report IT incidents with tracking and response times', + ai_new_project_start: 'Start date', + ai_new_project_fixed: 'The project is created as Planned, with you as owner and only member. AI proposes the name, activities and tasks; you will review them before creating.', + ai_new_project_type: 'Type', + ai_new_project_goal: 'Goal', + ai_new_project_desc: 'Description', + ai_new_project_end: 'Estimated end', + ai_new_project_review: 'Uncheck the activities you do not want. You can change the name here or later in the project.', + ai_new_project_create: 'Create project', nav_projects: 'Projects', nav_organization: 'Organization', nav_config: 'Settings', diff --git a/frontend/src/pages/Proyectos/AiPlanDialog.jsx b/frontend/src/pages/Proyectos/AiPlanDialog.jsx index 7a62e9e..995c3fb 100644 --- a/frontend/src/pages/Proyectos/AiPlanDialog.jsx +++ b/frontend/src/pages/Proyectos/AiPlanDialog.jsx @@ -73,11 +73,11 @@ export function AiPlanDialog({ open, projectId, onClose, onCreated }) {