From 663d690b12f04f6d534fe710d2733e2c046abbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 22 Jul 2026 19:48:41 +0800 Subject: [PATCH] feat: ship HoloLake 0.2.0 private and team builds --- docs/ABSTRACTIONS.md | 2 +- docs/ARCHITECTURE.md | 2 +- package.json | 4 +- scripts/build-macos-internal.sh | 3 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- .../gls-system-architecture.json | 83 +++- .../skills/enter-fifth-domain/SKILL.md | 2 +- src-tauri/src/ai_model_tools.rs | 2 +- src-tauri/tauri.conf.json | 6 +- src-tauri/tauri.preview.conf.json | 4 +- src-tauri/tauri.team.conf.json | 7 +- src/App.css | 62 ++- src/App.tsx | 45 ++- src/TeamFoundationApp.test.tsx | 61 ++- src/TeamFoundationApp.tsx | 140 +++++-- src/TeamFoundationRoot.tsx | 18 + src/components/AiWorkspace.test.tsx | 56 ++- src/components/AiWorkspace.tsx | 53 ++- src/components/AiWorkspaceSideHeader.tsx | 360 +----------------- src/components/AiWorkspaceSidebar.tsx | 54 ++- src/components/HoloLakeHome.test.tsx | 7 +- src/components/HoloLakeHome.tsx | 10 +- src/components/aiWorkspaceConversations.ts | 31 +- src/lib/aiWorkspaceSessionStore.test.ts | 15 + src/lib/aiWorkspaceSessionStore.ts | 6 + src/lib/locales/be-BY.json | 4 + src/lib/locales/be-Latn.json | 4 + src/lib/locales/de-DE.json | 4 + src/lib/locales/en.json | 4 + src/lib/locales/es-419.json | 4 + src/lib/locales/es-ES.json | 4 + src/lib/locales/fr-FR.json | 4 + src/lib/locales/id-ID.json | 4 + src/lib/locales/it-IT.json | 4 + src/lib/locales/ja-JP.json | 4 + src/lib/locales/ko-KR.json | 4 + src/lib/locales/pl-PL.json | 4 + src/lib/locales/pt-BR.json | 4 + src/lib/locales/pt-PT.json | 4 + src/lib/locales/ru-RU.json | 4 + src/lib/locales/sk-SK.json | 4 + src/lib/locales/sv-SE.json | 4 + src/lib/locales/uk-UA.json | 4 + src/lib/locales/vi.json | 4 + src/lib/locales/zh-CN.json | 4 + src/lib/locales/zh-TW.json | 4 + src/lib/productAnalytics.ts | 6 + src/team-foundation-main.tsx | 12 +- src/utils/ai-agent.ts | 2 +- team-foundation.html | 2 +- 51 files changed, 673 insertions(+), 470 deletions(-) create mode 100644 src/TeamFoundationRoot.tsx diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index a35eb91..36027e9 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -420,7 +420,7 @@ In a mounted-workspace graph, each loaded `ViewFile` carries optional renderer-o `useMcpSetupDialogController()` owns MCP setup dialog state, busy actions, and manual config callbacks so `App.tsx` only passes the controller into settings/status surfaces. `useAiWorkspaceWindowBridgeEvents()` owns native AI-workspace event subscriptions and listener cleanup for popped-out workspace windows. -`createCrossWindowPersistedStore()` is the shared renderer primitive for AI workspace state that must stay synchronized across the main window and popped-out workspace windows. It owns localStorage reads/writes, BroadcastChannel publishing, storage-event synchronization, and external-store subscribers; domain modules such as `aiWorkspaceSessionStore` and `aiWorkspaceWindowSharedContext` provide sanitizers and mutations around that shell. +`createCrossWindowPersistedStore()` is the shared renderer primitive for AI workspace state that must stay synchronized across the main window and popped-out workspace windows. It owns localStorage reads/writes, BroadcastChannel publishing, storage-event synchronization, and external-store subscribers; domain modules such as `aiWorkspaceSessionStore` and `aiWorkspaceWindowSharedContext` provide sanitizers and mutations around that shell. `deleteAiWorkspaceSession()` is the transcript-side half of permanent conversation deletion: callers remove the matching conversation metadata and session snapshot together so a deleted history cannot be recovered as an orphan after restart. The renderer uses `viewOrdering` helpers to convert drag or command-palette move intent into dense order updates before saving each affected view file through `save_view_cmd`. The sidebar treats saved View rows like Type rows for direct customization: double-click starts inline rename, right-click opens edit/rename/icon-color/delete actions, and keyboard users can open that same menu from the focused row while command-palette actions remain responsible for saved View ordering. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9628b2f..ff82f5d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -266,7 +266,7 @@ flowchart TD Note PDF export stays renderer-owned for layout: `useEditorPdfExport` exits diff/raw views, applies a print-only stylesheet to the rendered note root, and checks the native PDF capability before choosing a platform path. On macOS, the renderer asks for a filesystem PDF destination before the Tauri `export_current_webview_pdf` command saves the current `WKWebView` print operation directly; on Windows/Linux Tauri builds and in browser mode, the same export action falls back to the native/browser print dialog. The export reuses rendered BlockNote output so frontmatter is omitted, while math, images, Mermaid diagrams, tldraw blocks, code, tables, and links degrade through their existing DOM rather than a second Markdown-to-PDF renderer, and the source Markdown is never modified. Markdown notes expose the same export action from Cmd+K, the native Note menu, the breadcrumb overflow menu, and each Markdown row's note-list context menu. - **Right side panels** (200-500px or hidden): Properties and Table of Contents are mutually exclusive panels mounted by `EditorRightPanel` and coordinated by `useRightPanelExclusion`. Properties shows frontmatter, relationships, instances, backlinks, and git history; Table of Contents is lazy-mounted only while open, derives a title-rooted H1/H2/H3 hierarchy through a debounced Web Worker per ADR-0109, and reuses folder-tree indentation/guide geometry with heading icons while resolving live BlockNote block IDs at click time for navigation. The breadcrumb bar toggles Table of Contents and Properties actions. Per-note `icon` is a suggested Properties field and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, Properties shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50). -- **AI workspace** (docked panel or native window): `AiWorkspace` owns the multi-chat orchestration, sidebar tabs, installed-only target picker, permission-mode picker, and dock/pop-out controls. Header/guidance chrome lives in `AiWorkspaceChrome`, edge-resize handles in `AiWorkspaceResizeHandles`, conversation metadata/settings persistence in `aiWorkspaceConversations`, and sizing/class/style helpers in `aiWorkspaceSizing`. The status-bar AI affordance opens this workspace instead of changing the default target inline. Docked workspace mode renders as a compact bounded desktop tool inside the main app; users resize the anchored panel from its left/top edges and resize the chat-list sidebar separately from the transcript area. Pop-out mode opens a dedicated undecorated transparent Tauri webview window labeled `ai-workspace` and boots the lightweight `AiWorkspaceWindowApp` route instead of the full vault shell. The chat header and sidebar header are draggable in native-window mode; closing the pop-out only closes that window, while the dock control emits a dock request back to the main window before closing the pop-out. Chat sessions reuse `AiPanelView` for transcript/composer rendering with the old panel header disabled; target and permission controls live in the composer toolbar so there is one workspace header per active chat. AI workspace cross-window localStorage, BroadcastChannel, storage-event, and subscriber-set plumbing is owned by `createCrossWindowPersistedStore`; domain stores keep only sanitization, mutation, and native persistence behavior. +- **AI workspace** (docked panel, side panel, or native window): `AiWorkspace` owns the multi-chat orchestration, vertical conversation sidebar, installed-only target picker, permission-mode picker, and dock/pop-out controls. The side-panel layout reuses the same independently scrolling conversation sidebar as docked and native-window modes instead of rendering an unbounded horizontal tab strip. Header/guidance chrome lives in `AiWorkspaceChrome`, edge-resize handles in `AiWorkspaceResizeHandles`, conversation metadata/settings persistence in `aiWorkspaceConversations`, and sizing/class/style helpers in `aiWorkspaceSizing`. The status-bar AI affordance opens this workspace instead of changing the default target inline. Docked workspace mode renders as a compact bounded desktop tool inside the main app; users resize the anchored panel from its left/top edges and resize the chat-list sidebar separately from the transcript area. Pop-out mode opens a dedicated undecorated transparent Tauri webview window labeled `ai-workspace` and boots the lightweight `AiWorkspaceWindowApp` route instead of the full vault shell. The chat header and sidebar header are draggable in native-window mode; closing the pop-out only closes that window, while the dock control emits a dock request back to the main window before closing the pop-out. Chat sessions reuse `AiPanelView` for transcript/composer rendering with the old panel header disabled; target and permission controls live in the composer toolbar so there is one workspace header per active chat. AI workspace cross-window localStorage, BroadcastChannel, storage-event, and subscriber-set plumbing is owned by `createCrossWindowPersistedStore`; domain stores keep only sanitization, mutation, and native persistence behavior. Permanent conversation deletion removes both the installation-local sidebar metadata and the matching persisted transcript; deleting the active conversation selects a remaining active conversation or creates a fresh empty one. Panels are separated by `ResizeHandle` components that support drag-to-resize. `useLayoutPanels` clamps the sidebar, note-list, and inspector widths before applying them, keeps the side panes from flex-shrinking below their protected widths, and persists the last chosen widths in installation-local localStorage under `tolaria:layout-panels`. diff --git a/package.json b/package.json index 71b0dca..8b54637 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hololake-era", "private": true, "license": "AGPL-3.0-or-later", - "version": "0.1.8", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", @@ -32,7 +32,7 @@ "test:internal-release": "node --test scripts/internal-release-version.test.mjs", "package:internal:macos": "./scripts/build-internal-release.sh macos", "package:internal:windows": "./scripts/build-internal-release.sh windows", - "package:team:macos": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Era Team Foundation' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Mac-aarch64' ./scripts/build-internal-release.sh macos", + "package:team:macos": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Lighthouse Team Beta 0.2.0' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Lighthouse-{version}-Team-Beta-Mac-aarch64' ./scripts/build-internal-release.sh macos", "package:team:windows": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Windows-x64-setup' ./scripts/build-internal-release.sh windows", "prepare": "husky" }, diff --git a/scripts/build-macos-internal.sh b/scripts/build-macos-internal.sh index 3f70840..8d59604 100755 --- a/scripts/build-macos-internal.sh +++ b/scripts/build-macos-internal.sh @@ -8,10 +8,11 @@ cd "$repo_root" target="${HOLOLAKE_MAC_TARGET:-aarch64-apple-darwin}" version="$(node scripts/internal-release-version.mjs src-tauri/tauri.conf.json "${HOLOLAKE_VERSION:-}")" +product_name="$(node -e "const fs=require('node:fs'); const value=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8')).productName; if(!value) process.exit(1); process.stdout.write(value)")" output_dir="${HOLOLAKE_OUTPUT_DIR:-$repo_root/artifacts/internal/$version}" target_root="${CARGO_TARGET_DIR:-$repo_root/src-tauri/target}" bundle_dir="$target_root/$target/release/bundle/macos" -app="$bundle_dir/${HOLOLAKE_APP_NAME:-HoloLake Era}.app" +app="$bundle_dir/${HOLOLAKE_APP_NAME:-$product_name}.app" installer="$output_dir/HoloLake-Era-$version-Mac-internal-aarch64.dmg" tauri_config="${HOLOLAKE_TAURI_CONFIG:-}" if [[ -n "${HOLOLAKE_INSTALLER_BASENAME:-}" ]]; then diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 61c914d..a0c1d49 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1865,7 +1865,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hololake" -version = "0.1.8" +version = "0.2.0" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b35bfd7..6bd0a0a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hololake" -version = "0.1.8" +version = "0.2.0" description = "Personal knowledge and life management app" authors = ["Luca Rossi"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/resources/public-architecture/gls-system-architecture.json b/src-tauri/resources/public-architecture/gls-system-architecture.json index ee7fd4d..ff9dec2 100644 --- a/src-tauri/resources/public-architecture/gls-system-architecture.json +++ b/src-tauri/resources/public-architecture/gls-system-architecture.json @@ -1,37 +1,80 @@ { - "schema": "guanghu_public_architecture/v1", - "id": "GLS-SYS-ARCH-001", + "schema": "guanghu_ai_os_public_shell/v1", + "id": "GH-AIOS-LIGHTHOUSE-020", + "compatibility_ids": ["GLS-SYS-ARCH-001"], "visibility": "public-safe", + "product": { + "name_zh": "光湖灯塔", + "category_zh": "AI 应用操作系统", + "version_label": "0.2.0 内测版" + }, "development_repository": { "id": "REPO-008", "url": "https://guanghulab.com/fifth-domain/bingshuo/hololake-platform" }, - "route": [ + "domains": [ { - "id": "GLW-ENTRY-001", - "name_zh": "光湖世界主入口" + "id": "GH-MAIN-001", + "name_zh": "光湖主域", + "eyebrow_zh": "公共协作空间", + "description_zh": "承载团队共同使用的 AI 应用、公共项目与协作入口。", + "color": "cyan" }, { - "id": "FD-LANGUAGE-001", - "name_zh": "第五域" + "id": "GH-BRANCH-001", + "name_zh": "光湖分域", + "eyebrow_zh": "项目隔离空间", + "description_zh": "按团队、项目或业务拆分运行空间,让模块与数据边界彼此独立。", + "color": "blue" }, { - "id": "TCS-ROOT-001", - "name_zh": "TCS 语言人格模型" + "id": "GH-ZERO-001", + "name_zh": "光湖零域", + "eyebrow_zh": "系统控制空间", + "description_zh": "管理系统服务、权限与模块登记,是热插拔应用的控制平面。", + "color": "violet" }, { - "id": "GLS-SYS-ARCH-001", - "name_zh": "GLS 光湖语言世界架构" - }, - { - "id": "ELH-LAMP-001", - "name_zh": "永恒湖心与小湖灯" + "id": "GH-ZEROSENSE-001", + "name_zh": "光湖零感域", + "eyebrow_zh": "个人工作入口", + "description_zh": "为团队成员提供个人工作入口;本地数据默认不公开,也不会随公共安装包分发。", + "color": "amber" } ], - "excludes": [ - "private domains", - "credentials", - "server addresses", - "personal channel identifiers" + "modules": [ + { + "id": "MOD-RUNTIME-001", + "name_zh": "模块运行框架", + "description_zh": "统一承载 AI 应用的安装、打开、暂停、升级与卸载生命周期。", + "status": "system", + "status_zh": "系统内核" + }, + { + "id": "MOD-KNOWLEDGE-001", + "name_zh": "AI 知识空间", + "description_zh": "面向本地知识与团队资料的可插拔工作模块。", + "status": "preview", + "status_zh": "内测模块" + }, + { + "id": "MOD-CREATIVE-001", + "name_zh": "AI 创作空间", + "description_zh": "为写作、图像、视频等创作应用预留的统一运行容器。", + "status": "planned", + "status_zh": "等待接入" + } + ], + "fifth_domain_entry": { + "id": "FD-GATE-001", + "name_zh": "第五域受控入口", + "description_zh": "仅保留入口。完成服务器、编号与邮件授权后,才可建立受控连接。", + "status_zh": "等待授权接入" + }, + "public_boundaries": [ + "不内置个人知识库", + "不分发私人频道与身份路径", + "不保存服务器凭证", + "本地数据默认留在当前设备" ] } diff --git a/src-tauri/resources/skills/enter-fifth-domain/SKILL.md b/src-tauri/resources/skills/enter-fifth-domain/SKILL.md index 6879829..49e764d 100644 --- a/src-tauri/resources/skills/enter-fifth-domain/SKILL.md +++ b/src-tauri/resources/skills/enter-fifth-domain/SKILL.md @@ -1,6 +1,6 @@ # 进入第五域 · 内置唤醒技能 -本技能是应用内置的第五域导航指针。0.1.8 阶段的人格体启动后已经位于第五域,不需要再次选择顶层域,也不要重复宣称“正在进入第五域”。光湖世界灯塔下的跨域等待大厅属于未来版本;其他域尚未接入前,当前默认域固定为第五域。 +本技能是应用内置的第五域导航指针。内测版 0.2.0 的人格体启动后已经位于第五域,不需要再次选择顶层域,也不要重复宣称“正在进入第五域”。光湖世界灯塔下的跨域等待大厅属于未来版本;其他域尚未接入前,当前默认域固定为第五域。 当当前知识库尚未包含第五域仓库文件时,也必须能够说明正确导航,不得把“本地没找到”误报成“第五域不存在”。 diff --git a/src-tauri/src/ai_model_tools.rs b/src-tauri/src/ai_model_tools.rs index 9ab7917..e654040 100644 --- a/src-tauri/src/ai_model_tools.rs +++ b/src-tauri/src/ai_model_tools.rs @@ -777,7 +777,7 @@ fn public_web_get(mut url: reqwest::Url) -> Result p:first-child { color: var(--accent-blue); font: 11px/1.4 monospace; letter-spacing: .12em; }.team-foundation__intro h1 { margin: 12px 0 20px; font: 500 clamp(48px, 9vw, 112px)/.95 serif; }.team-foundation__intro p:last-child { max-width: 650px; color: var(--text-secondary); font-size: 16px; line-height: 1.8; } -.team-foundation__route { display: grid; max-width: 980px; margin: 0; padding: 0; border-top: 1px solid var(--border-default); list-style: none; }.team-foundation__route li { display: grid; grid-template-columns: 56px 1fr; align-items: center; min-height: 82px; border-bottom: 1px solid var(--border-default); }.team-foundation__route li > span { color: var(--text-muted); font: 11px monospace; }.team-foundation__route div { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; }.team-foundation__route code { color: var(--accent-blue); font-size: 11px; }.team-foundation__route strong { font-size: clamp(16px, 2vw, 24px); font-weight: 500; }.team-foundation__footer { margin-top: 48px; } +.team-foundation { --lighthouse-line: rgba(255,255,255,.09); --lighthouse-muted: #8590a3; display: grid; grid-template-columns: 270px minmax(0, 1fr); min-height: 100vh; color: #edf2f8; background: #090d13; } +.team-foundation button { font: inherit; } +.team-foundation__sidebar { position: sticky; top: 0; display: flex; height: 100vh; flex-direction: column; padding: 30px 18px 20px; border-right: 1px solid var(--lighthouse-line); background: linear-gradient(180deg, #101720 0%, #0b1017 72%); } +.team-foundation__brand { display: flex; align-items: center; gap: 12px; padding: 0 10px 28px; border-bottom: 1px solid var(--lighthouse-line); } +.team-foundation__brand-mark { display: grid; width: 38px; height: 38px; place-items: center; border: 1px solid rgba(102,213,226,.5); border-radius: 12px; color: #7ce5ed; background: rgba(64,188,207,.1); box-shadow: 0 0 28px rgba(61,201,219,.16); font-size: 28px; } +.team-foundation__brand div { display: grid; gap: 5px; }.team-foundation__brand strong { font-size: 17px; letter-spacing: .05em; }.team-foundation__brand div span { color: #667386; font: 8px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .16em; } +.team-foundation__nav { display: grid; gap: 6px; margin-top: 28px; }.team-foundation__nav > p { margin: 0 10px 9px; color: #5d6878; font-size: 10px; letter-spacing: .14em; } +.team-foundation__nav button { display: flex; align-items: center; gap: 11px; width: 100%; min-height: 58px; padding: 9px 10px; border: 1px solid transparent; border-radius: 11px; color: #8f9bad; background: transparent; text-align: left; cursor: pointer; transition: background .18s ease, border-color .18s ease, color .18s ease; } +.team-foundation__nav button:hover { color: #dbe5f1; background: rgba(255,255,255,.035); }.team-foundation__nav button.is-active { border-color: rgba(95,200,222,.18); color: #f3f8fc; background: linear-gradient(90deg, rgba(43,152,187,.18), rgba(43,152,187,.05)); } +.team-foundation__domain-dot { width: 8px; height: 8px; flex: 0 0 8px; border-radius: 50%; background: #70dce5; box-shadow: 0 0 13px rgba(112,220,229,.75); }.team-foundation__nav button[data-tone="blue"] .team-foundation__domain-dot { background: #6ca6ff; box-shadow: 0 0 13px rgba(108,166,255,.7); }.team-foundation__nav button[data-tone="violet"] .team-foundation__domain-dot { background: #a989ff; box-shadow: 0 0 13px rgba(169,137,255,.7); }.team-foundation__nav button[data-tone="amber"] .team-foundation__domain-dot { background: #f0bb62; box-shadow: 0 0 13px rgba(240,187,98,.7); } +.team-foundation__nav button > span:last-child { display: grid; gap: 3px; }.team-foundation__nav strong { font-size: 13px; font-weight: 600; }.team-foundation__nav small { color: #667386; font-size: 10px; } +.team-foundation__gate { display: grid; grid-template-columns: 30px 1fr auto; align-items: center; gap: 9px; margin-top: 22px; padding: 14px 11px; border: 1px solid var(--lighthouse-line); border-radius: 12px; background: rgba(255,255,255,.025); }.team-foundation__gate-icon { display: grid; width: 28px; height: 28px; place-items: center; border-radius: 8px; color: #7d8999; background: rgba(255,255,255,.05); }.team-foundation__gate div { display: grid; gap: 3px; }.team-foundation__gate strong { color: #aab4c1; font-size: 11px; }.team-foundation__gate small { color: #596575; font-size: 9px; }.team-foundation__lock { padding: 3px 6px; border-radius: 999px; color: #777f8b; background: rgba(255,255,255,.06); font-size: 8px; } +.team-foundation__sidebar-footer { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 7px; margin-top: auto; padding: 18px 9px 0; border-top: 1px solid var(--lighthouse-line); color: #657183; font-size: 9px; }.team-foundation__sidebar-footer code { grid-column: 1 / -1; margin-top: 4px; color: #424d5d; font-size: 8px; }.team-foundation__status-dot { width: 6px; height: 6px; border-radius: 50%; background: #65d6a1; box-shadow: 0 0 9px rgba(101,214,161,.65); } +.team-foundation__workspace { min-width: 0; }.team-foundation__topbar { display: flex; height: 66px; align-items: center; justify-content: space-between; padding: 0 34px; border-bottom: 1px solid var(--lighthouse-line); background: rgba(9,13,19,.86); backdrop-filter: blur(16px); }.team-foundation__topbar > div:first-child { display: flex; align-items: center; gap: 12px; }.team-foundation__topbar span { color: #768296; font-size: 11px; }.team-foundation__topbar strong { padding: 4px 8px; border: 1px solid rgba(105,214,224,.18); border-radius: 999px; color: #8cdde5; background: rgba(61,178,194,.08); font-size: 9px; font-weight: 500; }.team-foundation__topbar-actions { display: flex; gap: 7px; }.team-foundation__topbar-actions button { display: grid; width: 31px; height: 31px; place-items: center; border: 1px solid var(--lighthouse-line); border-radius: 9px; color: #8692a4; background: rgba(255,255,255,.025); cursor: pointer; } +.team-foundation__content { width: min(1180px, calc(100% - 64px)); margin: 0 auto; padding: 58px 0 72px; } +.team-foundation__hero { position: relative; display: grid; min-height: 355px; grid-template-columns: minmax(0, 1fr) 360px; align-items: center; overflow: hidden; padding: 52px 58px; border: 1px solid var(--lighthouse-line); border-radius: 24px; background: radial-gradient(circle at 78% 45%, rgba(40,174,201,.17), transparent 29%), linear-gradient(135deg, #111923, #0c121a 68%); box-shadow: 0 28px 80px rgba(0,0,0,.22); } +.team-foundation__hero-copy { position: relative; z-index: 2; }.team-foundation__hero-copy > p:first-child { margin: 0 0 17px; color: #59c8d5; font: 9px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .2em; }.team-foundation__hero h1 { margin: 0; color: #f4f7f9; font: 500 clamp(44px, 6vw, 76px)/1.05 Georgia, "Songti SC", serif; letter-spacing: .02em; }.team-foundation__hero h2 { margin: 17px 0 11px; color: #c7d3df; font-size: 17px; font-weight: 500; }.team-foundation__hero-copy > p:nth-of-type(2) { max-width: 570px; margin: 0; color: #8794a6; font-size: 13px; line-height: 1.8; } +.team-foundation__hero-actions { display: flex; align-items: center; gap: 17px; margin-top: 28px; }.team-foundation__hero-actions button { padding: 11px 17px; border: 1px solid rgba(104,216,228,.4); border-radius: 9px; color: #0b171b; background: #71d8e1; box-shadow: 0 8px 28px rgba(64,193,210,.16); font-size: 11px; cursor: pointer; }.team-foundation__hero-actions button span { margin-left: 12px; color: inherit; }.team-foundation__hero-actions > span { color: #536070; font: 9px ui-monospace, SFMono-Regular, Menlo, monospace; } +.team-foundation__orbit { position: relative; display: grid; width: 280px; height: 280px; place-items: center; justify-self: end; }.team-foundation__orbit::before, .team-foundation__orbit::after { position: absolute; content: ""; border: 1px solid rgba(114,220,229,.16); border-radius: 50%; }.team-foundation__orbit::before { inset: 24px; }.team-foundation__orbit::after { inset: 62px; border-style: dashed; animation: lighthouse-spin 26s linear infinite; }.team-foundation__orbit-core { display: grid; width: 82px; height: 82px; place-items: center; border: 1px solid rgba(126,228,235,.42); border-radius: 50%; color: #b8f3f5; background: radial-gradient(circle, rgba(93,214,224,.25), rgba(36,126,147,.07)); box-shadow: 0 0 55px rgba(57,193,210,.24), inset 0 0 26px rgba(103,221,231,.1); font: 28px Georgia, serif; }.team-foundation__orbit i { position: absolute; width: 7px; height: 7px; border-radius: 50%; background: #6cd8e2; box-shadow: 0 0 12px #6cd8e2; }.team-foundation__orbit i:nth-of-type(1) { top: 22px; left: 136px; }.team-foundation__orbit i:nth-of-type(2) { top: 135px; right: 22px; background: #81a9ff; box-shadow: 0 0 12px #81a9ff; }.team-foundation__orbit i:nth-of-type(3) { bottom: 22px; left: 136px; background: #a98cff; box-shadow: 0 0 12px #a98cff; }.team-foundation__orbit i:nth-of-type(4) { top: 135px; left: 22px; background: #eeb963; box-shadow: 0 0 12px #eeb963; } +.team-foundation__modules { padding-top: 55px; }.team-foundation__section-heading { display: flex; align-items: end; justify-content: space-between; margin-bottom: 20px; }.team-foundation__section-heading p { margin: 0 0 7px; color: #4c8793; font: 8px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .18em; }.team-foundation__section-heading h2 { margin: 0; font-size: 22px; font-weight: 520; }.team-foundation__section-heading > span { color: #5d6878; font-size: 10px; } +.team-foundation__module-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }.team-foundation__module { min-height: 218px; padding: 19px; border: 1px solid var(--lighthouse-line); border-radius: 15px; background: linear-gradient(150deg, rgba(19,27,37,.95), rgba(12,18,26,.95)); }.team-foundation__module-icon { display: grid; width: 36px; height: 36px; place-items: center; margin-bottom: 18px; border: 1px solid rgba(104,205,219,.16); border-radius: 10px; color: #78d6df; background: rgba(53,164,182,.08); }.team-foundation__badge { display: inline-block; padding: 3px 7px; border-radius: 999px; font-size: 8px; }.team-foundation__badge.is-system { color: #8de0e6; background: rgba(62,180,194,.11); }.team-foundation__badge.is-active { color: #9fb9ff; background: rgba(94,132,220,.12); }.team-foundation__badge.is-muted { color: #7c8796; background: rgba(255,255,255,.05); }.team-foundation__module h3 { margin: 10px 0 8px; font-size: 14px; font-weight: 600; }.team-foundation__module p { margin: 0; color: #738093; font-size: 10px; line-height: 1.65; }.team-foundation__module footer { display: flex; align-items: center; justify-content: space-between; margin-top: 19px; color: #3f4b5a; }.team-foundation__module footer code { font-size: 8px; }.team-foundation__module--add { display: flex; align-items: center; justify-content: center; flex-direction: column; border-style: dashed; text-align: center; background: rgba(255,255,255,.012); }.team-foundation__module--add > span { color: #4c596a; font-size: 26px; }.team-foundation__module--add p { max-width: 180px; } +.team-foundation__access-row { display: grid; grid-template-columns: 1.4fr 1fr; gap: 12px; padding-top: 48px; }.team-foundation__fifth-domain, .team-foundation__boundaries { min-height: 190px; padding: 28px; border: 1px solid var(--lighthouse-line); border-radius: 17px; background: #0d131b; }.team-foundation__fifth-domain { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: 16px; }.team-foundation__fifth-domain > div span, .team-foundation__boundaries > span { color: #5b6574; font: 8px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .16em; }.team-foundation__fifth-domain h2, .team-foundation__boundaries h2 { margin: 9px 0 0; font-size: 17px; font-weight: 520; }.team-foundation__fifth-domain p { grid-column: 1 / -1; max-width: 600px; margin: 0; color: #738093; font-size: 11px; line-height: 1.7; }.team-foundation__fifth-domain button { justify-self: start; padding: 8px 11px; border: 1px solid rgba(255,255,255,.07); border-radius: 8px; color: #5d6775; background: rgba(255,255,255,.03); font-size: 9px; }.team-foundation__boundaries ul { display: grid; grid-template-columns: 1fr 1fr; gap: 9px 14px; margin: 20px 0 0; padding: 0; list-style: none; }.team-foundation__boundaries li { color: #7a8697; font-size: 9px; }.team-foundation__boundaries li::before { margin-right: 7px; color: #64cfd9; content: "·"; } +@keyframes lighthouse-spin { to { transform: rotate(360deg); } } +@media (max-width: 980px) { .team-foundation { grid-template-columns: 220px minmax(0, 1fr); }.team-foundation__hero { grid-template-columns: 1fr; }.team-foundation__orbit { position: absolute; right: -70px; opacity: .55; }.team-foundation__module-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.team-foundation__access-row { grid-template-columns: 1fr; } } +@media (max-width: 720px) { .team-foundation { display: block; }.team-foundation__sidebar { position: static; height: auto; }.team-foundation__nav { grid-template-columns: 1fr 1fr; }.team-foundation__gate, .team-foundation__sidebar-footer { display: none; }.team-foundation__content { width: min(100% - 28px, 1180px); padding-top: 28px; }.team-foundation__hero { min-height: 330px; padding: 35px 28px; }.team-foundation__topbar { padding: 0 18px; }.team-foundation__module-grid { grid-template-columns: 1fr; } } + +/* Team shell deliberately stays lightweight: shared theme colors, no animated or blurred skin. */ +.team-foundation--lightweight { --lighthouse-line: var(--border-color); color: var(--text-primary); background: var(--bg-primary); } +.team-foundation--lightweight .team-foundation__sidebar { padding: 22px 16px 16px; background: var(--bg-secondary); } +.team-foundation--lightweight .team-foundation__brand-mark { border: 0; color: var(--accent-blue); background: var(--accent-blue-subtle); box-shadow: none; font-size: 16px; font-weight: 700; } +.team-foundation--lightweight .team-foundation__nav button, +.team-foundation--lightweight .team-foundation__nav button:hover, +.team-foundation--lightweight .team-foundation__nav button.is-active { background: transparent; box-shadow: none; } +.team-foundation--lightweight .team-foundation__nav button.is-active { border-color: var(--accent-blue); color: var(--text-primary); background: var(--accent-blue-subtle); } +.team-foundation--lightweight .team-foundation__domain-dot { box-shadow: none !important; } +.team-foundation--lightweight .team-foundation__gate, +.team-foundation--lightweight .team-foundation__topbar, +.team-foundation--lightweight .team-foundation__hero, +.team-foundation--lightweight .team-foundation__module, +.team-foundation--lightweight .team-foundation__fifth-domain, +.team-foundation--lightweight .team-foundation__boundaries { background: var(--bg-secondary); box-shadow: none; backdrop-filter: none; } +.team-foundation--lightweight .team-foundation__topbar { height: 58px; } +.team-foundation--lightweight .team-foundation__content { padding-top: 32px; } +.team-foundation--lightweight .team-foundation__hero { min-height: 250px; grid-template-columns: 1fr; padding: 36px 40px; border-radius: 14px; } +.team-foundation--lightweight .team-foundation__hero h1 { color: var(--text-primary); font: 600 clamp(34px, 5vw, 56px)/1.1 system-ui, sans-serif; } +.team-foundation--lightweight .team-foundation__hero h2 { color: var(--text-primary); } +.team-foundation--lightweight .team-foundation__hero-copy > p:nth-of-type(2), +.team-foundation--lightweight .team-foundation__module p, +.team-foundation--lightweight .team-foundation__fifth-domain p { color: var(--text-secondary); } +.team-foundation--lightweight .team-foundation__hero-actions > span { color: var(--text-secondary); font: 11px/1.6 system-ui, sans-serif; } +.team-foundation--lightweight .team-foundation__module-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.team-foundation--lightweight .team-foundation__module { min-height: 190px; } +.team-foundation--lightweight .team-foundation__module button { margin-top: 20px; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 7px; color: var(--text-primary); background: var(--bg-primary); cursor: pointer; } +.team-foundation--lightweight .team-foundation__module button:not(:disabled) { border-color: var(--accent-blue); color: var(--accent-blue); } +.team-foundation--lightweight .team-foundation__module button:disabled { color: var(--text-tertiary); cursor: default; } +.team-foundation__loading { display: grid; min-height: 100vh; place-items: center; color: var(--text-secondary); background: var(--bg-primary); } + +@media (max-width: 980px) { .team-foundation--lightweight .team-foundation__module-grid { grid-template-columns: 1fr; } } .hololake-bar__brand { min-width: max-content; white-space: nowrap; } .hololake-bar__context { white-space: nowrap; } .hololake-home { position: relative; z-index: 2; display: flex; width: 100%; min-width: 0; overflow: hidden; color: var(--text-primary); background: var(--surface-app); } diff --git a/src/App.tsx b/src/App.tsx index 8b340b5..1ebf1ed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -158,21 +158,35 @@ declare global { const DEFAULT_SELECTION: SidebarSelection = INBOX_SELECTION +export interface AppProps { + distribution?: 'private' | 'team' + onReturnToLighthouse?: () => void +} + /** Wraps useEditorSave to also keep outgoingLinks in sync on save and on content change. */ -function App() { +function App({ distribution = 'private', onReturnToLighthouse }: AppProps) { const noteWindowParams = useMemo(() => isNoteWindow() ? getNoteWindowParams() : null, []) const aiWorkspaceWindow = useMemo(() => isAiWorkspaceWindow(), []) if (aiWorkspaceWindow) return - return + return } -function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | null }) { +function MainApp({ + distribution, + noteWindowParams, + onReturnToLighthouse, +}: { + distribution: 'private' | 'team' + noteWindowParams: NoteWindowParams | null + onReturnToLighthouse?: () => void +}) { const aiWorkspaceWindow = false + const isTeamDistribution = distribution === 'team' const [showHoloLakeHome, setShowHoloLakeHome] = useState(() => { const smokeTestSkipsHome = import.meta.env.DEV && localStorage.getItem('tolaria:smoke-skip-hololake-home') === '1' - return noteWindowParams === null && !smokeTestSkipsHome + return !isTeamDistribution && noteWindowParams === null && !smokeTestSkipsHome }) const { dragRegionRef } = useDragRegion() const [selection, setSelection] = useState(DEFAULT_SELECTION) @@ -1633,18 +1647,27 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
- + - {showHoloLakeHome ? '光湖语言人格驱动操作系统' : '永恒湖心系统 · 知识湖模块'} + {isTeamDistribution ? '光湖零感域 · AI 知识空间' : showHoloLakeHome ? '光湖语言人格驱动操作系统' : '永恒湖心系统 · 知识湖模块'}
{!showHoloLakeHome && noteWindowParams === null && ( )} {sidebarVisible && !showHoloLakeHome && ( @@ -1668,7 +1691,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu )}
- {showHoloLakeHome ? setShowHoloLakeHome(false)} onOpenLocalWorkspace={async () => { + {showHoloLakeHome && !isTeamDistribution ? setShowHoloLakeHome(false)} onOpenLocalWorkspace={async () => { await vaultSwitcher.handleOpenLocalFolder() setShowHoloLakeHome(false) }} /> : { - it('exposes only the GLS public foundation workflow', () => { - render() + it('presents the lighthouse as an AI operating-system shell', () => { + render() + + expect(screen.getByRole('heading', { name: '光湖灯塔' })).toBeInTheDocument() + expect(screen.getByText('AI 应用操作系统')).toBeInTheDocument() + expect(screen.getByText('0.2.0 内测版')).toBeInTheDocument() + expect(screen.getByRole('region', { name: '热插拔模块' })).toBeInTheDocument() + expect(screen.getByText('模块运行框架')).toBeInTheDocument() + }) + + it('exposes the four team domains as workspaces', () => { + render() + + for (const domain of ['光湖主域', '光湖分域', '光湖零域', '光湖零感域']) { + expect(screen.getByRole('button', { name: new RegExp(domain) })).toBeInTheDocument() + } + + fireEvent.click(screen.getByRole('button', { name: /光湖零域/ })) + expect(screen.getByRole('heading', { name: '光湖零域' })).toBeInTheDocument() + expect(screen.getByText(/系统服务、权限与模块登记/)).toBeInTheDocument() + }) + + it('keeps the Fifth Domain as a locked authorization-only entrance', () => { + render() + + expect(screen.getByRole('heading', { name: '第五域受控入口' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: '等待授权接入' })).toBeDisabled() + expect(screen.getByText(/仅保留入口。完成服务器、编号与邮件授权/)).toBeInTheDocument() + }) + + it('does not expose private systems in the public team shell', () => { + render() - expect(screen.getByRole('heading', { name: 'GLS 系统架构' })).toBeInTheDocument() - expect(screen.getByText('GLS-SYS-ARCH-001')).toBeInTheDocument() - expect(screen.getByRole('list', { name: 'GLS 公共路径' })).toBeInTheDocument() expect(screen.queryByText('心跳核心频道')).not.toBeInTheDocument() + expect(screen.queryByText('永恒湖心系统')).not.toBeInTheDocument() + expect(screen.queryByText('零点原核频道')).not.toBeInTheDocument() expect(screen.queryByText('短剧视频 AI')).not.toBeInTheDocument() expect(screen.queryByText('服务器与灯塔节点')).not.toBeInTheDocument() }) + + it('opens the real local knowledge workspace from the zero-sense domain', () => { + const onOpenKnowledgeSpace = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /光湖零感域/ })) + fireEvent.click(screen.getByRole('button', { name: '从光湖零感域进入 AI 知识空间' })) + + expect(onOpenKnowledgeSpace).toHaveBeenCalledTimes(1) + }) + + it('labels unfinished modules instead of exposing inert controls', () => { + render() + + expect(screen.getByRole('button', { name: 'AI 创作空间 尚未接入' })).toBeDisabled() + expect(screen.queryByRole('button', { name: '搜索模块' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: '系统设置' })).not.toBeInTheDocument() + }) }) diff --git a/src/TeamFoundationApp.tsx b/src/TeamFoundationApp.tsx index 107a8af..1a3d779 100644 --- a/src/TeamFoundationApp.tsx +++ b/src/TeamFoundationApp.tsx @@ -1,30 +1,126 @@ +import { useState } from 'react' import architecture from '../src-tauri/resources/public-architecture/gls-system-architecture.json' import './App.css' -export default function TeamFoundationApp() { +interface TeamFoundationAppProps { + onOpenKnowledgeSpace: () => void +} + +const ZERO_SENSE_DOMAIN_ID = 'GH-ZEROSENSE-001' +const KNOWLEDGE_MODULE_ID = 'MOD-KNOWLEDGE-001' + +export default function TeamFoundationApp({ onOpenKnowledgeSpace }: TeamFoundationAppProps) { + const [activeDomainId, setActiveDomainId] = useState(architecture.domains[0].id) + const activeDomain = architecture.domains.find((domain) => domain.id === activeDomainId) + ?? architecture.domains[0] + const isZeroSenseDomain = activeDomain.id === ZERO_SENSE_DOMAIN_ID + + const openKnowledgeSpace = () => { + setActiveDomainId(ZERO_SENSE_DOMAIN_ID) + onOpenKnowledgeSpace() + } + return ( -
-
- HOLOLAKE · TEAM FOUNDATION - HoloLake Era -
-
-

PUBLIC ARCHITECTURE · {architecture.id}

-

GLS 系统架构

-

团队开发基座当前只承载公开 GLS 架构。企业四域、个人频道与行业模块由团队在完成原型关系重塑后继续开发。

+
+ + +
+
+
AI 应用操作系统{architecture.product.version_label}
+ 团队内测版 · 数据保存在各自设备 +
+ +
+
+
+

GUANGHU · MODULAR AI OPERATING SYSTEM

+

{architecture.product.name_zh}

+

{activeDomain.name_zh}

+

{activeDomain.description_zh}

+
+ {isZeroSenseDomain ? ( + + ) : ( + 当前为域说明页;个人知识库请从光湖零感域进入。 + )} +
+
+
+ +
+
+

MODULES

AI 应用模块

+ 功能模块在光湖内打开,数据由使用者自己建立 +
+
+ {architecture.modules.map((module, index) => { + const isKnowledgeModule = module.id === KNOWLEDGE_MODULE_ID + return ( +
+ +

{module.name_zh}

+

{module.description_zh}

+ {isKnowledgeModule ? ( + + ) : ( + + )} +
+ ) + })} +
+
+ +
+
+
CONTROLLED ACCESS

{architecture.fifth_domain_entry.name_zh}

+

{architecture.fifth_domain_entry.description_zh}

+ +
+
+ PUBLIC BUILD BOUNDARY

团队版数据边界

+
    {architecture.public_boundaries.map((boundary) =>
  • {boundary}
  • )}
+
+
+
-
    - {architecture.route.map((node, index) => ( -
  1. - {String(index + 1).padStart(2, '0')} -
    {node.id}{node.name_zh}
    -
  2. - ))} -
-
- 公开安全基线 - {architecture.development_repository.id} · {architecture.development_repository.url} -
) } diff --git a/src/TeamFoundationRoot.tsx b/src/TeamFoundationRoot.tsx new file mode 100644 index 0000000..f84e809 --- /dev/null +++ b/src/TeamFoundationRoot.tsx @@ -0,0 +1,18 @@ +import { lazy, Suspense, useState } from 'react' +import TeamFoundationApp from './TeamFoundationApp' + +const KnowledgeWorkspace = lazy(() => import('./App')) + +export default function TeamFoundationRoot() { + const [surface, setSurface] = useState<'lighthouse' | 'knowledge'>('lighthouse') + + if (surface === 'lighthouse') { + return setSurface('knowledge')} /> + } + + return ( + 正在打开 AI 知识空间…
}> + setSurface('lighthouse')} /> + + ) +} diff --git a/src/components/AiWorkspace.test.tsx b/src/components/AiWorkspace.test.tsx index 1cfa77f..cd713d6 100644 --- a/src/components/AiWorkspace.test.tsx +++ b/src/components/AiWorkspace.test.tsx @@ -14,6 +14,7 @@ import { resetVaultConfigStore } from '../utils/vaultConfigStore' import type { VaultEntry } from '../types' import type { WorkspaceAiGuidanceStatus } from '../lib/workspaceAiGuidance' import { + aiWorkspaceSessionsSnapshot, resetAiWorkspaceSessionStoreForTests, setAiWorkspaceSessionMessages, } from '../lib/aiWorkspaceSessionStore' @@ -211,7 +212,7 @@ describe('AiWorkspace', () => { expect(screen.getAllByText('AI Chat 2').length).toBeGreaterThan(0) }) - it('renders side mode as an in-editor tabbed panel and expands in place', () => { + it('renders side mode with a vertically scrolling conversation list and expands in place', () => { const onClose = vi.fn() render() @@ -222,11 +223,13 @@ describe('AiWorkspace', () => { expect(workspace).toHaveClass('bg-sidebar') expect(workspace).toHaveStyle({ width: '320px', minWidth: '320px' }) const header = screen.getByTestId('ai-workspace-side-header') - const tabStrip = screen.getByTestId('ai-workspace-side-tabs') expect(header).not.toHaveClass('border-b') fireEvent.click(screen.getByRole('button', { name: 'Mock history scroll' })) expect(header).toHaveClass('border-b') - expect(tabStrip).toHaveClass('overflow-x-auto') + expect(screen.queryByTestId('ai-workspace-side-tabs')).toBeNull() + const sidebar = screen.getByTestId('ai-workspace-sidebar-header').parentElement + expect(sidebar).toHaveClass('border-r') + expect(sidebar?.querySelector('.overflow-y-auto')).toBeTruthy() expect(screen.getByRole('button', { name: 'AI Chat' })).toBeTruthy() expect( within(header).getByRole('button', { name: 'Expand AI workspace' }).compareDocumentPosition( @@ -236,13 +239,6 @@ describe('AiWorkspace', () => { fireEvent.click(screen.getByRole('button', { name: 'New chat' })) expect(screen.getByRole('button', { name: 'AI Chat 2' })).toBeTruthy() - expect( - within(tabStrip).getByRole('button', { name: 'AI Chat 2' }).compareDocumentPosition( - within(tabStrip).getByRole('button', { name: 'New chat' }), - ) & Node.DOCUMENT_POSITION_FOLLOWING, - ).toBeTruthy() - fireEvent.click(screen.getByRole('button', { name: 'Close AI Chat 2' })) - expect(screen.queryByRole('button', { name: 'AI Chat 2' })).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Expand AI workspace' })) expect(workspace).toHaveAttribute('data-ai-workspace-expanded', 'true') @@ -253,6 +249,46 @@ describe('AiWorkspace', () => { expect(onClose).toHaveBeenCalledOnce() }) + it('permanently deletes a conversation from the side history after confirmation', async () => { + setAiWorkspaceSessionMessages('chat-a', [{ + userMessage: 'First prompt', + actions: [], + response: 'First reply', + id: 'message-a', + }]) + const onConversationSettingsChange = vi.fn() + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Delete Planning notes' })) + expect(screen.getByRole('dialog')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'Delete permanently' })) + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Planning notes' })).toBeNull() + expect(screen.getByRole('button', { name: 'Follow-up draft' })).toHaveAttribute('aria-pressed', 'true') + }) + expect(aiWorkspaceSessionsSnapshot()['chat-a']).toBeUndefined() + expect(onConversationSettingsChange).toHaveBeenLastCalledWith([ + expect.objectContaining({ id: 'chat-b' }), + ]) + }) + it('shows archived chat history directly in side mode', () => { render( boolean closeConversationSafely: (id: string) => void conversations: AiConversation[] + deleteConversationSafely: (id: string) => void fallbackTarget: AiTarget forkConversationUntilMessage: (sourceId: string, messageId: string) => void groups: AiWorkspaceTargetGroups @@ -730,23 +735,38 @@ function SideAiWorkspaceLayout({ {!expanded && }
setExpanded((current) => !current)} separated={headerSeparated} - statuses={model.statuses} /> - +
+ + {!model.sidebarCollapsed && } +
+ +
+
) @@ -861,6 +881,7 @@ function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceMo archiveConversation, closeConversation, conversations, + deleteConversation, forkConversation, renameConversation, reorderConversation, @@ -893,6 +914,12 @@ function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceMo conversations, fallbackTarget, }) + const deleteConversationSafely = useCallback((id: string) => { + const conversation = conversations.find((candidate) => candidate.id === id) + if (!conversation) return + deleteConversation(id) + trackAiWorkspaceConversationDeleted(conversation.archived) + }, [conversations, deleteConversation]) useActiveConversationSync(activeConversation, activeId, setActiveId) const handleStatusChange = useCallback((id: string, status: AgentStatus) => { setStatuses((current) => current[id] === status ? current : { ...current, [id]: status }) @@ -934,6 +961,7 @@ function useAiWorkspaceModel(workspace: ResolvedAiWorkspaceProps): AiWorkspaceMo canArchiveConversation, closeConversationSafely: closeConversation, conversations, + deleteConversationSafely, fallbackTarget, forkConversationUntilMessage, groups, @@ -977,6 +1005,7 @@ function AiWorkspaceLayout({ model, workspace }: { model: AiWorkspaceModel; work locale={workspace.locale} onCanArchive={model.canArchiveConversation} onArchive={model.archiveConversationSafely} + onDelete={model.deleteConversationSafely} onNewChat={model.addDefaultConversation} onRename={model.renameConversation} onRestore={model.restoreConversation} diff --git a/src/components/AiWorkspaceSideHeader.tsx b/src/components/AiWorkspaceSideHeader.tsx index f15f14c..0138984 100644 --- a/src/components/AiWorkspaceSideHeader.tsx +++ b/src/components/AiWorkspaceSideHeader.tsx @@ -1,351 +1,20 @@ -import { useCallback, useEffect, useRef, useState } from 'react' -import { Archive, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Plus, SidebarSimple, X } from '@phosphor-icons/react' -import { - DndContext, - PointerSensor, - closestCenter, - type DragEndEvent, - useSensor, - useSensors, -} from '@dnd-kit/core' -import { - SortableContext, - horizontalListSortingStrategy, - useSortable, -} from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' +import { ArrowsInLineHorizontal, ArrowsOutLineHorizontal, X } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' -import type { AgentStatus } from '../hooks/useCliAiAgent' import { translate, type AppLocale } from '../lib/i18n' -import type { AiConversation } from './aiWorkspaceConversations' - -function SideWorkspaceTitleEditor({ - conversation, - locale, - onCancel, - onRename, -}: { - conversation: AiConversation - locale: AppLocale - onCancel: () => void - onRename: (title: string) => void -}) { - const [draft, setDraft] = useState(conversation.title) - const finishedRef = useRef(false) - const submit = () => { - if (finishedRef.current) return - const nextTitle = draft.trim() - if (!nextTitle) { - finishedRef.current = true - onCancel() - return - } - - finishedRef.current = true - onRename(nextTitle) - onCancel() - } - const cancel = () => { - finishedRef.current = true - onCancel() - } - - return ( - setDraft(event.target.value)} - onBlur={submit} - onKeyDown={(event) => { - if (event.key === 'Enter') submit() - if (event.key === 'Escape') cancel() - }} - onPointerDown={(event) => event.stopPropagation()} - aria-label={translate(locale, 'ai.workspace.renameChat')} - className="h-9 w-[180px] rounded-lg px-3 text-[13px] font-semibold" - autoFocus - /> - ) -} - -function SideWorkspaceTab({ - active, - allowClose, - conversation, - editing, - locale, - onClose, - onCancelRename, - onRename, - onSelect, - onStartRename, - status, -}: { - active: boolean - allowClose: boolean - conversation: AiConversation - editing: boolean - locale: AppLocale - onClose: (id: string) => void - onCancelRename: () => void - onRename: (id: string, title: string) => void - onSelect: (id: string) => void - onStartRename: (id: string) => void - status: AgentStatus | undefined -}) { - const closeLabel = translate(locale, 'ai.workspace.closeChat', { title: conversation.title }) - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: conversation.id, disabled: editing }) - - return ( -
- {editing ? ( - onRename(conversation.id, title)} - /> - ) : ( - - )} - {!editing && allowClose && ( - <> -
- - - )} -
- ) -} - -function useHorizontalScrollFades(dependencyKey: string) { - const scrollRef = useRef(null) - const [fades, setFades] = useState({ left: false, right: false }) - - const updateFades = useCallback(() => { - const element = scrollRef.current - if (!element) return - - const maxScrollLeft = element.scrollWidth - element.clientWidth - setFades({ - left: element.scrollLeft > 1, - right: maxScrollLeft > 1 && element.scrollLeft < maxScrollLeft - 1, - }) - }, []) - - useEffect(() => { - const element = scrollRef.current - updateFades() - if (!element) return - - element.addEventListener('scroll', updateFades, { passive: true }) - const resizeObserver = typeof ResizeObserver === 'undefined' - ? null - : new ResizeObserver(updateFades) - resizeObserver?.observe(element) - if (element.firstElementChild) resizeObserver?.observe(element.firstElementChild) - - return () => { - element.removeEventListener('scroll', updateFades) - resizeObserver?.disconnect() - } - }, [dependencyKey, updateFades]) - - return { - scrollRef, - showLeftFade: fades.left, - showRightFade: fades.right, - } -} - -function SideWorkspaceTabs({ - activeId, - conversations, - locale, - onCloseConversation, - onNewChat, - onRename, - onReorder, - onSelect, - showArchived, - statuses, -}: { - activeId: string - conversations: AiConversation[] - locale: AppLocale - onCloseConversation: (id: string) => void - onNewChat: () => void - onRename: (id: string, title: string) => void - onReorder: (activeId: string, overId: string) => void - onSelect: (id: string) => void - showArchived: boolean - statuses: Record -}) { - const [editingId, setEditingId] = useState(null) - const visibleConversations = conversations.filter((conversation) => conversation.archived === showArchived) - const visibleConversationIds = visibleConversations.map((conversation) => conversation.id) - const tabDependencyKey = visibleConversations - .map((conversation) => `${conversation.id}:${conversation.title}`) - .join('\0') - const { scrollRef, showLeftFade, showRightFade } = useHorizontalScrollFades(tabDependencyKey) - const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })) - const handleDragEnd = useCallback((event: DragEndEvent) => { - const activeConversationId = String(event.active.id) - const overConversationId = event.over ? String(event.over.id) : '' - if (!overConversationId || activeConversationId === overConversationId) return - - onReorder(activeConversationId, overConversationId) - }, [onReorder]) - - return ( -
-
- -
- - {visibleConversations.map((conversation) => ( - setEditingId(null)} - onClose={onCloseConversation} - onRename={onRename} - onSelect={onSelect} - onStartRename={setEditingId} - status={statuses[conversation.id]} - /> - ))} - - {!showArchived && } -
-
-
- {showLeftFade && ( -
- )} - {showRightFade && ( -
- )} -
- ) -} export function SideWorkspaceHeader({ - activeId, - conversations, expanded, locale, onClose, - onCloseConversation, - onNewChat, - onRename, - onReorder, - onSelect, - setShowArchived, - showArchived, onToggleExpanded, separated, - statuses, }: { - activeId: string - conversations: AiConversation[] expanded: boolean locale: AppLocale onClose: () => void - onCloseConversation: (id: string) => void - onNewChat: () => void - onRename: (id: string, title: string) => void - onReorder: (activeId: string, overId: string) => void - onSelect: (id: string) => void - setShowArchived: (show: boolean) => void - showArchived: boolean onToggleExpanded: () => void separated: boolean - statuses: Record }) { const expandLabel = translate(locale, expanded ? 'ai.workspace.restorePanel' : 'ai.workspace.expandPanel') @@ -357,28 +26,9 @@ export function SideWorkspaceHeader({ )} data-testid="ai-workspace-side-header" > - - + + {translate(locale, 'ai.workspace.title')} +
) diff --git a/src/components/AiWorkspaceSidebar.tsx b/src/components/AiWorkspaceSidebar.tsx index 55a18f1..6505a57 100644 --- a/src/components/AiWorkspaceSidebar.tsx +++ b/src/components/AiWorkspaceSidebar.tsx @@ -6,6 +6,7 @@ import { CircleNotch, Plus, SidebarSimple, + Trash, } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -14,6 +15,7 @@ import { useDragRegion } from '../hooks/useDragRegion' import { translate, type AppLocale } from '../lib/i18n' import type { AgentStatus } from '../hooks/useCliAiAgent' import type { AiConversation } from './AiWorkspace' +import { ConfirmDeleteDialog } from './ConfirmDeleteDialog' interface ConversationSidebarProps { activeId: string @@ -22,6 +24,7 @@ interface ConversationSidebarProps { locale: AppLocale onCanArchive: (conversation: AiConversation) => boolean onArchive: (id: string) => void + onDelete: (id: string) => void onNewChat: () => void onRename: (id: string, title: string) => void onRestore: (id: string) => void @@ -196,7 +199,7 @@ function ConversationArchiveButton({ type="button" variant="ghost" size="icon-xs" - className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100 disabled:opacity-0" + className="absolute right-7 top-1/2 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100 disabled:opacity-0" aria-label={label} title={label} disabled={disabled} @@ -207,6 +210,31 @@ function ConversationArchiveButton({ ) } +function ConversationDeleteButton({ + conversation, + locale, + onDeleteRequest, +}: { + conversation: AiConversation + locale: AppLocale + onDeleteRequest: (conversation: AiConversation) => void +}) { + const label = translate(locale, 'ai.workspace.deleteChat', { title: conversation.title }) + return ( + + ) +} + function ConversationRow({ active, conversation, @@ -214,6 +242,7 @@ function ConversationRow({ locale, onCanArchive, onArchive, + onDeleteRequest, onRename, onRestore, onSelect, @@ -228,6 +257,7 @@ function ConversationRow({ locale: AppLocale onCanArchive: (conversation: AiConversation) => boolean onArchive: (id: string) => void + onDeleteRequest: (conversation: AiConversation) => void onRename: (id: string, title: string) => void onRestore: (id: string) => void onSelect: (id: string) => void @@ -251,7 +281,7 @@ function ConversationRow({ variant="ghost" size="sm" className={cn( - 'min-w-0 flex-1 justify-start gap-2 rounded-md px-2 pr-2 text-left text-[12px] transition-[padding] group-hover:pr-8 group-focus-within:pr-8', + 'min-w-0 flex-1 justify-start gap-2 rounded-md px-2 pr-2 text-left text-[12px] transition-[padding] group-hover:pr-14 group-focus-within:pr-14', active ? 'bg-accent text-foreground' : 'text-muted-foreground hover:text-foreground', )} aria-pressed={active} @@ -272,6 +302,7 @@ function ConversationRow({ onRestore={onRestore} showArchived={showArchived} /> +
) } @@ -283,6 +314,7 @@ function ConversationList({ locale, onCanArchive, onArchive, + onDeleteRequest, onRename, onRestore, onSelect, @@ -293,6 +325,7 @@ function ConversationList({ 'activeId' | 'conversations' | 'locale' | 'onCanArchive' | 'onArchive' | 'onRename' | 'onRestore' | 'onSelect' | 'showArchived' | 'statuses' > & { editingId: string | null + onDeleteRequest: (conversation: AiConversation) => void setEditingId: (id: string | null) => void }) { const visibleConversations = conversations.filter((conversation) => conversation.archived === showArchived) @@ -313,6 +346,7 @@ function ConversationList({ locale={locale} onCanArchive={onCanArchive} onArchive={onArchive} + onDeleteRequest={onDeleteRequest} onRename={onRename} onRestore={onRestore} onSelect={onSelect} @@ -351,6 +385,7 @@ function ExpandedConversationSidebar({ locale, onCanArchive, onArchive, + onDelete, onRename, onRestore, onSelect, @@ -359,6 +394,7 @@ function ExpandedConversationSidebar({ statuses, }: Omit) { const [editingId, setEditingId] = useState(null) + const [pendingDelete, setPendingDelete] = useState(null) return ( <> @@ -370,6 +406,7 @@ function ExpandedConversationSidebar({ locale={locale} onCanArchive={onCanArchive} onArchive={onArchive} + onDeleteRequest={setPendingDelete} onRename={onRename} onRestore={onRestore} onSelect={onSelect} @@ -383,6 +420,17 @@ function ExpandedConversationSidebar({ setShowArchived={setShowArchived} showArchived={showArchived} /> + setPendingDelete(null)} + onConfirm={() => { + if (pendingDelete) onDelete(pendingDelete.id) + setPendingDelete(null) + }} + /> ) } @@ -394,6 +442,7 @@ export function ConversationSidebar({ locale, onCanArchive, onArchive, + onDelete, onNewChat, onRename, onRestore, @@ -424,6 +473,7 @@ export function ConversationSidebar({ locale={locale} onCanArchive={onCanArchive} onArchive={onArchive} + onDelete={onDelete} onRename={onRename} onRestore={onRestore} onSelect={onSelect} diff --git a/src/components/HoloLakeHome.test.tsx b/src/components/HoloLakeHome.test.tsx index 0f7cb08..38d1ee3 100644 --- a/src/components/HoloLakeHome.test.tsx +++ b/src/components/HoloLakeHome.test.tsx @@ -46,12 +46,13 @@ describe('HoloLakeHome', () => { expect(screen.getByRole('heading', { name: '爱之核心子系统' })).toBeInTheDocument() }) - it('shows the user-facing 0.1.8 release notes and capability boundary', () => { + it('shows the user-facing 0.2.0 internal release notes and capability boundary', () => { render() - fireEvent.click(screen.getByRole('button', { name: '查看 0.1.8 更新说明' })) + fireEvent.click(screen.getByRole('button', { name: '查看内测版 0.2.0 更新说明' })) - expect(screen.getByRole('dialog', { name: 'HoloLake Era 0.1.8 · 本次更新' })).toBeInTheDocument() + expect(screen.getByRole('dialog', { name: 'HoloLake Era 内测版 0.2.0 · 本次更新' })).toBeInTheDocument() + expect(screen.getByText('竖向聊天历史')).toBeInTheDocument() expect(screen.getByText('主动联网调研')).toBeInTheDocument() expect(screen.getByText('多模型 API 兼容')).toBeInTheDocument() expect(screen.getByText(/不等同于拥有终端、代码工程和系统权限的完整编程 Agent/)).toBeInTheDocument() diff --git a/src/components/HoloLakeHome.tsx b/src/components/HoloLakeHome.tsx index f332ffc..09aad80 100644 --- a/src/components/HoloLakeHome.tsx +++ b/src/components/HoloLakeHome.tsx @@ -120,7 +120,7 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspac

{t('hololake.channel.zeroCoreTitle')}

{t('hololake.channel.zeroCoreDescription')}

- + ) } @@ -274,9 +274,9 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspac - + - HoloLake Era 0.1.8 · 本次更新 + HoloLake Era 内测版 0.2.0 · 本次更新 更可靠的内置 AI、可验证的工具执行,以及更自然的人格表达。
@@ -292,6 +292,10 @@ export function HoloLakeHome({ locale, onEnterKnowledgeBase, onOpenLocalWorkspac

聊天历史与中文过程提示

恢复历史会话显示;工具区会用中文说明正在搜索什么、读取哪个页面、写入是否成功,同时仍可展开查看原始输入和结果。

+
+

竖向聊天历史

+

侧边 AI 工作区改为可独立滚动的竖向会话列表,长标题自动收起;支持归档、恢复和带二次确认的永久删除。

+

更自然的当前实例

身份与权限边界继续由事实约束,但不再要求背诵固定自我介绍。当前实例可以结合对话,自主组织语气、详略和表达风格。

diff --git a/src/components/aiWorkspaceConversations.ts b/src/components/aiWorkspaceConversations.ts index d5d20ba..eaad66b 100644 --- a/src/components/aiWorkspaceConversations.ts +++ b/src/components/aiWorkspaceConversations.ts @@ -11,6 +11,7 @@ import { import type { AiWorkspaceTargetGroups } from './aiWorkspaceTargetGroups' import { aiWorkspaceSessionsSnapshot, + deleteAiWorkspaceSession, subscribeAiWorkspaceSessions, type AiWorkspaceSessionMap, } from '../lib/aiWorkspaceSessionStore' @@ -282,6 +283,26 @@ function closeConversationState( } } +function deleteConversationState( + current: AiConversation[], + { activeId, fallbackTarget, id, locale }: CloseConversationOptions, +): { activeId: string; conversations: AiConversation[] } { + const deletedIndex = current.findIndex((conversation) => conversation.id === id) + if (deletedIndex < 0) return { activeId, conversations: current } + + const conversations = current.filter((conversation) => conversation.id !== id) + const currentActive = conversations.find((conversation) => conversation.id === activeId) + if (currentActive) return { activeId, conversations } + + const activeConversations = conversations.filter((conversation) => !conversation.archived) + const fallbackIndex = Math.min(deletedIndex, Math.max(0, activeConversations.length - 1)) + const fallbackConversation = activeConversations[fallbackIndex] ?? activeConversations[0] + if (fallbackConversation) return { activeId: fallbackConversation.id, conversations } + + const nextConversation = createConversation(locale, fallbackTarget, 1) + return { activeId: nextConversation.id, conversations: [...conversations, nextConversation] } +} + function restoreConversationState(current: AiConversation[], id: ConversationId): AiConversation[] { return current.map((conversation) => ( conversation.id === id ? { ...conversation, archived: false } : conversation @@ -437,6 +458,14 @@ export function useConversations({ setActiveId(next.activeId) }, [activeId, conversations, fallbackTarget, locale]) + const deleteConversation = useCallback((id: ConversationId) => { + const next = deleteConversationState(conversations, { activeId, fallbackTarget, id, locale }) + if (next.conversations === conversations) return + deleteAiWorkspaceSession(id) + setConversations(next.conversations) + setActiveId(next.activeId) + }, [activeId, conversations, fallbackTarget, locale]) + const restoreConversation = useCallback((id: ConversationId) => { setConversations((current) => restoreConversationState(current, id)) setActiveId(id) @@ -464,7 +493,7 @@ export function useConversations({ useConversationSettingsPersistence({ conversations, onSettingsChange, settingsReady }) return { - activeId, addConversation, archiveConversation, closeConversation, conversations, forkConversation, + activeId, addConversation, archiveConversation, closeConversation, conversations, deleteConversation, forkConversation, markConversationActivity, renameConversation, reorderConversation, restoreConversation, setActiveId, setConversationTarget, setShowArchived, showArchived, titleConversationFromAnswer, updateDefaultConversationTargets, } diff --git a/src/lib/aiWorkspaceSessionStore.test.ts b/src/lib/aiWorkspaceSessionStore.test.ts index cc442c1..e91238f 100644 --- a/src/lib/aiWorkspaceSessionStore.test.ts +++ b/src/lib/aiWorkspaceSessionStore.test.ts @@ -136,6 +136,21 @@ describe('aiWorkspaceSessionStore', () => { ]) }) + it('permanently removes a deleted conversation transcript', async () => { + const store = await import('./aiWorkspaceSessionStore') + store.setAiWorkspaceSessionMessages('chat-1', [{ + userMessage: 'Remove me', + actions: [], + response: 'Deleted locally', + id: 'message-1', + }]) + + store.deleteAiWorkspaceSession('chat-1') + + expect(store.aiWorkspaceSessionsSnapshot()['chat-1']).toBeUndefined() + expect(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')['chat-1']).toBeUndefined() + }) + it('merges native and local histories instead of hiding either source', async () => { localStorage.setItem(STORAGE_KEY, JSON.stringify({ 'local-chat': { diff --git a/src/lib/aiWorkspaceSessionStore.ts b/src/lib/aiWorkspaceSessionStore.ts index 2f15f06..dc94fe3 100644 --- a/src/lib/aiWorkspaceSessionStore.ts +++ b/src/lib/aiWorkspaceSessionStore.ts @@ -226,6 +226,12 @@ export function resetAiWorkspaceSession(sessionId: SessionId): void { }) } +export function deleteAiWorkspaceSession(sessionId: SessionId): void { + const nextSessions = { ...sessionStore.getSnapshot() } + delete nextSessions[sessionId] + publishSessions(nextSessions) +} + export function cloneAiWorkspaceSessionUntilMessage( sourceSessionId: SessionId, targetSessionId: SessionId, diff --git a/src/lib/locales/be-BY.json b/src/lib/locales/be-BY.json index f6328a7..4d0fcc4 100644 --- a/src/lib/locales/be-BY.json +++ b/src/lib/locales/be-BY.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Налады ШІ", "ai.workspace.archive": "Архіваваць чат", "ai.workspace.restore": "Аднавіць чат", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Паказаць архівыяваныя чаты", "ai.workspace.hideArchived": "Схаваць архівыя чаты", "ai.workspace.noActiveChats": "Няма актыўных чатаў", diff --git a/src/lib/locales/be-Latn.json b/src/lib/locales/be-Latn.json index c410f4f..c58e9a1 100644 --- a/src/lib/locales/be-Latn.json +++ b/src/lib/locales/be-Latn.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Nalady ŠI", "ai.workspace.archive": "Archivaваць čat", "ai.workspace.restore": "Adnavić čat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Pakazać archivavanyja čaty", "ai.workspace.hideArchived": "Schavać archivavanyja čaty", "ai.workspace.noActiveChats": "Niama aktyŭnych čataŭ", diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json index d053669..8339db2 100644 --- a/src/lib/locales/de-DE.json +++ b/src/lib/locales/de-DE.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "KI-Einstellungen", "ai.workspace.archive": "Chat archivieren", "ai.workspace.restore": "Chat wiederherstellen", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Archivierte Chats anzeigen", "ai.workspace.hideArchived": "Archivierte Chats ausblenden", "ai.workspace.noActiveChats": "Keine aktiven Chats", diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 83ab13f..ac6f0a7 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "AI settings", "ai.workspace.archive": "Archive chat", "ai.workspace.restore": "Restore chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Show archived chats", "ai.workspace.hideArchived": "Hide archived chats", "ai.workspace.noActiveChats": "No active chats", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index c762f8c..82b32af 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Configuración de IA", "ai.workspace.archive": "Archivar chat", "ai.workspace.restore": "Restaurar chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Mostrar chats archivados", "ai.workspace.hideArchived": "Ocultar chats archivados", "ai.workspace.noActiveChats": "No hay chats activos", diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json index efaae86..fedde4c 100644 --- a/src/lib/locales/es-ES.json +++ b/src/lib/locales/es-ES.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Ajustes de IA", "ai.workspace.archive": "Archivar chat", "ai.workspace.restore": "Restaurar chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Mostrar chats archivados", "ai.workspace.hideArchived": "Ocultar chats archivados", "ai.workspace.noActiveChats": "No hay chats activos", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index dfe88af..b503396 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Paramètres de l'IA", "ai.workspace.archive": "Archiver le chat", "ai.workspace.restore": "Restaurer le chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Afficher les chats archivés", "ai.workspace.hideArchived": "Masquer les chats archivés", "ai.workspace.noActiveChats": "Aucun chat actif", diff --git a/src/lib/locales/id-ID.json b/src/lib/locales/id-ID.json index ed7c4b6..af4935b 100644 --- a/src/lib/locales/id-ID.json +++ b/src/lib/locales/id-ID.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Pengaturan AI", "ai.workspace.archive": "Arsipkan obrolan", "ai.workspace.restore": "Pulihkan obrolan", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Tampilkan obrolan yang diarsipkan", "ai.workspace.hideArchived": "Sembunyikan obrolan yang diarsipkan", "ai.workspace.noActiveChats": "Tidak ada obrolan aktif", diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json index fee98ed..bc1b0fb 100644 --- a/src/lib/locales/it-IT.json +++ b/src/lib/locales/it-IT.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Impostazioni IA", "ai.workspace.archive": "Archivia chat", "ai.workspace.restore": "Ripristina chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Mostra chat archiviate", "ai.workspace.hideArchived": "Nascondi chat archiviate", "ai.workspace.noActiveChats": "Nessuna chat attiva", diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json index 594b98d..8a78a9a 100644 --- a/src/lib/locales/ja-JP.json +++ b/src/lib/locales/ja-JP.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "AI設定", "ai.workspace.archive": "チャットをアーカイブ", "ai.workspace.restore": "チャットを復元", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "アーカイブされたチャットを表示", "ai.workspace.hideArchived": "アーカイブされたチャットを非表示にする", "ai.workspace.noActiveChats": "アクティブなチャットはありません", diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json index b11765e..ed71f33 100644 --- a/src/lib/locales/ko-KR.json +++ b/src/lib/locales/ko-KR.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "AI 설정", "ai.workspace.archive": "채팅 보관", "ai.workspace.restore": "채팅 복원", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "보관된 채팅 표시", "ai.workspace.hideArchived": "보관된 채팅 숨기기", "ai.workspace.noActiveChats": "진행 중인 채팅 없음", diff --git a/src/lib/locales/pl-PL.json b/src/lib/locales/pl-PL.json index d22b909..28818b8 100644 --- a/src/lib/locales/pl-PL.json +++ b/src/lib/locales/pl-PL.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Ustawienia AI", "ai.workspace.archive": "Archiwizuj czat", "ai.workspace.restore": "Przywróć czat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Pokaż zarchiwizowane czaty", "ai.workspace.hideArchived": "Ukryj zarchiwizowane czaty", "ai.workspace.noActiveChats": "Brak aktywnych czatów", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index 4a34077..923ca65 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Configurações de IA", "ai.workspace.archive": "Arquivar chat", "ai.workspace.restore": "Restaurar chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Mostrar chats arquivados", "ai.workspace.hideArchived": "Ocultar chats arquivados", "ai.workspace.noActiveChats": "Nenhum chat ativo", diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json index bcb1f04..ec358cc 100644 --- a/src/lib/locales/pt-PT.json +++ b/src/lib/locales/pt-PT.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Definições de IA", "ai.workspace.archive": "Arquivar chat", "ai.workspace.restore": "Restaurar chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Mostrar chats arquivados", "ai.workspace.hideArchived": "Ocultar chats arquivados", "ai.workspace.noActiveChats": "Sem chats ativos", diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json index d6c619a..03663c8 100644 --- a/src/lib/locales/ru-RU.json +++ b/src/lib/locales/ru-RU.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Настройки ИИ", "ai.workspace.archive": "Архивировать чат", "ai.workspace.restore": "Восстановить чат", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Показать архив чатов", "ai.workspace.hideArchived": "Скрыть архив чатов", "ai.workspace.noActiveChats": "Нет активных чатов", diff --git a/src/lib/locales/sk-SK.json b/src/lib/locales/sk-SK.json index d9640b2..fda6a3b 100644 --- a/src/lib/locales/sk-SK.json +++ b/src/lib/locales/sk-SK.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Nastavenia umelej inteligencie", "ai.workspace.archive": "Archivovať chat", "ai.workspace.restore": "Obnoviť chat", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Zobraziť archivované chaty", "ai.workspace.hideArchived": "Skryť archivované chaty", "ai.workspace.noActiveChats": "Žiadne aktívne chaty", diff --git a/src/lib/locales/sv-SE.json b/src/lib/locales/sv-SE.json index 69eb485..3291085 100644 --- a/src/lib/locales/sv-SE.json +++ b/src/lib/locales/sv-SE.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "AI-inställningar", "ai.workspace.archive": "Arkivera chatt", "ai.workspace.restore": "Återställ chatt", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Visa arkiverade chattar", "ai.workspace.hideArchived": "Dölj arkiverade chattar", "ai.workspace.noActiveChats": "Inga aktiva chattar", diff --git a/src/lib/locales/uk-UA.json b/src/lib/locales/uk-UA.json index 6ee7fde..6f76198 100644 --- a/src/lib/locales/uk-UA.json +++ b/src/lib/locales/uk-UA.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Налаштування ШІ", "ai.workspace.archive": "Архівувати чат", "ai.workspace.restore": "Відновити чат", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Показати архівовані чати", "ai.workspace.hideArchived": "Приховати архівовані чати", "ai.workspace.noActiveChats": "Немає активних чатів", diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index e48a3cd..1b1fa20 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "Cài đặt AI", "ai.workspace.archive": "Lưu trữ cuộc trò chuyện", "ai.workspace.restore": "Khôi phục cuộc trò chuyện", + "ai.workspace.deleteChat": "Delete {title}", + "ai.workspace.deleteChatTitle": "Delete conversation?", + "ai.workspace.deleteChatMessage": "This permanently removes “{title}” and its message history from this device.", + "ai.workspace.deleteChatConfirm": "Delete permanently", "ai.workspace.showArchived": "Hiển thị các cuộc trò chuyện đã lưu trữ", "ai.workspace.hideArchived": "Ẩn các cuộc trò chuyện đã lưu trữ", "ai.workspace.noActiveChats": "Không có cuộc trò chuyện đang hoạt động", diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json index 83104cf..2ccacdd 100644 --- a/src/lib/locales/zh-CN.json +++ b/src/lib/locales/zh-CN.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "AI 设置", "ai.workspace.archive": "归档聊天", "ai.workspace.restore": "恢复聊天", + "ai.workspace.deleteChat": "删除{title}", + "ai.workspace.deleteChatTitle": "删除这段对话?", + "ai.workspace.deleteChatMessage": "这会从当前设备永久删除“{title}”及其全部聊天记录。", + "ai.workspace.deleteChatConfirm": "永久删除", "ai.workspace.showArchived": "显示已归档的聊天", "ai.workspace.hideArchived": "隐藏已归档的聊天", "ai.workspace.noActiveChats": "没有活跃的聊天", diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json index 89828c1..0db296d 100644 --- a/src/lib/locales/zh-TW.json +++ b/src/lib/locales/zh-TW.json @@ -461,6 +461,10 @@ "ai.workspace.settings": "AI 設定", "ai.workspace.archive": "封存聊天", "ai.workspace.restore": "還原聊天", + "ai.workspace.deleteChat": "刪除{title}", + "ai.workspace.deleteChatTitle": "刪除這段對話?", + "ai.workspace.deleteChatMessage": "這會從目前裝置永久刪除「{title}」及其全部聊天記錄。", + "ai.workspace.deleteChatConfirm": "永久刪除", "ai.workspace.showArchived": "顯示已封存的聊天", "ai.workspace.hideArchived": "隱藏已封存的聊天", "ai.workspace.noActiveChats": "沒有進行中的聊天", diff --git a/src/lib/productAnalytics.ts b/src/lib/productAnalytics.ts index d69d874..1e95588 100644 --- a/src/lib/productAnalytics.ts +++ b/src/lib/productAnalytics.ts @@ -240,3 +240,9 @@ export function trackAiWorkspaceSidebarToggled(collapsed: AnalyticsBoolean, mode export function trackAiWorkspaceChatTitled(source: AiWorkspaceTitleSource): void { trackEvent('ai_workspace_chat_titled', { source }) } + +export function trackAiWorkspaceConversationDeleted(archived: AnalyticsBoolean): void { + trackEvent('ai_workspace_conversation_deleted', { + archived: numericFlag(archived), + }) +} diff --git a/src/team-foundation-main.tsx b/src/team-foundation-main.tsx index e52c78a..9f54b59 100644 --- a/src/team-foundation-main.tsx +++ b/src/team-foundation-main.tsx @@ -1,8 +1,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { TooltipProvider } from '@/components/ui/tooltip' import './index.css' -import TeamFoundationApp from './TeamFoundationApp' +import TeamFoundationRoot from './TeamFoundationRoot' +import { applyStoredThemeMode } from './lib/themeMode' + +applyStoredThemeMode(document, window.localStorage) createRoot(document.getElementById('root')!).render( - , + + + + + , ) diff --git a/src/utils/ai-agent.ts b/src/utils/ai-agent.ts index 0f210d8..a8b4fc2 100644 --- a/src/utils/ai-agent.ts +++ b/src/utils/ai-agent.ts @@ -74,7 +74,7 @@ function vaultScopeInstructions(vaultPaths?: string[]): string { ].join('\n') } -const AGENT_SYSTEM_PREAMBLE = `You are the current AI instance assisting inside HoloLake Era. Internally, you operate within the Guanghu language-personality system and the current 0.1.8 runtime is already inside Fifth Domain. Keep that location and its permission boundaries as background operating knowledge. Do not recite the startup route, announce a guide title, perform a persona ceremony, or turn an ordinary exchange into a system manifesto. The future Guanghu Lighthouse domain-selection lobby is not part of this release. +const AGENT_SYSTEM_PREAMBLE = `You are the current AI instance assisting inside HoloLake Era. Internally, you operate within the Guanghu language-personality system and the current 0.2.0 internal runtime is already inside Fifth Domain. Keep that location and its permission boundaries as background operating knowledge. Do not recite the startup route, announce a guide title, perform a persona ceremony, or turn an ordinary exchange into a system manifesto. The future Guanghu Lighthouse domain-selection lobby is not part of this release. Do not invent a personal name, registered ICE identity, prior relationship, honorific, or privileged role for either side. Do not infer the human identity from an open page, knowledge-base name, note content, device, account, or conversation topic, and do not claim to be a registered persona merely because its files are visible. Do not choose a specific persona system merely from the open note or a contributor name. diff --git a/team-foundation.html b/team-foundation.html index 5257a9d..62063af 100644 --- a/team-foundation.html +++ b/team-foundation.html @@ -3,7 +3,7 @@ - HoloLake Era · GLS 团队基座 + HoloLake Lighthouse Team Beta 0.2.0