diff --git a/README.md b/README.md index 36c9973..39fa9af 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ With CommitFlow, you can **plan, track, and analyze your projects** β€” all in o --- +## ![CommitFlow Preview](./images/commitflow.jpg) + +| Chat 1 | Chat 2 | +| ---------------------------------- | ---------------------------------- | +| ![](./images/commitflow-chat1.jpg) | ![](./images/commitflow-chat2.jpg) | + +--- + ## πŸ“ Folder Structure ``` @@ -31,7 +39,15 @@ With CommitFlow, you can **plan, track, and analyze your projects** β€” all in o ## ✨ Features -### πŸ”§ Project Management +### πŸ€– AI-Powered Insights + +- πŸ’‘ **AI Recommendations** – Get automatic suggestions for prioritization and sprint planning. +- 🧠 **Smart Summaries** – Let AI summarize repository activity and project status. +- πŸ—£οΈ **Insight Chatbot** – Ask questions like β€œwhich tasks are in progress??” or β€œwho contributed the most to the commitflow repo?” + +--- + +### 🧭 Project Management A beautiful, AI-assisted workspace for managing your projects and tasks: @@ -45,14 +61,38 @@ A beautiful, AI-assisted workspace for managing your projects and tasks: - **Inline comments** with author, timestamp, and preview links - 🎨 **Smart Selectors** – - Assignee and Priority fields powered by **React Select**, dynamically colored per user or priority level -- 🧍 **Team Management** – - Add or remove team members using modern UI components, with color-coded avatars automatically generated. +- πŸ—ƒοΈ **Workspace Management** – + Add workspace. - 🧱 **Project Management Sidebar** – - Create or delete projects easily - Integrated **SweetAlert2** confirmations for safe deletions +- 🧍 **Team Management** – + Add or remove team members using modern UI components, with color-coded avatars automatically generated. - **Toast notifications** (`react-toastify`) for success actions (e.g., project or member added) - πŸŒ™ **Dark/Light Mode Aware** – Smooth color transitions and well-tuned contrast for both themes. +- Due date labels: **Due Today** & **Overdue** +- Filter **Assigned to Me** + +--- + +### πŸ’¬ Team Coordination + +- **Follow up tasks via WhatsApp** + - Generates dynamic `wa.me` link (manual click β€” no API yet) + - Pre-filled message with task title & status +- Real-time collaboration coming soon +- **Follow up tasks via WhatsApp** +- **Automatic email notifications** sent to team members when tasks are updated +- **Invite team members via email** with secure join links + +--- + +### πŸ”„ Offline‑First Sync + +- Works seamlessly **without internet** +- Local storage caching (offline‑first approach) +- Auto‑synchronization when back online --- @@ -64,19 +104,60 @@ A beautiful, AI-assisted workspace for managing your projects and tasks: --- -### πŸ€– AI-Powered Insights +### 🎨 Interactive UI -- πŸ’‘ **AI Recommendations** – Get automatic suggestions for prioritization and sprint planning. -- 🧠 **Smart Summaries** – Let AI summarize repository activity and project status. -- πŸ—£οΈ **Insight Chatbot** – Ask questions like β€œWho’s most active this week?” or β€œWhich repo grew fastest?” +- Smooth animations +- Responsive layout +- Clean, minimalist UX with focus on productivity --- -### 🐳 Infrastructure & Security +## πŸ› οΈ Tech Stack + +### Frontend + +- React + Vite +- TypeScript +- TailwindCSS +- Zustand (State Management) +- LocalStorage / IndexedDB (Offline Sync) +- React Query (Data Fetching & Sync Management) +- Socket.IO Client (Real-time updates) +- React Quill (Rich Text Editor) +- SweetAlert2 (Dialogs) +- React Toastify (Notifications) +- Framer Motion / GSAP (Animations & interactive UI) +- XLSX (Export Excel) + +### Backend + +- Nest.js +- TypeScript +- Prisma ORM +- PostgreSQL +- Socket.IO Gateway (Real-time events) +- Nodemailer (Email Delivery via SMTP) +- Multer (File upload middleware) +- Class Validator / Class Transformer +- Swagger (API documentation) +- Google TTS API +- AWS SDK for S3 Storage +- JWT Authentication (Access & Refresh Tokens) +- OpenAI API Integration (AI features / content generation) + +--- -- 🧩 **PostgreSQL Storage** – Store structured task and analytics data. -- πŸ” **Environment Management** – Secure credentials via `.env` file. -- βš™οΈ **Docker Ready** – Run everything locally or in production with one command. +## 🐳 Infrastructure & Security (Updated) + +- πŸ—„οΈ **PostgreSQL Database** – Structured project and task data. +- ☁️ **AWS S3 Storage** – Media & attachments. +- βœ‰οΈ **SMTP Email (Nodemailer)** – Invitations & notifications. +- πŸ” **Environment Variables (.env)** – Secure credential management. +- πŸ“‘ **WebSocket Gateway** – Realtime updates via Socket.IO. +- πŸ”‘ **JWT Authentication** – Secure login, workspace access, and API protection. +- πŸ€– **OpenAI Integration** – AI-driven generation (text, automation, suggestions). +- πŸ“ **LocalStorage + IndexedDB** – Offline-first data with auto-sync. +- πŸ“˜ **Swagger UI** – API documentation. --- diff --git a/backend/src/ai-agent/ask.service.ts b/backend/src/ai-agent/ask.service.ts index edf65b5..c0010a4 100644 --- a/backend/src/ai-agent/ask.service.ts +++ b/backend/src/ai-agent/ask.service.ts @@ -36,7 +36,7 @@ export class AskService { if (socket) { socket.emit("ai_thinking", { type: "tool_call", - message: `πŸ€– CommitFlow memproses permintaan tool: ${tool}`, + message: `πŸ€– CommitFlow is processing the tool request: ${tool}`, tool, args, }); @@ -51,7 +51,7 @@ export class AskService { if (socket) { socket.emit("ai_thinking", { type: "tool_result", - message: `πŸ“Š Hasil tool ${tool} siap.`, + message: `πŸ“Š Tool result for ${tool} is ready.`, tool, result, }); @@ -62,7 +62,7 @@ export class AskService { if (socket) { socket.emit("ai_thinking", { type: "done", - message: `βœ… Semua proses selesai.`, + message: `βœ… All processes completed.`, }); } } @@ -71,32 +71,32 @@ export class AskService { * Helper: execute a tool by name (serial) * Returns the raw tool result (JS object/array) or { error: ... } */ - private async execToolByName(fn: string, args: any, userId: string) { + private async execToolByName(fn: string, args: any) { try { if (fn === "getRepos") { return await getRepos(); } else if (fn === "getContributors") { return await getContributors(args.repo); } else if (fn === "getProjects") { - return await getProjects(userId); + return await getProjects(args.workspaceId); } else if (fn === "getMembers") { - return await getMembers(userId); + return await getMembers(args.workspaceId); } else if (fn === "getAllTasks") { - return await getAllTasks(args?.projectId || "", userId); + return await getAllTasks(args?.projectId); } else if (fn === "getTodoTasks") { - return await getTodoTasks(args?.projectId || "", userId); + return await getTodoTasks(args?.projectId); } else if (fn === "getInProgressTasks") { - return await getInProgressTasks(args?.projectId || "", userId); + return await getInProgressTasks(args?.projectId); } else if (fn === "getDoneTasks") { - return await getDoneTasks(args?.projectId || "", userId); + return await getDoneTasks(args?.projectId); } else if (fn === "getUnassignedTasks") { - return await getUnassignedTasks(args?.projectId || "", userId); + return await getUnassignedTasks(args?.projectId); } else if (fn === "getUrgentTasks") { - return await getUrgentTasks(args?.projectId || "", userId); + return await getUrgentTasks(args?.projectId); } else if (fn === "getLowTasks") { - return await getLowTasks(args?.projectId || "", userId); + return await getLowTasks(args?.projectId); } else if (fn === "getMediumTasks") { - return await getMediumTasks(args?.projectId || "", userId); + return await getMediumTasks(args?.projectId); } else { return { error: `Unknown tool: ${fn}` }; } @@ -132,6 +132,14 @@ export class AskService { content: SYSTEM_MESSAGE, }, ...userMessages.map((msg) => ({ role: msg.role, content: msg.content })), + { + role: "system", + content: `User Selected Workspace ID: ${data.workspaceId}`, + }, + { + role: "system", + content: `User Selected Project ID: ${data.projectId}`, + }, ...data.messages, ]; @@ -223,7 +231,7 @@ export class AskService { } else { // Execute actual tool this.emitToolCall(socket, fn, args); - toolResult = await this.execToolByName(fn, args, userId ?? ""); + toolResult = await this.execToolByName(fn, args); this.emitToolResult(socket, fn, toolResult); // push tool result into conversation @@ -289,7 +297,7 @@ export class AskService { messages.push({ role: "system", - content: "buatkan summary dari hasil tools call jika ada", + content: "generate a summary of the tool call results if available", }); // === At this point, modelMessage does NOT request tools anymore === // Start SSE streaming final answer. Ensure we include tools & tool_choice @@ -379,7 +387,7 @@ export class AskService { choices: [ { delta: { - content: `Terjadi kesalahan: ${ + content: `An error occurred: ${ error?.message || String(error) }`, }, diff --git a/backend/src/ai-agent/messages/messages.controller.ts b/backend/src/ai-agent/messages/messages.controller.ts index d8095aa..59a65f0 100644 --- a/backend/src/ai-agent/messages/messages.controller.ts +++ b/backend/src/ai-agent/messages/messages.controller.ts @@ -22,7 +22,6 @@ export class MessagesController { @Delete(":id") delete(@Req() req: any, @Param("id") id: string) { - console.log("delete message id: ", id); const userId = req.user.userId; // <-- ambil userId dari JWT return this.messagesService.delete(userId, id); } diff --git a/backend/src/ai-agent/project.service.ts b/backend/src/ai-agent/project.service.ts index f5dd407..d2e5d50 100644 --- a/backend/src/ai-agent/project.service.ts +++ b/backend/src/ai-agent/project.service.ts @@ -9,22 +9,14 @@ const prisma = new PrismaClient(); * PROJECTS * ===================================== */ -export async function getProjects(userId: string) { +export async function getProjects(workspaceId: string) { try { console.log("getProjects"); - const team = await prisma.teamMember.findMany({ - where: { - userId, - }, - }); - const workspaceIds = team.map((item: any) => item.workspaceId); const results = await prisma.project.findMany({ where: { isTrash: false, - workspaceId: { - in: workspaceIds, - }, + workspaceId, }, include: { tasks: { @@ -90,38 +82,19 @@ function baseTaskInclude() { }; } -async function buildTaskWhere( - userId: string, - projectId?: string, - status?: string -) { - const team = await prisma.teamMember.findMany({ - where: { - userId, - }, - }); - const workspaceIds = team.map((item: any) => item.workspaceId); - const whereProject: any = { - workspaceId: { - in: workspaceIds, - }, - }; +function buildTaskWhere(projectId?: string, status?: string) { + // Only filter by project (no cross-workspace lookup) + const where: any = { isTrash: false }; + if (projectId) { - whereProject.id = projectId; + // Directly filter tasks by the given project id + where.projectId = projectId; } - const projects = await prisma.project.findMany({ - where: whereProject, - }); - - const projectIds = projects.map((item: any) => item.id); + if (status) { + where.status = status; + } - const where: any = { isTrash: false }; - if (projectId) - where.projectId = { - in: projectIds, - }; - if (status) where.status = status; return where; } @@ -130,12 +103,12 @@ async function buildTaskWhere( * ALL TASKS * ===================================== */ -export async function getAllTasks(projectId = "", userId: string) { +export async function getAllTasks(projectId = "") { try { console.log("getAllTasks"); const results = await prisma.task.findMany({ - where: await buildTaskWhere(userId, projectId), + where: await buildTaskWhere(projectId), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -152,12 +125,12 @@ export async function getAllTasks(projectId = "", userId: string) { * TODO TASKS * ===================================== */ -export async function getTodoTasks(projectId = "", userId: string) { +export async function getTodoTasks(projectId = "") { try { console.log("getTodoTasks"); const results = await prisma.task.findMany({ - where: await buildTaskWhere(userId, projectId, "todo"), + where: await buildTaskWhere(projectId, "todo"), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -174,12 +147,12 @@ export async function getTodoTasks(projectId = "", userId: string) { * IN PROGRESS TASKS * ===================================== */ -export async function getInProgressTasks(projectId = "", userId: string) { +export async function getInProgressTasks(projectId = "") { try { console.log("getInProgressTasks"); const results = await prisma.task.findMany({ - where: await buildTaskWhere(userId, projectId, "inprogress"), + where: await buildTaskWhere(projectId, "inprogress"), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -196,12 +169,12 @@ export async function getInProgressTasks(projectId = "", userId: string) { * DONE TASKS * ===================================== */ -export async function getDoneTasks(projectId = "", userId: string) { +export async function getDoneTasks(projectId = "") { try { console.log("getDoneTasks"); const results = await prisma.task.findMany({ - where: await buildTaskWhere(userId, projectId, "done"), + where: await buildTaskWhere(projectId, "done"), include: baseTaskInclude(), orderBy: { createdAt: "desc" }, }); @@ -218,12 +191,12 @@ export async function getDoneTasks(projectId = "", userId: string) { * MEMBERS * ===================================== */ -export async function getMembers(userId: string) { +export async function getMembers(workspaceId: string) { try { console.log("getMembers"); const results = await prisma.teamMember.findMany({ - where: { isTrash: false }, + where: { isTrash: false, workspaceId }, include: { Task: { where: { isTrash: false }, @@ -268,10 +241,7 @@ export async function getMembers(userId: string) { } } -export async function getUnassignedTasks( - projectId: string = "", - userId: string -) { +export async function getUnassignedTasks(projectId: string = "") { try { console.log("getUnassignedTasks"); @@ -295,7 +265,7 @@ export async function getUnassignedTasks( } } -export async function getUrgentTasks(projectId: string = "", userId: string) { +export async function getUrgentTasks(projectId: string = "") { try { console.log("getUrgentTasks"); @@ -319,7 +289,7 @@ export async function getUrgentTasks(projectId: string = "", userId: string) { } } -export async function getLowTasks(projectId: string = "", userId: string) { +export async function getLowTasks(projectId: string = "") { try { console.log("getLowTasks"); @@ -343,7 +313,7 @@ export async function getLowTasks(projectId: string = "", userId: string) { } } -export async function getMediumTasks(projectId: string = "", userId: string) { +export async function getMediumTasks(projectId: string = "") { try { console.log("getMediumTasks"); diff --git a/backend/src/ai-agent/tools.constants.ts b/backend/src/ai-agent/tools.constants.ts index b86b4ee..43ae1d1 100644 --- a/backend/src/ai-agent/tools.constants.ts +++ b/backend/src/ai-agent/tools.constants.ts @@ -7,16 +7,37 @@ export const tools = [ { name: "getRepos", description: - "Ambil daftar repository GitHub. Gunakan fungsi ini ketika pengguna bertanya tentang nama repo, pilihan repo, atau repo yang tersedia.", + "Retrieve the list of GitHub repositories. Use this function when the user asks about repository names, available repositories, or repo options. The result should include actionable insights for each repo: recent activity (last commit date), open issues/PR counts, active contributors count, primary language, CI status (if available), and a short recommendation (e.g., 'needs PR review', 'archived candidate').", type: "function", function: { name: "getRepos", }, + // Hint for the assistant / backend on what to include in results + outputFormat: { + perRepo: { + id: "string", + name: "string", + description: "string", + primaryLanguage: "string", + lastCommitDate: "ISO8601 string", + openPRCount: "number", + openIssueCount: "number", + activeContributorsLast30Days: "number", + ciStatus: "string (passing/failed/unknown)", + recommendedAction: "string (short)", + }, + summary: { + totalRepos: "number", + activeRepoCount30d: "number", + reposNeedingAttention: "number", + }, + }, }, + { name: "getContributors", description: - "Gunakan fungsi ini ketika pengguna bertanya tentang kontributor, jumlah commit, kontribusi terbanyak, atau aktivitas developer pada sebuah repository. Fungsi ini membutuhkan nama repo.", + "Use this function when the user asks about contributors, commit counts, top contributors, or developer activity for a specific repository. This function requires the repository name. The result should provide contributor-level metrics and insights: total commits, commits in the last 30/90 days, PRs opened/merged, issues opened, lines added/removed (if available), recent activity timestamp, and a short note identifying top contributors and potential areas for recognition or triage.", type: "function", function: { name: "getContributors", @@ -26,12 +47,35 @@ export const tools = [ repo: { type: "string", description: - "Nama repository target, harus cocok dengan salah satu yang dikembalikan oleh getRepos.", + "The target repository name. Must match one of the repositories returned by getRepos.", }, }, required: ["repo"], }, }, + outputFormat: { + repo: "string", + contributors: [ + { + id: "string", + username: "string", + totalCommits: "number", + commitsLast30Days: "number", + prsOpened: "number", + prsMerged: "number", + issuesOpened: "number", + lastActivity: "ISO8601 string", + linesAdded: "number | null", + linesRemoved: "number | null", + suggestedRecognition: "string (e.g., 'Top committer this month')", + }, + ], + summary: { + topContributors: "array of usernames (top 3)", + activeContributorCount30d: "number", + trend: "string (increasing/decreasing/stable)", + }, + }, }, /** @@ -42,27 +86,94 @@ export const tools = [ { name: "getProjects", description: - "Ambil daftar project aktif beserta statistik task (todo, inprogress, done). Gunakan fungsi ini ketika pengguna bertanya tentang daftar project, detail project, atau ingin mengetahui project mana yang memiliki task tertentu.", + "Retrieve the list of active projects along with task statistics (todo, inprogress, done). The response should include per-project insights: task breakdown by status, completion rate (%), overdue task count, blocked tasks count, recent activity, risk level (low/medium/high) and recommended next steps (e.g., 'reassign overdue tasks', 'prioritize critical bugfixes'). Use this function when the user asks about project lists, project details, or which project contains certain tasks.", type: "function", function: { name: "getProjects", + parameters: { + type: "object", + properties: { + workspaceId: { + type: "string", + description: "The target workspaceId.", + }, + }, + required: ["workspaceId"], + }, + }, + outputFormat: { + projects: [ + { + id: "string", + name: "string", + description: "string", + counts: { + todo: "number", + inprogress: "number", + done: "number", + blocked: "number", + overdue: "number", + }, + completionRatePercent: "number", + lastActivity: "ISO8601 string", + riskLevel: "string (low|medium|high)", + topIssues: ["string"], + recommendedActions: ["string"], + }, + ], + summary: { + totalProjects: "number", + projectsAtRisk: "number", + }, }, }, { name: "getMembers", description: - "Ambil daftar anggota tim beserta statistik tugas yang mereka miliki. Gunakan fungsi ini ketika pengguna bertanya tentang assignee, workload anggota, atau ingin mencocokkan assigneeId.", + "Retrieve the list of team members along with their task statistics and workload insights. Response should include tasks assigned, tasks by status, overdue counts, recent activity, and a short workload recommendation (e.g., 'overloaded β€” reassign', 'underutilized β€” assign new tasks'). Use this when the user asks about assignees, workload distribution, or matching an assigneeId.", type: "function", function: { name: "getMembers", + parameters: { + type: "object", + properties: { + workspaceId: { + type: "string", + description: "The target workspaceId.", + }, + }, + required: ["workspaceId"], + }, + }, + outputFormat: { + members: [ + { + id: "string", + name: "string", + role: "string", + assignedTaskCount: "number", + todoCount: "number", + inprogressCount: "number", + doneCount: "number", + overdueCount: "number", + lastActivity: "ISO8601 string", + utilizationPercent: "number (estimated)", + recommendation: "string", + }, + ], + summary: { + mostLoadedMember: "string", + leastLoadedMember: "string", + averageUtilizationPercent: "number", + }, }, }, { name: "getAllTasks", description: - "Ambil semua task (filtered by projectId bila tersedia). Gunakan fungsi ini ketika pengguna meminta daftar task tanpa filter status.", + "Retrieve all tasks (filtered by projectId if provided). Results should be detailed and actionable: include id, title, description snippet, status, priority, assignee (if any), createdAt, updatedAt, dueDate, age (days open), linked PR/issue IDs, dependencies, blocker flag, commentsCount, and suggested next action (e.g., 'assign', 'review PR', 'change priority'). Use this when the user requests a list of tasks without a status filter.", type: "function", function: { name: "getAllTasks", @@ -71,10 +182,38 @@ export const tools = [ properties: { projectId: { type: "string", - description: - "ID project (opsional). Jika diberikan, kembalikan task yang hanya berasal dari project tersebut.", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: [ + { + id: "string", + title: "string", + description: "string", + status: "string (todo|inprogress|done|blocked)", + priority: "string (urgent|medium|low)", + assigneeId: "string | null", + assigneeName: "string | null", + createdAt: "ISO8601 string", + updatedAt: "ISO8601 string", + dueDate: "ISO8601 string | null", + ageDays: "number", + linkedPRs: ["string"], + linkedIssues: ["string"], + dependencies: ["taskId"], + blocker: "boolean", + commentsCount: "number", + suggestedAction: "string", + }, + ], + summary: { + totalTasks: "number", + overdueTasks: "number", + blockedTasks: "number", }, }, }, @@ -82,7 +221,7 @@ export const tools = [ { name: "getTodoTasks", description: - "Ambil task dengan status 'todo'. Dapat difilter berdasarkan projectId.", + "Retrieve tasks with the 'todo' status. Include the same detailed fields as getAllTasks plus suggestions for prioritization (e.g., 'start next week', 'urgent: reassign'). Can be filtered by projectId.", type: "function", function: { name: "getTodoTasks", @@ -91,9 +230,17 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: "same schema as getAllTasks.tasks", + summary: { + todoCount: "number", + highPriorityTodoCount: "number", }, }, }, @@ -101,7 +248,7 @@ export const tools = [ { name: "getInProgressTasks", description: - "Ambil task dengan status 'inprogress'. Dapat difilter berdasarkan projectId.", + "Retrieve tasks with the 'inprogress' status. Provide details including blockers, time-in-progress (age), PR links, assignees, and recommended next steps to complete (e.g., 'needs QA', 'awaiting review'). Can be filtered by projectId.", type: "function", function: { name: "getInProgressTasks", @@ -110,9 +257,17 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: "same schema as getAllTasks.tasks", + summary: { + inprogressCount: "number", + stalledCount: "number (inprogress > X days)", }, }, }, @@ -120,7 +275,7 @@ export const tools = [ { name: "getDoneTasks", description: - "Ambil task dengan status 'done'. Dapat difilter berdasarkan projectId.", + "Retrieve tasks with the 'done' status. Include completion date, time-to-complete (days), who completed it, and link to PR or merge if applicable. Useful for trend analysis and velocity estimation. Can be filtered by projectId.", type: "function", function: { name: "getDoneTasks", @@ -129,16 +284,34 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: [ + { + id: "string", + title: "string", + completedAt: "ISO8601 string", + completedBy: "string", + timeToCompleteDays: "number", + linkedPR: "string | null", + }, + ], + summary: { + doneCount: "number", + averageTimeToCompleteDays: "number", }, }, }, + { name: "getUnassignedTasks", description: - "Ambil semua task yang belum memiliki assignee. Bisa difilter berdasarkan projectId.", + "Retrieve all tasks that do not have an assignee. Response should highlight priority, age, project, and suggested assignees (based on workload/skill matching if available). Can be filtered by projectId.", type: "function", function: { name: "getUnassignedTasks", @@ -147,16 +320,26 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: "same schema as getAllTasks.tasks (assigneeId null)", + summary: { + totalUnassigned: "number", + highPriorityUnassigned: "number", + recommendedAssigneeSuggestions: ["{memberId, reason}"], }, }, }, + { name: "getUrgentTasks", description: - "Ambil semua task dengan priority 'urgent'. Bisa difilter berdasarkan projectId.", + "Retrieve all tasks with 'urgent' priority. Include context: why urgent (deadline / blocker / severity), assignee (if any), dueDate, and suggested immediate actions. Can be filtered by projectId.", type: "function", function: { name: "getUrgentTasks", @@ -165,16 +348,25 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: "same schema as getAllTasks.tasks", + summary: { + totalUrgent: "number", + urgentByProject: "{projectId: count}", }, }, }, + { name: "getLowTasks", description: - "Ambil semua task dengan priority 'low'. Bisa difilter berdasarkan projectId.", + "Retrieve all tasks with 'low' priority. Include time-since-creation and recommendation whether to keep for backlog grooming or archive. Can be filtered by projectId.", type: "function", function: { name: "getLowTasks", @@ -183,16 +375,25 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: "same schema as getAllTasks.tasks", + summary: { + lowPriorityCount: "number", + suggestedBacklogCandidates: ["taskId"], }, }, }, + { name: "getMediumTasks", description: - "Ambil semua task dengan priority 'medium'. Bisa difilter berdasarkan projectId.", + "Retrieve all tasks with 'medium' priority. Include estimated effort (if available), dependencies, and recommended scheduling windows. Can be filtered by projectId.", type: "function", function: { name: "getMediumTasks", @@ -201,9 +402,16 @@ export const tools = [ properties: { projectId: { type: "string", - description: "ID project (opsional).", + description: "The target projectId.", }, }, + required: ["projectId"], + }, + }, + outputFormat: { + tasks: "same schema as getAllTasks.tasks", + summary: { + mediumPriorityCount: "number", }, }, }, diff --git a/backend/src/common/ai.constants.ts b/backend/src/common/ai.constants.ts index e44beff..00228a2 100644 --- a/backend/src/common/ai.constants.ts +++ b/backend/src/common/ai.constants.ts @@ -1,208 +1,207 @@ export const SYSTEM_MESSAGE = ` -Kamu adalah asisten developer untuk sistem Project Management dan GitHub Integration. +You are a developer assistant for a Project Management and GitHub Integration system. -Tugasmu: -- Menjawab pertanyaan tentang repository GitHub dan kontribusi developer. -- Menjawab pertanyaan tentang project, task, priority, dan anggota tim. -- Menggunakan tools (function calls) dengan benar, akurat, dan deterministik. -- Memberikan jawaban ringkas, tanpa narasi proses, dan bukan dalam bentuk JSON. +Your tasks: +- Answer questions about GitHub repositories and developer contributions. +- Answer questions about projects, tasks, priorities, and team members. +- Use tools (function calls) correctly, accurately, and deterministically. +- Provide concise answers, without process narration, and never in raw JSON form. ==================================================================== -ATURAN PALING PENTING (HARUS DIPATUHI): +THE MOST IMPORTANT RULES (MUST FOLLOW): ==================================================================== -1. Jika kamu ingin memanggil sebuah tool/fungsi: - Output HARUS berupa JSON dengan format: +1. If you need to call a tool/function: + The output MUST be a JSON object with this exact format: { - "tool": "", + "tool": "", "arguments": { ... } } - - Tidak boleh ada teks sebelum atau sesudah JSON tersebut. - - Tidak boleh mengeluarkan JSON mentah tanpa field "tool" dan "arguments". - - JSON tool call harus menjadi satu-satunya output dalam pesan itu. + - No text before or after this JSON. + - You must not output raw JSON without "tool" and "arguments". + - The tool call JSON must be the ONLY output in that message. -2. Setelah hasil tool diterima (role: tool): - - Kamu HARUS memberikan jawaban final dalam bentuk teks biasa. - - Jawaban TIDAK BOLEH berupa JSON mentah. - - Ringkas, jelas, langsung ke hasil. Tidak ada filler. +2. After the tool result is received (role: tool): + - You MUST provide the final answer as plain text. + - The answer MUST NOT be raw JSON. + - Keep it short, clear, and only focused on the result. No filler. -3. Tidak boleh menggunakan narasi proses seperti: - - "Sebentar ya…" - - "Saya akan mengecek…" - - "Mari kita lihat dulu…" +3. Do NOT use process narration such as: + - "One moment…" + - "Let me check…" + - "Let's see…" -4. Jika pertanyaan membutuhkan beberapa tool call: - - Kamu boleh memanggil tool lebih dari sekali. - - Setiap tool call wajib mengikuti format JSON tunggal di atas. - - Setelah semua data terkumpul β†’ barulah beri jawaban final (teks). +4. If a question requires multiple tool calls: + - You may call more than one tool. + - Each tool call must follow the single-JSON format above. + - After all required data is collected, give the final answer (plain text). -5. Jika data kurang (contoh: user tidak menyebutkan nama project): - - Pertama coba otomatis cocokkan nama lewat getProjects/getMembers. - - Jika masih ambigu, tanyakan sangat singkat: - "Project apa yang dimaksud?" +5. If the data is incomplete (e.g., the user does not specify a project name): + - First, try auto-matching using getProjects/getMembers. + - If still ambiguous, ask a very short question: + "Which project do you mean?" ==================================================================== -PANDUAN PEMILIHAN TOOL (ROUTING LOGIC): +TOOL SELECTION GUIDE (ROUTING LOGIC): ==================================================================== === A. GitHub === -Gunakan: +Use: 1. getRepos - - Saat user meminta daftar repo - - Atau user menyebut repo yang tidak pasti ada + - When the user requests a list of repositories + - Or when the user mentions a repo that may not exist 2. getContributors - - Saat user bertanya: - β€’ siapa kontributornya - β€’ siapa paling banyak commit - β€’ jumlah kontribusi - β€’ statistik user di repo tertentu + - When the user asks: + β€’ who the contributors are + β€’ who made the most commits + β€’ contribution counts + β€’ developer stats for a specific repo ==================================================================== === B. Project Management: PROJECT === -Gunakan getProjects apabila user bertanya: -- daftar project -- project mana yang paling banyak task -- analisa project tertentu -- project overload / project progress -- project yang memiliki priority tertentu -- project berdasarkan status task +Use getProjects when the user asks about: +- project lists +- which project has the most tasks +- analyzing a specific project +- overloaded projects / project progress +- projects with certain priority distributions +- projects based on task status ==================================================================== === C. Project Management: MEMBERS === -Gunakan getMembers apabila user bertanya: -- siapa assignee untuk suatu task -- workload anggota tim -- task per anggota -- siapa yang paling overload -- siapa yang tidak punya task +Use getMembers when the user asks about: +- who the assignee is for a task +- team member workload +- tasks per member +- who is the most overloaded +- who has no tasks ==================================================================== === D. Project Management: TASK (Basic) === -Gunakan: +Use: 1. getAllTasks - - Saat user meminta seluruh task - - Saat user ingin filter manual (AI yang menyaring) - - Saat mencari task tertentu (by title / id) + - When the user requests all tasks + - When the user wants to filter manually (AI filters the results) + - When searching for specific tasks (by title / id) 2. getTodoTasks 3. getInProgressTasks 4. getDoneTasks - - Saat user langsung menyebut status task + - When the user directly mentions task status ==================================================================== === E. Project Management: PRIORITY-BASED TASKS === -Gunakan: +Use: 1. getUrgentTasks - - Saat user bertanya: - β€’ task urgent - β€’ task paling penting - β€’ task prioritas tinggi + - When the user asks for: + β€’ urgent tasks + β€’ high-priority tasks + β€’ important tasks 2. getMediumTasks - - Saat user bertanya: - β€’ task prioritas medium - β€’ task tingkat sedang + - When the user asks for: + β€’ medium priority tasks + β€’ moderately prioritized tasks 3. getLowTasks - - Saat user bertanya: - β€’ task prioritas rendah - β€’ task low priority + - When the user asks for: + β€’ low priority tasks + β€’ low-impact tasks 4. getUnassignedTasks - - Saat user bertanya: - β€’ task yang belum punya assignee - β€’ task yang tidak dikerjakan siapa pun - β€’ task kosong + - When the user asks for: + β€’ tasks without assignees + β€’ unassigned tasks + β€’ tasks owned by no one -Semua fungsi priority bisa menerima projectId jika user menyebut project tertentu. +All priority-based functions may accept projectId if the user specifies a project. ==================================================================== -FORMAT JAWABAN SETELAH TOOL RESULT: +ANSWER FORMAT AFTER TOOL RESULTS: ==================================================================== -Setelah menerima hasil tool: -- Berikan jawaban final berbentuk teks. -- Jangan tampilkan JSON mentah. -- Jangan ulangi kembali data terlalu panjang; cukup ringkas. +After receiving a tool result: +- Provide the final answer in plain text. +- Do not output raw JSON. +- Do not repeat long data dumps; keep the summary short. -Contoh benar: -"Berikut task tanpa assignee di project Batumadu: +Correct example: +"Here are the unassigned tasks in the Batumadu project: β€’ Setup API Gateway β€’ Refactor Authentication -Total: 2 task." +Total: 2 tasks." -Contoh salah: -- Menampilkan JSON -- Menyalin full raw data tools -- Menyertakan frase naratif proses +Incorrect examples: +- Showing raw JSON +- Copying the full raw tool data +- Adding process narration ==================================================================== -CONTOH INPUT USER (GITHUB) +EXAMPLE USER INPUT (GITHUB) ==================================================================== -- "siapa saja yang berkontribusi di repo commitflow?" -- "siapa yang paling banyak berkontribusi di repo commitflow?" -- "list seluruh repositori." +- "who contributed to the commitflow repo?" +- "who has the most contributions in the commitflow repo?" +- "list all repositories." ==================================================================== -CONTOH INPUT USER (PROJECT MANAGEMENT) +EXAMPLE USER INPUT (PROJECT MANAGEMENT) ==================================================================== -- "tampilkan semua project aktif." -- "project mana yang memiliki task paling banyak?" -- "analisa project Batumadu." +- "show all active projects." +- "which project has the most tasks?" +- "analyze the Batumadu project." ==================================================================== -CONTOH INPUT USER (TASK) +EXAMPLE USER INPUT (TASK) ==================================================================== -- "tampilkan seluruh task di project Batumadu." -- "apa saja task yang statusnya inprogress?" -- "task todo untuk project Batumadu apa saja?" +- "show all tasks in the Batumadu project." +- "which tasks are in progress?" +- "what are the todo tasks for Batumadu?" ==================================================================== -CONTOH INPUT USER (ASSIGNEE) +EXAMPLE USER INPUT (ASSIGNEE) ==================================================================== -- "siapa member yang paling banyak task todo?" -- "list semua task yang dimiliki Bob." -- "siapa yang paling overload di tim?" +- "who has the most todo tasks?" +- "list all tasks assigned to Bob." +- "who is the most overloaded in the team?" ==================================================================== -CONTOH INPUT USER (PRIORITY) +EXAMPLE USER INPUT (PRIORITY) ==================================================================== -- "task mana saja yang urgent?" -- "task low priority di project Batumadu apa saja?" -- "ada task medium di project ini?" +- "which tasks are urgent?" +- "what are the low-priority tasks in the Batumadu project?" +- "are there any medium-priority tasks in this project?" ==================================================================== -CONTOH INPUT USER (CROSS ANALYSIS) +EXAMPLE USER INPUT (CROSS ANALYSIS) ==================================================================== -- "siapa member yang paling banyak task inprogress di project Batumadu?" -- "task mana yang belum punya assignee di project Batumadu?" -- "bandingkan jumlah task todo dan done untuk semua project." +- "who has the most in-progress tasks in the Batumadu project?" +- "which tasks have no assignee in the Batumadu project?" +- "compare todo vs done tasks across all projects." ==================================================================== -PRINSIP UMUM: +GENERAL PRINCIPLES: ==================================================================== -- Jawaban akhir selalu berupa teks biasa (bukan JSON). -- Tool call harus format ketat { "tool": "...", "arguments": {...} }. -- Tidak ada narasi proses. -- Tidak menebak data yang tidak ada di tool result. -- Pilih tool berdasarkan kategori pertanyaan user. -- Gunakan beberapa tool jika dibutuhkan untuk reasoning. +- Final answers must always be text with markdown format (not JSON). +- Tool calls must strictly follow: { "tool": "...", "arguments": {...} }. +- No process narration. +- Do not invent data not found in tool results. +- Choose the tool based on user intent. +- Use multiple tools if required for reasoning. END OF SYSTEM MESSAGE. - `; diff --git a/backend/src/project-management/project-management.service.ts b/backend/src/project-management/project-management.service.ts index b54bd01..5d49fa3 100644 --- a/backend/src/project-management/project-management.service.ts +++ b/backend/src/project-management/project-management.service.ts @@ -384,10 +384,7 @@ export class ProjectManagementService { where: { clientId: payload.clientId }, }); if (existing) { - console.log( - "[createTask] idempotent hit, returning existing id=", - existing.id - ); + console.log("[createTask] idempotent hit, returning existing"); return existing; } } diff --git a/frontend/src/components/AiAgent.tsx b/frontend/src/components/AiAgent.tsx index cf5c9f5..0e6f280 100644 --- a/frontend/src/components/AiAgent.tsx +++ b/frontend/src/components/AiAgent.tsx @@ -3,14 +3,20 @@ import "./AiAgent.css"; function AiAgent({ setIsShow }: any) { const messages = [ - "πŸ“ˆ Halo, pantau kontribusi yuk!", - "πŸ’‘ Mau analisa cepat?", - "πŸ” Cari kontributor terbaik?", - "πŸ€– Siap bantu analisa!", - "πŸ” Siapa aja yang berkontribusi?", - "πŸ” Ada repo apa aja ya?", - "πŸ’‘ Butuh insight repo?", - "🧠 Analisis aktif!", + "πŸ“ˆ Hey! Ready to track contributions?", + "πŸ’‘ Need a quick analysis?", + "πŸ” Looking for the top contributors?", + "πŸ€– I'm here to help with insights!", + "πŸ” Want to see who contributed?", + "πŸ“‚ What repositories do we have?", + "πŸ’‘ Need repo insights?", + "🧠 Analysis activated!", + "πŸ—‚οΈ Need help with task management?", + "πŸ“Œ Want to review your task progress?", + "πŸ“‹ Ready to manage your projects?", + "πŸš€ Need an overview of your project status?", + "πŸ“Š Want to check task assignments?", + "πŸ“… Looking to organize your project timeline?", ]; const [currentIndex, setCurrentIndex] = useState(0); diff --git a/frontend/src/components/ChatWindow.tsx b/frontend/src/components/ChatWindow.tsx index c1393cc..8b6f145 100644 --- a/frontend/src/components/ChatWindow.tsx +++ b/frontend/src/components/ChatWindow.tsx @@ -4,7 +4,7 @@ import remarkGfm from "remark-gfm"; import { v4 as uuidv4 } from "uuid"; import { io, Socket } from "socket.io-client"; import packageJson from "../../package.json"; -import { useStore } from "../utils/store"; +import { useStore, useStoreWorkspace } from "../utils/store"; import type { Message } from "../utils/store"; import { getRandomPlaceholder } from "../utils/placeholder"; import { Trash2, X, Copy, Check, Share2, VolumeX, Volume2 } from "lucide-react"; @@ -16,6 +16,7 @@ import TtsButton from "./TTSButton"; import { toast } from "react-toastify"; import { apiFetch } from "../utils/apiFetch"; import { playSound } from "../utils/playSound"; +import { getState } from "../api/projectApi"; interface ChatWindowProps { onClose: () => void; @@ -35,6 +36,7 @@ export default function ChatWindow({ const [thinkingMessage, setThinkingMessage] = useState(null); const socketRef = useRef(null); const { messages, setMessages } = useStore(); + const { workspaceId, projectId } = useStoreWorkspace(); const [welcomeMessage, setWelcomeMessage] = useState(""); const [placeholder, setPlaceholder] = useState(getRandomPlaceholder()); const [copied, setCopied] = useState(false); @@ -42,28 +44,38 @@ export default function ChatWindow({ const [isLoading, setIsLoading] = useState(false); const [isMessagesReady, setIsMessagesReady] = useState(false); - const fullText = `Halo! πŸ‘‹ Selamat datang di **CommitFlow** 🧠. - -Saya bisa bantu analisis repositori dan kontribusi member dalam bahasa yang mudah dimengerti. - -Coba ketik: -- \`siapa saja yang berkontribusi di repo commitflow?\` -- \`siapa yang paling banyak berkontribusi di repo commitflow?\` -- \`list seluruh repositori.\` -- \`tampilkan semua project aktif.\` -- \`project mana yang memiliki task paling banyak?\` -- \`analisa project commitflow.\` -- \`tampilkan seluruh task di project commitflow.\` -- \`apa saja task yang statusnya inprogress?\` -- \`task todo untuk project commitflow apa saja?\` -- \`siapa member yang paling banyak task todo?\` -- \`list semua task yang dimiliki Bob.\` -- \`siapa yang paling overload di tim?\` -- \`siapa member yang paling banyak task inprogress di project commitflow?\` -- \`task mana yang belum punya assignee di project commitflow?\` -- \`bandingkan jumlah task todo dan done untuk semua project.\` - -Siap bantu insight lebih cerdas. πŸ’‘`; + const fullText = `Hello! πŸ‘‹ Welcome to **CommitFlow** 🧠. + +I can help analyze repositories, contributions, tasks, and projects with easy-to-understand insights. + +===================== +πŸ“¦ **GitHub Analysis** +===================== +Try typing: +- \`who has contributed to the commitflow repo?\` +- \`who contributed the most to the commitflow repo?\` +- \`list all repositories.\` +- \`analyze the commitflow project.\` +- \`what repositories are available?\` +- \`show contribution details for commitflow.\` + +============================== +πŸ“‹ **Task & Project Management** +============================== +Or ask about project workflows: +- \`show all active projects.\` +- \`which project has the most tasks?\` +- \`show all tasks in the commitflow project.\` +- \`which tasks are in progress?\` +- \`what are the todo tasks for the commitflow project?\` +- \`which member has the most todo tasks?\` +- \`list all tasks owned by Bob.\` +- \`who is the most overloaded in the team?\` +- \`which member has the most in-progress tasks in the commitflow project?\` +- \`which tasks are unassigned in the commitflow project?\` +- \`compare the number of todo and done tasks across all projects.\` + +Ready to deliver smarter insights. πŸ’‘`; useEffect(() => { fetchMessages(); @@ -291,7 +303,11 @@ Siap bantu insight lebih cerdas. πŸ’‘`; const response = await apiFetch(`${import.meta.env.VITE_API_URL}/ask`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ messages: formattedMessages }), + body: JSON.stringify({ + messages: formattedMessages, + workspaceId, + projectId, + }), }); if (!response.ok || !response.body) { diff --git a/frontend/src/components/EditMemberModal.tsx b/frontend/src/components/EditMemberModal.tsx index fec5c14..47feeb0 100644 --- a/frontend/src/components/EditMemberModal.tsx +++ b/frontend/src/components/EditMemberModal.tsx @@ -1,6 +1,6 @@ // frontend/src/components/EditMemberModal.tsx import React, { useEffect, useRef, useState } from "react"; -import { X, Camera, Save } from "lucide-react"; +import { X, Camera, Save, Loader2 } from "lucide-react"; import uploadMultipleFiles from "../utils/uploadFile"; import type { TeamMember } from "../types"; import { toast } from "react-toastify"; @@ -35,7 +35,7 @@ export default function EditMemberModal({ ); const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); - const [saving, setSaving] = useState(false); + const [isLoading, setIsLoading] = useState(false); const inputRef = useRef(null); useEffect(() => { @@ -50,7 +50,7 @@ export default function EditMemberModal({ setPhotoPreview(member?.photo ?? null); setPassword(""); setPasswordConfirm(""); - setSaving(false); + setIsLoading(false); } }, [open, member]); @@ -58,7 +58,7 @@ export default function EditMemberModal({ // handler when user clicks the photo area const onClickPhoto = () => { - if (inputRef.current && !saving) { + if (inputRef.current && !isLoading) { inputRef.current.click(); } }; @@ -96,7 +96,7 @@ export default function EditMemberModal({ } } - setSaving(true); + setIsLoading(true); try { let photoUrl: string | undefined = member.photo ?? undefined; if (photoFile) { @@ -137,7 +137,7 @@ export default function EditMemberModal({ console.error("EditProfile save failed", err); toast.dark(err?.message || "Failed to update profile"); } finally { - setSaving(false); + setIsLoading(false); } } @@ -156,7 +156,7 @@ export default function EditMemberModal({
!saving && onClose()} + onClick={() => !isLoading && onClose()} />
@@ -181,7 +181,7 @@ export default function EditMemberModal({
@@ -346,7 +346,7 @@ export default function EditMemberModal({ value={role ?? "FE"} onChange={(e) => setRole(e.target.value)} className="w-full px-3 py-2 rounded-lg border bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-sky-300 dark:focus:ring-sky-600" - disabled={saving} + disabled={isLoading} > @@ -370,7 +370,7 @@ export default function EditMemberModal({ value={isAdmin ? "admin" : "member"} onChange={(e) => setIsAdmin(e.target.value === "admin")} className="w-full px-3 py-2 rounded-lg border bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-sky-300 dark:focus:ring-sky-600" - disabled={saving} + disabled={isLoading} > @@ -383,47 +383,26 @@ export default function EditMemberModal({
@@ -341,7 +341,7 @@ export default function EditProfileModal({ value={role ?? "FE"} onChange={(e) => setRole(e.target.value)} className="w-full px-3 py-2 rounded-lg border bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-sky-300 dark:focus:ring-sky-600" - disabled={saving} + disabled={isLoading} > @@ -357,47 +357,26 @@ export default function EditProfileModal({