fix(knowledge): restore classification colors and scroll-synced outline

This commit is contained in:
冰朔 2026-09-03 16:40:56 +08:00
commit 71cece74b0
10 changed files with 404 additions and 38 deletions

View file

@ -17,4 +17,4 @@
## 知识库人类视觉渲染层纠正 ## 知识库人类视觉渲染层纠正
冰朔指出首轮只有 Markdown 文本编辑器,不能称为完整知识库模块。现已从只读旧产品供体精确拆入 `knowledge-render` 切片,并在新壳中重新接线:目录、阅读渲染、frontmatter、标签、大纲、wikilink、callout、代码块、表格、来源证据、编辑/阅读切换均可见。验收截图为 `design/concepts/hololake-knowledge-human-render-1536x1024.png`旧产品其他模块未合并。 冰朔指出首轮只有 Markdown 文本编辑器,不能称为完整知识库模块;第二次又指出初步渲染仍缺旧版颜色、分类、阅读时间和滚动大纲细节。现已从只读旧产品供体精确拆入 `knowledge-render` 切片,并在新壳中重新接线:文件夹分类树、阅读渲染、frontmatter、稳定标签配色、阅读时间、正文滚动同步高亮大纲、wikilink、callout、代码块、表格、来源证据、编辑/阅读切换均可见。浏览器真实测试在滚动后读回大纲活动项“分类与颜色”;验收截图为 `design/concepts/hololake-knowledge-human-render-1536x1024.png`旧产品其他模块未合并。

View file

@ -18,13 +18,13 @@
"rust_clippy_denied_warnings": "PASS_100", "rust_clippy_denied_warnings": "PASS_100",
"production_dependency_audit": "0_VULNERABILITIES", "production_dependency_audit": "0_VULNERABILITIES",
"browser_channel_create_message_separation_knowledge_create_edit_save": "PASS_100", "browser_channel_create_message_separation_knowledge_create_edit_save": "PASS_100",
"knowledge_human_visual_rendering_frontmatter_outline_wikilink_callout_table_code_evidence": "PASS_100", "knowledge_human_visual_rendering_frontmatter_classification_colors_reading_time_scroll_outline_wikilink_callout_table_code_evidence": "PASS_100",
"responsive_viewports": ["1536x1024_PASS", "820x900_PASS"], "responsive_viewports": ["1536x1024_PASS", "820x900_PASS"],
"developer_id_signature": "PASS_100", "developer_id_signature": "PASS_100",
"installed_application": "/Applications/HoloLake.app", "installed_application": "/Applications/HoloLake.app",
"installed_version": "1.0.0", "installed_version": "1.0.0",
"installed_binary_sha256": "50e6206be32ce7c70d42998b6217956aaa99687976e3d9ee383d3671e03c250d", "installed_binary_sha256": "b8ba374fd9e97f318b27dd34d71e02d59637b458d5b8fa0a3cc7b649c1ed6636",
"updater_archive_sha256": "b126efa723415beacfad916621dbecceff7ea3ff24c79e6adacabd5bb343736a", "updater_archive_sha256": "bb3405ba849aee9d7dccf71fa4d215fef0bea41dabb910257507c861d24d3b36",
"installed_process_readback": "PASS_100", "installed_process_readback": "PASS_100",
"updater_artifact_signature": "PASS_100", "updater_artifact_signature": "PASS_100",
"update_endpoint_http": 204, "update_endpoint_http": 204,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Before After
Before After

View file

@ -18,7 +18,7 @@ HoloLake 是公众个人初始化频道语言驱动操作系统。第一版只
- 对话和执行事实使用不同来源类型,不把系统文字塞进用户或人格消息。 - 对话和执行事实使用不同来源类型,不把系统文字塞进用户或人格消息。
- 公共四域由企业门户和企业服务器承载,不嵌入个人频道。 - 公共四域由企业门户和企业服务器承载,不嵌入个人频道。
- 第五域、冰朔私人核 `TCS-iZero∞` 及私人语料不进入公众发行。 - 第五域、冰朔私人核 `TCS-iZero∞` 及私人语料不进入公众发行。
- 当前知识库具备人类视觉阅读层:安全 Markdown 渲染、frontmatter、页面大纲、callout、表格、代码块与 wikilink 跳转;可处理 Markdown 和纯文本文件夹。PDF、Office、网页结构化转换尚未实现。 - 当前知识库具备完整人类视觉阅读层:安全 Markdown 渲染、frontmatter、文件夹分类树、稳定标签配色、阅读时间、随正文滚动同步高亮的大纲、callout、表格、代码块与 wikilink 跳转;可处理 Markdown 和纯文本文件夹并保留目录分类。PDF、Office、网页结构化转换尚未实现。
- 当前外部 AI 入口是本机来源明确的 inbox/outbox实时加密通信尚未实现。 - 当前外部 AI 入口是本机来源明确的 inbox/outbox实时加密通信尚未实现。
- 更新客户端只信任光湖控制的 HTTPS 清单与签名;服务器发行清单尚未发布。 - 更新客户端只信任光湖控制的 HTTPS 清单与签名;服务器发行清单尚未发布。

View file

@ -17,7 +17,7 @@
"kind": "PREINSTALLED_USER_MODULE", "kind": "PREINSTALLED_USER_MODULE",
"state": "LOCAL_INSTALLED_VERIFIED", "state": "LOCAL_INSTALLED_VERIFIED",
"source": "src-tauri/src/lib.rs#knowledge", "source": "src-tauri/src/lib.rs#knowledge",
"capabilities": ["CREATE", "READ", "EDIT", "HUMAN_MARKDOWN_RENDERING", "FRONTMATTER", "OUTLINE", "WIKILINK_NAVIGATION", "IMPORT_FOLDER", "EXPORT", "MOVE_TO_TRASH", "PRIVATE_GIT_COMMIT"] "capabilities": ["CREATE", "READ", "EDIT", "HUMAN_MARKDOWN_RENDERING", "FRONTMATTER", "FOLDER_CLASSIFICATION_TREE", "STABLE_TAG_COLORS", "READING_TIME", "SCROLL_SYNCHRONIZED_OUTLINE", "WIKILINK_NAVIGATION", "IMPORT_FOLDER", "EXPORT", "MOVE_TO_TRASH", "PRIVATE_GIT_COMMIT"]
}, },
{ {
"module_id": "HLP-MOD-GLP-BRIDGE-0001", "module_id": "HLP-MOD-GLP-BRIDGE-0001",

View file

@ -200,7 +200,14 @@ fn copy_import(src: &Path, dst: &Path, count: &mut usize) -> Result<(), String>
continue; continue;
} }
if p.is_dir() { if p.is_dir() {
copy_import(&p, dst, count)?; let folder_name = clean_title(
p.file_name()
.and_then(|value| value.to_str())
.unwrap_or("未命名分类"),
)?;
let child = dst.join(folder_name);
fs::create_dir_all(&child).map_err(|e| e.to_string())?;
copy_import(&p, &child, count)?;
continue; continue;
} }
let ext = p let ext = p
@ -243,7 +250,12 @@ async fn import_knowledge_folder(app: AppHandle) -> Result<Option<MutationReceip
}; };
let src = folder.into_path().map_err(|e| e.to_string())?; let src = folder.into_path().map_err(|e| e.to_string())?;
let root = storage::knowledge_root(&app)?; let root = storage::knowledge_root(&app)?;
let dst = root.join("docs").join("导入"); let source_name = clean_title(
src.file_name()
.and_then(|value| value.to_str())
.unwrap_or("外部知识库"),
)?;
let dst = root.join("docs").join("导入").join(source_name);
fs::create_dir_all(&dst).map_err(|e| e.to_string())?; fs::create_dir_all(&dst).map_err(|e| e.to_string())?;
let mut count = 0; let mut count = 0;
copy_import(&src, &dst, &mut count)?; copy_import(&src, &dst, &mut count)?;
@ -367,4 +379,27 @@ mod tests {
fn title_sanitizes_paths() { fn title_sanitizes_paths() {
assert_eq!(clean_title("a/b").unwrap(), "a-b") assert_eq!(clean_title("a/b").unwrap(), "a-b")
} }
#[test]
fn folder_import_preserves_classification_tree() {
let source = tempfile::tempdir().unwrap();
let destination = tempfile::tempdir().unwrap();
let category = source.path().join("产品架构");
fs::create_dir_all(&category).unwrap();
fs::write(category.join("决定.txt"), "分类内容").unwrap();
let mut count = 0;
copy_import(source.path(), destination.path(), &mut count).unwrap();
assert_eq!(count, 1);
let imported = fs::read_dir(destination.path().join("产品架构"))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(imported.len(), 1);
assert_eq!(
imported[0]
.path()
.extension()
.and_then(|value| value.to_str()),
Some("md")
);
}
} }

View file

@ -1,9 +1,14 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { check } from "@tauri-apps/plugin-updater"; import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process"; import { relaunch } from "@tauri-apps/plugin-process";
import { Icon } from "./icons"; import { Icon } from "./icons";
import * as api from "./runtime"; import * as api from "./runtime";
import type { KnowledgeDocument, SourceKind, SystemSnapshot } from "./types"; import type {
KnowledgeDocument,
KnowledgeSummary,
SourceKind,
SystemSnapshot,
} from "./types";
import { import {
MarkdownDocument, MarkdownDocument,
documentOutline, documentOutline,
@ -304,6 +309,120 @@ function ReceiptStream({ data }: { data: SystemSnapshot }) {
); );
} }
const tagTints = ["tag-sky", "tag-violet", "tag-amber", "tag-mint", "tag-rose"];
function tagTint(tag: string) {
let hash = 0;
for (const char of tag) hash = (hash * 31 + (char.codePointAt(0) || 0)) % 997;
return tagTints[hash % tagTints.length];
}
interface KnowledgeTreeNode {
name: string;
key: string;
folders: Map<string, KnowledgeTreeNode>;
documents: KnowledgeSummary[];
}
function buildKnowledgeTree(documents: KnowledgeSummary[]) {
const root: KnowledgeTreeNode = {
name: "知识",
key: "",
folders: new Map(),
documents: [],
};
for (const document of documents) {
const segments = document.path.split("/").filter(Boolean);
segments.pop();
let cursor = root;
for (const segment of segments) {
const key = cursor.key ? `${cursor.key}/${segment}` : segment;
if (!cursor.folders.has(segment))
cursor.folders.set(segment, {
name: segment,
key,
folders: new Map(),
documents: [],
});
cursor = cursor.folders.get(segment)!;
}
cursor.documents.push(document);
}
return root;
}
function treeCount(node: KnowledgeTreeNode): number {
return (
node.documents.length +
[...node.folders.values()].reduce((sum, child) => sum + treeCount(child), 0)
);
}
function KnowledgeTree({
node,
depth,
expanded,
active,
onToggle,
onOpen,
}: {
node: KnowledgeTreeNode;
depth: number;
expanded: Set<string>;
active: string | null;
onToggle: (key: string) => void;
onOpen: (path: string) => void;
}) {
const folders = [...node.folders.values()].sort((a, b) =>
a.name.localeCompare(b.name, "zh-CN"),
);
const documents = [...node.documents].sort((a, b) =>
a.title.localeCompare(b.title, "zh-CN"),
);
return (
<>
{folders.map((folder) => {
const open = depth === 0 || expanded.has(folder.key);
return (
<div className="tree-branch" key={folder.key}>
<button
className="tree-folder"
style={{ paddingLeft: 10 + depth * 13 }}
onClick={() => onToggle(folder.key)}
>
<span className={open ? "tree-chevron open" : "tree-chevron"}>
</span>
<Icon name="folder" />
<span>{folder.name}</span>
<em>{treeCount(folder)}</em>
</button>
{open && (
<KnowledgeTree
node={folder}
depth={depth + 1}
expanded={expanded}
active={active}
onToggle={onToggle}
onOpen={onOpen}
/>
)}
</div>
);
})}
{documents.map((document) => (
<button
className={
active === document.path ? "tree-document active" : "tree-document"
}
style={{ paddingLeft: 28 + depth * 13 }}
key={document.path}
onClick={() => onOpen(document.path)}
>
<Icon name="file" />
<span>{document.title}</span>
</button>
))}
</>
);
}
function Knowledge({ function Knowledge({
data, data,
refresh, refresh,
@ -319,13 +438,17 @@ function Knowledge({
[query, setQuery] = useState(""), [query, setQuery] = useState(""),
[newTitle, setNewTitle] = useState<string | null>(null), [newTitle, setNewTitle] = useState<string | null>(null),
[editing, setEditing] = useState(false), [editing, setEditing] = useState(false),
[inspectorOpen, setInspectorOpen] = useState(true); [inspectorOpen, setInspectorOpen] = useState(true),
[expanded, setExpanded] = useState<Set<string>>(new Set(["导入"])),
[activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
const headingScrollTimer = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
if (selected) if (selected)
api.readDocument(selected).then((d) => { api.readDocument(selected).then((d) => {
setDoc(d); setDoc(d);
setBody(d.body); setBody(d.body);
setEditing(false); setEditing(false);
setActiveHeadingId(null);
}); });
}, [selected]); }, [selected]);
const list = useMemo( const list = useMemo(
@ -335,6 +458,7 @@ function Knowledge({
), ),
[data.documents, query], [data.documents, query],
); );
const tree = useMemo(() => buildKnowledgeTree(list), [list]);
const create = async () => { const create = async () => {
const title = newTitle?.trim(); const title = newTitle?.trim();
if (title) { if (title) {
@ -364,8 +488,33 @@ function Knowledge({
const outline = useMemo(() => documentOutline(doc?.body ?? ""), [doc?.body]); const outline = useMemo(() => documentOutline(doc?.body ?? ""), [doc?.body]);
const stats = useMemo(() => { const stats = useMemo(() => {
const chars = parsed.content.replace(/\s/g, "").length; const chars = parsed.content.replace(/\s/g, "").length;
return { chars, minutes: Math.max(1, Math.ceil(chars / 450)) }; return { chars, minutes: Math.max(1, Math.ceil(chars / 400)) };
}, [parsed.content]); }, [parsed.content]);
useEffect(() => {
if (!activeHeadingId) return;
document
.querySelector(".outline-list button.active")
?.scrollIntoView({ block: "nearest" });
}, [activeHeadingId]);
const handleReaderScroll = (event: React.UIEvent<HTMLDivElement>) => {
const container = event.currentTarget;
if (headingScrollTimer.current !== null) return;
headingScrollTimer.current = window.setTimeout(() => {
headingScrollTimer.current = null;
const containerTop = container.getBoundingClientRect().top;
let current: string | null = null;
for (const heading of Array.from(
container.querySelectorAll<HTMLElement>(
"h1[id], h2[id], h3[id], h4[id]",
),
)) {
if (heading.getBoundingClientRect().top - containerTop <= 28)
current = heading.id;
else break;
}
setActiveHeadingId(current);
}, 100);
};
const openWiki = (target: string) => { const openWiki = (target: string) => {
const normalized = target.replace(/[\s·•・\-_|*`]/g, "").toLowerCase(); const normalized = target.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
const hit = data.documents.find( const hit = data.documents.find(
@ -410,18 +559,23 @@ function Knowledge({
{list.length === 0 ? ( {list.length === 0 ? (
<p className="empty"></p> <p className="empty"></p>
) : ( ) : (
list.map((d) => ( <div className="knowledge-tree">
<button <KnowledgeTree
className={selected === d.path ? "selected" : ""} node={tree}
key={d.path} depth={0}
onClick={() => setSelected(d.path)} expanded={expanded}
> active={selected}
<strong>{d.title}</strong> onToggle={(key) =>
<small> setExpanded((current) => {
{new Date(d.updatedAt).toLocaleDateString("zh-CN")} const next = new Set(current);
</small> if (next.has(key)) next.delete(key);
</button> else next.add(key);
)) return next;
})
}
onOpen={setSelected}
/>
</div>
)} )}
</aside> </aside>
<section className="document-workspace"> <section className="document-workspace">
@ -465,7 +619,7 @@ function Knowledge({
onChange={(e) => setBody(e.target.value)} onChange={(e) => setBody(e.target.value)}
/> />
) : ( ) : (
<div className="document-scroll"> <div className="document-scroll" onScroll={handleReaderScroll}>
<header className="reader-heading"> <header className="reader-heading">
<h1>{doc.title}</h1> <h1>{doc.title}</h1>
<p> <p>
@ -475,10 +629,23 @@ function Knowledge({
{parsed.tags.length > 0 && ( {parsed.tags.length > 0 && (
<div className="meta-tags"> <div className="meta-tags">
{parsed.tags.map((tag) => ( {parsed.tags.map((tag) => (
<span key={tag}>{tag}</span> <span key={tag} className={tagTint(tag)}>
{tag}
</span>
))} ))}
</div> </div>
)} )}
<div className="meta-properties">
{Object.entries(parsed.metadata)
.filter(([key]) => key !== "tags")
.slice(0, 6)
.map(([key, value]) => (
<span key={key}>
<b>{key}</b>
{value}
</span>
))}
</div>
</header> </header>
<MarkdownDocument <MarkdownDocument
body={doc.body} body={doc.body}
@ -509,9 +676,13 @@ function Knowledge({
{outline.length ? ( {outline.length ? (
outline.map((item) => ( outline.map((item) => (
<button <button
className={activeHeadingId === item.id ? "active" : ""}
key={item.id} key={item.id}
style={{ paddingLeft: 12 + (item.level - 1) * 10 }} style={{ paddingLeft: 12 + (item.level - 1) * 10 }}
onClick={() => jumpToHeading(item.id)} onClick={() => {
jumpToHeading(item.id);
setActiveHeadingId(item.id);
}}
> >
{item.title} {item.title}
</button> </button>

View file

@ -1,12 +1,63 @@
import type { SVGProps } from "react"; import type { SVGProps } from "react";
export function Icon({name,...props}:{name:string}&SVGProps<SVGSVGElement>){ export function Icon({
name,
...props
}: { name: string } & SVGProps<SVGSVGElement>) {
const paths: Record<string, React.ReactNode> = { const paths: Record<string, React.ReactNode> = {
channel:<><path d="M4 5.5h16v11H8l-4 3v-14Z"/><path d="M8 10h8M8 13h5"/></>, channel: (
book:<><path d="M4 5.5A3.5 3.5 0 0 1 7.5 2H11v17H7.5A3.5 3.5 0 0 0 4 22V5.5ZM20 5.5A3.5 3.5 0 0 0 16.5 2H13v17h3.5A3.5 3.5 0 0 1 20 22V5.5Z"/></>, <>
cube:<><path d="m12 2 8 4.5v10L12 21l-8-4.5v-10L12 2Z"/><path d="m4 6.5 8 4.5 8-4.5M12 11v10"/></>, <path d="M4 5.5h16v11H8l-4 3v-14Z" />
history:<><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5M12 7v5l3 2"/></>, <path d="M8 10h8M8 13h5" />
building:<><path d="M4 21V4h10v17M14 9h6v12M8 8h2M8 12h2M8 16h2M17 13h1M17 17h1M2 21h20"/></>, </>
settings:<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z"/></> ),
book: (
<>
<path d="M4 5.5A3.5 3.5 0 0 1 7.5 2H11v17H7.5A3.5 3.5 0 0 0 4 22V5.5ZM20 5.5A3.5 3.5 0 0 0 16.5 2H13v17h3.5A3.5 3.5 0 0 1 20 22V5.5Z" />
</>
),
cube: (
<>
<path d="m12 2 8 4.5v10L12 21l-8-4.5v-10L12 2Z" />
<path d="m4 6.5 8 4.5 8-4.5M12 11v10" />
</>
),
history: (
<>
<path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
<path d="M3 3v5h5M12 7v5l3 2" />
</>
),
building: (
<>
<path d="M4 21V4h10v17M14 9h6v12M8 8h2M8 12h2M8 16h2M17 13h1M17 17h1M2 21h20" />
</>
),
settings: (
<>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" />
</>
),
folder: <path d="M3.5 7.5h6l2-2h9v13h-17Z" />,
file: (
<>
<path d="M6 3.5h8l4 4v13H6Z" />
<path d="M14 3.5v4h4" />
</>
),
}; };
return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>{paths[name]}</svg> return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
{paths[name]}
</svg>
);
} }

View file

@ -485,6 +485,71 @@ main {
font-size: 10px; font-size: 10px;
color: #6f879e; color: #6f879e;
} }
.knowledge-tree {
padding: 9px 0 18px;
}
.tree-branch {
position: relative;
}
.tree-folder,
.tree-document {
width: 100%;
min-height: 38px;
display: grid;
align-items: center;
gap: 7px;
border: 0 !important;
border-radius: 8px !important;
background: transparent !important;
color: #8fa5ba;
text-align: left;
}
.tree-folder {
grid-template-columns: 13px 18px minmax(0, 1fr) auto;
font-size: 13px;
font-weight: 590;
}
.tree-document {
grid-template-columns: 18px minmax(0, 1fr);
font-size: 12.5px;
font-weight: 480;
}
.tree-folder svg,
.tree-document svg {
width: 16px;
height: 16px;
color: #78bce6;
}
.tree-folder span,
.tree-document span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-folder em {
padding-right: 7px;
color: #617a91;
font-size: 10px;
font-style: normal;
}
.tree-folder:hover,
.tree-document:hover,
.tree-document.active {
color: #eef6ff;
background: rgba(78, 144, 194, 0.12) !important;
}
.tree-document.active {
box-shadow: inset 2px 0 #74bee9;
}
.tree-chevron {
display: grid;
place-items: center;
font-size: 18px;
transition: transform 0.14s ease;
}
.tree-chevron.open {
transform: rotate(90deg);
}
.knowledge-workspace > aside { .knowledge-workspace > aside {
border-right: 1px solid var(--line); border-right: 1px solid var(--line);
padding: 13px; padding: 13px;
@ -593,6 +658,45 @@ main {
background: rgba(71, 139, 188, 0.14); background: rgba(71, 139, 188, 0.14);
font-size: 10px; font-size: 10px;
} }
.meta-tags .tag-sky {
color: #a9dcff;
background: rgba(57, 145, 205, 0.18);
}
.meta-tags .tag-violet {
color: #d3b8ff;
background: rgba(128, 79, 190, 0.2);
}
.meta-tags .tag-amber {
color: #f6d28a;
background: rgba(188, 130, 35, 0.2);
}
.meta-tags .tag-mint {
color: #9fe2ba;
background: rgba(48, 148, 91, 0.2);
}
.meta-tags .tag-rose {
color: #ffb2ba;
background: rgba(185, 65, 81, 0.2);
}
.meta-properties {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 10px;
}
.meta-properties span {
display: inline-flex;
gap: 6px;
padding: 5px 8px;
border-radius: 6px;
color: #93a9bd;
background: rgba(255, 255, 255, 0.035);
font-size: 10px;
}
.meta-properties b {
color: #c5d4e1;
font-weight: 620;
}
.markdown-document { .markdown-document {
color: #cbd8e6; color: #cbd8e6;
font-size: 15px; font-size: 15px;
@ -726,6 +830,11 @@ main {
line-height: 1.45; line-height: 1.45;
padding-block: 7px; padding-block: 7px;
} }
.outline-list button.active {
color: #f5d791;
background: rgba(198, 146, 49, 0.12);
box-shadow: inset 2px 0 var(--gold);
}
.outline-list p { .outline-list p {
color: #72899f; color: #72899f;
font-size: 11px; font-size: 11px;

View file

@ -10,7 +10,7 @@
"canonical_tcs": "product-source/hololake-clean-desktop/language/HOLOLAKE-CLEAN-V1-FOUNDATION.tcs", "canonical_tcs": "product-source/hololake-clean-desktop/language/HOLOLAKE-CLEAN-V1-FOUNDATION.tcs",
"old_product_role": "READ_ONLY_DONOR_NO_WHOLE_MERGE", "old_product_role": "READ_ONLY_DONOR_NO_WHOLE_MERGE",
"first_preinstalled_module": "HLP-MOD-KB-0001", "first_preinstalled_module": "HLP-MOD-KB-0001",
"knowledge_module_state": "LOCAL_COMPLETE_HUMAN_VISUAL_RENDERING_CREATE_READ_EDIT_IMPORT_EXPORT_TRASH_PRIVATE_GIT", "knowledge_module_state": "LOCAL_COMPLETE_HUMAN_VISUAL_RENDERING_WITH_CLASSIFICATION_COLORS_READING_TIME_SCROLL_OUTLINE_CREATE_READ_EDIT_IMPORT_EXPORT_TRASH_PRIVATE_GIT",
"knowledge_human_rendering_source": "product-source/hololake-clean-desktop/src/modules/knowledge-render/index.tsx", "knowledge_human_rendering_source": "product-source/hololake-clean-desktop/src/modules/knowledge-render/index.tsx",
"typed_language_sources": ["USER_MESSAGE", "PERSONA_RESPONSE", "SYSTEM_CONTEXT", "PROTOCOL_EVENT", "AGENT_ACTION", "TOOL_RESULT", "SYSTEM_RECEIPT"], "typed_language_sources": ["USER_MESSAGE", "PERSONA_RESPONSE", "SYSTEM_CONTEXT", "PROTOCOL_EVENT", "AGENT_ACTION", "TOOL_RESULT", "SYSTEM_RECEIPT"],
"public_update_endpoint": "https://guanghulab.com/hololake/releases/latest.json", "public_update_endpoint": "https://guanghulab.com/hololake/releases/latest.json",