feat(web): Unterhaltungen-IA, Bot-Picker und schlanker Chat-Header (#71)
- Sidebar: Unterhaltungen (1:1-Bots im Home-Space) und Projekte (Projekt-Spaces + Gruppen) getrennt; Projekt-Spaces erscheinen nicht mehr unter Team/Unterhaltungen - + in Unterhaltungen öffnet Bot-Picker (bestehenden Bot öffnen oder neuen Bot erstellen) - Chat-Header: Bot-Avatar wieder sichtbar, Status nur bei Aktivität, kein doppeltes „1:1 conversation“ - Composer: generisches „Nachricht…“ statt Name dreifach - OoStage: kein doppelter Bot-Name bei laufendem Verlauf Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
2f296f60a1
commit
0474875142
@ -48,7 +48,7 @@ test("logout protects bot deep links and sign-in restores the session", async ({
|
|||||||
|
|
||||||
await page.waitForURL(/\/app\/[^/]+$/);
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
const protectedBotPath = new URL(page.url()).pathname;
|
const protectedBotPath = new URL(page.url()).pathname;
|
||||||
await expect(page.getByPlaceholder("Message Chief")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: new RegExp(userName, "i") }).click();
|
await page.getByRole("button", { name: new RegExp(userName, "i") }).click();
|
||||||
await expect(page.getByRole("button", { name: "Settings", exact: true })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Settings", exact: true })).toBeVisible();
|
||||||
|
|||||||
@ -33,7 +33,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
await form.getByRole("button", { name: "Create", exact: true }).click();
|
await form.getByRole("button", { name: "Create", exact: true }).click();
|
||||||
await createAborted;
|
await createAborted;
|
||||||
// Failed create keeps the form open on the current bot chat.
|
// Failed create keeps the form open on the current bot chat.
|
||||||
await expect(page.getByPlaceholder("Message Chief")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
|
||||||
await expect(page.getByTestId("create-bot-error")).toBeVisible();
|
await expect(page.getByTestId("create-bot-error")).toBeVisible();
|
||||||
expect(createFailed).toBe(true);
|
expect(createFailed).toBe(true);
|
||||||
@ -51,7 +51,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
await route.abort("failed");
|
await route.abort("failed");
|
||||||
});
|
});
|
||||||
await createBotFromPicker(page);
|
await createBotFromPicker(page);
|
||||||
await expect(page.getByPlaceholder("Message New Bot")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect.poll(() => failedPostCreateRefresh).toBe(true);
|
await expect.poll(() => failedPostCreateRefresh).toBe(true);
|
||||||
await page.unroute("**/rpc/spaces/list");
|
await page.unroute("**/rpc/spaces/list");
|
||||||
await expect(botList.getByRole("button", { name: /^New Bot/ })).toBeVisible();
|
await expect(botList.getByRole("button", { name: /^New Bot/ })).toBeVisible();
|
||||||
@ -82,7 +82,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
await expect(studio).toBeHidden();
|
await expect(studio).toBeHidden();
|
||||||
await page.getByRole("button", { name: "Save", exact: true }).click();
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
||||||
await expect(botList.getByRole("button", { name: /^Researcher/ })).toBeVisible();
|
await expect(botList.getByRole("button", { name: /^Researcher/ })).toBeVisible();
|
||||||
await expect(page.getByPlaceholder("Message Researcher")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
|
|
||||||
await page.locator("main").getByRole("button", { name: "Researcher", exact: true }).click();
|
await page.locator("main").getByRole("button", { name: "Researcher", exact: true }).click();
|
||||||
await expect(nameInput).toHaveValue("Researcher");
|
await expect(nameInput).toHaveValue("Researcher");
|
||||||
@ -136,12 +136,12 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
await descriptionInput.fill("Builds durable, source-backed research briefs.");
|
await descriptionInput.fill("Builds durable, source-backed research briefs.");
|
||||||
await page.getByRole("button", { name: "Save", exact: true }).click();
|
await page.getByRole("button", { name: "Save", exact: true }).click();
|
||||||
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
|
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
|
||||||
await expect(page.getByPlaceholder("Message Atlas")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "28-edited-bot-profile");
|
await captureScreenshot(page, testInfo, "28-edited-bot-profile");
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
|
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
|
||||||
await expect(page.getByPlaceholder("Message Atlas")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await page.locator("main").getByRole("button", { name: "Atlas", exact: true }).click();
|
await page.locator("main").getByRole("button", { name: "Atlas", exact: true }).click();
|
||||||
await expect(nameInput).toHaveValue("Atlas");
|
await expect(nameInput).toHaveValue("Atlas");
|
||||||
await expect(titleInput).toHaveValue("Research lead");
|
await expect(titleInput).toHaveValue("Research lead");
|
||||||
@ -157,7 +157,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
name: new RegExp(`^${longUnbrokenName}`),
|
name: new RegExp(`^${longUnbrokenName}`),
|
||||||
});
|
});
|
||||||
await expect(longNameBot).toBeVisible();
|
await expect(longNameBot).toBeVisible();
|
||||||
await expect(page.getByPlaceholder(`Message ${longUnbrokenName}`)).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
|
|
||||||
await longNameBot.click({ button: "right" });
|
await longNameBot.click({ button: "right" });
|
||||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
await page.getByRole("menuitem", { name: "Delete" }).click();
|
||||||
@ -176,6 +176,6 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) =
|
|||||||
await page.waitForURL((url) => url.pathname !== deletedBotPath);
|
await page.waitForURL((url) => url.pathname !== deletedBotPath);
|
||||||
await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0);
|
await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0);
|
||||||
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
||||||
await expect(page.getByPlaceholder("Message Chief")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "31-deleted-bot-fallback");
|
await captureScreenshot(page, testInfo, "31-deleted-bot-fallback");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -368,7 +368,7 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
|
|||||||
await expect(page.getByTestId("composer-steering-status")).toHaveCount(0);
|
await expect(page.getByTestId("composer-steering-status")).toHaveCount(0);
|
||||||
await expect(page.getByText("Messages sent now guide the next turn.")).toHaveCount(0);
|
await expect(page.getByText("Messages sent now guide the next turn.")).toHaveCount(0);
|
||||||
await expect(page.getByText(/^Steer /)).toHaveCount(0);
|
await expect(page.getByText(/^Steer /)).toHaveCount(0);
|
||||||
await expect(composer).toHaveAttribute("placeholder", "Message Chief");
|
await expect(composer).toHaveAttribute("placeholder", "Message…");
|
||||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
||||||
await composer.fill("Use the newer report and keep the answer short.");
|
await composer.fill("Use the newer report and keep the answer short.");
|
||||||
await page.keyboard.press("Tab");
|
await page.keyboard.press("Tab");
|
||||||
@ -386,7 +386,7 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
|
|||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
|
||||||
await expect(composer).toHaveAttribute("placeholder", "Message Chief");
|
await expect(composer).toHaveAttribute("placeholder", "Message…");
|
||||||
await captureScreenshot(page, testInfo, "14-active-bot-work-mobile");
|
await captureScreenshot(page, testInfo, "14-active-bot-work-mobile");
|
||||||
await page.setViewportSize({ width: 1280, height: 720 });
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||||||
expect(browserErrors).toEqual([]);
|
expect(browserErrors).toEqual([]);
|
||||||
|
|||||||
@ -178,6 +178,6 @@ export async function createNamedBot(
|
|||||||
computerMode: options.computerMode ?? "team",
|
computerMode: options.computerMode ?? "team",
|
||||||
});
|
});
|
||||||
await page.goto(`/app/${bot.id}`);
|
await page.goto(`/app/${bot.id}`);
|
||||||
await expect(page.getByPlaceholder(`Message ${name}`)).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
return bot.id;
|
return bot.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ test("create opens form, then empty chat; picker lists bots; sidebar collapses",
|
|||||||
await form.locator("label:has-text('Name') input").fill("New Bot");
|
await form.locator("label:has-text('Name') input").fill("New Bot");
|
||||||
await form.getByRole("button", { name: "Create", exact: true }).click();
|
await form.getByRole("button", { name: "Create", exact: true }).click();
|
||||||
await page.waitForURL(/\/app\/[^/]+$/);
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
await expect(page.getByPlaceholder("Message New Bot")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
||||||
await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0);
|
await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0);
|
||||||
await captureScreenshot(page, testInfo, "create-chat-sidepanel-closed");
|
await captureScreenshot(page, testInfo, "create-chat-sidepanel-closed");
|
||||||
@ -118,7 +118,7 @@ test("later bot waits before showing the focus card; sending cancels it", async
|
|||||||
|
|
||||||
await page.clock.install();
|
await page.clock.install();
|
||||||
await createBotFromPicker(page);
|
await createBotFromPicker(page);
|
||||||
await expect(page.getByPlaceholder("Message New Bot")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0);
|
await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
await page.clock.fastForward(9_000);
|
await page.clock.fastForward(9_000);
|
||||||
@ -150,7 +150,7 @@ test("plus picker can create a Private computer bot", async ({ page }, testInfo)
|
|||||||
await page.waitForURL(/\/app\/[^/]+$/);
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
|
|
||||||
await createBotFromPicker(page, { computerMode: "dedicated" });
|
await createBotFromPicker(page, { computerMode: "dedicated" });
|
||||||
await expect(page.getByPlaceholder("Message New Bot")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "create-private-computer-bot");
|
await captureScreenshot(page, testInfo, "create-private-computer-bot");
|
||||||
|
|
||||||
const botId = page.url().split("/").pop()!;
|
const botId = page.url().split("/").pop()!;
|
||||||
@ -179,7 +179,7 @@ test("second bot from plus opens create form before persist", async ({ page }, t
|
|||||||
await form.getByRole("button", { name: "Create", exact: true }).click();
|
await form.getByRole("button", { name: "Create", exact: true }).click();
|
||||||
await create;
|
await create;
|
||||||
await page.waitForURL(/\/app\/[^/]+$/);
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
await expect(page.getByPlaceholder("Message Researcher")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
||||||
await captureScreenshot(page, testInfo, "second-bot-created");
|
await captureScreenshot(page, testInfo, "second-bot-created");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,7 +27,7 @@ test("focus choice suggests apps and preserves a completed connection", async ({
|
|||||||
// The focus step suggests apps but must not rename the bot: the name the
|
// The focus step suggests apps but must not rename the bot: the name the
|
||||||
// user chose during creation ("Chief") is preserved.
|
// user chose during creation ("Chief") is preserved.
|
||||||
await expect(page.locator("main").getByText("Chief", { exact: true })).toBeVisible();
|
await expect(page.locator("main").getByText("Chief", { exact: true })).toBeVisible();
|
||||||
await expect(page.getByPlaceholder("Message Chief")).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByText("Slack", { exact: true })).toBeVisible();
|
await expect(page.getByText("Slack", { exact: true })).toBeVisible();
|
||||||
await expect(page.getByText("Gmail", { exact: true })).toBeVisible();
|
await expect(page.getByText("Gmail", { exact: true })).toBeVisible();
|
||||||
const connectionCards = page.getByRole("group", { name: / connection$/ });
|
const connectionCards = page.getByRole("group", { name: / connection$/ });
|
||||||
|
|||||||
@ -21,9 +21,10 @@ test("Projekte + opens new project dialog instead of group form", async ({ page
|
|||||||
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await page.waitForURL(/\/app/);
|
await page.waitForURL(/\/app/);
|
||||||
await expect(sidebar.getByText("Customer project", { exact: true })).toBeVisible({
|
await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({
|
||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
});
|
});
|
||||||
|
await expect(page.locator(".sidebar-unterhaltungen").getByText("Customer project")).toHaveCount(0);
|
||||||
await expect(
|
await expect(
|
||||||
page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }),
|
page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@ -37,6 +38,8 @@ test("spaces stay invisible by default and chat creation requires approval", asy
|
|||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
|
|
||||||
const sidebar = page.locator("aside").first();
|
const sidebar = page.locator("aside").first();
|
||||||
|
const personalSpaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id"));
|
||||||
|
expect(personalSpaceId).toBeTruthy();
|
||||||
await expect(sidebar.getByText("Personal", { exact: true })).toHaveCount(0);
|
await expect(sidebar.getByText("Personal", { exact: true })).toHaveCount(0);
|
||||||
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
await captureScreenshot(page, testInfo, "single-space-sidebar");
|
await captureScreenshot(page, testInfo, "single-space-sidebar");
|
||||||
@ -48,7 +51,7 @@ test("spaces stay invisible by default and chat creation requires approval", asy
|
|||||||
await captureScreenshot(page, testInfo, "new-space-dialog");
|
await captureScreenshot(page, testInfo, "new-space-dialog");
|
||||||
await dialog.getByRole("button", { name: "Cancel" }).click();
|
await dialog.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
|
||||||
const composer = page.getByRole("combobox", { name: "Message Chief" });
|
const composer = page.getByRole("combobox", { name: "Message" });
|
||||||
await composer.fill("Create a space named Customer support");
|
await composer.fill("Create a space named Customer support");
|
||||||
await composer.press("Enter");
|
await composer.press("Enter");
|
||||||
await expect(page.getByRole("button", { name: "Create space", exact: true })).toBeVisible({
|
await expect(page.getByRole("button", { name: "Create space", exact: true })).toBeVisible({
|
||||||
@ -61,16 +64,19 @@ test("spaces stay invisible by default and chat creation requires approval", asy
|
|||||||
await page.getByRole("button", { name: "Create space", exact: true }).click();
|
await page.getByRole("button", { name: "Create space", exact: true }).click();
|
||||||
await expect(page.getByText("Created", { exact: true })).toBeVisible();
|
await expect(page.getByText("Created", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
await expect(sidebar.getByText("Personal", { exact: true })).toBeVisible();
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible({
|
const unterhaltungen = page.locator(".sidebar-unterhaltungen");
|
||||||
|
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible({
|
||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
});
|
});
|
||||||
|
await expect(unterhaltungen.getByText("Customer support", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
|
|
||||||
const supportSpace = sidebar
|
const supportSpace = projekte.locator('[data-sidebar-group^="project-space:"]').filter({
|
||||||
.locator('[data-sidebar-group^="space:"]')
|
hasText: "Customer support",
|
||||||
.filter({ hasText: "Customer support" });
|
});
|
||||||
const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group");
|
const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group");
|
||||||
const supportSpaceId = supportSpaceGroup?.split(":")[1];
|
const supportSpaceId = supportSpaceGroup?.split(":")[2];
|
||||||
expect(supportSpaceId).toBeTruthy();
|
expect(supportSpaceId).toBeTruthy();
|
||||||
await supportSpace.getByRole("button", { name: "Open Customer support" }).click();
|
await supportSpace.getByRole("button", { name: "Open Customer support" }).click();
|
||||||
await page.waitForURL(/\/(onboarding|app)/);
|
await page.waitForURL(/\/(onboarding|app)/);
|
||||||
@ -79,23 +85,20 @@ test("spaces stay invisible by default and chat creation requires approval", asy
|
|||||||
.toBe(supportSpaceId);
|
.toBe(supportSpaceId);
|
||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
|
|
||||||
await expect(sidebar.getByText("Personal", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible();
|
||||||
await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible();
|
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(2);
|
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
||||||
await captureScreenshot(page, testInfo, "spaces-sidebar");
|
await captureScreenshot(page, testInfo, "spaces-sidebar");
|
||||||
|
|
||||||
const personalSpace = sidebar
|
await page.evaluate((id) => {
|
||||||
.locator('[data-sidebar-group^="space:"]')
|
window.localStorage.setItem("rakazo:space-id", id!);
|
||||||
.filter({ hasText: "Personal" });
|
}, personalSpaceId);
|
||||||
const personalSpaceGroup = await personalSpace.getAttribute("data-sidebar-group");
|
await page.reload();
|
||||||
const personalSpaceId = personalSpaceGroup?.split(":")[1];
|
|
||||||
expect(personalSpaceId).toBeTruthy();
|
|
||||||
await personalSpace.getByRole("button", { name: /^Chief/ }).click();
|
|
||||||
await page.waitForURL(/\/app\/[^/]+$/);
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
await expect
|
await expect
|
||||||
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
|
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
|
||||||
.toBe(personalSpaceId);
|
.toBe(personalSpaceId);
|
||||||
await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a new space auto-completes onboarding and can be deleted from its menu", async ({
|
test("a new space auto-completes onboarding and can be deleted from its menu", async ({
|
||||||
@ -114,27 +117,24 @@ test("a new space auto-completes onboarding and can be deleted from its menu", a
|
|||||||
// Model is already connected, so onboarding skips the form and lands in Chief.
|
// Model is already connected, so onboarding skips the form and lands in Chief.
|
||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
await expect(page).toHaveURL(/\/app\//);
|
await expect(page).toHaveURL(/\/app\//);
|
||||||
await expect(sidebar.getByText("Temporary", { exact: true })).toBeVisible();
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
const temporarySpace = sidebar
|
await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible();
|
||||||
.locator('[data-sidebar-group^="space:"]')
|
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
.filter({ hasText: "Temporary" });
|
|
||||||
await expect(temporarySpace.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
|
||||||
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(2);
|
|
||||||
await captureScreenshot(page, testInfo, "new-space-after-onboarding");
|
await captureScreenshot(page, testInfo, "new-space-after-onboarding");
|
||||||
|
|
||||||
// Spaces can only be deleted when empty — remove auto-created Chief first.
|
// Spaces can only be deleted when empty — remove auto-created Chief first.
|
||||||
await temporarySpace.getByRole("button", { name: /^Chief/ }).click({ button: "right" });
|
await sidebar.getByRole("button", { name: /^Chief/ }).click({ button: "right" });
|
||||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
await page.getByRole("menuitem", { name: "Delete" }).click();
|
||||||
const deleteBotDialog = page.getByRole("alertdialog", { name: /Delete Chief/ });
|
const deleteBotDialog = page.getByRole("alertdialog", { name: /Delete Chief/ });
|
||||||
await expect(deleteBotDialog).toBeVisible();
|
await expect(deleteBotDialog).toBeVisible();
|
||||||
await deleteBotDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
await deleteBotDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
||||||
await expect(temporarySpace.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
||||||
|
|
||||||
// The emptied space stays selectable instead of trapping the user.
|
// The emptied space stays selectable instead of trapping the user.
|
||||||
await expect(sidebar.getByRole("button", { name: "Open Temporary" })).toBeVisible();
|
await expect(projekte.getByRole("button", { name: "Open Temporary" })).toBeVisible();
|
||||||
|
|
||||||
// Deletion is discoverable through the visible Space actions menu.
|
// Deletion is discoverable through the visible Space actions menu.
|
||||||
await sidebar.getByRole("button", { name: "Actions for Temporary" }).click();
|
await projekte.getByRole("button", { name: "Actions for Temporary" }).click();
|
||||||
await captureScreenshot(page, testInfo, "space-actions-menu");
|
await captureScreenshot(page, testInfo, "space-actions-menu");
|
||||||
await page.getByRole("menuitem", { name: "Delete space" }).click();
|
await page.getByRole("menuitem", { name: "Delete space" }).click();
|
||||||
const deleteSpaceDialog = page.getByRole("alertdialog", { name: "Delete Temporary?" });
|
const deleteSpaceDialog = page.getByRole("alertdialog", { name: "Delete Temporary?" });
|
||||||
@ -169,14 +169,12 @@ test("deleting the last bot in a space stays in the app after first use", async
|
|||||||
await dialog.getByRole("button", { name: "Create space", exact: true }).click();
|
await dialog.getByRole("button", { name: "Create space", exact: true }).click();
|
||||||
await page.waitForURL(/\/(onboarding|app)/);
|
await page.waitForURL(/\/(onboarding|app)/);
|
||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
await expect(sidebar.getByText("Side", { exact: true })).toBeVisible();
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
|
await expect(projekte.getByText("Side", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
// Delete the only bot in the new space: the app must stay put instead of
|
// Delete the only bot in the new space: the app must stay put instead of
|
||||||
// forcing onboarding again.
|
// forcing onboarding again.
|
||||||
const sideBot = sidebar
|
const sideBot = sidebar.getByRole("button", { name: /^Chief/ });
|
||||||
.locator('[data-sidebar-group^="space:"]')
|
|
||||||
.filter({ hasText: "Side" })
|
|
||||||
.getByRole("button", { name: /^Chief/ });
|
|
||||||
await sideBot.click({ button: "right" });
|
await sideBot.click({ button: "right" });
|
||||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
await page.getByRole("menuitem", { name: "Delete" }).click();
|
||||||
const deleteDialog = page.getByRole("alertdialog", { name: /Delete Chief/ });
|
const deleteDialog = page.getByRole("alertdialog", { name: /Delete Chief/ });
|
||||||
@ -186,12 +184,12 @@ test("deleting the last bot in a space stays in the app after first use", async
|
|||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await expect(sidebar.getByRole("button", { name: "Open Side" })).toBeVisible();
|
await expect(projekte.getByRole("button", { name: "Open Side" })).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }),
|
page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await sidebar.getByRole("button", { name: "Actions for Side" }).click();
|
await projekte.getByRole("button", { name: "Actions for Side" }).click();
|
||||||
await page.getByRole("menuitem", { name: "Delete space" }).click();
|
await page.getByRole("menuitem", { name: "Delete space" }).click();
|
||||||
await page
|
await page
|
||||||
.getByRole("alertdialog", { name: "Delete Side?" })
|
.getByRole("alertdialog", { name: "Delete Side?" })
|
||||||
|
|||||||
@ -264,7 +264,7 @@ async function openBot(page: Page, name: string) {
|
|||||||
.getByRole("complementary")
|
.getByRole("complementary")
|
||||||
.getByRole("button", { name: new RegExp(`^${name}`) })
|
.getByRole("button", { name: new RegExp(`^${name}`) })
|
||||||
.click();
|
.click();
|
||||||
await expect(page.getByPlaceholder(`Message ${name}`)).toBeVisible();
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openComputerPanel(page: Page) {
|
async function openComputerPanel(page: Page) {
|
||||||
|
|||||||
59
apps/web/e2e/unterhaltungen-sidebar.spec.ts
Normal file
59
apps/web/e2e/unterhaltungen-sidebar.spec.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { completeOnboarding, openNewBot, rpc, signup } from "./helpers";
|
||||||
|
|
||||||
|
test("Unterhaltungen and Projekte stay separate; + opens bot picker", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `unterhaltungen-${stamp}@rakazo.test`, "password12", "Unterhaltungen UX");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await page.goto("/app");
|
||||||
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
|
|
||||||
|
const sidebar = page.locator("aside").first();
|
||||||
|
const unterhaltungen = page.locator(".sidebar-unterhaltungen");
|
||||||
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
|
|
||||||
|
await expect(unterhaltungen.getByText("Unterhaltungen", { exact: true })).toBeVisible();
|
||||||
|
await expect(projekte.getByText("Projekte", { exact: true })).toBeVisible();
|
||||||
|
await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toBeVisible();
|
||||||
|
|
||||||
|
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
||||||
|
const picker = page.getByTestId("conversation-bot-picker");
|
||||||
|
await expect(picker).toBeVisible();
|
||||||
|
await expect(picker.getByTestId("conversation-create-bot")).toBeVisible();
|
||||||
|
await expect(picker.getByText("Chief", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await picker.getByTestId("conversation-create-bot").click();
|
||||||
|
await expect(page.getByTestId("create-bot-form")).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await openNewBot(page);
|
||||||
|
const form = page.getByTestId("create-bot-form");
|
||||||
|
await form.locator("label:has-text('Name') input").fill("Researcher");
|
||||||
|
await form.getByRole("button", { name: "Create", exact: true }).click();
|
||||||
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
|
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
|
|
||||||
|
await projekte.getByRole("button", { name: "Create a project" }).click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "New project" });
|
||||||
|
await dialog.getByLabel("Name").fill("Side project");
|
||||||
|
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
|
await page.waitForURL(/\/app/);
|
||||||
|
await expect(projekte.getByText("Side project", { exact: true })).toBeVisible();
|
||||||
|
await expect(unterhaltungen.getByText("Side project", { exact: true })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chat header shows avatar once and composer uses generic placeholder", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `chat-header-${stamp}@rakazo.test`, "password12", "Chat Header");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await page.goto("/app");
|
||||||
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
|
|
||||||
|
const header = page.getByTestId("bot-settings-trigger");
|
||||||
|
await expect(header.locator(".rakazo-bot-avatar, [data-testid='bot-avatar']").first()).toBeVisible();
|
||||||
|
await expect(header.getByText("Chief", { exact: true })).toBeVisible();
|
||||||
|
await expect(header.getByText("1:1 conversation", { exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
|
const composer = page.getByRole("combobox", { name: "Message" });
|
||||||
|
await expect(composer).toHaveAttribute("placeholder", "Message…");
|
||||||
|
});
|
||||||
@ -170,8 +170,8 @@ export function OoStage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}>
|
<div className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}>
|
||||||
<div className="mb-3 flex w-full items-center justify-between gap-3">
|
{(hasMessages || phase !== "idle") && (
|
||||||
<div className="min-w-0">
|
<div className={`flex w-full justify-center ${hasMessages ? "mb-1" : "mb-3"}`}>
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
|
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
|
||||||
style={
|
style={
|
||||||
@ -195,15 +195,20 @@ export function OoStage({
|
|||||||
/>
|
/>
|
||||||
<span>{phaseLabel}</span>
|
<span>{phaseLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mt-1.5 truncate text-ui-title font-semibold tracking-tight text-foreground">
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasMessages ? (
|
||||||
|
<div className="mb-3 text-center">
|
||||||
|
<h2 className="truncate text-ui-title font-semibold tracking-tight text-foreground">
|
||||||
{botName}
|
{botName}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="truncate text-ui-body-sm text-muted-foreground">
|
{persona.roleLine ? (
|
||||||
{t`1:1 conversation`}
|
<p className="mt-0.5 truncate text-ui-body-sm text-muted-foreground">
|
||||||
{persona.roleLine ? ` · ${persona.roleLine}` : ""}
|
{persona.roleLine}
|
||||||
</p>
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
{awarenessLabel && awarenessEvent ? (
|
{awarenessLabel && awarenessEvent ? (
|
||||||
<div className="mb-3 w-full">
|
<div className="mb-3 w-full">
|
||||||
|
|||||||
@ -232,7 +232,7 @@ import {
|
|||||||
import type { SettingsSection } from "./SettingsOverlay";
|
import type { SettingsSection } from "./SettingsOverlay";
|
||||||
import { SpaceSearchResults } from "./SpaceSearch";
|
import { SpaceSearchResults } from "./SpaceSearch";
|
||||||
import { BotSettings, CreateBotForm } from "./shell/bot-panel";
|
import { BotSettings, CreateBotForm } from "./shell/bot-panel";
|
||||||
import { BotCreatePicker } from "./shell/bot-picker";
|
import { BotCreatePicker, ConversationBotPicker } from "./shell/bot-picker";
|
||||||
import { CommandPalette, isCommandPaletteHotkey } from "./shell/command-palette";
|
import { CommandPalette, isCommandPaletteHotkey } from "./shell/command-palette";
|
||||||
import {
|
import {
|
||||||
ClearConversationDialog,
|
ClearConversationDialog,
|
||||||
@ -339,6 +339,14 @@ function readCollapsedSidebarSections(userId: string | null | undefined): Set<st
|
|||||||
|
|
||||||
export type NavPillar = "heute" | "team" | "projekte";
|
export type NavPillar = "heute" | "team" | "projekte";
|
||||||
|
|
||||||
|
function stripSpaceTitlePrefix(title: string | null, spaceName: string): string | null {
|
||||||
|
if (!title) return null;
|
||||||
|
const prefix = `${spaceName} · `;
|
||||||
|
if (title.startsWith(prefix)) return title.slice(prefix.length);
|
||||||
|
if (title === spaceName) return null;
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
export function ShellPage() {
|
export function ShellPage() {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { botId, groupId } = useParams();
|
const { botId, groupId } = useParams();
|
||||||
@ -571,6 +579,7 @@ export function ShellPage() {
|
|||||||
const mobileSidebarSwipeRef = useRef<{ startX: number; startY: number } | null>(null);
|
const mobileSidebarSwipeRef = useRef<{ startX: number; startY: number } | null>(null);
|
||||||
const [draggedBotId, setDraggedBotId] = useState<string | null>(null);
|
const [draggedBotId, setDraggedBotId] = useState<string | null>(null);
|
||||||
const [createMenuOpen, setCreateMenuOpen] = useState(false);
|
const [createMenuOpen, setCreateMenuOpen] = useState(false);
|
||||||
|
const [conversationPickerOpen, setConversationPickerOpen] = useState(false);
|
||||||
const [botsSidebarCollapsed, setBotsSidebarCollapsed] = useState(false);
|
const [botsSidebarCollapsed, setBotsSidebarCollapsed] = useState(false);
|
||||||
const focusPromptAbortRef = useRef<AbortController | null>(null);
|
const focusPromptAbortRef = useRef<AbortController | null>(null);
|
||||||
const focusPromptBotIdRef = useRef<string | null>(null);
|
const focusPromptBotIdRef = useRef<string | null>(null);
|
||||||
@ -1555,6 +1564,71 @@ export function ShellPage() {
|
|||||||
[groups],
|
[groups],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const homeSpaceId = bootstrapMe?.spaceId;
|
||||||
|
|
||||||
|
const homeSpaceBots = useMemo(() => {
|
||||||
|
if (!homeSpaceId) return bots;
|
||||||
|
const home = spaces.find((space) => space.id === homeSpaceId);
|
||||||
|
return home?.bots ?? bots;
|
||||||
|
}, [bots, homeSpaceId, spaces]);
|
||||||
|
|
||||||
|
const conversationSidebarGroups = useMemo(() => {
|
||||||
|
if (!homeSpaceId) return [];
|
||||||
|
return sidebarGroups
|
||||||
|
.filter((group) => group.spaceId === homeSpaceId)
|
||||||
|
.map((group) => ({
|
||||||
|
...group,
|
||||||
|
title: stripSpaceTitlePrefix(group.title, group.spaceName),
|
||||||
|
showLock: false,
|
||||||
|
bots: group.bots.filter((item) => item.kind === "bot"),
|
||||||
|
}))
|
||||||
|
.filter((group) => group.bots.length > 0);
|
||||||
|
}, [homeSpaceId, sidebarGroups]);
|
||||||
|
|
||||||
|
const projectSidebarGroups = useMemo(() => {
|
||||||
|
if (!homeSpaceId) return [];
|
||||||
|
const entries: typeof sidebarGroups = [];
|
||||||
|
|
||||||
|
for (const space of spaces) {
|
||||||
|
if (space.id === homeSpaceId) continue;
|
||||||
|
const groupsInSpace = sidebarGroups.flatMap((group) =>
|
||||||
|
group.spaceId === space.id
|
||||||
|
? group.bots.filter(
|
||||||
|
(item) => item.kind === "group" && item.chat.id !== hauptthreadGroup?.id,
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
entries.push({
|
||||||
|
key: groupsInSpace.length > 0 ? `project-space:${space.id}` : `project-space:${space.id}:empty`,
|
||||||
|
title: space.name,
|
||||||
|
bots: groupsInSpace,
|
||||||
|
sectionId: null,
|
||||||
|
showLock: true,
|
||||||
|
emptySpaceId: groupsInSpace.length > 0 ? undefined : space.id,
|
||||||
|
spaceId: space.id,
|
||||||
|
spaceName: space.name,
|
||||||
|
canDeleteSpace: space.canDelete === true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of sidebarGroups) {
|
||||||
|
if (group.spaceId !== homeSpaceId) continue;
|
||||||
|
const groupChats = group.bots.filter(
|
||||||
|
(item) => item.kind === "group" && item.chat.id !== hauptthreadGroup?.id,
|
||||||
|
);
|
||||||
|
if (groupChats.length === 0) continue;
|
||||||
|
entries.push({
|
||||||
|
...group,
|
||||||
|
title: stripSpaceTitlePrefix(group.title, group.spaceName),
|
||||||
|
showLock: false,
|
||||||
|
emptySpaceId: undefined,
|
||||||
|
bots: groupChats,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}, [hauptthreadGroup?.id, homeSpaceId, sidebarGroups, spaces]);
|
||||||
|
|
||||||
const openSpaceChat = useCallback(
|
const openSpaceChat = useCallback(
|
||||||
(spaceId: string, path: string) => {
|
(spaceId: string, path: string) => {
|
||||||
setMobileSidebarOpen(false);
|
setMobileSidebarOpen(false);
|
||||||
@ -1574,6 +1648,23 @@ export function ShellPage() {
|
|||||||
},
|
},
|
||||||
[bootstrapMe?.spaceId, navigate],
|
[bootstrapMe?.spaceId, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openConversationBot = useCallback(
|
||||||
|
(botId: string) => {
|
||||||
|
setConversationPickerOpen(false);
|
||||||
|
setMobileSidebarOpen(false);
|
||||||
|
setActiveNavPillar("team");
|
||||||
|
const bot =
|
||||||
|
homeSpaceBots.find((item) => item.id === botId) ?? bots.find((item) => item.id === botId);
|
||||||
|
if (bot) {
|
||||||
|
openSpaceChat(bot.spaceId, `/app/${botId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`/app/${botId}`);
|
||||||
|
},
|
||||||
|
[bots, homeSpaceBots, navigate, openSpaceChat],
|
||||||
|
);
|
||||||
|
|
||||||
const flushBotOrder = useCallback(async () => {
|
const flushBotOrder = useCallback(async () => {
|
||||||
if (savingBotOrderRef.current) return;
|
if (savingBotOrderRef.current) return;
|
||||||
savingBotOrderRef.current = true;
|
savingBotOrderRef.current = true;
|
||||||
@ -3059,237 +3150,57 @@ export function ShellPage() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar-projekte">
|
<div className="sidebar-unterhaltungen">
|
||||||
{/* PROJEKTE SECTION */}
|
|
||||||
<div className="flex items-center justify-between pt-4 pb-1 mt-2 border-t border-white/5 px-2.5">
|
|
||||||
<div>
|
|
||||||
<div className="rk-shell-section-label">{t`Projekte`}</div>
|
|
||||||
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
|
|
||||||
{t`Threads by topic · folders`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={t`Create a project`}
|
|
||||||
onClick={() => openNewSpaceDialog("project")}
|
|
||||||
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
|
|
||||||
>
|
|
||||||
<Plus strokeWidth={2.5} className="size-icon-md" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{sidebarGroups
|
|
||||||
.map((group) => ({
|
|
||||||
...group,
|
|
||||||
bots: group.bots.filter(
|
|
||||||
(b) => b.kind === "group" && b.chat.id !== hauptthreadGroup?.id,
|
|
||||||
) as any[],
|
|
||||||
}))
|
|
||||||
.filter((group) => group.bots.length > 0 || group.emptySpaceId).length === 0 ? (
|
|
||||||
<div className="p-4 text-center text-ui-label text-muted-foreground">
|
|
||||||
<p>{t`No projects yet.`}</p>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="mt-2 text-ui-label"
|
|
||||||
onClick={() => openNewSpaceDialog("project")}
|
|
||||||
>
|
|
||||||
<Plus className="mr-1 size-icon-sm" />
|
|
||||||
{t`Create a project`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{sidebarGroups
|
|
||||||
.map((group) => ({
|
|
||||||
...group,
|
|
||||||
bots: group.bots.filter(
|
|
||||||
(b) => b.kind === "group" && b.chat.id !== hauptthreadGroup?.id,
|
|
||||||
) as any[],
|
|
||||||
}))
|
|
||||||
.filter((group) => group.bots.length > 0 || group.emptySpaceId)
|
|
||||||
.map((group) => {
|
|
||||||
const collapsed =
|
|
||||||
Boolean(group.title) && collapsedSidebarSections.has(group.key);
|
|
||||||
return (
|
|
||||||
<div key={`proj-${group.key}`} data-sidebar-group={group.key}>
|
|
||||||
{group.title ? (
|
|
||||||
<div className="flex items-center pt-3 pb-0.5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
|
|
||||||
onClick={() => {
|
|
||||||
if (group.emptySpaceId) {
|
|
||||||
openSpaceChat(group.emptySpaceId, "/onboarding");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toggleSidebarSection(group.key);
|
|
||||||
}}
|
|
||||||
onContextMenu={
|
|
||||||
group.sectionId
|
|
||||||
? (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const sections =
|
|
||||||
group.spaceId === bootstrapMe?.spaceId
|
|
||||||
? botSections
|
|
||||||
: (spaces.find((space) => space.id === group.spaceId)
|
|
||||||
?.botSections ?? []);
|
|
||||||
const section = sections.find(
|
|
||||||
(item) => item.id === group.sectionId,
|
|
||||||
);
|
|
||||||
if (!section) return;
|
|
||||||
sectionMenuAnchor.current = event.currentTarget;
|
|
||||||
setSectionMenu({
|
|
||||||
section,
|
|
||||||
spaceId: group.spaceId,
|
|
||||||
position: { x: event.clientX, y: event.clientY },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
|
|
||||||
>
|
|
||||||
<span className="flex min-w-0 items-center gap-1.5 truncate">
|
|
||||||
{group.showLock ? (
|
|
||||||
<Lock
|
|
||||||
strokeWidth={2}
|
|
||||||
aria-hidden="true"
|
|
||||||
className="size-icon-sm"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<span className="truncate">{group.title}</span>
|
|
||||||
</span>
|
|
||||||
{group.emptySpaceId ? null : (
|
|
||||||
<ChevronDown
|
|
||||||
strokeWidth={1.8}
|
|
||||||
className={`size-icon-md ${collapsed ? "-rotate-90 transition-transform" : "transition-transform"}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{!collapsed &&
|
|
||||||
group.bots.map((item) => (
|
|
||||||
<button
|
|
||||||
key={`${item.kind}:${item.chat.id}`}
|
|
||||||
type="button"
|
|
||||||
aria-label={t`Group conversation ${item.chat.name}`}
|
|
||||||
onClick={() => {
|
|
||||||
openSpaceChat(item.chat.spaceId, `/app/g/${item.chat.id}`);
|
|
||||||
}}
|
|
||||||
onContextMenu={(event) => {
|
|
||||||
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
|
|
||||||
event.preventDefault();
|
|
||||||
botMenuAnchor.current = event.currentTarget;
|
|
||||||
setBotMenu({
|
|
||||||
kind: item.kind,
|
|
||||||
id: item.chat.id,
|
|
||||||
position: { x: event.clientX, y: event.clientY },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
|
|
||||||
item.kind === "group" && inGroup && activeGroup?.id === item.chat.id
|
|
||||||
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
|
|
||||||
: "hover:bg-white/[0.05] border border-transparent"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<GroupAvatar
|
|
||||||
members={
|
|
||||||
item.chat.id === activeSnapshot?.groupId
|
|
||||||
? (activeSnapshot?.members ?? item.chat.members)
|
|
||||||
: item.chat.members
|
|
||||||
}
|
|
||||||
size={40}
|
|
||||||
/>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center justify-between gap-1.5">
|
|
||||||
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
|
||||||
<span
|
|
||||||
dir="auto"
|
|
||||||
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${
|
|
||||||
item.chat.unread
|
|
||||||
? "font-bold text-white"
|
|
||||||
: "font-semibold text-white/95"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.chat.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-1.5">
|
|
||||||
<span className="text-ui-body-sm text-zinc-400 tabular-nums font-medium">
|
|
||||||
{formatRosterTime(item.chat.updatedAt)}
|
|
||||||
</span>
|
|
||||||
{item.chat.unread ? (
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="inline-block h-2 w-2 rounded-full bg-emerald-400"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
dir="auto"
|
|
||||||
className={`mt-0.5 truncate text-ui-body leading-snug ${
|
|
||||||
item.chat.unread ? "font-medium text-white" : "text-zinc-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{cleanRosterPreview(item.chat.preview) ||
|
|
||||||
item.chat.members.map((member: any) => member.name).join(", ")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="sidebar-team">
|
|
||||||
{/* TEAM SECTION */}
|
|
||||||
<div className="flex items-center justify-between pt-4 pb-1 px-2.5">
|
<div className="flex items-center justify-between pt-4 pb-1 px-2.5">
|
||||||
<div>
|
<div>
|
||||||
<div className="rk-shell-section-label">{t`Team`}</div>
|
<div className="rk-shell-section-label">{t`Unterhaltungen`}</div>
|
||||||
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
|
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
|
||||||
{t`Your Oos — open a 1:1 conversation`}
|
{t`1:1 mit deinen Oos`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<Popover open={conversationPickerOpen} onOpenChange={setConversationPickerOpen}>
|
||||||
type="button"
|
<PopoverTrigger
|
||||||
aria-label="Neues Teammitglied"
|
type="button"
|
||||||
onClick={() => setPanel("create")}
|
aria-label={t`Neue Unterhaltung`}
|
||||||
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
|
data-testid="conversation-picker-trigger"
|
||||||
>
|
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
|
||||||
<Plus strokeWidth={2.5} className="size-icon-md" />
|
>
|
||||||
</button>
|
<Plus strokeWidth={2.5} className="size-icon-md" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
{conversationPickerOpen ? (
|
||||||
|
<PopoverContent
|
||||||
|
align="end"
|
||||||
|
className="w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
|
||||||
|
>
|
||||||
|
<ConversationBotPicker
|
||||||
|
bots={homeSpaceBots}
|
||||||
|
onCreateBot={() => {
|
||||||
|
setConversationPickerOpen(false);
|
||||||
|
setPanel("create");
|
||||||
|
}}
|
||||||
|
onOpenBot={openConversationBot}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
) : null}
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{sidebarGroups
|
{conversationSidebarGroups.length === 0 ? (
|
||||||
.map((group) => ({
|
|
||||||
...group,
|
|
||||||
bots: group.bots.filter((b) => b.kind === "bot") as any[],
|
|
||||||
}))
|
|
||||||
.filter((group) => group.bots.length > 0 || group.emptySpaceId).length === 0 ? (
|
|
||||||
<div className="p-4 text-center text-ui-label text-muted-foreground">
|
<div className="p-4 text-center text-ui-label text-muted-foreground">
|
||||||
<p>{t`No Oos in your team yet.`}</p>
|
<p>{t`Noch keine Unterhaltung.`}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-2 text-ui-label"
|
className="mt-2 text-ui-label"
|
||||||
onClick={() => setPanel("create")}
|
onClick={() => setConversationPickerOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 size-icon-sm" />
|
<Plus className="mr-1 size-icon-sm" />
|
||||||
{t`Add an Oo`}
|
{t`Neue Unterhaltung`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{sidebarGroups
|
{conversationSidebarGroups.map((group) => {
|
||||||
.map((group) => ({
|
|
||||||
...group,
|
|
||||||
bots: group.bots.filter((b) => b.kind === "bot") as any[],
|
|
||||||
}))
|
|
||||||
.filter((group) => group.bots.length > 0 || group.emptySpaceId)
|
|
||||||
.map((group) => {
|
|
||||||
const collapsed =
|
const collapsed =
|
||||||
Boolean(group.title) && collapsedSidebarSections.has(group.key);
|
Boolean(group.title) && collapsedSidebarSections.has(group.key);
|
||||||
const groupBotIds = group.bots.flatMap((item) =>
|
const groupBotIds = group.bots.flatMap((item) =>
|
||||||
@ -3443,17 +3354,8 @@ export function ShellPage() {
|
|||||||
: t`Group conversation ${item.chat.name}`
|
: t`Group conversation ${item.chat.name}`
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.kind === "bot") {
|
setActiveNavPillar("team");
|
||||||
setActiveNavPillar("team");
|
openSpaceChat(item.chat.spaceId, `/app/${item.chat.id}`);
|
||||||
} else {
|
|
||||||
setActiveNavPillar("projekte");
|
|
||||||
}
|
|
||||||
openSpaceChat(
|
|
||||||
item.chat.spaceId,
|
|
||||||
item.kind === "bot"
|
|
||||||
? `/app/${item.chat.id}`
|
|
||||||
: `/app/g/${item.chat.id}`,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
|
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
|
||||||
@ -3585,6 +3487,214 @@ export function ShellPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<div className="sidebar-projekte">
|
||||||
|
{/* PROJEKTE SECTION */}
|
||||||
|
<div className="flex items-center justify-between pt-4 pb-1 mt-2 border-t border-white/5 px-2.5">
|
||||||
|
<div>
|
||||||
|
<div className="rk-shell-section-label">{t`Projekte`}</div>
|
||||||
|
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
|
||||||
|
{t`Threads by topic · folders`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t`Create a project`}
|
||||||
|
onClick={() => openNewSpaceDialog("project")}
|
||||||
|
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus strokeWidth={2.5} className="size-icon-md" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{projectSidebarGroups.length === 0 ? (
|
||||||
|
<div className="p-4 text-center text-ui-label text-muted-foreground">
|
||||||
|
<p>{t`No projects yet.`}</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2 text-ui-label"
|
||||||
|
onClick={() => openNewSpaceDialog("project")}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 size-icon-sm" />
|
||||||
|
{t`Create a project`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{projectSidebarGroups.map((group) => {
|
||||||
|
const collapsed =
|
||||||
|
Boolean(group.title) && collapsedSidebarSections.has(group.key);
|
||||||
|
return (
|
||||||
|
<div key={`proj-${group.key}`} data-sidebar-group={group.key}>
|
||||||
|
{group.title ? (
|
||||||
|
<div className="flex items-center pt-3 pb-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
|
||||||
|
onClick={() => {
|
||||||
|
if (group.emptySpaceId) {
|
||||||
|
const space = spaces.find((item) => item.id === group.emptySpaceId);
|
||||||
|
openSpaceChat(
|
||||||
|
group.emptySpaceId,
|
||||||
|
space?.hasContent ? "/app" : "/onboarding",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleSidebarSection(group.key);
|
||||||
|
}}
|
||||||
|
onContextMenu={
|
||||||
|
group.sectionId
|
||||||
|
? (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const sections =
|
||||||
|
group.spaceId === bootstrapMe?.spaceId
|
||||||
|
? botSections
|
||||||
|
: (spaces.find((space) => space.id === group.spaceId)
|
||||||
|
?.botSections ?? []);
|
||||||
|
const section = sections.find(
|
||||||
|
(item) => item.id === group.sectionId,
|
||||||
|
);
|
||||||
|
if (!section) return;
|
||||||
|
sectionMenuAnchor.current = event.currentTarget;
|
||||||
|
setSectionMenu({
|
||||||
|
section,
|
||||||
|
spaceId: group.spaceId,
|
||||||
|
position: { x: event.clientX, y: event.clientY },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: group.canDeleteSpace
|
||||||
|
? (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
spaceMenuAnchor.current = event.currentTarget;
|
||||||
|
setSpaceMenu({
|
||||||
|
id: group.spaceId,
|
||||||
|
position: { x: event.clientX, y: event.clientY },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
|
||||||
|
aria-label={
|
||||||
|
group.emptySpaceId
|
||||||
|
? t`Open ${group.title}`
|
||||||
|
: collapsed
|
||||||
|
? t`Expand ${group.title}`
|
||||||
|
: t`Collapse ${group.title}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 items-center gap-1.5 truncate">
|
||||||
|
{group.showLock ? (
|
||||||
|
<Lock
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-icon-sm"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="truncate">{group.title}</span>
|
||||||
|
</span>
|
||||||
|
{group.emptySpaceId ? null : (
|
||||||
|
<ChevronDown
|
||||||
|
strokeWidth={1.8}
|
||||||
|
className={`size-icon-md ${collapsed ? "-rotate-90 transition-transform" : "transition-transform"}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{group.canDeleteSpace ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
aria-label={t`Actions for ${group.spaceName}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
|
spaceMenuAnchor.current = event.currentTarget;
|
||||||
|
setSpaceMenu({
|
||||||
|
id: group.spaceId,
|
||||||
|
position: { x: rect.left, y: rect.bottom },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreHorizontal aria-hidden="true" className="size-icon-md" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!collapsed &&
|
||||||
|
group.bots.map((item) => (
|
||||||
|
<button
|
||||||
|
key={`${item.kind}:${item.chat.id}`}
|
||||||
|
type="button"
|
||||||
|
aria-label={t`Group conversation ${item.chat.name}`}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveNavPillar("projekte");
|
||||||
|
openSpaceChat(item.chat.spaceId, `/app/g/${item.chat.id}`);
|
||||||
|
}}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
botMenuAnchor.current = event.currentTarget;
|
||||||
|
setBotMenu({
|
||||||
|
kind: item.kind,
|
||||||
|
id: item.chat.id,
|
||||||
|
position: { x: event.clientX, y: event.clientY },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
|
||||||
|
item.kind === "group" && inGroup && activeGroup?.id === item.chat.id
|
||||||
|
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
|
||||||
|
: "hover:bg-white/[0.05] border border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<GroupAvatar
|
||||||
|
members={
|
||||||
|
item.chat.id === activeSnapshot?.groupId
|
||||||
|
? (activeSnapshot?.members ?? item.chat.members)
|
||||||
|
: item.chat.members
|
||||||
|
}
|
||||||
|
size={40}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-1.5">
|
||||||
|
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||||
|
<span
|
||||||
|
dir="auto"
|
||||||
|
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${
|
||||||
|
item.chat.unread
|
||||||
|
? "font-bold text-white"
|
||||||
|
: "font-semibold text-white/95"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.chat.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
|
<span className="text-ui-body-sm text-zinc-400 tabular-nums font-medium">
|
||||||
|
{formatRosterTime(item.chat.updatedAt)}
|
||||||
|
</span>
|
||||||
|
{item.chat.unread ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="inline-block h-2 w-2 rounded-full bg-emerald-400"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
dir="auto"
|
||||||
|
className={`mt-0.5 truncate text-ui-body leading-snug ${
|
||||||
|
item.chat.unread ? "font-medium text-white" : "text-zinc-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cleanRosterPreview(item.chat.preview) ||
|
||||||
|
item.chat.members.map((member: any) => member.name).join(", ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -3929,8 +4039,27 @@ export function ShellPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
data-testid="bot-settings-trigger"
|
data-testid="bot-settings-trigger"
|
||||||
onClick={() => setPanel(inGroup ? "group-settings" : "settings")}
|
onClick={() => setPanel(inGroup ? "group-settings" : "settings")}
|
||||||
className="app-no-drag flex min-w-0 items-center"
|
className="app-no-drag flex min-w-0 items-center gap-3"
|
||||||
>
|
>
|
||||||
|
{!inGroup && active ? (
|
||||||
|
<BotAvatar
|
||||||
|
color={active.color}
|
||||||
|
identity={active.id}
|
||||||
|
size={36}
|
||||||
|
status={active.status}
|
||||||
|
className="shrink-0"
|
||||||
|
/>
|
||||||
|
) : inGroup ? (
|
||||||
|
<GroupAvatar
|
||||||
|
members={
|
||||||
|
activeGroup?.members ??
|
||||||
|
activeSnapshot?.members ??
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
size={36}
|
||||||
|
className="shrink-0"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div className="min-w-0 text-start flex flex-col justify-center">
|
<div className="min-w-0 text-start flex flex-col justify-center">
|
||||||
<span
|
<span
|
||||||
className="block truncate text-ui-body font-semibold text-foreground tracking-tight leading-snug"
|
className="block truncate text-ui-body font-semibold text-foreground tracking-tight leading-snug"
|
||||||
@ -3938,19 +4067,21 @@ export function ShellPage() {
|
|||||||
>
|
>
|
||||||
{inGroup
|
{inGroup
|
||||||
? (activeGroup?.name ?? activeSnapshot?.groupName ?? t`Group`)
|
? (activeGroup?.name ?? activeSnapshot?.groupName ?? t`Group`)
|
||||||
: (active?.name ?? t`Choose a conversation`)}
|
: (active?.name ?? t`Unterhaltung wählen`)}
|
||||||
</span>
|
</span>
|
||||||
{!inGroup && active ? (
|
{!inGroup && active && (composerRunning || active.status !== "idle") ? (
|
||||||
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
{active.title ? t`1:1 conversation · ${active.title}` : t`1:1 conversation`}
|
{composerRunning || active.status === "running"
|
||||||
|
? t`Beschäftigt`
|
||||||
|
: active.status}
|
||||||
</span>
|
</span>
|
||||||
) : inGroup && projectSessionKey ? (
|
) : inGroup && projectSessionKey ? (
|
||||||
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
{t`Group conversation`}
|
{t`Gruppenunterhaltung`}
|
||||||
</span>
|
</span>
|
||||||
) : inGroup ? (
|
) : inGroup ? (
|
||||||
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
<span className="block truncate text-ui-body-sm font-medium text-muted-foreground/80 leading-snug">
|
||||||
{t`Project · sessions & folders`}
|
{t`Projekt · Sessions & Ordner`}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@ -6317,14 +6448,8 @@ const Composer = memo(function Composer({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={
|
placeholder={showComposerPlaceholder ? t`Message…` : undefined}
|
||||||
showComposerPlaceholder
|
aria-label={t`Message`}
|
||||||
? activeName
|
|
||||||
? t`Frag ${activeName} was Schlaues…`
|
|
||||||
: t`Frag mich was Schlaues – oder lass uns handeln…`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
aria-label={activeName ? t`Message ${activeName}` : t`Message`}
|
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-autocomplete="list"
|
aria-autocomplete="list"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
|
|||||||
@ -12,6 +12,80 @@ import {
|
|||||||
import { Info, Lock, Plus, Users } from "lucide-react";
|
import { Info, Lock, Plus, Users } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
/** 1:1-Unterhaltung: bestehenden Bot öffnen oder neuen Bot anlegen. */
|
||||||
|
export function ConversationBotPicker({
|
||||||
|
bots,
|
||||||
|
onCreateBot,
|
||||||
|
onOpenBot,
|
||||||
|
}: {
|
||||||
|
bots: Bot[];
|
||||||
|
onCreateBot: () => void;
|
||||||
|
onOpenBot: (botId: string) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const needle = query.trim().toLowerCase();
|
||||||
|
const matched = useMemo(() => {
|
||||||
|
if (!needle) return bots;
|
||||||
|
return bots.filter(
|
||||||
|
(bot) => bot.name.toLowerCase().includes(needle) || bot.title.toLowerCase().includes(needle),
|
||||||
|
);
|
||||||
|
}, [bots, needle]);
|
||||||
|
const showCreate =
|
||||||
|
!needle ||
|
||||||
|
"neuen bot erstellen".includes(needle) ||
|
||||||
|
needle.split(/\s+/).every((part) => "neuen bot erstellen".includes(part));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="conversation-bot-picker" className="w-[min(320px,calc(100vw-2rem))]">
|
||||||
|
<label className="flex items-center gap-2 border-b border-border px-3 py-2">
|
||||||
|
<span className="shrink-0 text-ui-body-sm text-muted-foreground">
|
||||||
|
<Trans>Mit:</Trans>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
placeholder={t`Suchen`}
|
||||||
|
aria-label={t`Suchen`}
|
||||||
|
className="min-w-0 flex-1 bg-transparent text-ui-body text-foreground outline-none placeholder:text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Command shouldFilter={false} className="rounded-none border-0 bg-transparent p-0">
|
||||||
|
<CommandList className="max-h-72 p-1">
|
||||||
|
<CommandEmpty>
|
||||||
|
<Trans>Kein Bot gefunden</Trans>
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{showCreate ? (
|
||||||
|
<CommandItem
|
||||||
|
value="create-new-bot"
|
||||||
|
data-testid="conversation-create-bot"
|
||||||
|
onSelect={() => onCreateBot()}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Plus strokeWidth={1.8} aria-hidden="true" className="size-icon-md" />
|
||||||
|
<Trans>Neuen Bot erstellen</Trans>
|
||||||
|
</CommandItem>
|
||||||
|
) : null}
|
||||||
|
{matched.map((bot) => (
|
||||||
|
<CommandItem
|
||||||
|
key={bot.id}
|
||||||
|
value={bot.id}
|
||||||
|
data-testid={`conversation-picker-bot-${bot.id}`}
|
||||||
|
onSelect={() => onOpenBot(bot.id)}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<BotAvatar color={bot.color} identity={bot.id} size={22} status={bot.status} />
|
||||||
|
<span className="min-w-0 flex-1 truncate">{bot.name}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function BotCreatePicker({
|
export function BotCreatePicker({
|
||||||
bots,
|
bots,
|
||||||
onCreateBot,
|
onCreateBot,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user