feat: journal dashboard widgets, agent polish, and edit-mode live previews
Journal dashboard widgets and quick-add; rich-text quick-add dialogs. Dashboard draft sync for live edit previews; assistant bubble + API tools. Journal UX: stress slider, mood grid, query cap fix.
This commit is contained in:
@@ -1,13 +1,30 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("assistant chat smoke", async ({ page }) => {
|
||||
await page.goto("/assistant");
|
||||
await expect(page.getByRole("heading", { name: "Assistant" })).toBeVisible();
|
||||
test("assistant bubble is hidden until opted in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Open assistant" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
await page.getByLabel("Message").fill("hello assistant");
|
||||
test("assistant chat smoke after opt-in", async ({ page }) => {
|
||||
await page.goto("/settings?s=appearance");
|
||||
const assistantSwitch = page.getByRole("switch", { name: "AI assistant" });
|
||||
if (!(await assistantSwitch.isChecked())) {
|
||||
await assistantSwitch.click();
|
||||
}
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "Open assistant" }).click();
|
||||
await expect(page.getByRole("dialog", { name: "Assistant" })).toBeVisible();
|
||||
|
||||
await page.getByLabel("Assistant message").fill("hello assistant");
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
|
||||
await expect(page.getByText("You")).toBeVisible();
|
||||
await expect(page.getByText("hello assistant")).toBeVisible();
|
||||
await expect(page.getByText("Assistant", { exact: true }).nth(1)).toBeVisible();
|
||||
const dialog = page.getByRole("dialog", { name: "Assistant" });
|
||||
await expect(dialog.locator(".animate-spin").first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(dialog.locator(".animate-spin")).toHaveCount(0, { timeout: 30_000 });
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole("button", { name: "Open assistant" }).click();
|
||||
await expect(page.getByText("hello assistant")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { describeToolActivity } from "../../src/modules/agent/tool-labels";
|
||||
import { encodeSseEvent, thinkingLabel } from "../../src/modules/agent/server/progress";
|
||||
import { createMockLlmClient } from "../../src/lib/llm/mock";
|
||||
import { runAgentChat } from "../../src/modules/agent/server/run";
|
||||
|
||||
describe("describeToolActivity", () => {
|
||||
it("uses item text when adding to a list", () => {
|
||||
const label = describeToolActivity("add_list_item", JSON.stringify({ text: "milk" }));
|
||||
assert.match(label, /milk/);
|
||||
});
|
||||
|
||||
it("mentions the API path for call_api", () => {
|
||||
const label = describeToolActivity(
|
||||
"call_api",
|
||||
JSON.stringify({ method: "GET", path: "/api/v1/notes" }),
|
||||
);
|
||||
assert.match(label, /\/api\/v1\/notes/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent progress", () => {
|
||||
it("encodes SSE payloads", () => {
|
||||
const encoded = encodeSseEvent({ type: "thinking", label: "Planning…", round: 0 });
|
||||
assert.equal(encoded, 'data: {"type":"thinking","label":"Planning…","round":0}\n\n');
|
||||
});
|
||||
|
||||
it("uses different labels per round", () => {
|
||||
assert.match(thinkingLabel(0), /request/i);
|
||||
assert.match(thinkingLabel(1), /found/i);
|
||||
});
|
||||
|
||||
it("emits progress events during tool execution", async () => {
|
||||
const events: string[] = [];
|
||||
await runAgentChat({
|
||||
messages: [{ role: "user", content: "add milk to the shopping list" }],
|
||||
request: new Request("http://localhost:3000/api/agent/chat"),
|
||||
llm: createMockLlmClient(),
|
||||
executeTool: async () =>
|
||||
JSON.stringify({ status: 201, body: { id: "item-1", text: "milk" } }),
|
||||
onProgress: (event) => {
|
||||
if (event.type === "tool") events.push(event.label);
|
||||
if (event.type === "thinking") events.push(event.label);
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(events.some((label) => label.includes("milk")));
|
||||
assert.ok(events.some((label) => /request|found/i.test(label)));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
clearAssistantChat,
|
||||
loadAssistantChat,
|
||||
saveAssistantChat,
|
||||
} from "../../src/modules/agent/assistant-chat-storage";
|
||||
|
||||
const storage = new Map<string, string>();
|
||||
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
value: {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
storage.set(key, value);
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
storage.delete(key);
|
||||
},
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
describe("assistant chat storage", () => {
|
||||
it("round-trips messages per user", () => {
|
||||
storage.clear();
|
||||
const userId = "user-a";
|
||||
const messages = [
|
||||
{ role: "user" as const, content: "hello" },
|
||||
{ role: "assistant" as const, content: "hi there" },
|
||||
];
|
||||
|
||||
saveAssistantChat(userId, messages);
|
||||
assert.deepEqual(loadAssistantChat(userId), messages);
|
||||
});
|
||||
|
||||
it("keeps sessions isolated by user id", () => {
|
||||
storage.clear();
|
||||
saveAssistantChat("user-a", [{ role: "user", content: "for a" }]);
|
||||
saveAssistantChat("user-b", [{ role: "user", content: "for b" }]);
|
||||
|
||||
assert.deepEqual(loadAssistantChat("user-a"), [{ role: "user", content: "for a" }]);
|
||||
assert.deepEqual(loadAssistantChat("user-b"), [{ role: "user", content: "for b" }]);
|
||||
});
|
||||
|
||||
it("clears stored chat", () => {
|
||||
storage.clear();
|
||||
saveAssistantChat("user-a", [{ role: "user", content: "hello" }]);
|
||||
clearAssistantChat("user-a");
|
||||
assert.deepEqual(loadAssistantChat("user-a"), []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user