fix(web): + Starten for empty projects instead of fallback bot (#92)
* fix(web): show Starten for empty projects instead of fallback bot When a project space had no group conversations, the sidebar fell back to listing 1:1 bots (e.g. Chief from onboarding) under the project name. Bootstrap also redirected /app to the first bot, making it look like an existing conversation. Only show project group chats in Projekte, render + Starten for empty projects, and open the existing create flow (group or bot) on click. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(web): keep empty-project Starten view and route flows correctly Rebased onto main. Guard the Hauptthread auto-redirect when the current space is an empty project so the main Starten CTA stays visible. Split project start into create-bot vs create-group flows (main vs sidebar) and feed home-space bots into the group form when starting from a project. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(web): surface empty-project Starten on Projekte pillar Route bare /app in empty projects to the Projekte view, keep /app on stale bot/group URLs, and show the create-group side panel without an active thread. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(e2e): scope RPC to selected space and tighten Starten specs Pass x-rakazo-space-id from Playwright RPC helpers so archive/list hit the active project space. Trim the group-create Starten test to the side panel flow and navigate to /app after archiving the last bot. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * style(e2e): format empty-project-starten spec Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * fix(e2e): remove project bots so empty spaces stay deletable Archived bots still count as space content and hide the delete-space menu. Use bots/remove in spaces.spec when the test needs a deletable empty project space. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
bc5f41ecb1
commit
23084b460b
73
apps/web/e2e/empty-project-starten.spec.ts
Normal file
73
apps/web/e2e/empty-project-starten.spec.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { completeOnboarding, rpc, signup, unterhaltungenOnly } from "./helpers";
|
||||||
|
|
||||||
|
test("empty project shows Starten instead of a fallback bot", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `empty-project-${stamp}@rakazo.test`, "password12", "Empty Project");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
await page.goto("/app");
|
||||||
|
await page.waitForURL(/\/app\/[^/]+$/);
|
||||||
|
|
||||||
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
|
await projekte.getByTestId("create-project-trigger").click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "New project" });
|
||||||
|
await dialog.getByLabel("Name").fill("Launch");
|
||||||
|
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
|
await page.waitForURL(/\/app(?:\/)?$/);
|
||||||
|
|
||||||
|
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
||||||
|
await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByTestId("project-start-main")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId("project-start-main").click();
|
||||||
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Starten in an onboarded project opens the group create flow", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `project-start-${stamp}@rakazo.test`, "password12", "Project Start");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
|
await projekte.getByTestId("create-project-trigger").click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "New project" });
|
||||||
|
await dialog.getByLabel("Name").fill("Website");
|
||||||
|
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
|
await page.waitForURL(/\/app(?:\/)?$/);
|
||||||
|
await page.goto("/onboarding");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
|
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
||||||
|
|
||||||
|
await projekte.getByRole("button", { name: "Starten", exact: true }).click();
|
||||||
|
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create-group");
|
||||||
|
await expect(page.getByText("New group", { exact: true })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("side-panel").getByRole("button", { name: /^Chief/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("archiving the only bot keeps Starten visible in the project sidebar", async ({ page }) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `project-archive-${stamp}@rakazo.test`, "password12", "Project Archive");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
|
await projekte.getByTestId("create-project-trigger").click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "New project" });
|
||||||
|
await dialog.getByLabel("Name").fill("Side");
|
||||||
|
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
|
await page.waitForURL(/\/app(?:\/)?$/);
|
||||||
|
await page.goto("/onboarding");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
const [chief] = await rpc<Array<{ id: string; name: string }>>(page, "bots/list", {});
|
||||||
|
expect(chief?.name).toMatch(/^Chief/);
|
||||||
|
await rpc(page, "bots/archive", { botId: chief!.id });
|
||||||
|
|
||||||
|
await page.goto("/app");
|
||||||
|
await page.waitForURL(/\/app(?:\/)?$/);
|
||||||
|
await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByTestId("project-start-main")).toBeVisible();
|
||||||
|
});
|
||||||
@ -23,7 +23,11 @@ export function activeBotId(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {
|
export async function rpc<T>(page: Page, procedure: string, body: unknown): Promise<T> {
|
||||||
const response = await page.request.post(`/rpc/${procedure}`, { data: { json: body } });
|
const spaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id"));
|
||||||
|
const response = await page.request.post(`/rpc/${procedure}`, {
|
||||||
|
data: { json: body },
|
||||||
|
headers: spaceId ? { "x-rakazo-space-id": spaceId } : undefined,
|
||||||
|
});
|
||||||
const parsed = (await response.json()) as { json?: T; error?: { message?: string } };
|
const parsed = (await response.json()) as { json?: T; error?: { message?: string } };
|
||||||
if (!response.ok() || parsed.error) {
|
if (!response.ok() || parsed.error) {
|
||||||
throw new Error(`${procedure} ${response.status()}: ${parsed.error?.message ?? "failed"}`);
|
throw new Error(`${procedure} ${response.status()}: ${parsed.error?.message ?? "failed"}`);
|
||||||
|
|||||||
@ -41,7 +41,8 @@ test("Projekte + opens new project dialog instead of group form", async ({ page
|
|||||||
await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount(
|
await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount(
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
await expect(page.locator("main").getByRole("button", { name: "Create an Oo" })).toBeVisible();
|
await expect(page.getByTestId("project-start-main")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("project-start-main")).toHaveText(/Starten/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("spaces stay invisible by default and chat creation requires approval", async ({
|
test("spaces stay invisible by default and chat creation requires approval", async ({
|
||||||
@ -93,16 +94,18 @@ test("spaces stay invisible by default and chat creation requires approval", asy
|
|||||||
const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group");
|
const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group");
|
||||||
const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1];
|
const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1];
|
||||||
expect(supportSpaceId).toBeTruthy();
|
expect(supportSpaceId).toBeTruthy();
|
||||||
await supportSpace.getByRole("button", { name: "Open Customer support" }).click();
|
await supportSpace.getByRole("button", { name: "Select Customer support" }).click();
|
||||||
await page.waitForURL(/\/onboarding/);
|
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(supportSpaceId);
|
.toBe(supportSpaceId);
|
||||||
|
await page.goto("/onboarding");
|
||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
|
|
||||||
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
||||||
|
await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "spaces-sidebar");
|
await captureScreenshot(page, testInfo, "spaces-sidebar");
|
||||||
|
|
||||||
await page.evaluate((id) => {
|
await page.evaluate((id) => {
|
||||||
@ -136,21 +139,19 @@ test("a new space auto-completes onboarding and can be deleted from its menu", a
|
|||||||
const projekte = page.locator(".sidebar-projekte");
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
||||||
|
await expect(projekte.getByRole("button", { name: "Starten", exact: true })).toBeVisible();
|
||||||
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.
|
||||||
const temporaryChief = projekte.getByRole("button", { name: /^Chief/ });
|
const [temporaryChief] = await rpc<Array<{ id: string }>>(page, "bots/list", {});
|
||||||
await temporaryChief.click({ button: "right" });
|
expect(temporaryChief).toBeTruthy();
|
||||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
await rpc(page, "bots/remove", { botId: temporaryChief!.id, deleteMemories: false });
|
||||||
const deleteBotDialog = page.getByRole("alertdialog", { name: /Delete Chief/ });
|
await page.goto("/app");
|
||||||
await expect(deleteBotDialog).toBeVisible();
|
await page.waitForURL(/\/app(?:\/)?$/);
|
||||||
await deleteBotDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
|
||||||
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
|
||||||
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
|
||||||
|
|
||||||
// The emptied space stays selectable instead of trapping the user.
|
// The emptied space stays selectable instead of trapping the user.
|
||||||
await expect(projekte.getByRole("button", { name: "Open Temporary" }).first()).toBeVisible();
|
await expect(projekte.getByRole("button", { name: "Select Temporary" }).first()).toBeVisible();
|
||||||
|
|
||||||
// Deletion is discoverable through the visible Space actions menu.
|
// Deletion is discoverable through the visible Space actions menu.
|
||||||
await projectSpace(projekte, "Temporary")
|
await projectSpace(projekte, "Temporary")
|
||||||
@ -196,20 +197,14 @@ test("deleting the last bot in a space stays in the app after first use", async
|
|||||||
|
|
||||||
// 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 = projekte.getByRole("button", { name: /^Chief/ });
|
const [sideChief] = await rpc<Array<{ id: string }>>(page, "bots/list", {});
|
||||||
await sideBot.click({ button: "right" });
|
expect(sideChief).toBeTruthy();
|
||||||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
await rpc(page, "bots/remove", { botId: sideChief!.id, deleteMemories: false });
|
||||||
const deleteDialog = page.getByRole("alertdialog", { name: /Delete Chief/ });
|
|
||||||
await expect(deleteDialog).toBeVisible();
|
|
||||||
await deleteDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
|
||||||
await expect(sideBot).toHaveCount(0);
|
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await page.reload();
|
await page.goto("/app");
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await expect(projekte.getByRole("button", { name: "Open Side" }).first()).toBeVisible();
|
await expect(projekte.getByRole("button", { name: "Select Side" }).first()).toBeVisible();
|
||||||
await expect(
|
await expect(page.getByTestId("project-start-main")).toBeVisible();
|
||||||
page.locator("main").getByRole("button", { name: "Create new Bot", exact: true }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click();
|
await projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click();
|
||||||
await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible();
|
await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible();
|
||||||
|
|||||||
50
apps/web/src/lib/project-empty-state.test.ts
Normal file
50
apps/web/src/lib/project-empty-state.test.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
isEmptyProjectSpace,
|
||||||
|
isHauptthreadGroup,
|
||||||
|
projectGroupChatsInSpace,
|
||||||
|
} from "./project-empty-state.js";
|
||||||
|
|
||||||
|
describe("isHauptthreadGroup", () => {
|
||||||
|
it("recognizes Hauptthread and Team Chat", () => {
|
||||||
|
expect(isHauptthreadGroup({ id: "1", name: "Hauptthread" })).toBe(true);
|
||||||
|
expect(isHauptthreadGroup({ id: "2", name: "Team Chat" })).toBe(true);
|
||||||
|
expect(isHauptthreadGroup({ id: "3", name: "Launch" })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("projectGroupChatsInSpace", () => {
|
||||||
|
it("returns project groups in a space without Hauptthread", () => {
|
||||||
|
const groups = [
|
||||||
|
{ id: "g1", name: "Launch", spaceId: "space-a" },
|
||||||
|
{ id: "g2", name: "Hauptthread", spaceId: "space-a" },
|
||||||
|
{ id: "g3", name: "Ops", spaceId: "space-b" },
|
||||||
|
];
|
||||||
|
expect(projectGroupChatsInSpace(groups, "space-a").map((group) => group.id)).toEqual(["g1"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isEmptyProjectSpace", () => {
|
||||||
|
const spaces = [
|
||||||
|
{ id: "home", isDefault: true },
|
||||||
|
{ id: "project", isDefault: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
it("is false for the home space", () => {
|
||||||
|
expect(isEmptyProjectSpace("home", spaces, [], "home")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is true for a project space without group chats", () => {
|
||||||
|
expect(isEmptyProjectSpace("project", spaces, [], "home")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is false when the project space already has a group chat", () => {
|
||||||
|
const groups = [{ id: "g1", name: "Launch", spaceId: "project" }];
|
||||||
|
expect(isEmptyProjectSpace("project", spaces, groups, "home")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores Hauptthread when deciding emptiness", () => {
|
||||||
|
const groups = [{ id: "g1", name: "Hauptthread", spaceId: "project" }];
|
||||||
|
expect(isEmptyProjectSpace("project", spaces, groups, "home")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
apps/web/src/lib/project-empty-state.ts
Normal file
26
apps/web/src/lib/project-empty-state.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { Group, Space } from "@rakazo/contracts";
|
||||||
|
|
||||||
|
export function isHauptthreadGroup(group: Pick<Group, "id" | "name">): boolean {
|
||||||
|
return group.name === "Hauptthread" || group.name === "Team Chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project-scoped group chats in a space (excludes Hauptthread). */
|
||||||
|
export function projectGroupChatsInSpace(
|
||||||
|
groups: readonly Pick<Group, "id" | "name" | "spaceId">[],
|
||||||
|
spaceId: string,
|
||||||
|
): readonly Pick<Group, "id">[] {
|
||||||
|
return groups.filter((group) => group.spaceId === spaceId && !isHauptthreadGroup(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Non-default project space with no project group conversations yet. */
|
||||||
|
export function isEmptyProjectSpace(
|
||||||
|
spaceId: string | null | undefined,
|
||||||
|
spaces: readonly Pick<Space, "id" | "isDefault">[],
|
||||||
|
groups: readonly Pick<Group, "id" | "name" | "spaceId">[],
|
||||||
|
homeSpaceId: string | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!spaceId || !homeSpaceId || spaceId === homeSpaceId) return false;
|
||||||
|
const space = spaces.find((item) => item.id === spaceId);
|
||||||
|
if (!space || space.isDefault) return false;
|
||||||
|
return projectGroupChatsInSpace(groups, spaceId).length === 0;
|
||||||
|
}
|
||||||
@ -214,6 +214,7 @@ import {
|
|||||||
revokePendingAttachmentPreviews,
|
revokePendingAttachmentPreviews,
|
||||||
} from "../lib/pending-attachments";
|
} from "../lib/pending-attachments";
|
||||||
import { markAfterPaint, markOnce } from "../lib/performance";
|
import { markAfterPaint, markOnce } from "../lib/performance";
|
||||||
|
import { isEmptyProjectSpace } from "../lib/project-empty-state";
|
||||||
import { readProjectSidebarOrder, writeProjectSidebarOrder } from "../lib/project-sidebar-order";
|
import { readProjectSidebarOrder, writeProjectSidebarOrder } from "../lib/project-sidebar-order";
|
||||||
import { quoteDraftForSelection } from "../lib/quote-selection";
|
import { quoteDraftForSelection } from "../lib/quote-selection";
|
||||||
import { clearSpaceSelection, rpc, selectedSpaceId, selectSpace } from "../lib/rpc";
|
import { clearSpaceSelection, rpc, selectedSpaceId, selectSpace } from "../lib/rpc";
|
||||||
@ -851,7 +852,15 @@ export function ShellPage() {
|
|||||||
const inGroup = Boolean(groupId);
|
const inGroup = Boolean(groupId);
|
||||||
const projectSessionKey = searchParams.get("s");
|
const projectSessionKey = searchParams.get("s");
|
||||||
const voiceEntry = searchParams.get("voice") === "1";
|
const voiceEntry = searchParams.get("voice") === "1";
|
||||||
const active = inGroup ? undefined : (bots.find((b) => b.id === botId) ?? bots[0]);
|
const currentSpaceId = selectedSpaceId() ?? bootstrapMe?.spaceId;
|
||||||
|
const homeSpaceId = spaces.find((space) => space.isDefault)?.id ?? bootstrapMe?.spaceId;
|
||||||
|
const emptyProjectSpace = useMemo(
|
||||||
|
() => isEmptyProjectSpace(currentSpaceId, spaces, groups, homeSpaceId),
|
||||||
|
[currentSpaceId, groups, homeSpaceId, spaces],
|
||||||
|
);
|
||||||
|
const active = inGroup
|
||||||
|
? undefined
|
||||||
|
: (bots.find((b) => b.id === botId) ?? (emptyProjectSpace && !botId ? undefined : bots[0]));
|
||||||
const activeGroup = groups.find((group) => group.id === groupId);
|
const activeGroup = groups.find((group) => group.id === groupId);
|
||||||
const activeProjectSection = activeGroup?.sectionId
|
const activeProjectSection = activeGroup?.sectionId
|
||||||
? botSections.find((section) => section.id === activeGroup.sectionId)
|
? botSections.find((section) => section.id === activeGroup.sectionId)
|
||||||
@ -1039,13 +1048,28 @@ export function ShellPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentGroupId = routeGroupId.current;
|
const currentGroupId = routeGroupId.current;
|
||||||
|
const currentBotId = routeBotId.current;
|
||||||
|
const refreshHomeSpaceId =
|
||||||
|
navigation.spaces.find((space) => space.isDefault)?.id ?? selectedSpaceId() ?? undefined;
|
||||||
|
const refreshSpaceId = selectedSpaceId() ?? refreshHomeSpaceId;
|
||||||
|
const emptyProject = isEmptyProjectSpace(
|
||||||
|
refreshSpaceId,
|
||||||
|
navigation.spaces,
|
||||||
|
groupList,
|
||||||
|
refreshHomeSpaceId,
|
||||||
|
);
|
||||||
|
if (emptyProject) {
|
||||||
|
if (currentGroupId || (currentBotId && !list.some((bot) => bot.id === currentBotId))) {
|
||||||
|
navigate("/app", { replace: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (currentGroupId) {
|
if (currentGroupId) {
|
||||||
if (!groupList.some((group) => group.id === currentGroupId)) {
|
if (!groupList.some((group) => group.id === currentGroupId)) {
|
||||||
navigate(firstThreadRoute(list, groupList), { replace: true });
|
navigate(firstThreadRoute(list, groupList), { replace: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentBotId = routeBotId.current;
|
|
||||||
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
||||||
navigate(firstThreadRoute(list, groupList), { replace: true });
|
navigate(firstThreadRoute(list, groupList), { replace: true });
|
||||||
}
|
}
|
||||||
@ -1268,6 +1292,21 @@ export function ShellPage() {
|
|||||||
navigate("/onboarding", { replace: true });
|
navigate("/onboarding", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const bootstrapHomeSpaceId =
|
||||||
|
bootstrap.spaces.find((space) => space.isDefault)?.id ?? bootstrap.me.spaceId;
|
||||||
|
const bootstrapSpaceId = selectedSpaceId() ?? bootstrap.me.spaceId;
|
||||||
|
const emptyProject = isEmptyProjectSpace(
|
||||||
|
bootstrapSpaceId,
|
||||||
|
bootstrap.spaces,
|
||||||
|
groupList,
|
||||||
|
bootstrapHomeSpaceId,
|
||||||
|
);
|
||||||
|
if (emptyProject) {
|
||||||
|
if (groupId || (botId && !bootstrap.bots.some((bot) => bot.id === botId))) {
|
||||||
|
navigate("/app", { replace: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
if (!groupList.some((group) => group.id === groupId)) {
|
if (!groupList.some((group) => group.id === groupId)) {
|
||||||
navigate(firstThreadRoute(bootstrap.bots, groupList), { replace: true });
|
navigate(firstThreadRoute(bootstrap.bots, groupList), { replace: true });
|
||||||
@ -1663,8 +1702,6 @@ export function ShellPage() {
|
|||||||
[groups],
|
[groups],
|
||||||
);
|
);
|
||||||
|
|
||||||
const homeSpaceId = spaces.find((space) => space.isDefault)?.id ?? bootstrapMe?.spaceId;
|
|
||||||
|
|
||||||
const homeSpaceBots = useMemo(() => {
|
const homeSpaceBots = useMemo(() => {
|
||||||
if (!homeSpaceId) return bots;
|
if (!homeSpaceId) return bots;
|
||||||
const home = spaces.find((space) => space.id === homeSpaceId);
|
const home = spaces.find((space) => space.id === homeSpaceId);
|
||||||
@ -1710,10 +1747,7 @@ export function ShellPage() {
|
|||||||
)
|
)
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
const botsInSpace = sidebarGroups.flatMap((group) =>
|
const projectBots = dedupeSidebarBots(groupsInSpace);
|
||||||
group.spaceId === space.id ? group.bots.filter((item) => item.kind === "bot") : [],
|
|
||||||
);
|
|
||||||
const projectBots = dedupeSidebarBots(groupsInSpace.length > 0 ? groupsInSpace : botsInSpace);
|
|
||||||
entries.push({
|
entries.push({
|
||||||
key:
|
key:
|
||||||
projectBots.length > 0 ? `project-space:${space.id}` : `project-space:${space.id}:empty`,
|
projectBots.length > 0 ? `project-space:${space.id}` : `project-space:${space.id}:empty`,
|
||||||
@ -1806,6 +1840,14 @@ export function ShellPage() {
|
|||||||
[bootstrapMe?.spaceId, navigate],
|
[bootstrapMe?.spaceId, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openProjectStart = useCallback(
|
||||||
|
(spaceId: string, flow: "create" | "group") => {
|
||||||
|
setActiveNavPillar("projekte");
|
||||||
|
openSpaceChat(spaceId, `/app?project=start&flow=${flow}`);
|
||||||
|
},
|
||||||
|
[openSpaceChat],
|
||||||
|
);
|
||||||
|
|
||||||
const openConversationBot = useCallback(
|
const openConversationBot = useCallback(
|
||||||
(botId: string) => {
|
(botId: string) => {
|
||||||
setConversationPickerOpen(false);
|
setConversationPickerOpen(false);
|
||||||
@ -2031,6 +2073,42 @@ export function ShellPage() {
|
|||||||
void jumpToMessage({ groupId, messageId: projectSessionKey });
|
void jumpToMessage({ groupId, messageId: projectSessionKey });
|
||||||
}, [groupId, inGroup, projectSessionKey]);
|
}, [groupId, inGroup, projectSessionKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!botId && !groupId && emptyProjectSpace && initialBotsLoaded) {
|
||||||
|
setActiveNavPillar("projekte");
|
||||||
|
}
|
||||||
|
}, [botId, emptyProjectSpace, groupId, initialBotsLoaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.get("project") !== "start" || !emptyProjectSpace || !initialBotsLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActiveNavPillar("projekte");
|
||||||
|
const flow = searchParams.get("flow");
|
||||||
|
setPanel(
|
||||||
|
flow === "create"
|
||||||
|
? "create"
|
||||||
|
: flow === "group" && homeSpaceBots.length > 0
|
||||||
|
? "create-group"
|
||||||
|
: flow === "group"
|
||||||
|
? "create"
|
||||||
|
: bots.length === 0
|
||||||
|
? "create"
|
||||||
|
: "create-group",
|
||||||
|
);
|
||||||
|
const next = new URLSearchParams(searchParams);
|
||||||
|
next.delete("project");
|
||||||
|
next.delete("flow");
|
||||||
|
setSearchParams(next, { replace: true });
|
||||||
|
}, [
|
||||||
|
bots.length,
|
||||||
|
emptyProjectSpace,
|
||||||
|
homeSpaceBots.length,
|
||||||
|
initialBotsLoaded,
|
||||||
|
searchParams,
|
||||||
|
setSearchParams,
|
||||||
|
]);
|
||||||
|
|
||||||
const activeSnapshot = inGroup
|
const activeSnapshot = inGroup
|
||||||
? snapshot?.groupId === groupId
|
? snapshot?.groupId === groupId
|
||||||
? snapshot
|
? snapshot
|
||||||
@ -3213,7 +3291,7 @@ export function ShellPage() {
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active && !activeGroup && initialBotsLoaded) {
|
if (!active && !activeGroup && initialBotsLoaded && !emptyProjectSpace) {
|
||||||
const hauptthread = groups.find((g) => g.name === "Hauptthread" || g.name === "Team Chat");
|
const hauptthread = groups.find((g) => g.name === "Hauptthread" || g.name === "Team Chat");
|
||||||
if (hauptthread) {
|
if (hauptthread) {
|
||||||
navigate(`/app/g/${hauptthread.id}`);
|
navigate(`/app/g/${hauptthread.id}`);
|
||||||
@ -3227,7 +3305,7 @@ export function ShellPage() {
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [active, activeGroup, initialBotsLoaded, groups, bots, navigate]);
|
}, [active, activeGroup, emptyProjectSpace, initialBotsLoaded, groups, bots, navigate]);
|
||||||
|
|
||||||
const userName = session.data?.user.name ?? t`You`;
|
const userName = session.data?.user.name ?? t`You`;
|
||||||
const initials = userName
|
const initials = userName
|
||||||
@ -3842,13 +3920,7 @@ export function ShellPage() {
|
|||||||
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 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"
|
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 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={() => {
|
onClick={() => {
|
||||||
if (group.emptySpaceId) {
|
if (group.emptySpaceId) {
|
||||||
const space = spaces.find(
|
openSpaceChat(group.emptySpaceId, "/app");
|
||||||
(item) => item.id === group.emptySpaceId,
|
|
||||||
);
|
|
||||||
openSpaceChat(
|
|
||||||
group.emptySpaceId,
|
|
||||||
space?.hasContent ? "/app" : "/onboarding",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toggleSidebarSection(group.key);
|
toggleSidebarSection(group.key);
|
||||||
@ -3888,7 +3960,7 @@ export function ShellPage() {
|
|||||||
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
|
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
|
||||||
aria-label={
|
aria-label={
|
||||||
group.emptySpaceId
|
group.emptySpaceId
|
||||||
? t`Open ${group.title}`
|
? t`Select ${group.title}`
|
||||||
: collapsed
|
: collapsed
|
||||||
? t`Expand ${group.title}`
|
? t`Expand ${group.title}`
|
||||||
: t`Collapse ${group.title}`
|
: t`Collapse ${group.title}`
|
||||||
@ -3931,6 +4003,20 @@ export function ShellPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!collapsed && group.emptySpaceId ? (
|
||||||
|
<div className="px-3 pb-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full text-ui-label"
|
||||||
|
data-testid={`project-start-${group.spaceId}`}
|
||||||
|
onClick={() => openProjectStart(group.spaceId, "group")}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 size-icon-sm" />
|
||||||
|
{t`Starten`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{!collapsed &&
|
{!collapsed &&
|
||||||
group.bots.map((item) => (
|
group.bots.map((item) => (
|
||||||
<button
|
<button
|
||||||
@ -4394,7 +4480,17 @@ export function ShellPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!active && !activeGroup && initialBotsLoaded ? (
|
{!active && !activeGroup && emptyProjectSpace && initialBotsLoaded ? (
|
||||||
|
<div className="grid flex-1 place-items-center">
|
||||||
|
<Button
|
||||||
|
data-testid="project-start-main"
|
||||||
|
onClick={() => currentSpaceId && openProjectStart(currentSpaceId, "create")}
|
||||||
|
>
|
||||||
|
<Plus aria-hidden="true" className="size-icon-md" />
|
||||||
|
{t`Starten`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : !active && !activeGroup && initialBotsLoaded ? (
|
||||||
<div className="grid flex-1 place-items-center">
|
<div className="grid flex-1 place-items-center">
|
||||||
<Button onClick={() => setShowCreateBotModal(true)}>
|
<Button onClick={() => setShowCreateBotModal(true)}>
|
||||||
<Plus aria-hidden="true" className="size-icon-md" />
|
<Plus aria-hidden="true" className="size-icon-md" />
|
||||||
@ -4563,12 +4659,24 @@ export function ShellPage() {
|
|||||||
data-panel={panel ?? "closed"}
|
data-panel={panel ?? "closed"}
|
||||||
style={{
|
style={{
|
||||||
width:
|
width:
|
||||||
panel && (active || activeGroup || panel === "create" || panel === "browser")
|
panel &&
|
||||||
|
(active ||
|
||||||
|
activeGroup ||
|
||||||
|
panel === "create" ||
|
||||||
|
panel === "create-group" ||
|
||||||
|
panel === "browser")
|
||||||
? rightSidebarWidth
|
? rightSidebarWidth
|
||||||
: 0,
|
: 0,
|
||||||
}}
|
}}
|
||||||
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl md:relative ${
|
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl md:relative ${
|
||||||
panel && (active || activeGroup || panel === "create" || panel === "browser")
|
panel &&
|
||||||
|
(
|
||||||
|
active ||
|
||||||
|
activeGroup ||
|
||||||
|
panel === "create" ||
|
||||||
|
panel === "create-group" ||
|
||||||
|
panel === "browser"
|
||||||
|
)
|
||||||
? "max-w-[384px] md:max-w-none opacity-100"
|
? "max-w-[384px] md:max-w-none opacity-100"
|
||||||
: "pointer-events-none w-0 opacity-0 md:opacity-0 md:w-0"
|
: "pointer-events-none w-0 opacity-0 md:opacity-0 md:w-0"
|
||||||
} ${!isResizingRight ? "transition-[width,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]" : ""}`}
|
} ${!isResizingRight ? "transition-[width,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]" : ""}`}
|
||||||
@ -4577,7 +4685,7 @@ export function ShellPage() {
|
|||||||
onPointerDown={startResizingRight}
|
onPointerDown={startResizingRight}
|
||||||
className="absolute -left-2 top-0 bottom-0 w-4 cursor-col-resize z-50 hidden md:block"
|
className="absolute -left-2 top-0 bottom-0 w-4 cursor-col-resize z-50 hidden md:block"
|
||||||
/>
|
/>
|
||||||
{panel && (active || activeGroup || panel === "create") ? (
|
{panel && (active || activeGroup || panel === "create" || panel === "create-group") ? (
|
||||||
<div className="rk-scroll h-full w-full overflow-y-auto px-5 lg:px-7 2xl:px-9 py-[17px]">
|
<div className="rk-scroll h-full w-full overflow-y-auto px-5 lg:px-7 2xl:px-9 py-[17px]">
|
||||||
{panel !== "routine" &&
|
{panel !== "routine" &&
|
||||||
panel !== "create" &&
|
panel !== "create" &&
|
||||||
@ -4798,7 +4906,7 @@ export function ShellPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
{panel === "create-group" ? (
|
{panel === "create-group" ? (
|
||||||
<CreateGroupForm
|
<CreateGroupForm
|
||||||
bots={bots}
|
bots={emptyProjectSpace ? (homeSpaceBots as Bot[]) : bots}
|
||||||
onCancel={() => setPanel(null)}
|
onCancel={() => setPanel(null)}
|
||||||
onCreate={(input) => createGroup(input)}
|
onCreate={(input) => createGroup(input)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user