Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{"id":"zuko-w0p","title":"Chat context for contacts, deals, and companies (agentic CRM from chat)","description":"Epic: Enable users to add contacts, deals, and companies as context to a chat so all conversations happen in that context. Support autocomplete when adding entities. Send context preferences to backend and use them for responses. Future: allow updating contact/deal/company (e.g. email, phone, status) from natural language in chat without leaving to Contacts/Deals UI. Source: Call transcript with Yuva Kumar.","status":"open","priority":2,"issue_type":"epic","created_at":"2026-03-03T11:05:05.271667+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:05:05.271667+05:30"}
{"id":"zuko-w0p.1","title":"Chat attachment dropdown: add Contact, Deal, Company options","description":"UI change: In the new-chat plus menu (attachment dropdown), make 'Add photos and files' the first option. Add a second row with three options in one line: Add contact, Add deal, Add company. These allow the user to attach CRM entities as context to the chat. Small PR: dropdown component + icons/labels.","acceptance_criteria":"Dropdown shows photos/files first; second row shows Add contact | Add deal | Add company. Clicking each opens the corresponding picker (picker implementation is a separate task).","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:19.94792+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:20.051803+05:30","dependencies":[{"issue_id":"zuko-w0p.1","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:19.954587+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.2","title":"Contact/Deal/Company picker with autocomplete","description":"When user clicks Add contact (or Add deal / Add company), show a search input. Typing (e.g. 'bill') shows autocomplete suggestions (e.g. Bill Gates). User selects one or more; selected entities are added to chat context. Support multiple entities and combination of types (one contact + one deal + one company). Reuse or integrate with existing contact/deal/company APIs for search. Small PR: picker component + search API usage.","acceptance_criteria":"Type partial name → list filters with autocomplete. Select item → added to context. Can add multiple contacts/deals/companies. Context displayed (e.g. chips) in chat input area.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:24.879993+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:05:24.879993+05:30","dependencies":[{"issue_id":"zuko-w0p.2","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:24.887111+05:30","created_by":"babanitturu"},{"issue_id":"zuko-w0p.2","depends_on_id":"zuko-w0p.1","type":"blocks","created_at":"2026-03-03T11:06:07.04574+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.3","title":"Context preferences: backend accept/store and frontend send multiple contextEntities; agent responds in context","description":"Single task covering both backend and frontend for chat context.\n\nBackend: Chat API accepts context preferences (e.g. contactIds[], dealIds[], companyIds[] or a unified contextEntities[]). Persist or pass context with the message/conversation so the agent can answer in the context of the selected contact/deal/company. Define DTOs and update chat controller and message storage.\n\nFrontend: Wire the contact/deal/company picker selections to the chat API. When the user sends a message, include the current context (selected entities) in the request. Multiple contextEntities can be sent from the UI (e.g. one contact + one deal + one company). Ensure context is sent with each message in the conversation.\n\nThe agent should respond in the context of the provided entities (e.g. in the context of this contact, deal, and company).","acceptance_criteria":"Backend: POST chat/messages accepts optional context payload (multiple contextEntities). Context is stored or associated with the conversation/message. Frontend: Sending a message includes selected contact/deal/company context. Agent responses are in the context of the provided entities.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:29.628034+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:28.404006+05:30","dependencies":[{"issue_id":"zuko-w0p.3","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:29.634675+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.3","title":"Context preferences: frontend send multiple contextEntities(contact, company, deals); agent should be able to respond with multile context combinations.\n","description":"Single task covering both backend and frontend for chat context.\n\nBackend: Chat API accepts context preferences (e.g. contactIds[], dealIds[], companyIds[] or a unified contextEntities[]). Persist or pass context with the message/conversation so the agent can answer in the context of the selected contact/deal/company. Define DTOs and update chat controller and message storage.\n\nFrontend: Wire the contact/deal/company picker selections to the chat API. When the user sends a message, include the current context (selected entities) in the request. Multiple contextEntities can be sent from the UI (e.g. one contact + one deal + one company). Ensure context is sent with each message in the conversation.\n\nThe agent should respond in the context of the provided entities (e.g. in the context of this contact, deal, and company).","acceptance_criteria":"Backend: POST chat/messages accepts optional context payload (multiple contextEntities). Context is stored or associated with the conversation/message. Frontend: Sending a message includes selected contact/deal/company context. Agent responses are in the context of the provided entities.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:29.628034+05:30","created_by":"babanitturu","updated_at":"2026-03-03T14:55:54.35405+05:30","dependencies":[{"issue_id":"zuko-w0p.3","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:29.634675+05:30","created_by":"babanitturu"}]}
{"id":"zuko-w0p.4","title":"Frontend: send context preferences with every chat message","description":"Wire the contact/deal/company picker selections to the chat API. When user sends a message, include the current context (selected contact IDs, deal IDs, company IDs) in the request. Ensure context is sent with each message in the conversation. Small PR: state in chat component + API client payload.","acceptance_criteria":"Sending a message includes context preferences when user has added contacts/deals/companies. Backend receives and can log/use them.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:33.442733+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:29.74942+05:30","dependencies":[{"issue_id":"zuko-w0p.4","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:33.44944+05:30","created_by":"babanitturu"}],"deleted_at":"2026-03-03T11:16:29.74942+05:30","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
{"id":"zuko-w0p.5","title":"Backend: use chat context in prompt/retrieval (contact, deal, company data)","description":"When a message has context (contactIds, dealIds, companyIds), fetch the relevant entity data (contact details, deal details, company details) and include them in the system prompt or RAG context so the model answers in the context of that person/deal/company. Small PR: service layer that loads entities and injects into prompt/context.","acceptance_criteria":"With context set, model responses are grounded in the selected contact/deal/company data. Without context, behavior unchanged.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:37.003106+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:16:15.535151+05:30","dependencies":[{"issue_id":"zuko-w0p.5","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:37.012167+05:30","created_by":"babanitturu"},{"issue_id":"zuko-w0p.5","depends_on_id":"zuko-w0p.3","type":"blocks","created_at":"2026-03-03T11:06:07.217888+05:30","created_by":"babanitturu"}],"deleted_at":"2026-03-03T11:16:15.535151+05:30","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
{"id":"zuko-w0p.6","title":"Agentic updates from chat (update contact/deal/company via natural language)","description":"Allow user to say in chat e.g. 'This person'\\''s email changed to X, phone to Y', 'Update deal status to closed'. Agent interprets intent and updates the contact/deal/company (or creates structured actions) so user does not need to go to Contacts/Deals UI to edit.\n","acceptance_criteria":"User can request contact/deal/company updates in natural language in a context-aware chat; backend applies updates (with confirmation or audit).","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T11:05:41.718235+05:30","created_by":"babanitturu","updated_at":"2026-03-03T11:09:36.961438+05:30","dependencies":[{"issue_id":"zuko-w0p.6","depends_on_id":"zuko-w0p","type":"parent-child","created_at":"2026-03-03T11:05:41.726704+05:30","created_by":"babanitturu"},{"issue_id":"zuko-w0p.6","depends_on_id":"zuko-w0p.3","type":"blocks","created_at":"2026-03-03T11:16:30.415474+05:30","created_by":"babanitturu"}]}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ test-output
/.codex/config.toml
/.codex/config.toml.bak
/.codex/skills
/.cursor/commands/
/.opencode/skill
/.skillz
/.vibe/skills
Expand Down
18 changes: 0 additions & 18 deletions apps/web/jest.config.cts

This file was deleted.

9 changes: 8 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,15 @@
}
},
"test": {
"executor": "@nx/vitest:test",
"outputs": ["{workspaceRoot}/apps/web/test-output/vitest/coverage"],
"options": {
"command": "jest --forceExit"
"configFile": "apps/web/vitest.config.ts"
},
"configurations": {
"watch": {
"watch": true
}
}
}
}
Expand Down
42 changes: 21 additions & 21 deletions apps/web/src/app/api/chat/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
/**
* @jest-environment node
* @vitest-environment node
*/
import { beforeEach, afterEach, it, expect, vi } from "vitest";
import { POST } from "./route";
import { NextRequest } from "next/server";

// Mock Next.js cookies
jest.mock("next/headers", () => ({
cookies: jest.fn(() => ({
vi.mock("next/headers", () => ({
cookies: vi.fn(() => ({
toString: () => "mock-session-cookie=value",
})),
}));

// Mock global fetch before importing route
const mockFetch = jest.fn();
global.fetch = mockFetch as any;
const mockFetch = vi.fn();
Object.defineProperty(globalThis, 'fetch', { value: mockFetch, writable: true });

describe("/api/chat POST", () => {
beforeEach(() => {
jest.clearAllMocks();
// Suppress console.log in tests
jest.spyOn(console, "log").mockImplementation();
jest.spyOn(console, "error").mockImplementation();
vi.clearAllMocks();
vi.spyOn(console, "log").mockImplementation(vi.fn());
vi.spyOn(console, "error").mockImplementation(vi.fn());
});

afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});

it("extracts chatId from referer header and forwards to backend", async () => {
Expand All @@ -47,7 +47,7 @@ describe("/api/chat POST", () => {
},
});

(global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockBackendResponse);

// Create mock NextRequest with referer header
const request = new NextRequest("http://localhost:3000/api/chat", {
Expand Down Expand Up @@ -79,7 +79,7 @@ describe("/api/chat POST", () => {
);

expect(response.status).toBe(200);
await response.text(); // Consume stream to prevent Jest open handles
await response.text(); // Consume stream to prevent open handles
});

it("handles missing referer header gracefully", async () => {
Expand All @@ -93,7 +93,7 @@ describe("/api/chat POST", () => {
status: 200,
});

(global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockBackendResponse);

// Create request without referer header
const request = new NextRequest("http://localhost:3000/api/chat", {
Expand All @@ -108,7 +108,7 @@ describe("/api/chat POST", () => {
await response.text();

// Verify chatId is null when referer is missing
const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const fetchCall = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
const bodyArg = fetchCall[1].body;
const parsedBody = JSON.parse(bodyArg);

Expand Down Expand Up @@ -140,13 +140,13 @@ describe("/api/chat POST", () => {
];

for (const { referer, expectedChatId } of testCases) {
jest.clearAllMocks();
vi.clearAllMocks();

const mockBackendResponse = new Response("mock response", {
status: 200,
});

(global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockBackendResponse);

const request = new NextRequest("http://localhost:3000/api/chat", {
method: "POST",
Expand All @@ -160,7 +160,7 @@ describe("/api/chat POST", () => {
const response = await POST(request);
await response.text();

const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const fetchCall = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
const bodyArg = fetchCall[1].body;
const parsedBody = JSON.parse(bodyArg);

Expand All @@ -178,7 +178,7 @@ describe("/api/chat POST", () => {
status: 403,
});

(global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockBackendResponse);

const request = new NextRequest("http://localhost:3000/api/chat", {
method: "POST",
Expand All @@ -202,7 +202,7 @@ describe("/api/chat POST", () => {
messages: [],
};

(global.fetch as jest.Mock).mockRejectedValue(new Error("Network error"));
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Network error"));

const request = new NextRequest("http://localhost:3000/api/chat", {
method: "POST",
Expand All @@ -222,7 +222,7 @@ describe("/api/chat POST", () => {

it("includes correct headers when forwarding to backend", async () => {
const mockBackendResponse = new Response("ok", { status: 200 });
(global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockBackendResponse);

const request = new NextRequest("http://localhost:3000/api/chat", {
method: "POST",
Expand Down Expand Up @@ -258,7 +258,7 @@ describe("/api/chat POST", () => {
},
});

(global.fetch as jest.Mock).mockResolvedValue(mockBackendResponse);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockBackendResponse);

const request = new NextRequest("http://localhost:3000/api/chat", {
method: "POST",
Expand Down
5 changes: 2 additions & 3 deletions apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@zuko/ui-kit": ["../../libs/ui-kit/src/index.ts"],
"@zuko/ui-kit/*": ["../../libs/ui-kit/src/*"]
},
"types": ["jest", "node"],
"types": ["node"],
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
},
Expand All @@ -40,8 +40,7 @@
"out-tsc",
"dist",
"node_modules",
"jest.config.ts",
"jest.config.cts",
"vitest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
".next",
Expand Down
7 changes: 3 additions & 4 deletions apps/web/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/jest",
"outDir": "./out-tsc/vitest",
"jsx": "preserve",
"types": ["jest", "node"],
"types": ["vitest/globals", "node"],
"module": "esnext",
"moduleResolution": "bundler"
},
"include": [
"jest.config.ts",
"jest.config.cts",
"vitest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
Expand Down
23 changes: 23 additions & 0 deletions apps/web/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
test: {
name: '@zuko/web',
watch: false,
globals: true,
environment: 'node',
include: ['src/**/*.{test,spec}.{ts,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8',
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
8 changes: 7 additions & 1 deletion nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@
"options": {
"targetName": "test"
},
"exclude": ["apps/backend-e2e/**/*"]
"exclude": ["apps/backend-e2e/**/*", "apps/web/**/*"]
},
{
"plugin": "@nx/vitest",
"options": {
"testTargetName": "test"
}
},
{
"plugin": "@nx/docker",
Expand Down