Files
Genarrative/apps/ai-game-creator-shell/src/view/layout.tsx
T
k88936 6981648796
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m48s
Project CI / Native shell tests (pull_request) Failing after 45s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Failing after 1m52s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m42s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m1s
合并 origin/master:Supervisor 永久退役,项目对话收敛为 DirectProject 与 Design Agent
- 解决 refactor/split-direct-project 与 origin/master 在 App.tsx、立项策划聊天视图、Direct composer/引用输入区、styles.css、Rust direct user item 与 appSurface 用例上的冲突,按「Supervisor 永久退役」口径保留 DirectProject 独立聊天容器与 Design Agent 两条产品路径
- 采纳 master 的策划 Agent V1/V2 退役:删除 GDD 审批卡、策划输入卡、planningLane、planningSessionV2、planningSessionContract、规划展示适配与 Rust planning_*_v2 命令、模块、契约及对应用例,不保留兼容别名或双跑路径
- 把 master「折叠思考显示单行预览」的目的落到当前结构:新增共享表现 chat/components/AgentReasoning/AgentReasoning.tsx(折叠态单行纯文本预览 + 箭头、展开态安全 Markdown),DirectProject 回合与策划回合共用,删掉两处写死的 pre 折叠实现
- 把 master「策划入口可选模型 / 推理档」的目的接到当前策划输入盒:复用 ConversationModelSelect 与 ComposerReasoningEffortSelect,配置写回仍走客户端配置通道
- App.tsx 删除只服务退役 Supervisor / 策划 V2 的 state、ref、effect、回调与死参数,并删除两条读路径都退役后的 workspaceProjectKind;openWorkspace 的工程类型入参保留为未使用契约
- Rust 侧保留本分支 canonical→wire 投影、无审计 Direct 回合与 direct user item 严格校验,并入 master 的 prepare_new_web_project_at 前置复核
- 更新 ADR 与 shared-memory 决策记录:策划当前只有 Design Agent、两条路径的共享表现清单,以及本次合并的口径、代价与验证证据
- 验证:AGC 与仓库 typecheck、check:encoding、check:doc-index、git diff --check、改动文件 eslint 0 error;AGC vitest 168 个文件中除 5 个 jsdom localStorage 环境失败文件与本分支既有 resourceTagStatsRefresh 失败外全绿,appSurface 198 passed / 13 skipped;Rust 定向用例 direct_codex_user_item、skill_pack、sessions 全过(整套分片在本容器受 /sbin -> usr/bin 触发沙箱预检失败,与本合并无关)
2026-09-21 21:09:34 +08:00

362 lines
12 KiB
TypeScript

import { openUrl } from '@tauri-apps/plugin-opener';
import {
CircleHelp,
FolderKanban,
Home,
LayoutTemplate,
Plus,
Settings,
User,
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import BRAND_ICON from '../../../../packages/shared/src/icons/taonier-product-ip.png';
import { useWalletStore } from '../stores/useWalletStore';
function cx(...classNames: Array<string | false | null | undefined>) {
return classNames.filter(Boolean).join(' ');
}
const sidebarIconButtonClass =
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text) hover:bg-(--platform-nav-item-hover-fill)';
const sidebarHelpButtonClass =
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text)';
const sidebarMenuItemClass =
'flex h-[30px] w-full cursor-pointer items-center justify-start gap-2 whitespace-nowrap rounded-[7px] border-0 bg-transparent px-[9px] py-0 text-left text-xs text-(--platform-text-strong) hover:bg-(--platform-nav-item-hover-fill) focus-visible:bg-(--platform-nav-item-hover-fill)';
export type LauncherView =
| 'home'
| 'projects'
| 'guide'
| 'contact'
| 'news'
| 'template-library'
| 'project-development';
type SidebarUserInfo = {
displayName?: string | null;
phoneNumberMasked?: string | null;
publicUserCode?: string | null;
};
type LauncherSidebarProps = {
activeView: LauncherView;
templateLibraryEnabled: boolean;
currentUser: SidebarUserInfo;
onViewChange: (view: LauncherView) => void;
onNoticeRequest: (title: string) => void;
onRechargeRequest: () => void;
onRuntimeConfigOpen: () => void;
onLogout: () => void;
};
type SidebarAccountMenuProps = {
currentUser: SidebarUserInfo;
onClose: () => void;
onLogout: () => void;
onNoticeRequest: (title: string) => void;
onRechargeRequest: () => void;
};
function formatMudPoints(value: number | null | undefined) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return '--';
}
return Math.max(0, Math.floor(value)).toLocaleString('zh-CN');
}
function SidebarAccountMenu({
currentUser,
onClose,
onLogout,
onNoticeRequest,
onRechargeRequest,
}: SidebarAccountMenuProps) {
const walletBalance = useWalletStore(
(state) => state.mudPointBalance?.totalPoints ?? null,
);
const walletBalanceError = useWalletStore(
(state) => state.mudPointBalanceError || null,
);
const walletBalanceLabel = formatMudPoints(walletBalance);
const walletBalanceStatus = walletBalanceError
? walletBalanceError
: walletBalance !== null
? '泥点余额已读取'
: '正在读取泥点余额';
function showNotice(title: string) {
onClose();
onNoticeRequest(title);
}
return (
<div
className="absolute -bottom-1 left-14 z-90 grid w-54.5 gap-1.5 rounded-lg border border-(--platform-subpanel-border) [background:var(--platform-subpanel-fill)] p-2.5 text-(--platform-text-strong)"
role="menu"
>
<strong className="text-[13px]">
{currentUser.displayName || '陶泥儿用户'}
</strong>
<span className="text-[11px] text-(--platform-text-soft)">
{currentUser.phoneNumberMasked ||
currentUser.publicUserCode ||
'账号信息读取中'}
</span>
<div
className="grid gap-2 rounded-xl bg-[linear-gradient(135deg,#eaa06a,#cf7a4a_58%,#b55c3b)] p-3 text-white shadow-[0_18px_38px_rgb(189_103_60_/_22%)]"
role="group"
aria-label="会员方案"
>
{/*SHOULD NOT SHOW IN THIS VERSION*/}
{/*<span className="grid size-8 shrink-0 place-items-center rounded-[0.85rem] bg-white/25">*/}
{/* <Crown size={16} aria-hidden="true" />*/}
{/*</span>*/}
{/*<span className="min-w-0">*/}
{/* <span className="block truncate text-sm font-black">*/}
{/* /!*{membershipLabel}*!/*/}
{/* </span>*/}
{/* <span className="block truncate text-[11px] font-semibold text-white/85">*/}
{/* 会员方案*/}
{/* </span>*/}
{/*</span>*/}
<div className="flex items-center justify-between gap-2">
<span
className="min-w-0 text-[12px] font-bold"
title={walletBalanceStatus}
>
剩余泥点 {walletBalanceLabel}
</span>
<button
type="button"
className="grid size-7 shrink-0 place-items-center rounded-full border border-white/70 bg-white/15 text-white"
aria-label="充值泥点"
onClick={() => {
onClose();
onRechargeRequest();
}}
>
<Plus size={14} aria-hidden="true" />
</button>
{/*<button*/}
{/* type="button"*/}
{/* className="h-8 rounded-full border border-white/85 bg-white/10 px-3 text-[12px] font-black text-white"*/}
{/* onClick={() => {*/}
{/* closeMenus();*/}
{/* // TODO upgrade component and logic is outdated on this branch*/}
{/* }}*/}
{/*>*/}
{/* 升级*/}
{/*</button>*/}
</div>
</div>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={
() => {
onClose();
showNotice('使用指南');
}
// TODO doc is not ready
}
>
使用指南
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() =>
openUrl(
'https://kcnz41bksl1c.feishu.cn/share/base/form/shrcn1p9WpuZMDxrI5ysLLJhw7k',
)
}
>
反馈联系
</button>
<button
type="button"
role="menuitem"
className={sidebarMenuItemClass}
onClick={() => {
onClose();
onLogout();
}}
>
退出登录
</button>
</div>
);
}
export function Sidebar({
activeView,
templateLibraryEnabled,
currentUser,
onViewChange,
onNoticeRequest,
onRechargeRequest,
onRuntimeConfigOpen,
onLogout,
}: LauncherSidebarProps) {
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
const accountMenuRef = useRef<HTMLDivElement | null>(null);
const helpMenuRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
function closeSidebarMenusIfOutside(target: EventTarget | null) {
if (!(target instanceof Node)) {
return;
}
if (
accountMenuRef.current?.contains(target) ||
helpMenuRef.current?.contains(target)
) {
return;
}
setAccountMenuOpen(false);
}
function handlePointerDown(event: PointerEvent) {
closeSidebarMenusIfOutside(event.target);
}
function handleFocusIn(event: FocusEvent) {
closeSidebarMenusIfOutside(event.target);
}
function handleWindowBlur() {
setAccountMenuOpen(false);
}
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('focusin', handleFocusIn);
window.addEventListener('blur', handleWindowBlur);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('focusin', handleFocusIn);
window.removeEventListener('blur', handleWindowBlur);
};
}, []);
function closeMenus() {
setAccountMenuOpen(false);
}
function changeView(view: LauncherView) {
closeMenus();
onViewChange(view);
}
return (
<aside className="sticky top-0 z-40 flex h-screen w-full flex-col items-center gap-4 overflow-visible border-r border-(--platform-surface-border) bg-(--platform-nav-fill) px-0 pb-4.5 pt-4">
<div className="grid place-items-center" aria-label="陶泥儿">
<span className="grid size-12 place-items-center rounded-full bg-(--platform-button-primary-fill) text-[10px] font-bold tracking-normal text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow)">
<img src={BRAND_ICON} alt="brand logo" />
</span>
</div>
<nav
className="grid justify-items-center gap-3.25"
aria-label="客户端导航"
>
<button
type="button"
aria-label="首页"
title="首页"
className={cx(
sidebarIconButtonClass,
activeView === 'home' &&
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
)}
onClick={() => changeView('home')}
>
<Home size={17} aria-hidden="true" />
</button>
<button
type="button"
aria-label="项目组"
title="项目组"
className={cx(
sidebarIconButtonClass,
activeView === 'projects' &&
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
)}
onClick={() => changeView('projects')}
>
<FolderKanban size={17} aria-hidden="true" />
</button>
{templateLibraryEnabled ? (
<button
type="button"
aria-label="模板库"
title="模板库"
className={cx(
sidebarIconButtonClass,
activeView === 'template-library' &&
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
)}
onClick={() => changeView('template-library')}
>
<LayoutTemplate size={17} aria-hidden="true" />
</button>
) : null}
</nav>
<div className="relative z-[60] mt-auto grid justify-items-center gap-[13px]">
<div className="relative" ref={accountMenuRef}>
<button
type="button"
className={sidebarIconButtonClass}
onClick={() => {
setAccountMenuOpen((open) => !open);
}}
aria-expanded={accountMenuOpen}
aria-label="我的"
title="我的"
>
<User size={16} aria-hidden="true" />
</button>
{accountMenuOpen ? (
<SidebarAccountMenu
currentUser={currentUser}
onClose={closeMenus}
onLogout={onLogout}
onNoticeRequest={onNoticeRequest}
onRechargeRequest={onRechargeRequest}
/>
) : null}
</div>
<div className="relative" ref={helpMenuRef}>
<button
type="button"
aria-label="帮助"
title="帮助"
className={sidebarHelpButtonClass}
onClick={() => {
closeMenus();
onNoticeRequest('使用指南');
// TODO open the guide
// TODO doc is not ready
}}
>
<CircleHelp size={17} aria-hidden="true" />
</button>
</div>
<button
type="button"
className={sidebarIconButtonClass}
onClick={() => {
closeMenus();
onRuntimeConfigOpen();
}}
aria-label="配置"
title="配置"
>
<Settings size={16} aria-hidden="true" />
</button>
</div>
</aside>
);
}