feat: update Makelore modules and conversations
This commit is contained in:
@@ -8,8 +8,6 @@ import {
|
||||
ExternalLink,
|
||||
FolderKanban,
|
||||
LogOut,
|
||||
PanelLeft,
|
||||
PanelLeftClose,
|
||||
Plus,
|
||||
Settings as SettingsIcon,
|
||||
UserCircle2,
|
||||
@@ -17,9 +15,9 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import logoSvg from '@/assets/logo.svg';
|
||||
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
import { AgentProfileApiError } from '@/lib/agent-profile';
|
||||
import { getAiModuleForPath } from '@/lib/ai-modules';
|
||||
@@ -39,7 +37,6 @@ import { useOpencodeStore, type ProjectDirectoryMode } from '@/stores/opencode';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import type { OpencodeProject } from '@/types/opencode';
|
||||
import { buildConfiguredModelOptions } from '@/lib/model-options';
|
||||
import { projectTemplates, type ProjectTemplateId } from '../../../shared/project-config';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { EMPTY_AGENT_USER_PROFILE, getProfileAccountKey, isUserProfileComplete, useUserProfileStore, type UserGender } from '@/stores/user-profile';
|
||||
import { toast } from 'sonner';
|
||||
@@ -55,7 +52,7 @@ type TokenUsageState = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type TemplateEntryError = {
|
||||
type ProjectEntryError = {
|
||||
project: OpencodeProject;
|
||||
message: string;
|
||||
};
|
||||
@@ -120,67 +117,15 @@ function getSubscriptionMembershipLabel(user: AuthUser | null, state: TokenUsage
|
||||
return `${levelLabel} 会员`;
|
||||
}
|
||||
|
||||
function getTokenUsageSummary(user: AuthUser | null, state: TokenUsageState): string {
|
||||
if (!user) return '登录后查看套餐额度';
|
||||
if (state.status === 'loaded' && state.usage) {
|
||||
return `5小时剩余 ${formatRemainingPercent(state.usage.five_hour_remaining_percent)} · 本周剩余 ${formatRemainingPercent(state.usage.weekly_remaining_percent)}`;
|
||||
}
|
||||
if (state.status === 'loading') return '套餐额度加载中';
|
||||
if (state.status === 'error') return state.error || '暂时无法获取套餐额度';
|
||||
return '打开后刷新套餐额度';
|
||||
}
|
||||
type SidebarProps = {
|
||||
sidebarPeekOpen?: boolean;
|
||||
onSidebarPeekChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
function ProjectTemplateChoices({
|
||||
selectedTemplateId,
|
||||
disabled,
|
||||
recommendedTemplateId,
|
||||
templates = projectTemplates,
|
||||
onSelect,
|
||||
}: {
|
||||
selectedTemplateId: ProjectTemplateId | '';
|
||||
disabled?: boolean;
|
||||
recommendedTemplateId?: ProjectTemplateId;
|
||||
templates?: typeof projectTemplates;
|
||||
onSelect: (templateId: ProjectTemplateId) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{templates.map((template) => {
|
||||
const templateId = template.id;
|
||||
const selected = selectedTemplateId === templateId;
|
||||
const recommended = recommendedTemplateId === templateId;
|
||||
return (
|
||||
<button
|
||||
key={templateId}
|
||||
type="button"
|
||||
data-testid={`project-template-option-${templateId}`}
|
||||
aria-pressed={selected}
|
||||
disabled={disabled}
|
||||
onClick={() => onSelect(templateId)}
|
||||
className={cn(
|
||||
'rounded-lg border-2 px-3 py-2 text-left transition-colors',
|
||||
selected
|
||||
? 'border-[#26384D] bg-[#DCE6EF] text-[#26384D] shadow-[3px_3px_0_#26384D]'
|
||||
: 'border-[#26384D] bg-white text-[#26384D] hover:bg-[#EEF3F7]',
|
||||
disabled && 'cursor-not-allowed opacity-60',
|
||||
)}
|
||||
>
|
||||
<span className="block text-sm font-black">
|
||||
{template.title}
|
||||
{recommended ? '(推荐)' : ''}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs font-bold text-[#68788B]">{template.description}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: SidebarProps = {}) {
|
||||
const { t } = useTranslation('common');
|
||||
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
||||
const sidebarPinnedCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const sidebarCollapsed = sidebarPinnedCollapsed && !sidebarPeekOpen;
|
||||
const opencodeProjects = useOpencodeStore((state) => state.projects);
|
||||
const activeProject = useOpencodeStore((state) => state.activeProject);
|
||||
const loadProjectList = useOpencodeStore((state) => state.loadProjectList);
|
||||
@@ -217,11 +162,10 @@ export function Sidebar() {
|
||||
const [newProjectName, setNewProjectName] = useState('');
|
||||
const [newProjectSelectedPath, setNewProjectSelectedPath] = useState('');
|
||||
const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState<ProjectDirectoryMode>('use-selected-directory');
|
||||
const [newProjectTemplateId, setNewProjectTemplateId] = useState<ProjectTemplateId | ''>('');
|
||||
const [newProjectDefaultModel, setNewProjectDefaultModel] = useState('');
|
||||
const [createProjectError, setCreateProjectError] = useState<string | null>(null);
|
||||
const [creatingProject, setCreatingProject] = useState(false);
|
||||
const [templateEntryError, setTemplateEntryError] = useState<TemplateEntryError | null>(null);
|
||||
const [projectEntryError, setProjectEntryError] = useState<ProjectEntryError | null>(null);
|
||||
const [removeProjectCandidate, setRemoveProjectCandidate] = useState<OpencodeProject | null>(null);
|
||||
const accountButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -230,7 +174,6 @@ export function Sidebar() {
|
||||
const location = useLocation();
|
||||
const isPaintingModule = getAiModuleForPath(location.pathname) === 'painting';
|
||||
const projectConfigPath = '/project-config';
|
||||
const availableProjectTemplates = projectTemplates;
|
||||
const configuredModelOptions = useMemo(
|
||||
() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId),
|
||||
[providerAccounts, providerDefaultAccountId, providerStatuses],
|
||||
@@ -251,7 +194,6 @@ export function Sidebar() {
|
||||
const settingsLabel = t('sidebar.settings', '设置');
|
||||
const memberLabel = getSubscriptionPlanLabel(authUser, tokenUsageState);
|
||||
const membershipLabel = getSubscriptionMembershipLabel(authUser, tokenUsageState);
|
||||
const tokenUsageSummary = getTokenUsageSummary(authUser, tokenUsageState);
|
||||
const tokenUsageExhausted = isTokenUsageExhausted(tokenUsageState);
|
||||
const fiveHourRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.five_hour_refresh_at);
|
||||
const weeklyRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.weekly_refresh_at);
|
||||
@@ -449,7 +391,6 @@ export function Sidebar() {
|
||||
setNewProjectName('');
|
||||
setNewProjectSelectedPath('');
|
||||
setNewProjectDirectoryMode('use-selected-directory');
|
||||
setNewProjectTemplateId('');
|
||||
setNewProjectDefaultModel('');
|
||||
setCreateProjectError(null);
|
||||
setCreateDialogOpen(true);
|
||||
@@ -458,7 +399,6 @@ export function Sidebar() {
|
||||
const closeCreateProject = () => {
|
||||
if (creatingProject) return;
|
||||
setCreateDialogOpen(false);
|
||||
setNewProjectTemplateId('');
|
||||
setNewProjectDefaultModel('');
|
||||
setCreateProjectError(null);
|
||||
};
|
||||
@@ -475,14 +415,14 @@ export function Sidebar() {
|
||||
setCreateProjectError(null);
|
||||
};
|
||||
|
||||
const showTemplateEntryError = (project: OpencodeProject, message: string) => {
|
||||
setTemplateEntryError({ project, message });
|
||||
const showProjectEntryError = (project: OpencodeProject, message: string) => {
|
||||
setProjectEntryError({ project, message });
|
||||
};
|
||||
|
||||
const enterProject = async (project: OpencodeProject) => {
|
||||
const result = await loadProjectConfig(project.id);
|
||||
if (result.status !== 'valid' || !result.config) {
|
||||
showTemplateEntryError(project, result.status === 'missing' ? '项目缺少 .niancode/project.json' : result.error ?? '项目配置无效');
|
||||
showProjectEntryError(project, result.status === 'missing' ? '项目缺少 .niancode/project.json' : result.error ?? '项目配置无效');
|
||||
return;
|
||||
}
|
||||
activateProjectLocally(project);
|
||||
@@ -493,11 +433,11 @@ export function Sidebar() {
|
||||
};
|
||||
|
||||
const readAndEnterProject = async (project: OpencodeProject) => {
|
||||
setTemplateEntryError(null);
|
||||
setProjectEntryError(null);
|
||||
try {
|
||||
await enterProject(project);
|
||||
} catch (error) {
|
||||
showTemplateEntryError(project, error instanceof Error ? error.message : String(error));
|
||||
showProjectEntryError(project, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -514,11 +454,6 @@ export function Sidebar() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newProjectTemplateId) {
|
||||
setCreateProjectError('请选择项目模板');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreatingProject(true);
|
||||
setCreateProjectError(null);
|
||||
try {
|
||||
@@ -526,11 +461,9 @@ export function Sidebar() {
|
||||
directoryMode: newProjectDirectoryMode,
|
||||
selectedPath,
|
||||
...(newProjectDirectoryMode === 'create-child-directory' ? { projectName } : {}),
|
||||
templateId: newProjectTemplateId,
|
||||
...(newProjectDefaultModel ? { defaultModel: newProjectDefaultModel } : {}),
|
||||
});
|
||||
setCreateDialogOpen(false);
|
||||
setNewProjectTemplateId('');
|
||||
setNewProjectDefaultModel('');
|
||||
await enterProject(project);
|
||||
} catch (error) {
|
||||
@@ -544,26 +477,10 @@ export function Sidebar() {
|
||||
await readAndEnterProject(project);
|
||||
};
|
||||
|
||||
const openProjectConfig = async (project: OpencodeProject) => {
|
||||
setTemplateEntryError(null);
|
||||
try {
|
||||
const result = await loadProjectConfig(project.id);
|
||||
if (result.status !== 'valid' || !result.config) {
|
||||
showTemplateEntryError(project, result.status === 'missing' ? '项目缺少 .niancode/project.json' : result.error ?? '项目配置无效');
|
||||
return;
|
||||
}
|
||||
activateProjectLocally(project);
|
||||
navigate(projectConfigPath);
|
||||
void setActiveProject(project.id).catch(() => undefined);
|
||||
} catch (error) {
|
||||
showTemplateEntryError(project, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const requestRemoveUnavailableProject = () => {
|
||||
if (!templateEntryError) return;
|
||||
setTemplateEntryError(null);
|
||||
setRemoveProjectCandidate(templateEntryError.project);
|
||||
if (!projectEntryError) return;
|
||||
setProjectEntryError(null);
|
||||
setRemoveProjectCandidate(projectEntryError.project);
|
||||
};
|
||||
|
||||
const handleRemoveUnavailableProject = async () => {
|
||||
@@ -602,67 +519,49 @@ export function Sidebar() {
|
||||
return (
|
||||
<aside
|
||||
data-testid="sidebar"
|
||||
aria-hidden={sidebarCollapsed}
|
||||
onPointerEnter={() => onSidebarPeekChange?.(true)}
|
||||
onPointerLeave={() => onSidebarPeekChange?.(false)}
|
||||
className={cn(
|
||||
'flex min-h-0 shrink-0 flex-col overflow-hidden border-r-4 border-[#26384D] bg-[#F6F8FA] text-[#26384D] transition-all duration-300',
|
||||
sidebarCollapsed ? 'w-16' : 'w-72',
|
||||
'flex min-h-0 shrink-0 flex-col overflow-hidden border-r border-border/80 bg-surface-sidebar text-foreground transition-[width,opacity,box-shadow] duration-200 ease-spring motion-reduce:transition-none',
|
||||
sidebarCollapsed ? 'pointer-events-none w-0 border-r-0 opacity-0' : 'w-64 opacity-100',
|
||||
sidebarPinnedCollapsed && sidebarPeekOpen && 'absolute inset-y-0 left-0 z-40 shadow-float',
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex h-16 items-center border-b-4 border-[#26384D] bg-[#F6F8FA] p-2 text-[#26384D]', sidebarCollapsed ? 'justify-center' : 'justify-between')}>
|
||||
{!sidebarCollapsed && (
|
||||
<div className="flex items-center gap-2 overflow-hidden px-2">
|
||||
<img
|
||||
src={logoSvg}
|
||||
alt="Makelore logo"
|
||||
className="h-9 w-10 shrink-0 object-contain"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[15px] font-semibold leading-5 tracking-[-0.01em]">Makelore 工作台</div>
|
||||
<div className="truncate text-[11px] font-medium leading-4 text-[#68788B]">一念成光,万物可创。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-[#26384D] hover:bg-[#FFFFFF]/40"
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<PanelLeft className="h-[18px] w-[18px]" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-[18px] w-[18px]" />
|
||||
)}
|
||||
</Button>
|
||||
<div className={cn('bg-surface-sidebar px-3 py-2 text-foreground', sidebarCollapsed && 'px-2')}>
|
||||
<div className="min-w-0 w-full">
|
||||
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto p-2">
|
||||
<div className="min-h-0 flex-1 overflow-auto px-2 py-2">
|
||||
{isPaintingModule ? (
|
||||
<ImageWorkspaceSidebar sidebarCollapsed={sidebarCollapsed} />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
data-testid="sidebar-create-project"
|
||||
className={cn(
|
||||
'mb-2 w-full border-2 border-[#26384D] bg-[#E9EFF5] text-[#26384D] shadow-[3px_3px_0_#26384D] hover:bg-[#DCE6EF]',
|
||||
sidebarCollapsed ? 'px-0' : '',
|
||||
'motion-press mb-1 flex min-h-9 w-full items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium text-foreground shadow-none transition-colors hover:bg-surface-subtle active:scale-[0.985]',
|
||||
sidebarCollapsed ? 'justify-center px-0' : '',
|
||||
)}
|
||||
aria-label="新建项目"
|
||||
onClick={openCreateProject}
|
||||
>
|
||||
<Plus className={cn('h-4 w-4', sidebarCollapsed ? '' : 'mr-2')} />
|
||||
<Plus className="h-4 w-4 shrink-0 text-brand" />
|
||||
{!sidebarCollapsed && '新建项目'}
|
||||
</Button>
|
||||
<div className="mt-4 border-t-2 border-[#DCE6EF] pt-4">
|
||||
</button>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProjectsOpen(!projectsOpen)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm font-black transition-colors hover:bg-[#FFFFFF] hover:text-[#26384D]',
|
||||
'motion-press flex min-h-8 w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs font-semibold text-muted-foreground hover:bg-surface-subtle hover:text-foreground',
|
||||
sidebarCollapsed ? 'justify-center px-0' : '',
|
||||
)}
|
||||
>
|
||||
<FolderKanban className="h-[18px] w-[18px] shrink-0" />
|
||||
<FolderKanban className="h-4 w-4 shrink-0" />
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="min-w-0 flex-1 truncate">项目列表</span>
|
||||
@@ -672,10 +571,10 @@ export function Sidebar() {
|
||||
</button>
|
||||
|
||||
{projectsOpen && !sidebarCollapsed ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="mt-1 space-y-0.5">
|
||||
{visibleProjects.length === 0 ? (
|
||||
<div className="rounded-lg border-2 border-dashed border-[#DCE6EF] bg-white/70 p-3 text-xs font-bold text-[#68788B]">
|
||||
暂无真实项目。添加本地项目后,请先选择项目模板。
|
||||
<div className="px-2 py-2 text-xs text-muted-foreground">
|
||||
暂无真实项目。添加本地项目后即可开始配置。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -687,20 +586,20 @@ export function Sidebar() {
|
||||
key={project.id}
|
||||
data-testid={`sidebar-course-project-${project.id}`}
|
||||
className={cn(
|
||||
'w-full rounded-lg border-2 p-3 text-left transition-[background-color,border-color,box-shadow,color]',
|
||||
'motion-press w-full rounded-lg px-2 py-1.5 text-left transition-[background-color,color]',
|
||||
active
|
||||
? 'border-[#26384D] bg-[#DCE6EF] text-[#26384D] shadow-[4px_4px_0_#26384D]'
|
||||
: 'border-[#DCE6EF] bg-[#FFFFFF] text-[#26384D]',
|
||||
? 'bg-brand-soft text-foreground'
|
||||
: 'bg-transparent text-foreground hover:bg-surface-subtle',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-black">{project.name}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">{project.name}</span>
|
||||
<div className="flex shrink-0 -space-x-1.5" aria-label="项目 Agent">
|
||||
{projectConfig?.agents.map((agent) => (
|
||||
<span
|
||||
key={agent.id}
|
||||
title={agent.name.trim() || agent.roleName}
|
||||
className="h-6 w-6 overflow-hidden rounded-full border-2 border-[#26384D] bg-white"
|
||||
className="h-5 w-5 overflow-hidden rounded-full border border-border bg-white"
|
||||
>
|
||||
<img
|
||||
src={getAgentAvatarSrc(agent.avatarId)}
|
||||
@@ -711,29 +610,15 @@ export function Sidebar() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 truncate text-[11px] font-bold opacity-80">
|
||||
{projectConfig
|
||||
? projectTemplates.find((item) => item.id === projectConfig.templateId)?.title ?? projectConfig.templateId
|
||||
: '读取项目配置'}
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`进入项目 ${project.name}`}
|
||||
onClick={() => void openProject(project)}
|
||||
className="flex min-w-0 flex-1 items-center justify-center rounded-md border-2 border-[#26384D] bg-white px-3 py-1.5 text-xs font-black transition-colors hover:bg-[#EEF3F7]"
|
||||
className="motion-press flex min-w-0 flex-1 items-center justify-center rounded-md bg-transparent px-1.5 py-1 text-left text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground"
|
||||
>
|
||||
进入项目
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`配置项目 ${project.name}`}
|
||||
title="项目配置"
|
||||
onClick={() => void openProjectConfig(project)}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border-2 border-[#26384D] bg-white transition-colors hover:bg-[#FDE8E1]"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -747,27 +632,32 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
{!isPaintingModule && createDialogOpen ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 px-4">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="create-project-dialog-title"
|
||||
className="w-full max-w-md rounded-lg border-4 border-[#26384D] bg-[#FFFFFF] p-4 text-[#26384D] shadow-[8px_8px_0_#26384D]"
|
||||
<Dialog
|
||||
open={createDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) closeCreateProject();
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-h-[calc(100vh-2rem)] max-w-xl overflow-y-auto p-5"
|
||||
onInteractOutside={(event) => {
|
||||
if (creatingProject) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<DialogHeader className="flex-row items-start justify-between gap-3 space-y-0">
|
||||
<div>
|
||||
<h2 id="create-project-dialog-title" className="text-xl font-black">新建项目</h2>
|
||||
<p className="mt-1 text-xs font-bold text-[#68788B]">默认直接使用所选文件夹,也可以在所选位置新建下级文件夹。</p>
|
||||
<DialogTitle className="text-xl">新建项目</DialogTitle>
|
||||
<DialogDescription className="mt-1 text-xs">默认直接使用所选文件夹,也可以在所选位置新建下级文件夹。</DialogDescription>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border-2 border-[#26384D] px-2 py-1 text-xs font-black hover:bg-white"
|
||||
className="rounded-md border border-foreground/15 px-2 py-1 text-xs font-semibold hover:bg-white"
|
||||
onClick={closeCreateProject}
|
||||
disabled={creatingProject}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
className="mt-4 space-y-3"
|
||||
@@ -777,12 +667,12 @@ export function Sidebar() {
|
||||
}}
|
||||
>
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="text-xs font-black">项目目录方式</legend>
|
||||
<legend className="text-xs font-semibold">项目目录方式</legend>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<label
|
||||
className={cn(
|
||||
'cursor-pointer rounded-md border-2 border-[#26384D] px-3 py-2 text-xs font-black',
|
||||
newProjectDirectoryMode === 'use-selected-directory' ? 'bg-[#DCE6EF]' : 'bg-white',
|
||||
'cursor-pointer rounded-md border border-foreground/15 px-3 py-2 text-xs font-semibold',
|
||||
newProjectDirectoryMode === 'use-selected-directory' ? 'bg-brand-soft' : 'bg-white',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -801,8 +691,8 @@ export function Sidebar() {
|
||||
</label>
|
||||
<label
|
||||
className={cn(
|
||||
'cursor-pointer rounded-md border-2 border-[#26384D] px-3 py-2 text-xs font-black',
|
||||
newProjectDirectoryMode === 'create-child-directory' ? 'bg-[#DCE6EF]' : 'bg-white',
|
||||
'cursor-pointer rounded-md border border-foreground/15 px-3 py-2 text-xs font-semibold',
|
||||
newProjectDirectoryMode === 'create-child-directory' ? 'bg-brand-soft' : 'bg-white',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -824,7 +714,7 @@ export function Sidebar() {
|
||||
|
||||
{newProjectDirectoryMode === 'create-child-directory' ? (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-black" htmlFor="new-project-name">
|
||||
<label className="block text-xs font-semibold" htmlFor="new-project-name">
|
||||
项目名称
|
||||
</label>
|
||||
<input
|
||||
@@ -834,14 +724,14 @@ export function Sidebar() {
|
||||
setNewProjectName(event.target.value);
|
||||
setCreateProjectError(null);
|
||||
}}
|
||||
className="w-full rounded-md border-2 border-[#26384D] bg-white px-3 py-2 text-sm font-bold outline-none focus:ring-2 focus:ring-[#DCE6EF]"
|
||||
className="w-full rounded-md border border-foreground/15 bg-white px-3 py-2 text-sm font-medium outline-none focus:ring-2 focus:ring-brand/20"
|
||||
autoFocus
|
||||
disabled={creatingProject}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<label className="block text-xs font-black" htmlFor="new-project-path">
|
||||
<label className="block text-xs font-semibold" htmlFor="new-project-path">
|
||||
项目路径
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
@@ -849,19 +739,19 @@ export function Sidebar() {
|
||||
id="new-project-path"
|
||||
value={newProjectSelectedPath}
|
||||
readOnly
|
||||
className="min-w-0 flex-1 rounded-md border-2 border-[#26384D] bg-white px-3 py-2 text-sm font-bold outline-none"
|
||||
className="min-w-0 flex-1 rounded-md border border-foreground/15 bg-white px-3 py-2 text-sm font-medium outline-none"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="shrink-0 border-2 border-[#26384D] bg-white text-[#26384D]"
|
||||
className="shrink-0 border border-foreground/15 bg-white text-foreground"
|
||||
onClick={() => void chooseProjectPath()}
|
||||
disabled={creatingProject}
|
||||
>
|
||||
选择路径
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[11px] font-bold text-[#68788B]">
|
||||
<p className="text-[11px] font-medium text-muted-foreground">
|
||||
{newProjectDirectoryMode === 'use-selected-directory'
|
||||
? selectedProjectFolderName
|
||||
? `将直接接入此文件夹,项目名称使用“${selectedProjectFolderName}”,不会重命名目录。`
|
||||
@@ -870,27 +760,14 @@ export function Sidebar() {
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-black">项目模板</div>
|
||||
<ProjectTemplateChoices
|
||||
templates={availableProjectTemplates}
|
||||
selectedTemplateId={newProjectTemplateId}
|
||||
disabled={creatingProject}
|
||||
onSelect={(templateId) => {
|
||||
setNewProjectTemplateId(templateId);
|
||||
setCreateProjectError(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-black" htmlFor="new-project-model">项目默认模型</label>
|
||||
<label className="block text-xs font-semibold" htmlFor="new-project-model">项目默认模型</label>
|
||||
<select
|
||||
id="new-project-model"
|
||||
aria-label="项目默认模型"
|
||||
value={newProjectDefaultModel}
|
||||
onChange={(event) => setNewProjectDefaultModel(event.target.value)}
|
||||
disabled={creatingProject}
|
||||
className="w-full rounded-md border-2 border-[#26384D] bg-white px-3 py-2 text-sm font-bold outline-none focus:ring-2 focus:ring-[#DCE6EF]"
|
||||
className="w-full rounded-md border border-foreground/15 bg-white px-3 py-2 text-sm font-medium outline-none focus:ring-2 focus:ring-brand/20"
|
||||
>
|
||||
<option value="">使用运行时默认模型</option>
|
||||
{configuredModelOptions.map((option) => (
|
||||
@@ -899,7 +776,7 @@ export function Sidebar() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[11px] font-bold text-[#68788B]">
|
||||
<p className="text-[11px] font-medium text-muted-foreground">
|
||||
{configuredModelOptions.length > 0
|
||||
? '模型由服务端配置提供,项目只保存选择,不保存 Key。'
|
||||
: '暂时没有可选模型,创建后会使用运行时默认模型。'}
|
||||
@@ -907,7 +784,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
{createProjectError ? (
|
||||
<div className="rounded-md border-2 border-[#26384D] bg-[#FCE9E5] px-3 py-2 text-xs font-black">
|
||||
<div className="rounded-md border border-foreground/15 bg-accent-soft px-3 py-2 text-xs font-semibold">
|
||||
{createProjectError}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -916,7 +793,7 @@ export function Sidebar() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="border-2 border-[#26384D] bg-white text-[#26384D]"
|
||||
className="border border-foreground/15 bg-white text-foreground"
|
||||
onClick={closeCreateProject}
|
||||
disabled={creatingProject}
|
||||
>
|
||||
@@ -924,47 +801,46 @@ export function Sidebar() {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="border-2 border-[#26384D] bg-[#DCE6EF] text-[#26384D]"
|
||||
className="border border-foreground/15 bg-brand-soft text-foreground"
|
||||
disabled={creatingProject
|
||||
|| !newProjectSelectedPath.trim()
|
||||
|| !newProjectTemplateId
|
||||
|| (newProjectDirectoryMode === 'create-child-directory' && !newProjectName.trim())}
|
||||
>
|
||||
确认创建
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null}
|
||||
|
||||
{!isPaintingModule && templateEntryError ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 px-4">
|
||||
{!isPaintingModule && projectEntryError ? (
|
||||
<div className="glass-surface fixed inset-0 z-50 flex items-center justify-center bg-foreground/15 px-4 backdrop-blur-[3px]">
|
||||
<div
|
||||
role="alertdialog"
|
||||
data-testid="project-template-error"
|
||||
className="w-full max-w-md rounded-lg border-4 border-[#26384D] bg-[#FFFFFF] p-4 text-[#26384D] shadow-[8px_8px_0_#26384D]"
|
||||
data-testid="project-config-error"
|
||||
className="glass-surface w-full max-w-md rounded-2xl border border-border/80 bg-background p-5 text-foreground shadow-float"
|
||||
>
|
||||
<h2 className="text-xl font-black">项目配置不可用</h2>
|
||||
<p className="mt-2 text-sm font-bold leading-6 text-[#68788B]">
|
||||
<h2 className="text-xl font-semibold">项目配置不可用</h2>
|
||||
<p className="mt-2 text-sm font-medium leading-6 text-muted-foreground">
|
||||
无法进入项目,请检查 .niancode/project.json。
|
||||
</p>
|
||||
<pre className="mt-3 max-h-32 overflow-auto rounded-md border-2 border-[#26384D] bg-white p-2 text-xs font-bold">
|
||||
{templateEntryError.message}
|
||||
<pre className="mt-3 max-h-32 overflow-auto rounded-md border border-foreground/15 bg-white p-2 text-xs font-medium">
|
||||
{projectEntryError.message}
|
||||
</pre>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="border-2 border-[#26384D] bg-[#FCE9E5] text-[#26384D]"
|
||||
className="border border-foreground/15 bg-accent-soft text-foreground"
|
||||
onClick={requestRemoveUnavailableProject}
|
||||
>
|
||||
移除项目卡片
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="border-2 border-[#26384D] bg-[#DCE6EF] text-[#26384D]"
|
||||
onClick={() => setTemplateEntryError(null)}
|
||||
className="border border-foreground/15 bg-brand-soft text-foreground"
|
||||
onClick={() => setProjectEntryError(null)}
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
@@ -988,176 +864,159 @@ export function Sidebar() {
|
||||
) : null}
|
||||
|
||||
{profileDialogOpen ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 px-4">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="sidebar-profile-dialog-title"
|
||||
className="w-full max-w-sm rounded-lg border-4 border-[#26384D] bg-[#FFFFFF] p-4 text-[#26384D] shadow-[8px_8px_0_#26384D]"
|
||||
<Dialog
|
||||
open={profileDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !profileRequired && !profileSaving) setProfileDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-w-sm p-5"
|
||||
onInteractOutside={(event) => {
|
||||
if (profileRequired || profileSaving) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<DialogHeader className="flex-row items-start justify-between gap-3 space-y-0">
|
||||
<div>
|
||||
<h2 id="sidebar-profile-dialog-title" className="text-xl font-black">个人资料</h2>
|
||||
<p className="mt-1 text-xs font-bold text-[#68788B]">帮助所有 Agent 更自然地了解和协作</p>
|
||||
<DialogTitle className="text-xl">个人资料</DialogTitle>
|
||||
<DialogDescription className="mt-1 text-xs">帮助所有 Agent 更自然地了解和协作</DialogDescription>
|
||||
</div>
|
||||
{!profileRequired ? <button
|
||||
type="button"
|
||||
aria-label="关闭个人资料"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md border-2 border-[#26384D] bg-white text-[#26384D] transition-transform active:scale-[0.96]"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md border border-foreground/15 bg-white text-foreground transition-transform active:scale-[0.985]"
|
||||
onClick={() => setProfileDialogOpen(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button> : null}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{profileRequired ? <div className="mt-4 rounded-lg border-2 border-[#26384D] bg-[#E9EFF5] p-3 text-sm font-black">首次使用前请先填写名字。个人资料与登录账号信息相互独立。</div> : null}
|
||||
{profileRequired ? <div className="mt-4 rounded-lg border border-foreground/15 bg-surface-tertiary p-3 text-sm font-semibold">首次使用前请先填写名字。个人资料与登录账号信息相互独立。</div> : null}
|
||||
|
||||
<div className="mt-4 grid gap-4 text-sm font-bold">
|
||||
<div><Label htmlFor="profile-display-name">名字 <span className="text-red-700">*</span></Label><Input id="profile-display-name" value={profileName} maxLength={100} onChange={(event) => setProfileName(event.target.value)} placeholder="希望 Agent 如何称呼你" className="mt-2 border-2 border-[#26384D] bg-white" /></div>
|
||||
<div><Label htmlFor="profile-age">年龄(选填)</Label><Input id="profile-age" type="number" min={1} max={150} value={profileAge} onChange={(event) => setProfileAge(event.target.value)} className="mt-2 border-2 border-[#26384D] bg-white" /></div>
|
||||
<div><Label htmlFor="profile-gender">性别(选填)</Label><select id="profile-gender" value={profileGender} onChange={(event) => setProfileGender(event.target.value as UserGender)} className="mt-2 h-10 w-full rounded-md border-2 border-[#26384D] bg-white px-3"><option value="undisclosed">不透露</option><option value="male">男</option><option value="female">女</option><option value="other">其他</option></select></div>
|
||||
{profileError ? <p role="alert" className="text-sm font-black text-red-700">{profileError}</p> : null}
|
||||
{profileSyncState?.status === 'loading' ? <p className="text-xs font-bold text-[#68788B]">正在同步云端个人资料…</p> : null}
|
||||
<Button type="button" onClick={() => void handleSaveProfile()} disabled={profileSaving || profileSyncState?.status === 'loading'} className="border-2 border-[#26384D] bg-[#DCE6EF] font-black text-[#26384D] shadow-[3px_3px_0_#26384D]">{profileSaving ? '正在同步…' : '保存个人资料'}</Button>
|
||||
<div className="mt-4 grid gap-4 text-sm font-medium">
|
||||
<div><Label htmlFor="profile-display-name">名字 <span className="text-red-700">*</span></Label><Input id="profile-display-name" value={profileName} maxLength={100} onChange={(event) => setProfileName(event.target.value)} placeholder="希望 Agent 如何称呼你" className="mt-2 border border-foreground/15 bg-white" /></div>
|
||||
<div><Label htmlFor="profile-age">年龄(选填)</Label><Input id="profile-age" type="number" min={1} max={150} value={profileAge} onChange={(event) => setProfileAge(event.target.value)} className="mt-2 border border-foreground/15 bg-white" /></div>
|
||||
<div><Label htmlFor="profile-gender">性别(选填)</Label><select id="profile-gender" value={profileGender} onChange={(event) => setProfileGender(event.target.value as UserGender)} className="mt-2 h-10 w-full rounded-md border border-foreground/15 bg-white px-3"><option value="undisclosed">不透露</option><option value="male">男</option><option value="female">女</option><option value="other">其他</option></select></div>
|
||||
{profileError ? <p role="alert" className="text-sm font-semibold text-red-700">{profileError}</p> : null}
|
||||
{profileSyncState?.status === 'loading' ? <p className="text-xs font-medium text-muted-foreground">正在同步云端个人资料…</p> : null}
|
||||
<Button type="button" onClick={() => void handleSaveProfile()} disabled={profileSaving || profileSyncState?.status === 'loading'} className="border border-foreground/15 bg-brand-soft font-semibold text-foreground shadow-soft">{profileSaving ? '正在同步…' : '保存个人资料'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null}
|
||||
|
||||
<div className="mt-auto border-t-4 border-[#26384D] p-2">
|
||||
<div className="mb-2 border-b border-[#26384D]/15 pb-2">
|
||||
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} />
|
||||
</div>
|
||||
<div className={cn(
|
||||
'flex items-center gap-2',
|
||||
sidebarCollapsed && 'flex-col-reverse',
|
||||
)}>
|
||||
<button
|
||||
ref={accountButtonRef}
|
||||
type="button"
|
||||
data-testid="sidebar-member-menu-trigger"
|
||||
aria-label={`打开会员菜单:${accountName}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountMenuOpen}
|
||||
onClick={() => setAccountMenuOpen((open) => !open)}
|
||||
className={cn(
|
||||
'flex min-h-[52px] min-w-0 flex-1 items-center gap-2.5 rounded-lg border-2 border-[#26384D] bg-[#FFFFFF] px-2.5 py-2 text-left text-sm font-black text-[#26384D] shadow-[3px_3px_0_#26384D] transition-[background-color,color,transform] hover:bg-[#EEF3F7] active:scale-[0.96]',
|
||||
accountMenuOpen && 'bg-[#FFFFFF] text-[#26384D]',
|
||||
sidebarCollapsed && 'min-h-11 w-full justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border-2 border-[#26384D] bg-[#FDE8E1] text-xs font-black text-[#26384D]">
|
||||
{accountInitial}
|
||||
</span>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">{accountName}</span>
|
||||
<span className="mt-0.5 flex items-center gap-1 text-[11px] font-bold opacity-80">
|
||||
<Crown className="h-3 w-3" />
|
||||
{memberLabel}
|
||||
<div className="mt-auto px-2 pb-2 pt-1">
|
||||
<div className="relative">
|
||||
<div className={cn(
|
||||
'flex items-center gap-2',
|
||||
sidebarCollapsed && 'flex-col-reverse',
|
||||
)}>
|
||||
<button
|
||||
ref={accountButtonRef}
|
||||
type="button"
|
||||
data-testid="sidebar-member-menu-trigger"
|
||||
aria-label={`打开会员菜单:${accountName}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountMenuOpen}
|
||||
onClick={() => setAccountMenuOpen((open) => !open)}
|
||||
className={cn(
|
||||
'motion-press flex min-h-9 min-w-0 flex-1 items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1 text-left text-sm font-medium text-foreground shadow-none transition-[background-color,color,transform] hover:bg-surface-subtle',
|
||||
accountMenuOpen && 'bg-background text-foreground',
|
||||
sidebarCollapsed && 'min-h-9 w-full justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent-soft text-xs font-semibold text-foreground">
|
||||
{accountInitial}
|
||||
</span>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">{accountName}</span>
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className={cn('h-4 w-4 shrink-0 transition-transform', accountMenuOpen && 'rotate-90')} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<SidebarUpdateButton collapsed={sidebarCollapsed} />
|
||||
</div>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 transition-transform', accountMenuOpen && 'rotate-90')} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<SidebarUpdateButton collapsed={sidebarCollapsed} />
|
||||
</div>
|
||||
|
||||
{accountMenuOpen ? (
|
||||
<div
|
||||
ref={accountMenuRef}
|
||||
role="menu"
|
||||
data-testid="sidebar-account-menu"
|
||||
className={cn(
|
||||
'fixed bottom-[72px] z-50 w-64 rounded-lg border-2 border-[#26384D] bg-[#FFFFFF] p-2 text-[#26384D] shadow-[6px_6px_0_#26384D]',
|
||||
sidebarCollapsed ? 'left-2' : 'left-3',
|
||||
)}
|
||||
>
|
||||
<div className="border-b border-[#26384D]/15 px-2 py-2">
|
||||
{accountMenuOpen ? (
|
||||
<div
|
||||
ref={accountMenuRef}
|
||||
role="menu"
|
||||
data-testid="sidebar-account-menu"
|
||||
className={cn(
|
||||
'glass-surface z-50 max-h-[calc(100vh-7rem)] max-w-[calc(100vw-1rem)] overflow-y-auto rounded-2xl border border-border/80 p-1 text-foreground shadow-float',
|
||||
sidebarCollapsed
|
||||
? 'fixed bottom-[72px] left-2 w-64'
|
||||
: 'absolute bottom-[calc(100%+0.5rem)] left-0 w-full',
|
||||
)}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border-2 border-[#26384D] bg-[#FDE8E1] text-xs font-black text-[#26384D]">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent-soft text-xs font-semibold text-foreground">
|
||||
{accountInitial}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-black">{accountFullName}</p>
|
||||
<p className="mt-0.5 text-[11px] font-bold text-[#68788B]">{membershipLabel}</p>
|
||||
<p className="truncate text-sm font-semibold">{accountFullName}</p>
|
||||
<p className="mt-0.5 text-[10px] font-medium text-muted-foreground">{membershipLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid gap-1">
|
||||
<div className="mt-1 grid gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="sidebar-account-usage-menuitem"
|
||||
aria-expanded={usageDrawerOpen}
|
||||
aria-controls="sidebar-account-usage-drawer"
|
||||
className="flex min-h-10 w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm font-bold transition-colors hover:bg-[#EEF3F7] active:scale-[0.96]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
|
||||
onClick={() => setUsageDrawerOpen((open) => !open)}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">剩余用量</span>
|
||||
<span
|
||||
data-testid="sidebar-account-usage-summary"
|
||||
className="mt-0.5 block truncate text-[11px] text-[#68788B]"
|
||||
>
|
||||
{tokenUsageSummary}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className={cn('h-4 w-4 shrink-0 text-[#68788B] transition-transform', usageDrawerOpen && 'rotate-90')} />
|
||||
<BarChart3 className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">剩余用量</span>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform', usageDrawerOpen && 'rotate-90')} />
|
||||
</button>
|
||||
{usageDrawerOpen ? (
|
||||
<div
|
||||
id="sidebar-account-usage-drawer"
|
||||
data-testid="sidebar-account-usage-drawer"
|
||||
className="ml-6 rounded-md border border-[#26384D]/20 bg-white px-2 py-2 text-xs font-bold text-[#68788B]"
|
||||
className="ml-5 grid gap-0.5 rounded-md bg-surface-subtle/70 px-1 py-0.5 text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-h-6 items-center justify-between gap-3 px-1">
|
||||
<span>订阅等级</span>
|
||||
<span className="text-[#26384D]">{memberLabel}</span>
|
||||
<span className="text-foreground">{memberLabel}</span>
|
||||
</div>
|
||||
<div className="mt-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span>5 小时</span>
|
||||
<span className="text-[#26384D]">
|
||||
{formatRemainingPercent(tokenUsageState.usage?.five_hour_remaining_percent)}
|
||||
</span>
|
||||
<div className="flex min-h-6 items-center justify-between gap-3 px-1">
|
||||
<span>5 小时</span>
|
||||
<span className="text-foreground">
|
||||
{formatRemainingPercent(tokenUsageState.usage?.five_hour_remaining_percent)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex min-h-6 items-center justify-between gap-3 px-1">
|
||||
<span>1 周</span>
|
||||
<span className="text-foreground">
|
||||
{formatRemainingPercent(tokenUsageState.usage?.weekly_remaining_percent)}
|
||||
</span>
|
||||
</div>
|
||||
{fiveHourRefreshLabel ? (
|
||||
<div data-testid="sidebar-five-hour-refresh-at" className="px-1 text-[10px] font-semibold text-muted-foreground">
|
||||
{fiveHourRefreshLabel}
|
||||
</div>
|
||||
{fiveHourRefreshLabel ? (
|
||||
<div
|
||||
data-testid="sidebar-five-hour-refresh-at"
|
||||
className="mt-0.5 text-[11px] font-semibold text-[#68788B]"
|
||||
>
|
||||
{fiveHourRefreshLabel}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span>1 周</span>
|
||||
<span className="text-[#26384D]">
|
||||
{formatRemainingPercent(tokenUsageState.usage?.weekly_remaining_percent)}
|
||||
</span>
|
||||
) : null}
|
||||
{weeklyRefreshLabel ? (
|
||||
<div data-testid="sidebar-weekly-refresh-at" className="px-1 text-[10px] font-semibold text-muted-foreground">
|
||||
{weeklyRefreshLabel}
|
||||
</div>
|
||||
{weeklyRefreshLabel ? (
|
||||
<div
|
||||
data-testid="sidebar-weekly-refresh-at"
|
||||
className="mt-0.5 text-[11px] font-semibold text-[#68788B]"
|
||||
>
|
||||
{weeklyRefreshLabel}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{tokenUsageExhausted ? (
|
||||
<div className="mt-2 rounded-md border border-[#F26A3D] bg-[#F6F8FA] p-2 text-[#26384D]">
|
||||
<p className="leading-4 text-[#68788B]">当前额度已用尽,升级订阅后可继续使用。</p>
|
||||
<div className="mt-1 rounded-md border border-accent/50 bg-background p-1.5 text-foreground">
|
||||
<p className="text-[11px] leading-4 text-muted-foreground">当前额度已用尽,升级订阅后可继续使用。</p>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="sidebar-account-upgrade-button"
|
||||
className="mt-2 flex min-h-8 w-full items-center justify-center gap-1.5 rounded-md border-2 border-[#26384D] bg-[#FDE8E1] px-2 py-1.5 text-xs font-black text-[#26384D] shadow-[2px_2px_0_#26384D] transition-transform hover:bg-[#FFFFFF] active:scale-[0.96]"
|
||||
className="mt-1.5 flex min-h-8 w-full items-center justify-center gap-1.5 rounded-md border border-foreground/15 bg-accent-soft px-2 py-1.5 text-xs font-semibold text-foreground shadow-soft transition-transform hover:bg-background active:scale-[0.985]"
|
||||
onClick={openSubscriptionUpgrade}
|
||||
>
|
||||
<Crown className="h-3.5 w-3.5" />
|
||||
@@ -1171,37 +1030,38 @@ export function Sidebar() {
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="flex min-h-10 w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm font-bold transition-colors hover:bg-[#EEF3F7] active:scale-[0.96]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
|
||||
onClick={openProfileDialog}
|
||||
>
|
||||
<UserCircle2 className="h-4 w-4 shrink-0" />
|
||||
<UserCircle2 className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">个人资料</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="sidebar-nav-settings"
|
||||
className="flex min-h-10 w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm font-bold transition-colors hover:bg-[#EEF3F7] active:scale-[0.96]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium transition-colors hover:bg-surface-subtle active:scale-[0.985]"
|
||||
onClick={() => navigateFromAccountMenu('/settings')}
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4 shrink-0" />
|
||||
<SettingsIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">{settingsLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 border-t border-[#26384D]/15 pt-2">
|
||||
<div className="mt-1 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="flex min-h-10 w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm font-bold text-[#FCE9E5] transition-colors hover:bg-[#FCE9E5] hover:text-[#26384D] active:scale-[0.96]"
|
||||
className="flex min-h-8 w-full items-center gap-2 rounded-md px-1 py-1 text-left text-sm font-medium text-accent-strong transition-colors hover:bg-accent-soft hover:text-foreground active:scale-[0.985]"
|
||||
onClick={() => void handleAccountLogout()}
|
||||
>
|
||||
<LogOut className="h-4 w-4 shrink-0" />
|
||||
<LogOut className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user