Merge pull request #2737 from langbot-app/agent/sg-claude/07b0571b414f

feat(web): rename credits label, shorten workspace switcher, add upgrade button
This commit is contained in:
RockChinQ
2026-10-21 16:23:45 +08:00
committed by GitHub
6 changed files with 69 additions and 18 deletions
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState, lazy, Suspense } from 'react';
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
@@ -70,6 +70,9 @@ import { Button } from '@/components/ui/button';
import WorkspaceSwitcher, {
OPEN_WORKSPACE_SETTINGS_EVENT,
} from '@/app/home/components/workspace-settings/WorkspaceSwitcher';
const CloudPlansDialog = lazy(
() => import('@/app/home/components/workspace-settings/CloudPlansDialog'),
);
import NewVersionDialog from '@/app/home/components/new-version-dialog/NewVersionDialog';
import SettingsDialog, {
SettingsSection,
@@ -1893,6 +1896,7 @@ export default function HomeSidebar({
const [hasNewVersion, setHasNewVersion] = useState(false);
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
const [feedbackOpen, setFeedbackOpen] = useState(false);
const [cloudPlansOpen, setCloudPlansOpen] = useState(false);
const [userEmail, setUserEmail] = useState<string>('');
const [starCount, setStarCount] = useState<number | null>(null);
const [userMenuOpen, setUserMenuOpen] = useState(false);
@@ -2163,8 +2167,26 @@ export default function HomeSidebar({
</SidebarHeader>
{showWorkspaceSwitcher && (
<div className="px-2 group-data-[collapsible=icon]:px-0">
<WorkspaceSwitcher className="w-full group-data-[collapsible=icon]:min-w-0 group-data-[collapsible=icon]:px-2" />
<div className="flex items-center gap-1 px-2 group-data-[collapsible=icon]:px-0">
<WorkspaceSwitcher className="min-w-0 flex-1 group-data-[collapsible=icon]:min-w-0 group-data-[collapsible=icon]:px-2" />
{currentWorkspace?.workspace.source === 'cloud_projection' && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-9 shrink-0 text-muted-foreground hover:text-foreground group-data-[collapsible=icon]:hidden"
aria-label={t('workspace.upgradePlan')}
onClick={() => setCloudPlansOpen(true)}
>
<ArrowUp className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
{t('workspace.upgradePlan')}
</TooltipContent>
</Tooltip>
)}
</div>
)}
@@ -2473,6 +2495,16 @@ export default function HomeSidebar({
</DialogContent>
</Dialog>
{currentWorkspace && (
<Suspense fallback={null}>
<CloudPlansDialog
open={cloudPlansOpen}
onOpenChange={setCloudPlansOpen}
workspace={currentWorkspace}
spaceURL={systemInfo.cloud_service_url}
/>
</Suspense>
)}
<SettingsDialog
open={settingsOpen}
onOpenChange={handleSettingsOpenChange}
@@ -40,11 +40,13 @@ export default function WorkspaceSwitcher({
<Button
variant="outline"
size="sm"
className={cn('h-9 min-w-0 justify-start px-2.5 text-sm', className)}
className={cn('h-9 min-w-0 gap-1.5 px-2.5 text-sm', className)}
aria-label={t('workspace.switchWorkspace')}
>
<Building2 className="size-4 shrink-0" />
<span className="truncate">{currentWorkspace.workspace.name}</span>
<span className="max-w-[6rem] truncate">
{currentWorkspace.workspace.name}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64 p-1.5">
+2 -2
View File
@@ -103,9 +103,9 @@ const enUS = {
retentionHelp:
'Message history is automatically removed after this period.',
credits: '{{count}} credits every month',
creditsLabel: 'Monthly credits',
creditsLabel: 'Model credits',
creditsHelp:
'Credits are granted each subscription month, including on yearly plans.',
'Credits are granted each subscription month, including on yearly plans. When credits run out, you can still use your own configured model providers.',
platforms: 'Mainstream platforms supported',
excludedPlatforms: 'Excluded: {{platforms}}',
allPlatforms: 'All currently available platforms',
+3 -2
View File
@@ -102,8 +102,9 @@ const jaJP = {
retentionLabel: 'メッセージ履歴の保存期間',
retentionHelp: '保存期間を過ぎた履歴は自動的に削除されます。',
credits: '毎月 {{count}} クレジット',
creditsLabel: '毎月のクレジット',
creditsHelp: '年払いでも、契約の各月にクレジットが付与されます。',
creditsLabel: 'モデルクレジット',
creditsHelp:
'年払いでも、契約の各月にクレジットが付与されます。クレジットが不足しても、独自に設定したモデルプロバイダーを引き続き利用できます。',
platforms: '主要プラットフォームに対応',
excludedPlatforms: '対象外:{{platforms}}',
allPlatforms: '現在利用可能なすべてのプラットフォーム',
+3 -2
View File
@@ -98,8 +98,9 @@ const zhHans = {
retentionLabel: '消息记录保留时长',
retentionHelp: '超过保留时长的消息记录会自动清理。',
credits: '每月赠送 {{count}} 积分',
creditsLabel: '每月赠送积分',
creditsHelp: '每个订阅月自动发放,年付计划同样按月发放。',
creditsLabel: '模型积分',
creditsHelp:
'每个订阅月自动发放,年付计划同样按月发放。积分用尽后,仍可接入自行配置的模型供应商继续使用。',
platforms: '支持主流平台',
excludedPlatforms: '不支持:{{platforms}}',
allPlatforms: '支持当前所有可用平台',
+22 -7
View File
@@ -55,6 +55,11 @@ for (const scenario of [
});
await expect(trigger).toHaveCount(scenario === 'singleton-local' ? 0 : 1);
if (scenario === 'singleton-local') return;
const upgrade = page.getByRole('button', {
name: 'Change or upgrade plan',
exact: true,
});
await expect(upgrade).toHaveCount(1);
const sidebar = page.locator('[data-slot="sidebar-inner"]');
const header = sidebar.locator('[data-slot="sidebar-header"]');
const home = sidebar.getByText('Home', { exact: true });
@@ -62,20 +67,30 @@ for (const scenario of [
name: 'Models',
exact: true,
});
const [headerBox, triggerBox, homeBox, navBox] = await Promise.all([
header.boundingBox(),
trigger.boundingBox(),
home.boundingBox(),
navigation.boundingBox(),
]);
const [headerBox, triggerBox, homeBox, navBox, upgradeBox] =
await Promise.all([
header.boundingBox(),
trigger.boundingBox(),
home.boundingBox(),
navigation.boundingBox(),
upgrade.boundingBox(),
]);
expect(triggerBox!.y).toBeGreaterThanOrEqual(
headerBox!.y + headerBox!.height,
);
expect(triggerBox!.y + triggerBox!.height).toBeLessThanOrEqual(homeBox!.y);
expect(Math.abs(triggerBox!.x - navBox!.x)).toBeLessThanOrEqual(1);
// The upgrade control shares the switcher row and owns its trailing edge,
// so the row stays flush with the navigation column while the switcher
// itself ends before the control.
expect(Math.abs(triggerBox!.y - upgradeBox!.y)).toBeLessThanOrEqual(1);
expect(upgradeBox!.x).toBeGreaterThanOrEqual(
triggerBox!.x + triggerBox!.width,
);
expect(
Math.abs(triggerBox!.x + triggerBox!.width - navBox!.x - navBox!.width),
Math.abs(upgradeBox!.x + upgradeBox!.width - navBox!.x - navBox!.width),
).toBeLessThanOrEqual(1);
await expect(upgrade).toBeVisible();
await trigger.click();
const row = page
.getByRole('menuitem')