diff --git a/apps/web/e2e/auth-lifecycle.spec.ts b/apps/web/e2e/auth-lifecycle.spec.ts index 7221059..3ef6599 100644 --- a/apps/web/e2e/auth-lifecycle.spec.ts +++ b/apps/web/e2e/auth-lifecycle.spec.ts @@ -48,7 +48,7 @@ test("logout protects bot deep links and sign-in restores the session", async ({ await page.waitForURL(/\/app\/[^/]+$/); 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 expect(page.getByRole("button", { name: "Settings", exact: true })).toBeVisible(); diff --git a/apps/web/e2e/bot-crud.spec.ts b/apps/web/e2e/bot-crud.spec.ts index c7d612c..0780649 100644 --- a/apps/web/e2e/bot-crud.spec.ts +++ b/apps/web/e2e/bot-crud.spec.ts @@ -33,7 +33,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await form.getByRole("button", { name: "Create", exact: true }).click(); await createAborted; // 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("create-bot-error")).toBeVisible(); expect(createFailed).toBe(true); @@ -51,7 +51,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = await route.abort("failed"); }); 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 page.unroute("**/rpc/spaces/list"); 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 page.getByRole("button", { name: "Save", exact: true }).click(); 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 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 page.getByRole("button", { name: "Save", exact: true }).click(); 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 page.reload(); 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 expect(nameInput).toHaveValue("Atlas"); await expect(titleInput).toHaveValue("Research lead"); @@ -157,7 +157,7 @@ test("bot creation, editing, and deletion persist", async ({ page }, testInfo) = name: new RegExp(`^${longUnbrokenName}`), }); 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 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 expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0); 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"); }); diff --git a/apps/web/e2e/golden.spec.ts b/apps/web/e2e/golden.spec.ts index 91fe500..01215fb 100644 --- a/apps/web/e2e/golden.spec.ts +++ b/apps/web/e2e/golden.spec.ts @@ -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.getByText("Messages sent now guide the next turn.")).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 composer.fill("Use the newer report and keep the answer short."); 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 expect(page.getByRole("button", { name: "Send", 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 page.setViewportSize({ width: 1280, height: 720 }); expect(browserErrors).toEqual([]); diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index 5e57f49..1142e0b 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -178,6 +178,6 @@ export async function createNamedBot( computerMode: options.computerMode ?? "team", }); await page.goto(`/app/${bot.id}`); - await expect(page.getByPlaceholder(`Message ${name}`)).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); return bot.id; } diff --git a/apps/web/e2e/new-bot-ux.spec.ts b/apps/web/e2e/new-bot-ux.spec.ts index 9aec67f..046a7d6 100644 --- a/apps/web/e2e/new-bot-ux.spec.ts +++ b/apps/web/e2e/new-bot-ux.spec.ts @@ -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.getByRole("button", { name: "Create", exact: true }).click(); 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.getByText("What do you want me on first?", { exact: true })).toHaveCount(0); 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 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 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 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"); 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 create; 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 captureScreenshot(page, testInfo, "second-bot-created"); }); diff --git a/apps/web/e2e/onboarding-conversation.spec.ts b/apps/web/e2e/onboarding-conversation.spec.ts index 60f52dd..3639d9a 100644 --- a/apps/web/e2e/onboarding-conversation.spec.ts +++ b/apps/web/e2e/onboarding-conversation.spec.ts @@ -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 // user chose during creation ("Chief") is preserved. 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("Gmail", { exact: true })).toBeVisible(); const connectionCards = page.getByRole("group", { name: / connection$/ }); diff --git a/apps/web/e2e/spaces.spec.ts b/apps/web/e2e/spaces.spec.ts index 06b560a..9af9000 100644 --- a/apps/web/e2e/spaces.spec.ts +++ b/apps/web/e2e/spaces.spec.ts @@ -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 expect(page).not.toHaveURL(/\/onboarding/); 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, }); + await expect(page.locator(".sidebar-unterhaltungen").getByText("Customer project")).toHaveCount(0); await expect( page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }), ).toBeVisible(); @@ -37,6 +38,8 @@ test("spaces stay invisible by default and chat creation requires approval", asy await completeOnboarding(page); 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.getByRole("button", { name: /^Chief/ })).toHaveCount(1); 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 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.press("Enter"); 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 expect(page.getByText("Created", { exact: true })).toBeVisible(); - await expect(sidebar.getByText("Personal", { exact: true })).toBeVisible(); - await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible({ + const projekte = page.locator(".sidebar-projekte"); + const unterhaltungen = page.locator(".sidebar-unterhaltungen"); + await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible({ 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 - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Customer support" }); + const supportSpace = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ + hasText: "Customer support", + }); const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group"); - const supportSpaceId = supportSpaceGroup?.split(":")[1]; + const supportSpaceId = supportSpaceGroup?.split(":")[2]; expect(supportSpaceId).toBeTruthy(); await supportSpace.getByRole("button", { name: "Open Customer support" }).click(); await page.waitForURL(/\/(onboarding|app)/); @@ -79,23 +85,20 @@ test("spaces stay invisible by default and chat creation requires approval", asy .toBe(supportSpaceId); await completeOnboarding(page); - await expect(sidebar.getByText("Personal", { exact: true })).toBeVisible(); - await expect(sidebar.getByText("Customer support", { exact: true })).toBeVisible(); - await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(2); + await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); + await expect(unterhaltungen.getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); await captureScreenshot(page, testInfo, "spaces-sidebar"); - const personalSpace = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Personal" }); - const personalSpaceGroup = await personalSpace.getAttribute("data-sidebar-group"); - const personalSpaceId = personalSpaceGroup?.split(":")[1]; - expect(personalSpaceId).toBeTruthy(); - await personalSpace.getByRole("button", { name: /^Chief/ }).click(); + await page.evaluate((id) => { + window.localStorage.setItem("rakazo:space-id", id!); + }, personalSpaceId); + await page.reload(); await page.waitForURL(/\/app\/[^/]+$/); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id"))) .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 ({ @@ -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. await completeOnboarding(page); await expect(page).toHaveURL(/\/app\//); - await expect(sidebar.getByText("Temporary", { exact: true })).toBeVisible(); - const temporarySpace = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Temporary" }); - await expect(temporarySpace.getByRole("button", { name: /^Chief/ })).toBeVisible(); - await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(2); + const projekte = page.locator(".sidebar-projekte"); + await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible(); + await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1); await captureScreenshot(page, testInfo, "new-space-after-onboarding"); // 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(); const deleteBotDialog = page.getByRole("alertdialog", { name: /Delete Chief/ }); await expect(deleteBotDialog).toBeVisible(); 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. - 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. - 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 page.getByRole("menuitem", { name: "Delete space" }).click(); 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 page.waitForURL(/\/(onboarding|app)/); 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 // forcing onboarding again. - const sideBot = sidebar - .locator('[data-sidebar-group^="space:"]') - .filter({ hasText: "Side" }) - .getByRole("button", { name: /^Chief/ }); + const sideBot = sidebar.getByRole("button", { name: /^Chief/ }); await sideBot.click({ button: "right" }); await page.getByRole("menuitem", { name: "Delete" }).click(); 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 page.reload(); 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( page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }), ).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("alertdialog", { name: "Delete Side?" }) diff --git a/apps/web/e2e/team-computer.spec.ts b/apps/web/e2e/team-computer.spec.ts index 9df2d9d..b6c09af 100644 --- a/apps/web/e2e/team-computer.spec.ts +++ b/apps/web/e2e/team-computer.spec.ts @@ -264,7 +264,7 @@ async function openBot(page: Page, name: string) { .getByRole("complementary") .getByRole("button", { name: new RegExp(`^${name}`) }) .click(); - await expect(page.getByPlaceholder(`Message ${name}`)).toBeVisible(); + await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible(); } async function openComputerPanel(page: Page) { diff --git a/apps/web/e2e/unterhaltungen-sidebar.spec.ts b/apps/web/e2e/unterhaltungen-sidebar.spec.ts new file mode 100644 index 0000000..7cb9f26 --- /dev/null +++ b/apps/web/e2e/unterhaltungen-sidebar.spec.ts @@ -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…"); +}); diff --git a/apps/web/src/components/stage/OoStage.tsx b/apps/web/src/components/stage/OoStage.tsx index 02b8c47..7821ad0 100644 --- a/apps/web/src/components/stage/OoStage.tsx +++ b/apps/web/src/components/stage/OoStage.tsx @@ -170,8 +170,8 @@ export function OoStage({ />
-
-
+ {(hasMessages || phase !== "idle") && ( +
{phaseLabel}
-

+

+ )} + {!hasMessages ? ( +
+

{botName}

-

- {t`1:1 conversation`} - {persona.roleLine ? ` · ${persona.roleLine}` : ""} -

+ {persona.roleLine ? ( +

+ {persona.roleLine} +

+ ) : null}
-
+ ) : null} {awarenessLabel && awarenessEvent ? (
diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index 3703d92..299462a 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -232,7 +232,7 @@ import { import type { SettingsSection } from "./SettingsOverlay"; import { SpaceSearchResults } from "./SpaceSearch"; 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 { ClearConversationDialog, @@ -339,6 +339,14 @@ function readCollapsedSidebarSections(userId: string | null | undefined): Set(null); const [draggedBotId, setDraggedBotId] = useState(null); const [createMenuOpen, setCreateMenuOpen] = useState(false); + const [conversationPickerOpen, setConversationPickerOpen] = useState(false); const [botsSidebarCollapsed, setBotsSidebarCollapsed] = useState(false); const focusPromptAbortRef = useRef(null); const focusPromptBotIdRef = useRef(null); @@ -1555,6 +1564,71 @@ export function ShellPage() { [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( (spaceId: string, path: string) => { setMobileSidebarOpen(false); @@ -1574,6 +1648,23 @@ export function ShellPage() { }, [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 () => { if (savingBotOrderRef.current) return; savingBotOrderRef.current = true; @@ -3059,237 +3150,57 @@ export function ShellPage() {
-
- {/* PROJEKTE SECTION */} -
-
-
{t`Projekte`}
-

- {t`Threads by topic · folders`} -

-
-
- -
-
- {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 ? ( -
-

{t`No projects yet.`}

- -
- ) : 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 ( -
- {group.title ? ( -
- -
- ) : null} - {!collapsed && - group.bots.map((item) => ( - - ))} -
- ); - })} -
-
- {/* TEAM SECTION */} +
-
{t`Team`}
+
{t`Unterhaltungen`}

- {t`Your Oos — open a 1:1 conversation`} + {t`1:1 mit deinen Oos`}

- + + + + + {conversationPickerOpen ? ( + + { + setConversationPickerOpen(false); + setPanel("create"); + }} + onOpenBot={openConversationBot} + /> + + ) : null} +
- {sidebarGroups - .map((group) => ({ - ...group, - bots: group.bots.filter((b) => b.kind === "bot") as any[], - })) - .filter((group) => group.bots.length > 0 || group.emptySpaceId).length === 0 ? ( + {conversationSidebarGroups.length === 0 ? (
-

{t`No Oos in your team yet.`}

+

{t`Noch keine Unterhaltung.`}

) : null} - {sidebarGroups - .map((group) => ({ - ...group, - bots: group.bots.filter((b) => b.kind === "bot") as any[], - })) - .filter((group) => group.bots.length > 0 || group.emptySpaceId) - .map((group) => { + {conversationSidebarGroups.map((group) => { const collapsed = Boolean(group.title) && collapsedSidebarSections.has(group.key); const groupBotIds = group.bots.flatMap((item) => @@ -3443,17 +3354,8 @@ export function ShellPage() { : t`Group conversation ${item.chat.name}` } onClick={() => { - if (item.kind === "bot") { - setActiveNavPillar("team"); - } else { - setActiveNavPillar("projekte"); - } - openSpaceChat( - item.chat.spaceId, - item.kind === "bot" - ? `/app/${item.chat.id}` - : `/app/g/${item.chat.id}`, - ); + setActiveNavPillar("team"); + openSpaceChat(item.chat.spaceId, `/app/${item.chat.id}`); }} onContextMenu={(event) => { if (item.chat.spaceId !== bootstrapMe?.spaceId) return; @@ -3585,6 +3487,214 @@ export function ShellPage() {
); })} +
+ {/* PROJEKTE SECTION */} +
+
+
{t`Projekte`}
+

+ {t`Threads by topic · folders`} +

+
+
+ +
+
+ {projectSidebarGroups.length === 0 ? ( +
+

{t`No projects yet.`}

+ +
+ ) : null} + {projectSidebarGroups.map((group) => { + const collapsed = + Boolean(group.title) && collapsedSidebarSections.has(group.key); + return ( +
+ {group.title ? ( +
+ + {group.canDeleteSpace ? ( + + ) : null} +
+ ) : null} + {!collapsed && + group.bots.map((item) => ( + + ))} +
+ ); + })} +
)} @@ -3929,8 +4039,27 @@ export function ShellPage() { type="button" data-testid="bot-settings-trigger" 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 ? ( + + ) : inGroup ? ( + + ) : null}
{inGroup ? (activeGroup?.name ?? activeSnapshot?.groupName ?? t`Group`) - : (active?.name ?? t`Choose a conversation`)} + : (active?.name ?? t`Unterhaltung wählen`)} - {!inGroup && active ? ( + {!inGroup && active && (composerRunning || active.status !== "idle") ? ( - {active.title ? t`1:1 conversation · ${active.title}` : t`1:1 conversation`} + {composerRunning || active.status === "running" + ? t`Beschäftigt` + : active.status} ) : inGroup && projectSessionKey ? ( - {t`Group conversation`} + {t`Gruppenunterhaltung`} ) : inGroup ? ( - {t`Project · sessions & folders`} + {t`Projekt · Sessions & Ordner`} ) : null}
@@ -6317,14 +6448,8 @@ const Composer = memo(function Composer({ } }} disabled={disabled} - placeholder={ - showComposerPlaceholder - ? activeName - ? t`Frag ${activeName} was Schlaues…` - : t`Frag mich was Schlaues – oder lass uns handeln…` - : undefined - } - aria-label={activeName ? t`Message ${activeName}` : t`Message`} + placeholder={showComposerPlaceholder ? t`Message…` : undefined} + aria-label={t`Message`} role="combobox" aria-autocomplete="list" aria-haspopup="listbox" diff --git a/apps/web/src/pages/shell/bot-picker.tsx b/apps/web/src/pages/shell/bot-picker.tsx index a4fe25d..bc117ff 100644 --- a/apps/web/src/pages/shell/bot-picker.tsx +++ b/apps/web/src/pages/shell/bot-picker.tsx @@ -12,6 +12,80 @@ import { import { Info, Lock, Plus, Users } from "lucide-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 ( +
+ + + + + Kein Bot gefunden + + + {showCreate ? ( + onCreateBot()} + className="gap-2" + > + + ) : null} + {matched.map((bot) => ( + onOpenBot(bot.id)} + className="gap-2" + > + + {bot.name} + + ))} + + + +
+ ); +} + export function BotCreatePicker({ bots, onCreateBot,