Merge pull request #2 from cynarAI/cursor/onboarding-nav-settings-728e

Settings-first navigation: integrations and live trading banner
This commit is contained in:
cynarAI 2026-09-21 19:18:50 +02:00 committed by GitHub
commit b9c0bace03
24 changed files with 384 additions and 302 deletions

View File

@ -50,8 +50,8 @@ test("logout protects bot deep links and sign-in restores the session", async ({
const protectedBotPath = new URL(page.url()).pathname;
await expect(page.getByPlaceholder("Message Chief")).toBeVisible();
await page.getByRole("button", { name: new RegExp(userName, "i") }).click();
await expect(page.getByRole("button", { name: "Settings", exact: true })).toBeVisible();
await expect(page.getByTestId("settings-entry")).toBeVisible();
await page.getByTestId("user-menu-trigger").click();
await expect(page.getByRole("button", { name: "Usage", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Log out" })).toBeVisible();
await expect(
@ -152,8 +152,7 @@ test("changes and recovers an email password", async ({ page }, testInfo) => {
await completeOnboarding(page);
await page.waitForURL(/\/app\/[^/]+$/);
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByTestId("settings-entry").click();
const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible();
await expect(page.getByTestId("sidebar-search").locator("input")).toHaveAttribute(

View File

@ -3,6 +3,7 @@ import {
activeBotId,
captureScreenshot,
completeOnboarding,
openIntegrations,
realSandboxTimeout,
rpc,
signup,
@ -134,7 +135,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
await expect(page.getByText("Monday briefing")).toBeVisible();
await captureScreenshot(page, testInfo, "10-routine-created");
await page.getByText("Integrations").click();
await openIntegrations(page);
await expect(page.getByPlaceholder("Search apps")).toBeVisible();
const featured = page.getByTestId("featured-connectors");
await expect(featured).toContainText(
@ -178,8 +179,8 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
await expect(detail.getByLabel("Account label").nth(1)).toHaveValue("Work");
await captureScreenshot(page, testInfo, "11a2-multi-account-plugins");
await page.getByRole("button", { name: "Close integrations" }).click();
await page.getByText("Integrations").click();
await page.getByRole("button", { name: "Close integrations settings" }).click();
await openIntegrations(page);
await expect(page.getByPlaceholder("Search apps")).toBeVisible();
const gmailTileAgain = page.getByTestId("connection-tile-gmail");
await expect(gmailTileAgain.getByRole("button", { name: "Added", exact: true })).toBeVisible();
@ -296,7 +297,7 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
).toBeVisible();
await captureScreenshot(page, testInfo, "11d-provider-emulators");
await page.getByRole("button", { name: "Close integrations" }).click();
await page.getByRole("button", { name: "Close integrations settings" }).click();
await page.getByText("Chief").first().click();
const gear = page.getByRole("button", { name: "Show settings" });

View File

@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, signup } from "./helpers";
import { captureScreenshot, completeOnboarding, openIntegrations, signup } from "./helpers";
test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Executor, Treg order", async ({
page,
@ -8,7 +8,7 @@ test("advanced GraphQL install shows Add GraphQL in MCP, OpenAPI, GraphQL, Execu
await signup(page, `graphql-source-${stamp}@rakazo.test`, "password12", `GraphQL ${stamp}`);
await completeOnboarding(page);
await page.getByText("Integrations").click();
await openIntegrations(page);
await expect(page.getByPlaceholder("Search apps")).toBeVisible();
await expect(page.getByRole("button", { name: "Add GraphQL", exact: true })).toBeHidden();

View File

@ -12,6 +12,23 @@ async function createBot(page: Page, name: string) {
return createNamedBot(page, name);
}
test("start group conversation lives under Conversations, not Projekte", async ({ page }) => {
const stamp = Date.now();
await signup(page, `group-ia-${stamp}@rakazo.test`, "password12", "Group IA");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const conversations = page.locator(".sidebar-conversations");
await expect(conversations.getByText("Conversations", { exact: true })).toBeVisible();
const startGroup = conversations.getByTestId("start-group-conversation");
await expect(startGroup).toBeVisible();
await expect(page.locator(".sidebar-projekte").getByTestId("start-group-conversation")).toHaveCount(
0,
);
await expect(page.locator(".sidebar-projekte")).not.toContainText("Start a group conversation");
});
test("create group from + and see two bots in one transcript", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `group-${stamp}@rakazo.test`, "password12", "Group E2E");

View File

@ -127,10 +127,9 @@ export async function createBotFromPicker(
/** Open the user Settings overlay, optionally switching to a sidebar section. */
export async function openUserSettings(
page: Page,
section?: "general" | "models" | "memory" | "voice" | "usage" | "computer" | "updates",
section?: "general" | "integrations" | "models" | "memory" | "voice" | "usage" | "computer" | "updates",
) {
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByTestId("settings-entry").click();
const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible();
if (section && section !== "general") {
@ -139,6 +138,11 @@ export async function openUserSettings(
return settings;
}
/** Open Integrations inside the Settings overlay. */
export async function openIntegrations(page: Page) {
return openUserSettings(page, "integrations");
}
/** Create a named bot via RPC for test setup (skips the + picker). */
export async function createNamedBot(
page: Page,

View File

@ -245,8 +245,7 @@ test("configured server owners manage providers from settings", async ({ page },
await signup(page, `configured-owner-${Date.now()}@rakazo.test`, "password12", "Server Owner");
await expect(page.getByRole("heading", { name: "Server integrations" })).toBeHidden();
await completeOnboarding(page);
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByTestId("settings-entry").click();
const settings = page.getByTestId("user-settings");
const link = settings.getByRole("link", { name: "Server integrations", exact: true });
await expect(link).toBeVisible();

View File

@ -83,7 +83,8 @@ test("connects an MCP server through the OAuth popup callback", async ({ page },
});
});
await page.getByText("Integrations", { exact: true }).click();
await page.getByTestId("settings-entry").click();
await page.getByTestId("settings-nav-integrations").click();
await page.getByTestId("integrations-advanced").evaluate((element) => {
(element as HTMLDetailsElement).open = true;
});

View File

@ -9,12 +9,12 @@ test("settings shell is two-pane and deep-links Models Memory Voice Usage", asyn
await signup(page, `settings-shell-${stamp}@rakazo.test`, "password12", userName);
await completeOnboarding(page);
await expect(page.getByTestId("settings-entry")).toBeVisible();
await page.getByTestId("user-menu-trigger").click();
const menu = page.locator('[data-slot="popover-content"]');
await expect(menu.getByRole("button", { name: "Settings", exact: true })).toBeVisible();
await expect(menu.getByRole("button", { name: "Usage", exact: true })).toBeVisible();
await expect(menu.getByRole("button", { name: "Log out", exact: true })).toBeVisible();
await expect(menu.getByRole("button", { name: "Models", exact: true })).toHaveCount(0);
await expect(menu.getByRole("button", { name: "Settings", exact: true })).toHaveCount(0);
await captureScreenshot(page, testInfo, "settings-account-menu-lean");
await page.keyboard.press("Escape");
@ -25,9 +25,16 @@ test("settings shell is two-pane and deep-links Models Memory Voice Usage", asyn
"page",
);
await expect(settings.getByRole("heading", { name: "General", exact: true })).toBeVisible();
await expect(settings.getByTestId("live-trading-banner")).toBeVisible();
await expect(settings.getByRole("heading", { name: "Account", exact: true })).toBeVisible();
await captureScreenshot(page, testInfo, "settings-shell-general");
await settings.getByTestId("settings-nav-integrations").click();
await expect(settings).toHaveAttribute("data-settings-section", "integrations");
await expect(settings.getByRole("heading", { name: "Integrations", exact: true })).toBeVisible();
await expect(settings.getByTestId("integrations-settings")).toBeVisible();
await captureScreenshot(page, testInfo, "settings-shell-integrations");
await settings.getByTestId("settings-nav-models").click();
await expect(settings).toHaveAttribute("data-settings-section", "models");
await expect(settings.getByRole("heading", { name: "Models", exact: true })).toBeVisible();

View File

@ -49,8 +49,7 @@ test("account settings appearance control switches to light mode", async ({ page
await signup(page, `ui-appearance-${stamp}@rakazo.test`, "password12", "Appearance QA");
await completeOnboarding(page, testInfo);
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByTestId("settings-entry").click();
const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible();
await expect(settings.getByRole("heading", { name: "Appearance", exact: true })).toBeVisible();
@ -84,8 +83,7 @@ test("account settings appearance control switches to light mode", async ({ page
await expect(inlinePath).toHaveCSS("color", "rgb(26, 26, 26)");
await captureScreenshot(page, testInfo, "inline-code-light");
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByTestId("settings-entry").click();
await expect(settings).toBeVisible();
await settings.getByTestId("ui-appearance-dark").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
@ -114,9 +112,9 @@ test("sidebar bot rows hover with the same tone as the integrations row", async
const chiefBg = await readChiefBg();
await captureScreenshot(page, testInfo, "sidebar-row-hover");
const integrations = sidebar.getByRole("button", { name: "Integrations", exact: true });
await integrations.hover();
const settingsEntry = sidebar.getByTestId("settings-entry");
await settingsEntry.hover();
await expect
.poll(() => integrations.evaluate((el) => getComputedStyle(el).backgroundColor))
.poll(() => settingsEntry.evaluate((el) => getComputedStyle(el).backgroundColor))
.toBe(chiefBg);
});

View File

@ -77,8 +77,7 @@ test("account settings language picker includes Russian and persists it", async
await signup(page, `ui-locale-ru-${stamp}@rakazo.test`, "password12", "Locale QA");
await completeOnboarding(page, testInfo);
await page.getByTestId("user-menu-trigger").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByTestId("settings-entry").click();
const settings = page.getByTestId("user-settings");
await expect(settings).toBeVisible();

View File

@ -477,6 +477,17 @@
"Server URL": "Server-URL",
"Set up voice to call": "Stimme für Anrufe einrichten",
"Settings": "Einstellungen",
"Conversations": "Unterhaltungen",
"One-to-one and group threads": "Einzel- und Gruppengespräche",
"No conversations yet.": "Noch keine Unterhaltungen.",
"Add an Oo": "Oo hinzufügen",
"Start a group conversation": "Gruppengespräch starten",
"No projects yet.": "Noch keine Projekte.",
"Create a project": "Projekt erstellen",
"Threads by topic · folders": "Threads nach Thema · Ordner",
"Discover now": "Jetzt entdecken",
"Live Trading": "Live Trading",
"Close integrations settings": "Integrationen-Einstellungen schließen",
"Settings: General": "Einstellungen: Allgemein",
"Settings: Usage": "Einstellungen: Nutzung",
"Setup help": "Einrichtungshilfe",

View File

@ -200,9 +200,10 @@ msgstr "Aktionen für {0}"
msgid "Actions for space"
msgstr ""
#: src/pages/AccountSettingsOverlay.tsx:76
#: src/pages/RoutineEditor.tsx:297
msgid "Active"
msgstr ""
msgstr "Aktiv"
#: src/pages/ModelSettingsOverlay.tsx:393
msgid "Active model"
@ -220,6 +221,9 @@ msgid "Add"
msgstr "Hinzufügen"
#: src/components/ApprovalRulesSettings.tsx:151
msgid "Add an Oo"
msgstr "Oo hinzufügen"
msgid "Add a model in Settings to use this."
msgstr ""
@ -814,6 +818,10 @@ msgstr "Bildvorschau schließen"
msgid "Close integrations"
msgstr "Integrationen schließen"
#: src/pages/SettingsOverlay.tsx:108
msgid "Close integrations settings"
msgstr "Integrationen-Einstellungen schließen"
#: src/pages/McpServersOverlay.tsx:258
msgid "Close MCP servers"
msgstr "MCP-Server schließen"
@ -1017,6 +1025,9 @@ msgstr "Die Verbindung zu {0} steht noch aus. Du kannst dieses Fenster schließe
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Unterhaltungen"
msgid "Context limit"
msgstr "Kontextlimit"
@ -1422,6 +1433,10 @@ msgstr "Neue Gruppe erstellen"
msgid "Create new Space"
msgstr "Neuen Space erstellen"
#: src/pages/Shell.tsx:3093
msgid "Create a project"
msgstr "Projekt erstellen"
#: src/pages/RoutineEditor.tsx:122
#: src/pages/RoutineEditor.tsx:123
msgid "Create Routine"
@ -2098,6 +2113,10 @@ msgstr ""
msgid "Listen"
msgstr ""
#: src/pages/AccountSettingsOverlay.tsx:72
msgid "Live Trading"
msgstr "Live Trading"
#: src/pages/ExternalConversationSettings.tsx:93
msgid "Listening"
msgstr ""
@ -2522,6 +2541,13 @@ msgstr "Noch keine Nachrichten mit {peerBotName}."
msgid "No model catalog is available."
msgstr "Kein Modellkatalog verfügbar."
#: src/pages/Shell.tsx:3086
msgid "No conversations yet."
msgstr "Noch keine Unterhaltungen."
msgid "No projects yet."
msgstr "Noch keine Projekte."
#: src/pages/ModelSettingsOverlay.tsx:453
msgid "No providers found."
msgstr "Keine Anbieter gefunden."
@ -3402,6 +3428,9 @@ msgid "Speaking…"
msgstr "Spricht…"
#: src/components/teach/TeachComputerOverlay.tsx:250
msgid "Start a group conversation"
msgstr "Gruppengespräch starten"
msgid "Start recording"
msgstr "Aufnahme starten"
@ -3595,6 +3624,9 @@ msgid "To:"
msgstr "An:"
#: src/pages/PluginsOverlay.tsx:1111
msgid "Threads by topic · folders"
msgstr "Threads nach Thema · Ordner"
msgid "Tool sources"
msgstr "Toolquellen"
@ -3864,11 +3896,18 @@ msgstr "Hilfe & Vibe-Passung"
msgid "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot."
msgstr "Vertrau uns: Es sind viele von uns. Einfach anfangen — Thermomix oder harte Mathe, gleicher ruhiger Einstieg — mit Experten-Spezialisten wenn du sie brauchst, kein mittelmäßiger Alleskönner."
#: src/pages/AccountSettingsOverlay.tsx:88
msgid "Discover now"
msgstr "Jetzt entdecken"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx:96
msgid "Discovery"
msgstr "Entdeckung"
#: src/pages/Onboarding.tsx:428
msgid "One-to-one and group threads"
msgstr "Einzel- und Gruppengespräche"
msgid "One connection — then we take it from here"
msgstr "Eine Verbindung — danach übernehmen wir"

View File

@ -220,6 +220,9 @@ msgid "Add"
msgstr "Add"
#: src/components/ApprovalRulesSettings.tsx:151
msgid "Add an Oo"
msgstr "Add an Oo"
msgid "Add a model in Settings to use this."
msgstr "Add a model in Settings to use this."
@ -814,6 +817,10 @@ msgstr "Close image preview"
msgid "Close integrations"
msgstr "Close integrations"
#: src/pages/SettingsOverlay.tsx:108
msgid "Close integrations settings"
msgstr "Close integrations settings"
#: src/pages/McpServersOverlay.tsx:258
msgid "Close MCP servers"
msgstr "Close MCP servers"
@ -1017,6 +1024,9 @@ msgstr "Connection to {0} is still pending. You can close this and check again."
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Conversations"
msgid "Context limit"
msgstr "Context limit"
@ -1422,6 +1432,10 @@ msgstr "Create new Group"
msgid "Create new Space"
msgstr "Create new Space"
#: src/pages/Shell.tsx:3093
msgid "Create a project"
msgstr "Create a project"
#: src/pages/RoutineEditor.tsx:122
#: src/pages/RoutineEditor.tsx:123
msgid "Create Routine"
@ -2098,6 +2112,10 @@ msgstr "Link a chat app"
msgid "Listen"
msgstr "Listen"
#: src/pages/AccountSettingsOverlay.tsx:72
msgid "Live Trading"
msgstr "Live Trading"
#: src/pages/ExternalConversationSettings.tsx:93
msgid "Listening"
msgstr "Listening"
@ -2523,6 +2541,12 @@ msgid "No model catalog is available."
msgstr "No model catalog is available."
#: src/pages/ModelSettingsOverlay.tsx:453
msgid "No conversations yet."
msgstr "No conversations yet."
msgid "No projects yet."
msgstr "No projects yet."
msgid "No providers found."
msgstr "No providers found."
@ -3402,6 +3426,9 @@ msgid "Speaking…"
msgstr "Speaking…"
#: src/components/teach/TeachComputerOverlay.tsx:250
msgid "Start a group conversation"
msgstr "Start a group conversation"
msgid "Start recording"
msgstr "Start recording"
@ -3595,6 +3622,9 @@ msgid "To:"
msgstr "To:"
#: src/pages/PluginsOverlay.tsx:1111
msgid "Threads by topic · folders"
msgstr "Threads by topic · folders"
msgid "Tool sources"
msgstr "Tool sources"
@ -3864,11 +3894,18 @@ msgstr "Help & vibe fit"
msgid "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot."
msgstr "Trust us: there are many of us. Simple to start — Thermomix or hard math, same calm entry — with expert-depth specialists when you need them, not one mediocre everything-bot."
#: src/pages/AccountSettingsOverlay.tsx:88
msgid "Discover now"
msgstr "Discover now"
#: src/components/onboarding/OnboardingDiscoveryShell.tsx:96
msgid "Discovery"
msgstr "Discovery"
#: src/pages/Onboarding.tsx:428
msgid "One-to-one and group threads"
msgstr "One-to-one and group threads"
msgid "One connection — then we take it from here"
msgstr "One connection — then we take it from here"

View File

@ -1017,6 +1017,9 @@ msgstr "La conexión con {0} sigue pendiente. Puedes cerrar esto y volver a comp
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Conversaciones"
msgid "Context limit"
msgstr "Límite de contexto"

View File

@ -1017,6 +1017,9 @@ msgstr "{0} से कनेक्शन अभी लंबित है। आ
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "बातचीत"
msgid "Context limit"
msgstr "कॉन्टेक्स्ट सीमा"

View File

@ -1017,6 +1017,9 @@ msgstr "{0} 연결이 아직 진행 중입니다. 이 창을 닫고 나중에
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "대화"
msgid "Context limit"
msgstr "컨텍스트 한도"

View File

@ -1017,6 +1017,9 @@ msgstr "A conexão com o {0} ainda está pendente. Você pode fechar isso e veri
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Conversas"
msgid "Context limit"
msgstr "Limite de contexto"

View File

@ -1017,6 +1017,9 @@ msgstr "Соединение с {0} все еще не завершено. Вы
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Беседы"
msgid "Context limit"
msgstr ""

View File

@ -1017,6 +1017,9 @@ msgstr "{0} bağlantısı hâlâ beklemede. Bunu kapatıp daha sonra tekrar kont
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "Sohbetler"
msgid "Context limit"
msgstr "Bağlam sınırı"

View File

@ -1017,6 +1017,9 @@ msgstr "与 {0} 的连接仍在等待。可以关闭此窗口稍后再看。"
#: src/pages/ModelSettingsOverlay.tsx:590
#: src/pages/Onboarding.tsx:519
msgid "Conversations"
msgstr "对话"
msgid "Context limit"
msgstr "上下文限制"

View File

@ -1,7 +1,7 @@
import { Trans, useLingui } from "@lingui/react/macro";
import type { AvatarStyle } from "@rakazo/contracts";
import { BotAvatar, Button, Field, FieldLabel, Input, Toggle } from "@rakazo/ui-web";
import { ChevronDown } from "lucide-react";
import { Activity, ChevronDown } from "lucide-react";
import {
type KeyboardEvent as ReactKeyboardEvent,
type RefObject,
@ -33,8 +33,66 @@ export type SettingsGeneralProps = {
messagingEnabled?: boolean;
onOpenMessaging?: () => void;
isDeploymentOwner?: boolean;
tradingMode?: "live" | "paper";
onDiscoverLiveTrading?: () => void;
};
export function LiveTradingBanner({
tradingMode,
onDiscover,
}: {
tradingMode: "live" | "paper";
onDiscover: () => void;
}) {
const isLive = tradingMode === "live";
return (
<section
className={`rounded-xl border px-4 py-4 ${
isLive ? "border-amber-500/30 bg-amber-500/5" : "border-border bg-card/40"
}`}
data-testid="live-trading-banner"
>
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<span
className={`grid size-10 shrink-0 place-items-center rounded-lg ${
isLive
? "bg-amber-500/10 text-amber-500 shadow-[0_0_12px_rgba(245,158,11,0.2)]"
: "bg-muted text-muted-foreground"
}`}
>
<Activity
strokeWidth={1.8}
className={`size-5 ${isLive ? "animate-pulse" : ""}`}
/>
</span>
<div className="min-w-0">
<h3 className="text-ui-body font-medium text-foreground">
<Trans>Live Trading</Trans>
</h3>
{isLive ? (
<p className="mt-0.5 text-ui-body-sm text-muted-foreground/70">
<Trans>Active</Trans>
</p>
) : null}
</div>
</div>
{!isLive ? (
<Button
type="button"
variant="secondary"
className="shrink-0 rounded-full"
onClick={onDiscover}
>
<Trans>Discover now</Trans>
</Button>
) : null}
</div>
</section>
);
}
export function GeneralSettingsPanels({
email,
name,
@ -43,6 +101,8 @@ export function GeneralSettingsPanels({
messagingEnabled = false,
onOpenMessaging,
isDeploymentOwner = false,
tradingMode,
onDiscoverLiveTrading,
}: SettingsGeneralProps) {
const { t } = useLingui();
const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale());
@ -78,6 +138,9 @@ export function GeneralSettingsPanels({
return (
<div className="space-y-5">
{tradingMode && onDiscoverLiveTrading ? (
<LiveTradingBanner tradingMode={tradingMode} onDiscover={onDiscoverLiveTrading} />
) : null}
<section className="rounded-xl border border-border px-4 py-4">
<h3 className="text-ui-body font-medium text-foreground">
<Trans>Account</Trans>

View File

@ -73,10 +73,13 @@ export function PluginsOverlay({
onClose,
onOpenMcp,
activeBotId,
embedded = false,
}: {
onClose: () => void;
onOpenMcp?: () => void;
activeBotId?: string;
/** Render panel body only for the shared Settings shell. */
embedded?: boolean;
}) {
const { t } = useLingui();
const [setupOpen, setSetupOpen] = useState(false);
@ -663,44 +666,27 @@ export function PluginsOverlay({
);
}
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent
showCloseButton={false}
className="flex h-[760px] max-h-[calc(100%-2rem)] w-[1080px] max-w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden rounded-2xl bg-card p-0 sm:max-w-[1080px]"
const searchField = !detailItem ? (
<div className={embedded ? "px-6 pt-1 sm:px-8" : "px-8 pt-4"}>
<Input
value={query}
onChange={(event) => {
setQuery(event.target.value);
setVisibleCount(CONNECTION_CATALOG_PAGE_SIZE);
}}
aria-label={t`Search apps`}
placeholder={t`Search apps`}
className="h-11 rounded-xl px-4 md:text-ui-body"
/>
</div>
) : null;
const catalogBody = (
<>
<div
id="integration-list"
className={`rk-scroll flex-1 overflow-y-auto py-6 ${embedded ? "px-6 sm:px-8" : "px-8"}`}
>
<DialogHeader className="flex-row items-start justify-between px-8 pt-7">
<DialogTitle className="text-2xl text-foreground">
<Trans>Integrations</Trans>
</DialogTitle>
<DialogClose
render={<Button variant="ghost" size="icon-sm" aria-label={t`Close integrations`} />}
>
<X />
</DialogClose>
</DialogHeader>
{!detailItem ? (
<div className="px-8 pt-4">
<Input
value={query}
onChange={(event) => {
setQuery(event.target.value);
setVisibleCount(CONNECTION_CATALOG_PAGE_SIZE);
}}
aria-label={t`Search apps`}
placeholder={t`Search apps`}
className="h-11 rounded-xl px-4 md:text-ui-body"
/>
</div>
) : null}
<div id="integration-list" className="rk-scroll flex-1 overflow-y-auto px-8 py-6">
<Button
variant="outline"
className="mb-4"
@ -1165,6 +1151,43 @@ export function PluginsOverlay({
</>
)}
</div>
</>
);
if (embedded) {
return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="integrations-settings">
{searchField}
{catalogBody}
</div>
);
}
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DialogContent
showCloseButton={false}
className="flex h-[760px] max-h-[calc(100%-2rem)] w-[1080px] max-w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden rounded-2xl bg-card p-0 sm:max-w-[1080px]"
>
<DialogHeader className="flex-row items-start justify-between px-8 pt-7">
<DialogTitle className="text-2xl text-foreground">
<Trans>Integrations</Trans>
</DialogTitle>
<DialogClose
render={<Button variant="ghost" size="icon-sm" aria-label={t`Close integrations`} />}
>
<X />
</DialogClose>
</DialogHeader>
{searchField}
{catalogBody}
</DialogContent>
</Dialog>
);

View File

@ -1,7 +1,17 @@
import { useLingui } from "@lingui/react/macro";
import type { AvatarStyle, SpaceMemoryConfig } from "@rakazo/contracts";
import { Button, Dialog, DialogClose, DialogContent, DialogTitle } from "@rakazo/ui-web";
import { Brain, CloudDownload, Cpu, Gauge, Monitor, Settings, Volume2, XIcon } from "lucide-react";
import {
Brain,
CloudDownload,
Cpu,
Gauge,
LayoutGrid,
Monitor,
Settings,
Volume2,
XIcon,
} from "lucide-react";
import { type ComponentType, useEffect, useRef, useState } from "react";
import { computersAreUnavailable } from "../components/ComputersUnavailableHint";
import {
@ -12,10 +22,12 @@ import {
} from "./AccountSettingsOverlay";
import { MemorySettingsOverlay } from "./MemorySettingsOverlay";
import { ModelSettingsOverlay } from "./ModelSettingsOverlay";
import { PluginsOverlay } from "./PluginsOverlay";
import { VoiceSettingsOverlay } from "./VoiceSettingsOverlay";
export type SettingsSection =
| "general"
| "integrations"
| "models"
| "memory"
| "voice"
@ -44,6 +56,10 @@ export function SettingsOverlay({
onMemoryConfigChange,
onClose,
onVoiceStatusMaybeChanged,
activeBotId,
onOpenMcp,
tradingMode,
onDiscoverLiveTrading,
}: {
email?: string | null;
name: string;
@ -59,6 +75,10 @@ export function SettingsOverlay({
onMemoryConfigChange: (config: SpaceMemoryConfig | null) => void;
onClose: () => void;
onVoiceStatusMaybeChanged?: () => void | Promise<void>;
activeBotId?: string;
onOpenMcp?: () => void;
tradingMode?: "live" | "paper";
onDiscoverLiveTrading?: () => void;
}) {
const { t } = useLingui();
const panelRef = useRef<HTMLDivElement>(null);
@ -81,6 +101,7 @@ export function SettingsOverlay({
const navItems: NavItem[] = [
{ id: "general", label: t`General`, icon: Settings },
{ id: "integrations", label: t`Integrations`, icon: LayoutGrid },
{ id: "models", label: t`Models`, icon: Cpu },
{ id: "memory", label: t`Memory`, icon: Brain },
{ id: "voice", label: t`Voice`, icon: Volume2 },
@ -100,7 +121,9 @@ export function SettingsOverlay({
? t`Close memory settings`
: section === "voice"
? t`Close voice settings`
: t`Close user settings`;
: section === "integrations"
? t`Close integrations settings`
: t`Close user settings`;
async function refreshVoiceStatus() {
await onVoiceStatusMaybeChanged?.();
@ -115,7 +138,7 @@ export function SettingsOverlay({
leaveSettings(onClose);
}
const widePane = section === "models" || section === "voice";
const widePane = section === "models" || section === "voice" || section === "integrations";
return (
<Dialog
@ -189,7 +212,10 @@ export function SettingsOverlay({
<div
className={`min-h-0 flex-1 ${
section === "models" || section === "voice" || section === "memory"
section === "models" ||
section === "voice" ||
section === "memory" ||
section === "integrations"
? "flex flex-col overflow-hidden"
: "rk-scroll overflow-y-auto overscroll-contain px-6 pb-6 pt-5 sm:px-8 sm:pb-8"
}`}
@ -205,6 +231,16 @@ export function SettingsOverlay({
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined
}
isDeploymentOwner={isDeploymentOwner}
tradingMode={tradingMode}
onDiscoverLiveTrading={onDiscoverLiveTrading}
/>
) : null}
{section === "integrations" ? (
<PluginsOverlay
embedded
activeBotId={activeBotId}
onClose={requestClose}
onOpenMcp={onOpenMcp ? () => leaveSettings(onOpenMcp) : undefined}
/>
) : null}
{section === "usage" ? (

View File

@ -93,7 +93,6 @@ import {
Copy,
ExternalLink,
Gauge,
LayoutGrid,
Lock,
LogOut,
Maximize2,
@ -116,6 +115,7 @@ import {
Square,
TextQuote,
Trash2,
Users,
Volume2,
X,
} from "lucide-react";
@ -266,9 +266,6 @@ const SettingsOverlay = lazy(() =>
const PeerMessagesOverlay = lazy(() =>
import("./PeerMessagesOverlay").then((module) => ({ default: module.PeerMessagesOverlay })),
);
const PluginsOverlay = lazy(() =>
import("./PluginsOverlay").then((module) => ({ default: module.PluginsOverlay })),
);
const McpServersOverlay = lazy(() =>
import("./McpServersOverlay").then((module) => ({ default: module.McpServersOverlay })),
);
@ -358,15 +355,14 @@ export function ShellPage() {
return "live";
});
const toggleTradingMode = useCallback(() => {
setTradingMode((prev) => {
const next = prev === "live" ? "paper" : "live";
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.setItem("evoo_trading_mode", next);
}
return next;
});
}, []);
const discoverLiveTrading = useCallback(() => {
setTradingMode("live");
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.setItem("evoo_trading_mode", "live");
}
setActiveNavPillar("heute");
navigate("/app");
}, [navigate]);
useEffect(() => {
if (botId) setActiveNavPillar("team");
@ -539,7 +535,6 @@ export function ShellPage() {
function updateSnapshot(update: (prev: ThreadSnapshot | null) => ThreadSnapshot | null) {
commitSnapshot(update(snapshotRef.current));
}
const [pluginsOpen, setPluginsOpen] = useState(false);
const [mcpOpen, setMcpOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [settingsSection, setSettingsSection] = useState<SettingsSection>("general");
@ -3049,205 +3044,47 @@ export function ShellPage() {
<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`Group conversations · folders`}
{t`Threads by topic · folders`}
</p>
</div>
<div className="flex items-center gap-1.5">
<button
type="button"
aria-label="Einstellungen"
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
>
<Settings strokeWidth={2} className="size-icon-sm" />
</button>
<button
type="button"
aria-label="Neues Projekt"
onClick={() => setPanel("create-group")}
aria-label={t`Create a project`}
onClick={() => setNewSpaceOpen(true)}
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 ? (
{spaces.filter((space) => !space.isDefault).length === 0 ? (
<div className="p-4 text-center text-ui-label text-muted-foreground">
<p>{t`No project conversations yet.`}</p>
<p>{t`No projects yet.`}</p>
<Button
variant="outline"
size="sm"
className="mt-2 text-ui-label"
onClick={() => setPanel("create-group")}
onClick={() => setNewSpaceOpen(true)}
>
<Plus className="mr-1 size-icon-sm" />
{t`Start a group conversation`}
{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="sidebar-conversations">
<div className="flex items-center justify-between pt-4 pb-1 px-2.5">
<div>
<div className="rk-shell-section-label">{t`Team`}</div>
<div className="rk-shell-section-label">{t`Conversations`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
{t`Your Oos — open a 1:1 conversation`}
{t`One-to-one and group threads`}
</p>
</div>
<div className="flex items-center">
<button
type="button"
aria-label="Neues Teammitglied"
aria-label={t`Add an Oo`}
onClick={() => setPanel("create")}
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
>
@ -3255,14 +3092,34 @@ export function ShellPage() {
</button>
</div>
</div>
<button
type="button"
data-testid="start-group-conversation"
onClick={() => {
setMobileSidebarOpen(false);
setPanel("create-group");
}}
className="mx-0 mb-2 flex w-full items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent"
>
<span className="grid h-[30px] w-[30px] place-items-center rounded-lg bg-accent text-foreground/80">
<Users strokeWidth={1.8} className="size-icon-md" />
</span>
<span className="text-ui-body font-medium text-foreground/90">
<Trans>Start a group conversation</Trans>
</span>
</button>
{sidebarGroups
.map((group) => ({
...group,
bots: group.bots.filter((b) => b.kind === "bot") as any[],
bots: group.bots.filter(
(b) =>
b.kind === "bot" ||
(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 Oos in your team yet.`}</p>
<p>{t`No conversations yet.`}</p>
<Button
variant="outline"
size="sm"
@ -3277,7 +3134,11 @@ export function ShellPage() {
{sidebarGroups
.map((group) => ({
...group,
bots: group.bots.filter((b) => b.kind === "bot") as any[],
bots: group.bots.filter(
(b) =>
b.kind === "bot" ||
(b.kind === "group" && b.chat.id !== hauptthreadGroup?.id),
) as any[],
}))
.filter((group) => group.bots.length > 0 || group.emptySpaceId)
.map((group) => {
@ -3666,35 +3527,16 @@ export function ShellPage() {
</div>
<button
type="button"
onClick={toggleTradingMode}
className="mx-3 lg:mx-5 2xl:mx-7 mb-1 flex items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent group"
>
<span
className={`grid h-[30px] w-[30px] place-items-center rounded-lg transition-colors ${
tradingMode === "live"
? "bg-amber-500/10 text-amber-500 shadow-[0_0_12px_rgba(245,158,11,0.2)]"
: "bg-indigo-500/10 text-indigo-400"
}`}
>
<Activity
strokeWidth={1.8}
className={`size-icon-md ${tradingMode === "live" ? "animate-pulse" : ""}`}
/>
</span>
<span className="text-ui-body font-medium text-foreground/90">
{tradingMode === "live" ? "Live Trading" : "Paper Sim"}
</span>
</button>
<button
type="button"
onClick={() => setPluginsOpen(true)}
data-testid="settings-entry"
aria-label={t`Settings`}
onClick={() => openSettings("general")}
className="mx-3 lg:mx-5 2xl:mx-7 mb-1 flex items-center gap-3 rounded-xl px-2.5 py-2 hover:bg-sidebar-accent"
>
<span className="grid h-[30px] w-[30px] place-items-center rounded-lg bg-accent text-foreground/80">
<LayoutGrid strokeWidth={1.8} className="size-icon-md" />
<Settings strokeWidth={1.8} className="size-icon-md" />
</span>
<span className="text-ui-body font-medium text-foreground/90">
<Trans>Integrations</Trans>
<Trans>Settings</Trans>
</span>
</button>
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
@ -3713,18 +3555,6 @@ export function ShellPage() {
align="start"
className="w-[calc(316px-1.5rem)] max-w-[calc(100vw-3rem)] gap-0 p-1 data-closed:animate-none"
>
<Button
variant="ghost"
className="w-full justify-start font-normal"
aria-label={t`Settings`}
onClick={() => {
setMenuOpen(false);
openSettings("general");
}}
>
<Settings className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Settings</Trans>
</Button>
<Button
variant="ghost"
className="w-full justify-start font-normal"
@ -4841,16 +4671,6 @@ export function ShellPage() {
/>
) : null}
{pluginsOpen ? (
<PluginsOverlay
activeBotId={activeBotId.current}
onClose={() => setPluginsOpen(false)}
onOpenMcp={() => {
setPluginsOpen(false);
setMcpOpen(true);
}}
/>
) : null}
{mcpOpen ? <McpServersOverlay onClose={() => setMcpOpen(false)} /> : null}
{messagingSettingsOpen ? (
<MessagingSettingsOverlay onClose={() => setMessagingSettingsOpen(false)} />
@ -4891,6 +4711,13 @@ export function ShellPage() {
setVoiceStatus(null);
}
}}
activeBotId={activeBotId.current}
tradingMode={tradingMode}
onDiscoverLiveTrading={discoverLiveTrading}
onOpenMcp={() => {
setSettingsOpen(false);
setMcpOpen(true);
}}
onClose={() => {
setSettingsOpen(false);
setSettingsSection("general");