mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 07:10:49 +02:00
The page added in #2462 lived at `/{slug}/dashboard` and was titled "Dashboard", which collides with the conventional meaning ("personal landing surface") and doesn't tell new users what the page is for. Its actual contents — token spend, cost, run time, task counts — map cleanly onto the OpenAI / Anthropic / Vercel "Usage" surface, so rename to that. Renames (user-visible) - Route: `/{slug}/dashboard` → `/{slug}/usage` (web App Router + desktop memory router) - Sidebar entry: label "Dashboard" / "看板" → "Usage" / "用量", icon LayoutDashboard → BarChart3 (page header icon swapped in sync) - Page title in en/zh-Hans - Reserved-slugs: add `usage` to workspace route segments group; `dashboard` stays reserved in the marketing group (back-compat against workspace slug collisions + keeps the name free for a future Home page) - i18n namespace `dashboard` → `usage` across resources-types.ts, locales/index.ts, and the moved JSON files - WORKSPACE_ROUTE_SEGMENTS in editor link-handler - paths.workspace(slug).dashboard() → .usage(), with matching test expectation updates Per-agent leaderboard polish (`packages/views/dashboard/components/ dashboard-page.tsx`) - Card title "Cost & run time by agent" → "Leaderboard" with a 4-way Segmented control: Tokens / Cost / Time / Tasks - Active metric drives row order, progress-bar width, and the emphasised column header / cell — keeping ranking, visual quantity, and column emphasis in lockstep so users always see what's being measured - Default sort = Tokens (most universally meaningful; Cost still one click away) - Project filter dropdown: - Show ProjectIcon next to the selected project + each list item; FolderKanban as the "All projects" fallback (matches ProjectPicker language) - alignItemWithTrigger={false} so "All projects" doesn't get pushed above the trigger and clipped when the header sits at the top of the viewport (was the root cause of "can't re-select All projects" once a project was selected) - max-h-72 to cap the dropdown when workspaces accrue many projects; matches the runtime-detail Select precedent - Folder name `packages/views/dashboard/*` and `DashboardPage` component name intentionally left in place — user-visible rename only, no broad code refactor. Old `/dashboard` routes are not redirected because the page only landed in #2462 (a few days ago); no real users, external links, or desktop-tab persistence have settled on it yet.
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
/**
|
|
* Centralized URL path builder. All navigation in shared packages (packages/views)
|
|
* MUST go through this module — no hardcoded string paths.
|
|
*
|
|
* Two kinds of paths:
|
|
* - workspace-scoped: paths.workspace(slug).xxx() — carry workspace in URL
|
|
* - global: paths.login(), paths.newWorkspace(), paths.invite(id) — pre-workspace routes
|
|
*
|
|
* Why pure functions + builder pattern:
|
|
* - Changing a route shape (e.g. adding workspace slug prefix) becomes a single-file edit
|
|
* - IDs are always URL-encoded here so callers can't forget
|
|
* - Zero runtime deps means this module is safe in Node (tests) and browsers
|
|
*/
|
|
|
|
const encode = (id: string) => encodeURIComponent(id);
|
|
|
|
function workspaceScoped(slug: string) {
|
|
const ws = `/${encode(slug)}`;
|
|
return {
|
|
root: () => `${ws}/issues`,
|
|
usage: () => `${ws}/usage`,
|
|
issues: () => `${ws}/issues`,
|
|
issueDetail: (id: string) => `${ws}/issues/${encode(id)}`,
|
|
projects: () => `${ws}/projects`,
|
|
projectDetail: (id: string) => `${ws}/projects/${encode(id)}`,
|
|
autopilots: () => `${ws}/autopilots`,
|
|
autopilotDetail: (id: string) => `${ws}/autopilots/${encode(id)}`,
|
|
agents: () => `${ws}/agents`,
|
|
agentDetail: (id: string) => `${ws}/agents/${encode(id)}`,
|
|
inbox: () => `${ws}/inbox`,
|
|
myIssues: () => `${ws}/my-issues`,
|
|
runtimes: () => `${ws}/runtimes`,
|
|
runtimeDetail: (id: string) => `${ws}/runtimes/${encode(id)}`,
|
|
skills: () => `${ws}/skills`,
|
|
skillDetail: (id: string) => `${ws}/skills/${encode(id)}`,
|
|
settings: () => `${ws}/settings`,
|
|
};
|
|
}
|
|
|
|
export const paths = {
|
|
workspace: workspaceScoped,
|
|
|
|
// Global (pre-workspace) routes
|
|
login: () => "/login",
|
|
newWorkspace: () => "/workspaces/new",
|
|
invite: (id: string) => `/invite/${encode(id)}`,
|
|
invitations: () => "/invitations",
|
|
onboarding: () => "/onboarding",
|
|
authCallback: () => "/auth/callback",
|
|
root: () => "/",
|
|
};
|
|
|
|
export type WorkspacePaths = ReturnType<typeof workspaceScoped>;
|
|
|
|
// Prefixes — not slug names — because we match against full URL paths.
|
|
// A path is global if it equals or begins with any of these.
|
|
// Note: `/workspaces/` (trailing slash) is the prefix — `workspaces` is reserved,
|
|
// so any path starting with `/workspaces/...` is system-owned, not user-owned.
|
|
const GLOBAL_PREFIXES = ["/login", "/workspaces/", "/invite/", "/invitations", "/onboarding", "/auth/", "/logout", "/signup"];
|
|
|
|
export function isGlobalPath(path: string): boolean {
|
|
return GLOBAL_PREFIXES.some((p) => path === p || path.startsWith(p));
|
|
}
|