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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

### `@liveblocks/core`

- Throttle incoming AI delta updates to prevent excessive re-renders during fast
streaming.
- Optimized partial JSON parser for improved tool invocation streaming
performance
performance.

## v3.5.1

Expand Down
7 changes: 4 additions & 3 deletions e2e/next-ai-kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lint": "next lint",
"format": "(eslint --fix app/ test/ || true) && prettier --write app/ test/",
"test": "playwright test --max-failures=1",
"test:ui": "playwright test --ui --workers=1 --max-failures=1"
"test:headed": "playwright --max-failures=1 test --headed",
"test:ui": "playwright test --max-failures=1 --workers=1 --ui"
},
"dependencies": {
"@liveblocks/client": "*",
Expand All @@ -23,11 +24,11 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@playwright/test": "^1.49.1",
"@playwright/test": "^1.55.0",
"@tailwindcss/postcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"playwright": "^1.54.2",
"playwright": "^1.55.0",
"tailwindcss": "^4",
"typescript": "^5"
}
Expand Down
1 change: 0 additions & 1 deletion e2e/next-ai-kitchen-sink/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export default defineConfig({
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
headless: process.env.CI || process.env.HEADLESS ? true : false,
viewport: { width: 640, height: 800 },
permissions: ["clipboard-write", "clipboard-read"],
/* Maximum time each action such as `click()` can take. 10s local, 15s CI. */
Expand Down
65 changes: 38 additions & 27 deletions e2e/next-ai-kitchen-sink/test/knowledge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,37 +70,46 @@ test.describe("Knowledge Registration", () => {
test("should use registered knowledge about current view and todos", async ({
page,
}) => {
// Create unique test chat and go to knowledge page
const chatId = createRandomChat(page);
await page.goto(`/knowledge/${chatId}`);

// Wait for the page to load - verify default tab is "Todo app"
await expect(
page.locator('button.font-bold:has-text("Todo app")')
).toBeVisible();
await test.step("Setup and verify todo app view", async () => {
await page.goto(`/knowledge/${chatId}`, { waitUntil: "networkidle" });

// Verify the default todos are visible
await expect(page.locator('li:has-text("Get groceries")')).toBeVisible();
await expect(page.locator('li:has-text("Go to the gym")')).toBeVisible();
await expect(page.locator('li:has-text("Cook dinner")')).toBeVisible();
// Wait for the page to load - verify default tab is "Todo app"
await expect(
page.locator('button.font-bold:has-text("Todo app")')
).toBeVisible();

// Ask AI about the current view - it should know we're on the Todo list
await sendAiMessage(page, "What is the current view in the app?");
// Verify the default todos are visible
await expect(page.locator('li:has-text("Get groceries")')).toBeVisible();
await expect(page.locator('li:has-text("Go to the gym")')).toBeVisible();
await expect(page.locator('li:has-text("Cook dinner")')).toBeVisible();
});

// Wait for AI response that should mention the todo view
await expect(
page.locator("text=Todo").or(page.locator("text=todo"))
).toBeVisible({ timeout: 30000 });
await test.step("Test AI knowledge of current view", async () => {
// Ask AI about the current view - it should know we're on the Todo list
await sendAiMessage(page, "What is the current view in the app?");

// Ask AI about the todos - it should know the specific items
await sendAiMessage(page, "What todos do I have?");
// Wait for AI response that should mention the todo view
await expect(
page.locator("text=Todo").or(page.locator("text=todo"))
).toBeVisible({ timeout: 30000 });
});

// Wait for AI response that should include the todo items
await page.waitForTimeout(10000);
// The AI should mention the specific todos in its response - use first() to handle duplicates
await expect(
page.locator("text=groceries").or(page.locator("text=Groceries")).first()
).toBeVisible({ timeout: 20000 });
await test.step("Test AI knowledge of specific todos", async () => {
// Ask AI about the todos - it should know the specific items
await sendAiMessage(page, "What todos do I have?");

// Wait for AI response that should include the todo items
await page.waitForTimeout(10000);
// The AI should mention the specific todos in its response - use first() to handle duplicates
await expect(
page
.locator("text=groceries")
.or(page.locator("text=Groceries"))
.first()
).toBeVisible({ timeout: 20000 });
});
});

test("should use nickname knowledge when enabled", async ({ page }) => {
Expand Down Expand Up @@ -191,7 +200,7 @@ test.describe("Knowledge Registration", () => {
await page.goto(`/knowledge/${chatId}`);

// Start on Todo app tab
await expect(page.getByTestId("tab-todo-app")).toHaveClass(/font-bold/);
await expect(page.getByTestId("tab-todo-app")).toContainClass("font-bold");

// Ask about current view
await sendAiMessage(page, "What view am I currently in?");
Expand All @@ -201,7 +210,9 @@ test.describe("Knowledge Registration", () => {

// Switch to "Another app" tab
await page.getByTestId("tab-another-app").click();
await expect(page.getByTestId("tab-another-app")).toHaveClass(/font-bold/);
await expect(page.getByTestId("tab-another-app")).toContainClass(
"font-bold"
);
await expect(page.locator("text=Another part of the app")).toBeVisible();

// Ask about current view again - should now know it's "Another app"
Expand All @@ -214,7 +225,7 @@ test.describe("Knowledge Registration", () => {

// Switch to "Both" tab
await page.getByTestId("tab-both").click();
await expect(page.getByTestId("tab-both")).toHaveClass(/font-bold/);
await expect(page.getByTestId("tab-both")).toContainClass("font-bold");

// Ask about current view - should know it's "Both apps"
await sendAiMessage(page, "What view am I in now?");
Expand Down
112 changes: 64 additions & 48 deletions e2e/next-ai-kitchen-sink/test/simple-chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,37 @@ test.describe("Simple Chat", () => {
const chatId = createRandomChat(page);
const { textInput, sendButton } = await setupSimpleChat(page, chatId);

// Perform the ping-pong interaction
await textInput.fill("ping");
await sendButton.click();

// Ensure the send button turns into an abort button (should now show StopIcon)
// The button should change to show "Abort response" aria-label
await expect(sendButton).toHaveAttribute("aria-label", "Abort response");
await test.step("Send ping message", async () => {
// Perform the ping-pong interaction
await textInput.fill("ping");
await sendButton.click();
});

// Wait for the send button to become enabled again (back to send state)
await expect(sendButton).toHaveAttribute("aria-label", "Send", {
timeout: 15000, // Give it up to 15 seconds for the AI response
await test.step("Verify AI is processing request", async () => {
// Ensure the send button turns into an abort button (should now show StopIcon)
// The button should change to show "Abort response" aria-label
await expect(sendButton).toHaveAttribute("aria-label", "Abort response");
});

// Check that a response message containing "pong" is received
// Look for assistant messages using the correct class
const assistantMessage = page
.locator(".lb-ai-chat-assistant-message")
.last();
await expect(assistantMessage).toBeVisible({ timeout: 15000 });
await test.step("Wait for AI response completion", async () => {
// Wait for the send button to become enabled again (back to send state)
await expect(sendButton).toHaveAttribute("aria-label", "Send", {
timeout: 15000, // Give it up to 15 seconds for the AI response
});
});

// Check if it contains "pong"
await expect(assistantMessage).toContainText("pong", {
timeout: 15000,
await test.step("Verify pong response received", async () => {
// Check that a response message containing "pong" is received
// Look for assistant messages using the correct class
const assistantMessage = page
.locator(".lb-ai-chat-assistant-message")
.last();
await expect(assistantMessage).toBeVisible({ timeout: 15000 });

// Check if it contains "pong"
await expect(assistantMessage).toContainText("pong", {
timeout: 15000,
});
});
});

Expand All @@ -71,44 +79,52 @@ test.describe("Simple Chat", () => {
const chatId = createRandomChat(page);
const { textInput, sendButton } = await setupSimpleChat(page, chatId);

// Ask a question that should generate a long response
await textInput.fill(
"Write a detailed 500-word essay about the history of artificial intelligence, covering major milestones from the 1950s to today."
);
await test.step("Send long-form request", async () => {
// Ask a question that should generate a long response
await textInput.fill(
"Write a detailed 500-word essay about the history of artificial intelligence, covering major milestones from the 1950s to today."
);

// The button should be enabled once we have text
await expect(sendButton).toBeEnabled({ timeout: 15000 });
await sendButton.click();
// The button should be enabled once we have text
await expect(sendButton).toBeEnabled({ timeout: 15000 });
await sendButton.click();
});

// Verify the button changes to abort state
await expect(sendButton).toHaveAttribute("aria-label", "Abort response");
await test.step("Verify AI starts processing and abort", async () => {
// Verify the button changes to abort state
await expect(sendButton).toHaveAttribute("aria-label", "Abort response");

// Click the abort button while the AI is generating
await sendButton.click();
// Click the abort button while the AI is generating
await sendButton.click();

// Verify the button goes back to send state
await expect(sendButton).toHaveAttribute("aria-label", "Send", {
timeout: 15000,
// Verify the button goes back to send state
await expect(sendButton).toHaveAttribute("aria-label", "Send", {
timeout: 15000,
});
});

// Verify the user message exists
const userMessage = page.locator(".lb-ai-chat-user-message").last();
await expect(userMessage).toBeVisible();
await expect(userMessage).toContainText("Write a detailed 500-word essay");
await test.step("Verify messages and abort behavior", async () => {
// Verify the user message exists
const userMessage = page.locator(".lb-ai-chat-user-message").last();
await expect(userMessage).toBeVisible();
await expect(userMessage).toContainText(
"Write a detailed 500-word essay"
);

// The assistant message should exist (created optimistically) but may be hidden when aborted
const assistantMessage = page
.locator(".lb-ai-chat-assistant-message")
.last();
// The assistant message should exist (created optimistically) but may be hidden when aborted
const assistantMessage = page
.locator(".lb-ai-chat-assistant-message")
.last();

// The assistant message must exist in the DOM (created optimistically)
await expect(assistantMessage).toHaveCount(1);
// The assistant message must exist in the DOM (created optimistically)
await expect(assistantMessage).toHaveCount(1);

// Get the message text - even if hidden, we can still read the content
const messageText = await assistantMessage.textContent();
// Get the message text - even if hidden, we can still read the content
const messageText = await assistantMessage.textContent();

// The message should be incomplete due to abort - much shorter than a full 500-word essay
// A full essay would be significantly longer than 500 characters
expect(messageText?.length || 0).toBeLessThan(500);
// The message should be incomplete due to abort - much shorter than a full 500-word essay
// A full essay would be significantly longer than 500 characters
expect(messageText?.length || 0).toBeLessThan(500);
});
});
});
Loading
Loading