mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-05 02:41:43 +00:00
feat(studio): add bilingual browser demo to documentation site (#581)
* feat(studio): add bilingual browser demo to documentation site * fix(studio): preserve literal wikilinks during Markdown parsing * fix(studio): preserve Markdown references and escaped wikilinks
This commit is contained in:
parent
1648b7ce93
commit
d529ec5256
81 changed files with 2529 additions and 140 deletions
22
.github/workflows/_build-docs.yml
vendored
22
.github/workflows/_build-docs.yml
vendored
|
|
@ -37,11 +37,26 @@ jobs:
|
|||
with:
|
||||
node-version: '22.22.3'
|
||||
cache: npm
|
||||
cache-dependency-path: github-pages/package-lock.json
|
||||
cache-dependency-path: |
|
||||
github-pages/package-lock.json
|
||||
reme_studio/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Studio dependencies
|
||||
run: npm ci --prefix ../reme_studio
|
||||
|
||||
- name: Check Studio types and lint
|
||||
if: inputs.run_tests
|
||||
working-directory: reme_studio
|
||||
run: npm run format:check && npm run lint
|
||||
|
||||
- name: Test browser demo behavior
|
||||
if: inputs.run_tests
|
||||
working-directory: reme_studio
|
||||
run: node --test tests/demo-workspace.test.mjs tests/wikilinks.test.mjs
|
||||
|
||||
- name: Run tests
|
||||
if: inputs.run_tests
|
||||
run: npm test
|
||||
|
|
@ -49,6 +64,11 @@ jobs:
|
|||
- name: Build documentation
|
||||
run: npm run build
|
||||
|
||||
- name: Verify browser demo bundle
|
||||
if: inputs.run_tests
|
||||
working-directory: reme_studio
|
||||
run: node --test tests/demo-build.test.mjs
|
||||
|
||||
- name: Configure Pages
|
||||
if: inputs.upload_pages_artifact
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
|
||||
|
|
|
|||
4
.github/workflows/ci-docs.yml
vendored
4
.github/workflows/ci-docs.yml
vendored
|
|
@ -14,9 +14,7 @@ on:
|
|||
- 'reme/config/default.yaml'
|
||||
- 'integrations/claude_code/README.md'
|
||||
- 'integrations/hermes_agent/README.md'
|
||||
- 'reme_studio/README*.md'
|
||||
- 'reme_studio/figures/**'
|
||||
- 'reme_studio/public/og.jpg'
|
||||
- 'reme_studio/**'
|
||||
- 'integrations/dsh/README*.md'
|
||||
- 'integrations/dsh/figures/**'
|
||||
- 'integrations/openclaw/README*.md'
|
||||
|
|
|
|||
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
|
|
@ -11,9 +11,7 @@ on:
|
|||
- "integrations/hermes_agent/README.md"
|
||||
- "README.md"
|
||||
- "README_ZH.md"
|
||||
- "reme_studio/README*.md"
|
||||
- "reme_studio/figures/**"
|
||||
- "reme_studio/public/og.jpg"
|
||||
- "reme_studio/**"
|
||||
- "integrations/dsh/README*.md"
|
||||
- "integrations/dsh/figures/**"
|
||||
- "integrations/openclaw/README*.md"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ function nav(language: "zh" | "en"): DefaultTheme.NavItem[] {
|
|||
return [
|
||||
{ text: zh ? "首页" : "Home", link: `/${language}/` },
|
||||
{ text: zh ? "文档" : "Docs", link: `/${language}/quick_start` },
|
||||
{ text: "Studio", link: `/${language}/workspace/studio` },
|
||||
{ text: zh ? "体验Studio" : "Try Studio", link: `/studio/?lang=${language}`, target: "_self" },
|
||||
{ text: zh ? "集成" : "Integrations", link: `/${language}/integrations` },
|
||||
{ text: zh ? "插件" : "Plugins", link: `/${language}/plugin_management` },
|
||||
{ text: zh ? "评测" : "Benchmarks", link: `/${language}/benchmarks/longmemeval` },
|
||||
|
|
@ -126,6 +126,7 @@ function docsSidebar(language: "zh" | "en"): DefaultTheme.SidebarItem[] {
|
|||
{ text: zh ? "快速开始" : "Quick Start", link: `/${language}/quick_start` },
|
||||
{ text: zh ? "基础配置" : "Configuration", link: `/${language}/configuration` },
|
||||
{ text: zh ? "服务与部署" : "Services and Deployment", link: `/${language}/services` },
|
||||
{ text: "ReMe Studio", link: `/${language}/workspace/studio` },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const translations = {
|
|||
sectionTitle: "从记忆工作区,到自动研究",
|
||||
sectionLead: "三个完整入口,把 ReMe 用到真实工作流中。",
|
||||
products: [
|
||||
{ mark: "▣", label: "WORKSPACE", title: "ReMe Studio", detail: "在本地 Web 工作区中浏览、编辑、搜索记忆,并探索 wikilink 图谱。", href: "/zh/workspace/studio", tone: "mint" },
|
||||
{ mark: "▣", label: "WORKSPACE", title: "ReMe Studio", detail: "在本地 Web 工作区中浏览、编辑、搜索记忆,并探索 wikilink 图谱。", href: "/studio/?lang=zh", tone: "mint" },
|
||||
{ mark: "◌", label: "DISCOVER", title: "Daily Paper", detail: "筛选值得阅读的论文,分析 PDF,并生成文件化笔记与五分钟简报。", href: "/zh/plugins/daily-paper", tone: "cyan" },
|
||||
{ mark: "↗", label: "RESEARCH", title: "Auto Fin", detail: "连接最新财联社新闻与本地历史记忆,生成带 wikilink 的研究报告。", href: "/zh/plugins/auto-fin", tone: "amber" },
|
||||
],
|
||||
|
|
@ -120,7 +120,7 @@ const translations = {
|
|||
sectionTitle: "From memory workspace to automated research",
|
||||
sectionLead: "Three complete paths for putting ReMe into real workflows.",
|
||||
products: [
|
||||
{ mark: "▣", label: "WORKSPACE", title: "ReMe Studio", detail: "Browse, edit, and search memory in a local web workspace, then explore its wikilink graph.", href: "/en/workspace/studio", tone: "mint" },
|
||||
{ mark: "▣", label: "WORKSPACE", title: "ReMe Studio", detail: "Browse, edit, and search memory in a local web workspace, then explore its wikilink graph.", href: "/studio/?lang=en", tone: "mint" },
|
||||
{ mark: "◌", label: "DISCOVER", title: "Daily Paper", detail: "Select useful papers, analyze PDFs, and create file-native notes plus a five-minute brief.", href: "/en/plugins/daily-paper", tone: "cyan" },
|
||||
{ mark: "↗", label: "RESEARCH", title: "Auto Fin", detail: "Connect recent CLS news with local memory to create traceable, wikilink-backed reports.", href: "/en/plugins/auto-fin", tone: "amber" },
|
||||
],
|
||||
|
|
@ -293,6 +293,7 @@ onMounted(async () => {
|
|||
class="product-card"
|
||||
:class="product.tone"
|
||||
:href="localLink(product.href)"
|
||||
:target="product.href.startsWith('/studio/') ? '_self' : undefined"
|
||||
>
|
||||
<span class="product-mark">{{ product.mark }}</span>
|
||||
<span class="product-label">{{ product.label }}</span>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ the build. Do not edit `.generated/` or `dist/`.
|
|||
```bash
|
||||
cd github-pages
|
||||
npm ci
|
||||
npm ci --prefix ../reme_studio
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
|
@ -29,13 +30,16 @@ npm run preview
|
|||
```
|
||||
|
||||
The test suite verifies bilingual core pages, canonical-source mappings, generated Job coverage, and disposable output.
|
||||
The production build is written to `github-pages/dist/` for the existing GitHub Pages workflow.
|
||||
The production build is written to `github-pages/dist/` for the existing GitHub Pages workflow. Development and build
|
||||
commands also compile the shared Studio UI in browser-demo mode and include it at `/studio/`; install both packages'
|
||||
dependencies before running them. The demo needs no ReMe service or model credentials.
|
||||
|
||||
## Sources
|
||||
|
||||
- `docs/`: canonical guides, VitePress configuration, theme, and brand assets
|
||||
- `reme/config/default.yaml`: generated callable Job reference
|
||||
- `reme_studio/README*.md`: ReMe Studio
|
||||
- `reme_studio/README*.md`: ReMe Studio documentation
|
||||
- `reme_studio/demo/`: browser data adapter, bilingual Markdown examples, tags, and scripted conversations
|
||||
- `integrations/{dsh,openclaw}/README*.md`: TypeScript host plugins
|
||||
- `plugins/*/README*.md`: plugin guides
|
||||
- `benchmark/*/README*.md`: benchmark guides
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"content": "node scripts/generate-content.mjs",
|
||||
"dev": "npm run content && vitepress dev .generated/site",
|
||||
"build": "npm run content && vitepress build .generated/site --outDir dist && node scripts/verify-build.mjs",
|
||||
"dev": "npm run content && node scripts/build-studio.mjs && vitepress dev .generated/site",
|
||||
"build": "npm run content && node scripts/build-studio.mjs && vitepress build .generated/site --outDir dist && node scripts/verify-build.mjs",
|
||||
"preview": "vitepress preview .generated/site --outDir dist",
|
||||
"test": "npm run content && node --test tests/*.test.mjs"
|
||||
},
|
||||
|
|
|
|||
12
github-pages/scripts/build-studio.mjs
Normal file
12
github-pages/scripts/build-studio.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { cp, rm } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const studioDir = fileURLToPath(new URL("../../reme_studio/", import.meta.url));
|
||||
const destination = new URL("../.generated/site/public/studio/", import.meta.url);
|
||||
execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build:demo"], {
|
||||
cwd: studioDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
await rm(destination, { recursive: true, force: true });
|
||||
await cp(new URL("../../reme_studio/dist-demo/", import.meta.url), destination, { recursive: true });
|
||||
|
|
@ -60,6 +60,8 @@ const requiredFiles = [
|
|||
"en/blog_20260920.html",
|
||||
"zh/services.html",
|
||||
"en/services.html",
|
||||
"studio/index.html",
|
||||
"studio/reme-icon.svg",
|
||||
"zh/workspace/studio.html",
|
||||
"en/workspace/studio.html",
|
||||
"zh/reference/jobs.html",
|
||||
|
|
@ -84,10 +86,30 @@ assert.match(ChineseHomepage, /89\.4%/);
|
|||
assert.match(ChineseHomepage, /公开、透明的访问趋势/);
|
||||
|
||||
const ChineseStudio = await readFile(path.join(outputDir, "zh/workspace/studio.html"), "utf8");
|
||||
assert.match(ChineseStudio, /class="VPLink link VPNavBarMenuLink active" href="\/zh\/workspace\/studio"/);
|
||||
assert.match(ChineseStudio, /<span[^>]*>Studio<\/span>/);
|
||||
assert.match(ChineseStudio, /href="\/studio\/\?lang=zh"/);
|
||||
assert.match(ChineseStudio, /<span[^>]*>体验Studio<\/span>/);
|
||||
assert.doesNotMatch(ChineseStudio, /<aside[^>]*class="VPSidebar/);
|
||||
|
||||
const EnglishStudio = await readFile(path.join(outputDir, "en/workspace/studio.html"), "utf8");
|
||||
assert.match(EnglishStudio, /href="\/studio\/\?lang=en"/);
|
||||
assert.match(EnglishStudio, /<span[^>]*>Try Studio<\/span>/);
|
||||
|
||||
// Studio is a separate static app: every entry must bypass VitePress routing.
|
||||
for (const language of ["zh", "en"]) {
|
||||
for (const page of ["index.html", "workspace/studio.html"]) {
|
||||
const html = await readFile(path.join(outputDir, language, page), "utf8");
|
||||
const links = [...html.matchAll(/<a\b[^>]*href="\/studio\/\?lang=[^"]+"[^>]*>/g)];
|
||||
assert.ok(links.length, `${language}/${page}: Studio entry exists`);
|
||||
for (const [link] of links) assert.match(link, /target="_self"/, "Studio must use full-page navigation");
|
||||
}
|
||||
}
|
||||
|
||||
const studioDemo = await readFile(path.join(outputDir, "studio/index.html"), "utf8");
|
||||
assert.match(studioDemo, /<title>Try ReMe Studio<\/title>/);
|
||||
for (const match of studioDemo.matchAll(/(?:src|href)="(\.\/[^"?#]+)"/g)) {
|
||||
await access(path.join(outputDir, "studio", match[1]));
|
||||
}
|
||||
|
||||
const ChineseTraffic = await readFile(path.join(outputDir, "zh/traffic.html"), "utf8");
|
||||
assert.match(ChineseTraffic, /S1OZK1PSDLEpyiU5/);
|
||||
|
||||
|
|
|
|||
|
|
@ -144,8 +144,7 @@ test("tracks every generated input in documentation CI and deployment", async ()
|
|||
"integrations/dsh/README*.md",
|
||||
"integrations/dsh/figures/**",
|
||||
"integrations/openclaw/README*.md",
|
||||
"reme_studio/README*.md",
|
||||
"reme_studio/figures/**",
|
||||
"reme_studio/**",
|
||||
"benchmark/toolmemory/gitcha.png",
|
||||
];
|
||||
for (const workflow of ["ci-docs.yml", "deploy-docs.yml"]) {
|
||||
|
|
|
|||
3
reme_studio/.gitignore
vendored
3
reme_studio/.gitignore
vendored
|
|
@ -38,6 +38,9 @@ yarn-error.log*
|
|||
|
||||
/dist/
|
||||
/dist-static/
|
||||
/dist-demo/
|
||||
!demo/content/*/daily/
|
||||
!demo/content/*/daily/**
|
||||
/.wrangler/
|
||||
/outputs/
|
||||
/work/
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
.vinext/
|
||||
dist/
|
||||
dist-static/
|
||||
dist-demo/
|
||||
out/
|
||||
src/reme_studio/static/
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,21 @@ indexes, catalogs, graphs, caches, and runtime metadata stay derived and rebuild
|
|||
> All screenshots in this README were captured from a real local ReMe service with the Studio UI set to English. The
|
||||
> isolated `Project Aurora` workspace is fictional and contains no user data, credentials, or private endpoints.
|
||||
|
||||
## Try in your browser
|
||||
|
||||
Open [Try Studio](https://reme.agentscope.io/studio/?lang=en) without installing or starting a backend. It reuses the
|
||||
local Studio UI with 21 connected, fictional Aurora documents: resources, daily notes, durable knowledge, personal
|
||||
preferences, and procedures.
|
||||
|
||||
Edit, save, and download files, explore wikilink graphs, and filter across directories with keywords and `memory_tags`.
|
||||
Tag file counts and graphs are derived from current example files and update after saves. Multiple selected tags match
|
||||
any selected tag. Changes and drafts stay in this browser, separately for each language. Reset demo restores the
|
||||
starting content; clearing site data also removes changes, so download anything you want to keep.
|
||||
|
||||
Agent conversations are labeled scripts. Tool blocks read the current example files; answers do not regenerate after
|
||||
edits. The demo runs no model, vector retrieval, automatic memory extraction, or background tasks and does not connect
|
||||
to a local ReMe service. Follow the installation instructions below to use your real workspace.
|
||||
|
||||
## What Studio provides
|
||||
|
||||
| Area | Capabilities |
|
||||
|
|
@ -369,6 +384,17 @@ intended edit, and save again. This conflict is deliberate protection against si
|
|||
File features can work while chat is unavailable. Verify the configured Agent wrapper, model, API key, and provider
|
||||
endpoint in the ReMe backend, then check Settings → Status. Do not place credentials in frontend environment variables.
|
||||
|
||||
## Develop the browser demo
|
||||
|
||||
```bash
|
||||
npm run dev:demo
|
||||
npm run build:demo
|
||||
```
|
||||
|
||||
The demo builds to `dist-demo/` for static hosting. The GitHub Pages build includes it at `/studio/`. Example sources
|
||||
live in `demo/content/{zh,en}/`; `demo/api.ts` derives files, tags, and graphs from those Markdown documents. Service
|
||||
builds continue to use the HTTP API.
|
||||
|
||||
## Validation
|
||||
|
||||
Run proportionate checks from `reme_studio/`:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,18 @@ Studio 遵循 ReMe 的本地优先理念:Markdown 和其他工作区文件始
|
|||
|
||||
> 本 README 中的所有截图均来自真实运行的本地 ReMe 服务,Studio 界面统一设置为英文。截图使用隔离的虚构 `Project Aurora` 工作区,不包含用户数据、凭证或私有服务地址。
|
||||
|
||||
## 浏览器体验
|
||||
|
||||
打开 [体验Studio](https://reme.agentscope.io/studio/?lang=zh),无需安装或启动后端。它复用本地 Studio 的界面,
|
||||
提供 21 篇相互关联的虚构 Aurora 项目文档:原始资料、日记、长期知识、个人偏好和操作流程。
|
||||
|
||||
可以编辑、保存、下载文件,探索 wikilink 图谱,并用关键词和 `memory_tags` 跨目录筛选。标签文件数和图谱
|
||||
从当前示例文件计算,保存后更新;多标签筛选匹配任意一个所选标签。修改与草稿按语言保存在当前浏览器,
|
||||
「重置示例」恢复初始内容。清除网站数据也会清除修改,请下载需要保留的文件。
|
||||
|
||||
Agent 对话是明确标注的预设演示,工具块读取当前示例文件;回答不会随编辑重新生成。演示不运行模型、
|
||||
向量检索、自动记忆提炼或后台任务,也不连接本机 ReMe 服务。要使用自己的真实工作区,请按下面的安装说明启动 ReMe。
|
||||
|
||||
## Studio 提供什么
|
||||
|
||||
| 区域 | 能力 |
|
||||
|
|
@ -328,6 +340,16 @@ VITE_REME_API_URL=http://127.0.0.1:2333 npm run dev:static
|
|||
|
||||
文件功能可在 Chat 不可用时正常工作。请检查 ReMe 后端配置的 Agent wrapper、模型、API key 和提供商地址,再查看 Settings → Status。不要把凭证放进前端环境变量。
|
||||
|
||||
## 开发浏览器演示
|
||||
|
||||
```bash
|
||||
npm run dev:demo
|
||||
npm run build:demo
|
||||
```
|
||||
|
||||
演示构建输出到 `dist-demo/`,可作为静态目录托管;GitHub Pages 构建会将其放在 `/studio/`。示例源文件位于
|
||||
`demo/content/{zh,en}/`,演示数据层位于 `demo/api.ts`,其文件、标签和图谱来自这些 Markdown。真实服务构建仍使用 HTTP API。
|
||||
|
||||
## 验证
|
||||
|
||||
在 `reme_studio/` 中运行与改动范围相称的检查:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import dynamic from "next/dynamic";
|
||||
import { IS_DEMO } from "../studio-mode";
|
||||
import WorkspaceMarkdown from "./WorkspaceMarkdown";
|
||||
const DemoTags = IS_DEMO
|
||||
? dynamic(() => import("../../demo/DemoTags"), { ssr: false })
|
||||
: null;
|
||||
import { parseMarkdownFrontmatter } from "./markdown";
|
||||
import styles from "./files-workspace.module.css";
|
||||
|
||||
|
|
@ -50,12 +54,18 @@ function MarkdownPreview({ content }: { content: string }) {
|
|||
{entries.map(({ key, value }, index) => (
|
||||
<div className={styles.frontmatterRow} key={`${key}:${index}`}>
|
||||
<dt>{key}</dt>
|
||||
<dd>{value}</dd>
|
||||
<dd>
|
||||
{key === "memory_tags" && DemoTags ? (
|
||||
<DemoTags content={content} />
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{body}</ReactMarkdown>
|
||||
<WorkspaceMarkdown content={body} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,10 @@ import {
|
|||
MessageSquarePlus,
|
||||
Network,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getAppConfig,
|
||||
listWorkspaceFiles,
|
||||
readWorkspaceFile,
|
||||
REME_API_ENDPOINT,
|
||||
} from "../api";
|
||||
import { getAppConfig, listWorkspaceFiles, REME_API_ENDPOINT } from "../api";
|
||||
import { useI18n } from "../i18n";
|
||||
import { WORKSPACE_CHANGED } from "../studio-mode";
|
||||
import { openWorkspaceFile } from "../open-file";
|
||||
import { useWorkspaceStore } from "../store";
|
||||
import type {
|
||||
AppConfig,
|
||||
|
|
@ -73,14 +70,7 @@ function DirectoryNode({
|
|||
}) {
|
||||
const { t } = useI18n();
|
||||
const [expanded, setExpanded] = useState(depth === 0);
|
||||
const {
|
||||
tabs,
|
||||
activeTabId,
|
||||
openMarkdown,
|
||||
openGraph,
|
||||
hydrateMarkdown,
|
||||
failMarkdown,
|
||||
} = useWorkspaceStore();
|
||||
const { tabs, activeTabId, openGraph } = useWorkspaceStore();
|
||||
const active = tabs.find((tab) => tab.id === activeTabId);
|
||||
const selected = active?.type === "markdown" && active.path === node.path;
|
||||
if (node.type === "directory") {
|
||||
|
|
@ -136,20 +126,7 @@ function DirectoryNode({
|
|||
}
|
||||
const open = async () => {
|
||||
if (!isEditable(node.name)) return;
|
||||
const existing = tabs.some(
|
||||
(tab) => tab.type === "markdown" && tab.path === node.path,
|
||||
);
|
||||
const id = openMarkdown(node.path);
|
||||
if (existing) return;
|
||||
try {
|
||||
const file = await readWorkspaceFile(node.path);
|
||||
hydrateMarkdown(id, file.content, file.stat.mtime);
|
||||
} catch (error) {
|
||||
failMarkdown(
|
||||
id,
|
||||
error instanceof Error ? error.message : t("fileReadFailed"),
|
||||
);
|
||||
}
|
||||
await openWorkspaceFile(node.path);
|
||||
};
|
||||
const absolutePath = workspaceDir
|
||||
? absoluteWorkspacePath(workspaceDir, node.path)
|
||||
|
|
@ -227,10 +204,14 @@ export default function FilesNavigator({
|
|||
if (!document.hidden) void refresh();
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
window.addEventListener(WORKSPACE_CHANGED, onVisibilityChange);
|
||||
window.addEventListener("storage", onVisibilityChange);
|
||||
return () => {
|
||||
mounted = false;
|
||||
window.clearInterval(timer);
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
window.removeEventListener(WORKSPACE_CHANGED, onVisibilityChange);
|
||||
window.removeEventListener("storage", onVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
const visiblePaths = useMemo(
|
||||
|
|
|
|||
32
reme_studio/app/files-workspace/MarkdownEditor.tsx
Normal file
32
reme_studio/app/files-workspace/MarkdownEditor.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import Editor from "@monaco-editor/react";
|
||||
import "../monaco-setup";
|
||||
import { useThemeStore } from "../theme";
|
||||
import { getLanguage } from "./get-language";
|
||||
|
||||
export default function MarkdownEditor({
|
||||
path,
|
||||
content,
|
||||
onChange,
|
||||
}: {
|
||||
path: string;
|
||||
content: string;
|
||||
onChange: (content: string) => void;
|
||||
}) {
|
||||
const theme = useThemeStore((state) => state.resolved);
|
||||
return (
|
||||
<Editor
|
||||
path={path}
|
||||
language={getLanguage(path)}
|
||||
value={content}
|
||||
theme={theme === "dark" ? "vs-dark" : "vs"}
|
||||
onChange={(value) => onChange(value ?? "")}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
wordWrap: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,9 +16,10 @@ import {
|
|||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from "lucide-react";
|
||||
import { getGraphSnapshot, readWorkspaceFile } from "../api";
|
||||
import { getGraphSnapshot } from "../api";
|
||||
import { useI18n } from "../i18n";
|
||||
import { useWorkspaceStore } from "../store";
|
||||
import { WORKSPACE_CHANGED } from "../studio-mode";
|
||||
import { openWorkspaceFile } from "../open-file";
|
||||
import type { GraphSnapshot, MemoryGraphRoot } from "../types";
|
||||
import {
|
||||
edgePath,
|
||||
|
|
@ -75,10 +76,6 @@ export default function MemoryGraphView({ root }: { root: MemoryGraphRoot }) {
|
|||
const [draggingId, setDraggingId] = useState("");
|
||||
const dragSession = useRef<DragSession | undefined>(undefined);
|
||||
const didDrag = useRef(false);
|
||||
const tabs = useWorkspaceStore((state) => state.tabs);
|
||||
const openMarkdown = useWorkspaceStore((state) => state.openMarkdown);
|
||||
const hydrateMarkdown = useWorkspaceStore((state) => state.hydrateMarkdown);
|
||||
const failMarkdown = useWorkspaceStore((state) => state.failMarkdown);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
|
@ -109,10 +106,14 @@ export default function MemoryGraphView({ root }: { root: MemoryGraphRoot }) {
|
|||
if (!document.hidden) void load();
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
window.addEventListener(WORKSPACE_CHANGED, onVisibilityChange);
|
||||
window.addEventListener("storage", onVisibilityChange);
|
||||
return () => {
|
||||
mounted = false;
|
||||
window.clearInterval(timer);
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
window.removeEventListener(WORKSPACE_CHANGED, onVisibilityChange);
|
||||
window.removeEventListener("storage", onVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -170,20 +171,7 @@ export default function MemoryGraphView({ root }: { root: MemoryGraphRoot }) {
|
|||
|
||||
const openFile = async (node: PositionedGraphNode) => {
|
||||
if (!node.indexed || node.virtual) return;
|
||||
const existing = tabs.some(
|
||||
(tab) => tab.type === "markdown" && tab.path === node.path,
|
||||
);
|
||||
const id = openMarkdown(node.path);
|
||||
if (existing) return;
|
||||
try {
|
||||
const file = await readWorkspaceFile(node.path);
|
||||
hydrateMarkdown(id, file.content, file.stat.mtime);
|
||||
} catch (openError) {
|
||||
failMarkdown(
|
||||
id,
|
||||
openError instanceof Error ? openError.message : t("fileReadFailed"),
|
||||
);
|
||||
}
|
||||
await openWorkspaceFile(node.path);
|
||||
};
|
||||
|
||||
const startDrag = (event: ReactPointerEvent<SVGGElement>, nodeId: string) => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Check, Code2, Download, Eye, LoaderCircle, Save } from "lucide-react";
|
||||
import { saveWorkspaceFile } from "../api";
|
||||
import { useI18n } from "../i18n";
|
||||
import "../monaco-setup";
|
||||
import { useWorkspaceStore } from "../store";
|
||||
import { useThemeStore } from "../theme";
|
||||
import type { WorkspaceTab } from "../types";
|
||||
import FilePreview from "./FilePreview";
|
||||
import { getLanguage } from "./get-language";
|
||||
import styles from "./files-workspace.module.css";
|
||||
|
||||
const MarkdownEditor = dynamic(() => import("./MarkdownEditor"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
type FileTab = Extract<WorkspaceTab, { type: "markdown" }>;
|
||||
|
||||
/** Minimal ReMe port of QwenPaw's TabbedEditor: preview-first, Monaco edit, download and Cmd/Ctrl+S. */
|
||||
|
|
@ -24,7 +25,6 @@ export default function TabbedEditor({ tab }: { tab: FileTab }) {
|
|||
const update = useWorkspaceStore((state) => state.updateMarkdown);
|
||||
const markSaved = useWorkspaceStore((state) => state.markSaved);
|
||||
const dirty = tab.content !== tab.savedContent;
|
||||
const theme = useThemeStore((state) => state.resolved);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
if (!dirty || saving) return;
|
||||
|
|
@ -137,19 +137,10 @@ export default function TabbedEditor({ tab }: { tab: FileTab }) {
|
|||
{preview ? (
|
||||
<FilePreview filePath={tab.path} content={tab.content} />
|
||||
) : (
|
||||
<Editor
|
||||
<MarkdownEditor
|
||||
path={tab.path}
|
||||
language={getLanguage(tab.path)}
|
||||
value={tab.content}
|
||||
theme={theme === "dark" ? "vs-dark" : "vs"}
|
||||
onChange={(value) => update(tab.id, value ?? "")}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
wordWrap: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
content={tab.content}
|
||||
onChange={(content) => update(tab.id, content)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
30
reme_studio/app/files-workspace/WorkspaceMarkdown.tsx
Normal file
30
reme_studio/app/files-workspace/WorkspaceMarkdown.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { openWorkspaceFile } from "../open-file";
|
||||
import { remarkWikilinks, workspaceLinkTarget } from "./wikilinks";
|
||||
|
||||
export default function WorkspaceMarkdown({ content }: { content: string }) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkWikilinks]}
|
||||
components={{
|
||||
a: ({ href, title, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
title={title}
|
||||
onClick={(event) => {
|
||||
const path = workspaceLinkTarget(href);
|
||||
if (!path) return;
|
||||
event.preventDefault();
|
||||
void openWorkspaceFile(path);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
149
reme_studio/app/files-workspace/wikilinks.ts
Normal file
149
reme_studio/app/files-workspace/wikilinks.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import type { CompileContext } from "mdast-util-from-markdown";
|
||||
import type {
|
||||
Effects,
|
||||
State,
|
||||
Token,
|
||||
TokenizeContext,
|
||||
} from "micromark-util-types";
|
||||
import type { Processor } from "unified";
|
||||
import type {} from "remark-parse";
|
||||
|
||||
declare module "mdast" {
|
||||
interface PhrasingContentMap {
|
||||
remeWikilink: { type: "remeWikilink"; value: string };
|
||||
}
|
||||
interface RootContentMap {
|
||||
remeWikilink: PhrasingContentMap["remeWikilink"];
|
||||
}
|
||||
}
|
||||
|
||||
declare module "micromark-util-types" {
|
||||
interface TokenTypeMap {
|
||||
remeWikilink: "remeWikilink";
|
||||
}
|
||||
}
|
||||
|
||||
const WIKILINK_PATTERN =
|
||||
/^\[\[([^[\]|#\n]+?)(?:#[^[\]|\n]+)?(?:\|([^[\]\n]+))?\]\]$/;
|
||||
|
||||
/** Keep Markdown punctuation inside a wikilink literal during inline parsing. */
|
||||
function tokenizeWikilink(
|
||||
this: TokenizeContext,
|
||||
effects: Effects,
|
||||
ok: State,
|
||||
nok: State,
|
||||
): State {
|
||||
const serialize = this.sliceSerialize.bind(this);
|
||||
let token: Token;
|
||||
return start;
|
||||
|
||||
function start(code: number | null) {
|
||||
if (code !== 91) return nok(code);
|
||||
token = effects.enter("remeWikilink");
|
||||
effects.consume(code);
|
||||
return secondBracket;
|
||||
}
|
||||
|
||||
function secondBracket(code: number | null) {
|
||||
if (code !== 91) return nok(code);
|
||||
effects.consume(code);
|
||||
return inside;
|
||||
}
|
||||
|
||||
function inside(code: number | null): State | undefined {
|
||||
// Micromark uses negative codes for line endings, tabs, and virtual spaces.
|
||||
if (code === null || code <= -3 || code === 91) return nok(code);
|
||||
effects.consume(code);
|
||||
return code === 93 ? closingBracket : inside;
|
||||
}
|
||||
|
||||
function closingBracket(code: number | null) {
|
||||
if (code !== 93) return nok(code);
|
||||
effects.consume(code);
|
||||
effects.exit("remeWikilink");
|
||||
const match = WIKILINK_PATTERN.exec(serialize(token));
|
||||
return match?.[1].trim() ? ok : nok;
|
||||
}
|
||||
}
|
||||
|
||||
function enterWikilink(this: CompileContext, token: Token) {
|
||||
// A dedicated node also keeps GFM's autolink transform out of literal aliases.
|
||||
this.enter(
|
||||
{ type: "remeWikilink", value: this.sliceSerialize(token) },
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
function exitWikilink(this: CompileContext, token: Token) {
|
||||
this.exit(token);
|
||||
}
|
||||
|
||||
interface MarkdownNode {
|
||||
type: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
children?: MarkdownNode[];
|
||||
}
|
||||
|
||||
/** Ignore malformed fragments authored directly in Markdown. */
|
||||
export function workspaceLinkTarget(href?: string): string | undefined {
|
||||
const prefix = "#reme-file=";
|
||||
if (!href?.startsWith(prefix)) return;
|
||||
try {
|
||||
return decodeURIComponent(href.slice(prefix.length)) || undefined;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve literal workspace wikilinks without rewriting code or existing links. */
|
||||
export function remarkWikilinks(this: Processor) {
|
||||
const data = this.data();
|
||||
(data.micromarkExtensions ||= []).push({
|
||||
text: { 91: { name: "remeWikilink", tokenize: tokenizeWikilink } },
|
||||
});
|
||||
(data.fromMarkdownExtensions ||= []).push({
|
||||
enter: { remeWikilink: enterWikilink },
|
||||
exit: { remeWikilink: exitWikilink },
|
||||
});
|
||||
return (tree: MarkdownNode) => {
|
||||
const literalize = (node: MarkdownNode) => {
|
||||
if (node.type === "remeWikilink") node.type = "text";
|
||||
node.children?.forEach(literalize);
|
||||
};
|
||||
const visit = (node: MarkdownNode) => {
|
||||
if (
|
||||
!node.children ||
|
||||
[
|
||||
"link",
|
||||
"linkReference",
|
||||
"image",
|
||||
"imageReference",
|
||||
"code",
|
||||
"inlineCode",
|
||||
].includes(node.type)
|
||||
) {
|
||||
literalize(node);
|
||||
return;
|
||||
}
|
||||
node.children = node.children.map((child) => {
|
||||
if (child.type !== "remeWikilink" || !child.value) {
|
||||
visit(child);
|
||||
return child;
|
||||
}
|
||||
const match = WIKILINK_PATTERN.exec(child.value);
|
||||
if (!match?.[1].trim()) {
|
||||
literalize(child);
|
||||
return child;
|
||||
}
|
||||
const target = match[1].trim();
|
||||
return {
|
||||
type: "link",
|
||||
url: `#reme-file=${encodeURIComponent(target)}`,
|
||||
children: [{ type: "text", value: match[2]?.trim() || target }],
|
||||
};
|
||||
});
|
||||
};
|
||||
visit(tree);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,11 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { IS_DEMO } from "./studio-mode.ts";
|
||||
|
||||
export type Language = "zh" | "en";
|
||||
|
||||
const messages = {
|
||||
zh: {
|
||||
demoCloseSearch: "关闭搜索",
|
||||
demoTitle: "浏览器演示",
|
||||
demoDescription: "示例文件 · 修改保存在此浏览器 · 无需服务",
|
||||
demoGuide: "体验指南",
|
||||
demoSearch: "搜索 / 标签",
|
||||
demoReset: "重置示例",
|
||||
demoResetConfirm:
|
||||
"重置当前语言的示例?此浏览器中的示例修改、草稿和对话将被清除。",
|
||||
demoSearchDescription:
|
||||
"关键词与 memory tags 跨目录筛选;演示使用浏览器关键词匹配。",
|
||||
demoSearchPlaceholder: "搜索内容或路径,例如:离线",
|
||||
demoTagHint: "文件数 · 多标签匹配任意一个",
|
||||
demoClearTags: "清除标签",
|
||||
demoMatches: "{count} 篇文档",
|
||||
demoNoResults: "没有匹配的文档,试试其他关键词或标签。",
|
||||
demoChatDescription: "预设对话展示标签检索、文件读取与引用。未运行模型。",
|
||||
demoChatHint: "示例回答不会随文档编辑重新生成。",
|
||||
memoryWorkspace: "记忆工作区",
|
||||
newAgentChat: "新建 Agent 对话",
|
||||
newConversation: "新对话",
|
||||
|
|
@ -80,7 +98,7 @@ const messages = {
|
|||
lightTheme: "浅色",
|
||||
darkTheme: "深色",
|
||||
systemTheme: "跟随系统",
|
||||
documentation: "文档资料",
|
||||
officialWebsite: "ReMe 官网",
|
||||
github: "GitHub",
|
||||
settings: "设置",
|
||||
settingsTitle: "ReMe 设置",
|
||||
|
|
@ -138,6 +156,25 @@ const messages = {
|
|||
agentUnavailable: "无法连接 Agent(HTTP {status})",
|
||||
},
|
||||
en: {
|
||||
demoCloseSearch: "Close search",
|
||||
demoTitle: "Browser demo",
|
||||
demoDescription:
|
||||
"Example files · Changes stay in this browser · No server needed",
|
||||
demoGuide: "Start here",
|
||||
demoSearch: "Search / Tags",
|
||||
demoReset: "Reset demo",
|
||||
demoResetConfirm:
|
||||
"Reset this language’s demo? Edited example files, drafts, and chats in this browser will be cleared.",
|
||||
demoSearchDescription:
|
||||
"Filter across directories by keywords and memory tags. This demo uses browser keyword matching.",
|
||||
demoSearchPlaceholder: "Search content or paths, e.g. offline",
|
||||
demoTagHint: "File counts · Match any selected tag",
|
||||
demoClearTags: "Clear tags",
|
||||
demoMatches: "{count} documents",
|
||||
demoNoResults: "No matches. Try another keyword or tag.",
|
||||
demoChatDescription:
|
||||
"Scripted conversations show tag searches, file reads, and citations. No model is running.",
|
||||
demoChatHint: "Scripted answers do not regenerate after edits.",
|
||||
memoryWorkspace: "Memory workspace",
|
||||
newAgentChat: "New Agent chat",
|
||||
newConversation: "New chat",
|
||||
|
|
@ -215,7 +252,7 @@ const messages = {
|
|||
lightTheme: "Light",
|
||||
darkTheme: "Dark",
|
||||
systemTheme: "System",
|
||||
documentation: "Documentation",
|
||||
officialWebsite: "ReMe Website",
|
||||
github: "GitHub",
|
||||
settings: "Settings",
|
||||
settingsTitle: "ReMe Settings",
|
||||
|
|
@ -300,21 +337,44 @@ const applyLanguage = (language: Language) => {
|
|||
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
||||
};
|
||||
|
||||
function initialLanguage(): Language {
|
||||
if (typeof window === "undefined") return "zh";
|
||||
const requested = IS_DEMO
|
||||
? new URLSearchParams(window.location.search).get("lang")
|
||||
: null;
|
||||
let saved: string | null = null;
|
||||
try {
|
||||
saved = localStorage.getItem("reme-language");
|
||||
} catch {
|
||||
/* Use the browser language. */
|
||||
}
|
||||
const preferred = requested || saved;
|
||||
return preferred === "zh" || preferred === "en"
|
||||
? preferred
|
||||
: navigator.language.startsWith("zh")
|
||||
? "zh"
|
||||
: "en";
|
||||
}
|
||||
|
||||
export const useLanguageStore = create<LanguageState>((set) => ({
|
||||
language: "zh",
|
||||
language: IS_DEMO ? initialLanguage() : "zh",
|
||||
hydrate: () => {
|
||||
const saved = localStorage.getItem("reme-language");
|
||||
const language: Language =
|
||||
saved === "zh" || saved === "en"
|
||||
? saved
|
||||
: navigator.language.startsWith("zh")
|
||||
? "zh"
|
||||
: "en";
|
||||
const language = initialLanguage();
|
||||
applyLanguage(language);
|
||||
set({ language });
|
||||
},
|
||||
setLanguage: (language) => {
|
||||
localStorage.setItem("reme-language", language);
|
||||
try {
|
||||
localStorage.setItem("reme-language", language);
|
||||
} catch {
|
||||
/* Keep the preference in memory. */
|
||||
}
|
||||
if (IS_DEMO) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("lang", language);
|
||||
window.location.assign(url.href);
|
||||
return;
|
||||
}
|
||||
applyLanguage(language);
|
||||
set({ language });
|
||||
},
|
||||
|
|
|
|||
21
reme_studio/app/open-file.ts
Normal file
21
reme_studio/app/open-file.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { readWorkspaceFile } from "./api";
|
||||
import { useWorkspaceStore } from "./store";
|
||||
|
||||
/** Open one workspace file consistently from the navigator, graph, or a link. */
|
||||
export async function openWorkspaceFile(path: string) {
|
||||
const store = useWorkspaceStore.getState();
|
||||
const existing = store.tabs.some(
|
||||
(tab) => tab.type === "markdown" && tab.path === path,
|
||||
);
|
||||
const id = store.openMarkdown(path);
|
||||
if (existing) return;
|
||||
try {
|
||||
const file = await readWorkspaceFile(path);
|
||||
store.hydrateMarkdown(id, file.content, file.stat.mtime);
|
||||
} catch (error) {
|
||||
store.failMarkdown(
|
||||
id,
|
||||
error instanceof Error ? error.message : "Failed to read file",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { IS_DEMO } from "./studio-mode";
|
||||
import { useLanguageStore } from "./i18n";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import {
|
||||
applyStreamChunk,
|
||||
|
|
@ -54,7 +56,9 @@ interface WorkspaceState {
|
|||
|
||||
const fileTitle = (path: string) => path.split("/").pop() || path;
|
||||
// Preserve the original key so existing tabs, chats, and unsaved drafts survive the Studio rename.
|
||||
const WORKSPACE_STORAGE_KEY = "reme-workspace";
|
||||
const WORKSPACE_STORAGE_KEY = IS_DEMO
|
||||
? `reme-demo-tabs-v1-${useLanguageStore.getState().language}`
|
||||
: "reme-workspace";
|
||||
|
||||
export const useWorkspaceStore = create<WorkspaceState>()(
|
||||
persist(
|
||||
|
|
|
|||
2
reme_studio/app/studio-mode.ts
Normal file
2
reme_studio/app/studio-mode.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const IS_DEMO = process.env.NEXT_PUBLIC_REME_DEMO === "true";
|
||||
export const WORKSPACE_CHANGED = "reme-workspace-changed";
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import WorkspaceMarkdown from "./files-workspace/WorkspaceMarkdown";
|
||||
import { IS_DEMO } from "./studio-mode";
|
||||
import { openWorkspaceFile } from "./open-file";
|
||||
import { SparkMenuExpandLine, SparkMenuFoldLine } from "@agentscope-ai/icons";
|
||||
import {
|
||||
BookOpenText,
|
||||
|
|
@ -40,9 +41,15 @@ import {
|
|||
appendWorkspaceFileReference,
|
||||
WORKSPACE_FILE_DRAG_TYPE,
|
||||
} from "./workspace-drag";
|
||||
import SettingsCenter from "./settings-center";
|
||||
const SettingsCenter = IS_DEMO
|
||||
? null
|
||||
: dynamic(() => import("./settings-center"), { ssr: false });
|
||||
const DemoBar = IS_DEMO
|
||||
? dynamic(() => import("../demo/DemoBar"), { ssr: false })
|
||||
: null;
|
||||
import { hasUnsavedChanges, unsavedTabsClosedBy } from "./tab-close";
|
||||
import packageJson from "../package.json";
|
||||
import { scenarios } from "../demo/scenarios";
|
||||
|
||||
const TabbedEditor = dynamic(() => import("./files-workspace/TabbedEditor"), {
|
||||
ssr: false,
|
||||
|
|
@ -239,7 +246,7 @@ function Tabs() {
|
|||
function MarkdownView({ content }: { content: string }) {
|
||||
return (
|
||||
<article className="markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
<WorkspaceMarkdown content={content} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
@ -356,7 +363,7 @@ function ChatBlockView({
|
|||
}
|
||||
|
||||
function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
|
||||
const { t } = useI18n();
|
||||
const { language, t } = useI18n();
|
||||
const [input, setInput] = useState("");
|
||||
const [fileDragOver, setFileDragOver] = useState(false);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -369,6 +376,19 @@ function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
|
|||
}, [tab.messages]);
|
||||
useEffect(() => () => controller.current?.abort(), []);
|
||||
|
||||
const suggestions = (
|
||||
<div className="suggestions">
|
||||
{(IS_DEMO
|
||||
? scenarios[language].map((scenario) => scenario.question)
|
||||
: [t("promptRecent"), t("promptTasks"), t("promptIdeas")]
|
||||
).map((question) => (
|
||||
<button key={question} onClick={() => setInput(question)}>
|
||||
{question}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const send = async () => {
|
||||
const query = input.trim();
|
||||
if (!query || tab.streaming) return;
|
||||
|
|
@ -406,16 +426,8 @@ function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
|
|||
<div className="chat-empty">
|
||||
<div className="agent-logo" aria-hidden="true" />
|
||||
<h1>{t("chatTitle")}</h1>
|
||||
<p>{t("chatDescription")}</p>
|
||||
<div className="suggestions">
|
||||
{[t("promptRecent"), t("promptTasks"), t("promptIdeas")].map(
|
||||
(text) => (
|
||||
<button key={text} onClick={() => setInput(text)}>
|
||||
{text}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<p>{t(IS_DEMO ? "demoChatDescription" : "chatDescription")}</p>
|
||||
{suggestions}
|
||||
</div>
|
||||
)}
|
||||
{tab.messages.map((message) => (
|
||||
|
|
@ -451,6 +463,7 @@ function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
{IS_DEMO && tab.messages.length > 0 && suggestions}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -495,7 +508,7 @@ function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
|
|||
<Send size={17} />
|
||||
</button>
|
||||
</div>
|
||||
<span>{t("composerHint")}</span>
|
||||
<span>{t(IS_DEMO ? "demoChatHint" : "composerHint")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -516,6 +529,9 @@ function Workspace() {
|
|||
let mounted = true;
|
||||
void Promise.resolve(useWorkspaceStore.persist.rehydrate()).then(
|
||||
async () => {
|
||||
if (IS_DEMO && !useWorkspaceStore.getState().tabs.length && mounted) {
|
||||
await openWorkspaceFile("START_HERE.md");
|
||||
}
|
||||
const restoredFiles = useWorkspaceStore
|
||||
.getState()
|
||||
.tabs.filter(
|
||||
|
|
@ -648,14 +664,14 @@ function Workspace() {
|
|||
: active?.title || t("workspace")}
|
||||
</span>
|
||||
<div className="topbar-actions">
|
||||
<nav className="resource-links" aria-label={t("documentation")}>
|
||||
<nav className="resource-links" aria-label={t("officialWebsite")}>
|
||||
<a
|
||||
href="https://reme.agentscope.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<BookOpenText size={17} />
|
||||
<span>{t("documentation")}</span>
|
||||
<span>{t("officialWebsite")}</span>
|
||||
</a>
|
||||
<i aria-hidden="true" />
|
||||
<a
|
||||
|
|
@ -687,16 +703,19 @@ function Workspace() {
|
|||
</button>
|
||||
</div>
|
||||
<ThemeMenu />
|
||||
<button
|
||||
className="settings-trigger"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
aria-label={t("settings")}
|
||||
title={t("settings")}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
{SettingsCenter && (
|
||||
<button
|
||||
className="settings-trigger"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
aria-label={t("settings")}
|
||||
title={t("settings")}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
{DemoBar && <DemoBar />}
|
||||
<div className="surface">
|
||||
<FilesNavigator
|
||||
open={navOpen}
|
||||
|
|
@ -736,10 +755,12 @@ function Workspace() {
|
|||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<SettingsCenter
|
||||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
{SettingsCenter && (
|
||||
<SettingsCenter
|
||||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
157
reme_studio/demo/DemoBar.tsx
Normal file
157
reme_studio/demo/DemoBar.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { BookOpen, Network, RotateCcw, Search, X } from "lucide-react";
|
||||
import { useI18n } from "../app/i18n";
|
||||
import { openWorkspaceFile } from "../app/open-file";
|
||||
import { useWorkspaceStore } from "../app/store";
|
||||
import { WORKSPACE_CHANGED } from "../app/studio-mode";
|
||||
import { demoWorkspace } from "./api";
|
||||
import { DEMO_SEARCH } from "./DemoTags";
|
||||
import { WELCOME_PATH } from "./workspace";
|
||||
import styles from "./demo.module.css";
|
||||
|
||||
export default function DemoBar() {
|
||||
const { language, t } = useI18n();
|
||||
const [query, setQuery] = useState("");
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [, setRevision] = useState(0);
|
||||
const [error, setError] = useState("");
|
||||
const dialog = useRef<HTMLDialogElement>(null);
|
||||
const workspace = demoWorkspace();
|
||||
const results = workspace.search(query, selected);
|
||||
const tags = workspace.tags();
|
||||
const refresh = () => setRevision((value) => value + 1);
|
||||
useEffect(() => {
|
||||
const search = (event: Event) => {
|
||||
setQuery("");
|
||||
setSelected([(event as CustomEvent<string>).detail]);
|
||||
dialog.current?.showModal();
|
||||
};
|
||||
window.addEventListener(WORKSPACE_CHANGED, refresh);
|
||||
window.addEventListener("storage", refresh);
|
||||
window.addEventListener(DEMO_SEARCH, search);
|
||||
return () => {
|
||||
window.removeEventListener(WORKSPACE_CHANGED, refresh);
|
||||
window.removeEventListener("storage", refresh);
|
||||
window.removeEventListener(DEMO_SEARCH, search);
|
||||
};
|
||||
}, []);
|
||||
const reset = () => {
|
||||
if (!window.confirm(t("demoResetConfirm"))) return;
|
||||
try {
|
||||
workspace.reset();
|
||||
useWorkspaceStore.persist.clearStorage();
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : t("unknownError"));
|
||||
}
|
||||
};
|
||||
const showFile = (path: string) => {
|
||||
dialog.current?.close();
|
||||
void openWorkspaceFile(path);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className={styles.bar}>
|
||||
<div className={styles.description}>
|
||||
<strong>{t("demoTitle")}</strong>
|
||||
<span>{t("demoDescription")}</span>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<button onClick={() => void openWorkspaceFile(WELCOME_PATH)}>
|
||||
<BookOpen size={14} />
|
||||
{t("demoGuide")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => useWorkspaceStore.getState().openGraph("wiki")}
|
||||
>
|
||||
<Network size={14} />
|
||||
{t("memoryGraphShort")}
|
||||
</button>
|
||||
<button onClick={() => dialog.current?.showModal()}>
|
||||
<Search size={14} />
|
||||
{t("demoSearch")}
|
||||
</button>
|
||||
<button onClick={reset}>
|
||||
<RotateCcw size={14} />
|
||||
{t("demoReset")}
|
||||
</button>
|
||||
</div>
|
||||
{error && <span role="alert">{error}</span>}
|
||||
</div>
|
||||
<dialog
|
||||
ref={dialog}
|
||||
className={styles.dialog}
|
||||
aria-labelledby="demo-search-title"
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<h2 id="demo-search-title">{t("demoSearch")}</h2>
|
||||
<p>{t("demoSearchDescription")}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => dialog.current?.close()}
|
||||
aria-label={t("demoCloseSearch")}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</header>
|
||||
<label className={styles.search}>
|
||||
<Search size={16} />
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={t("demoSearchPlaceholder")}
|
||||
aria-label={t("demoSearchPlaceholder")}
|
||||
/>
|
||||
</label>
|
||||
<div className={styles.tagHeading}>
|
||||
<strong>memory_tags</strong>
|
||||
<span>{t("demoTagHint")}</span>
|
||||
{selected.length > 0 && (
|
||||
<button onClick={() => setSelected([])}>
|
||||
{t("demoClearTags")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.tags}>
|
||||
{tags.map(({ tag, count }) => (
|
||||
<button
|
||||
key={tag}
|
||||
className={styles.tag}
|
||||
aria-pressed={selected.includes(tag)}
|
||||
onClick={() =>
|
||||
setSelected((current) =>
|
||||
current.includes(tag)
|
||||
? current.filter((value) => value !== tag)
|
||||
: [...current, tag],
|
||||
)
|
||||
}
|
||||
>
|
||||
{tag}
|
||||
<span>{count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className={styles.count} role="status">
|
||||
{t("demoMatches", { count: String(results.length) })}
|
||||
</p>
|
||||
<div className={styles.results} lang={language}>
|
||||
{results.map((file) => (
|
||||
<button
|
||||
className={styles.result}
|
||||
key={file.path}
|
||||
onClick={() => showFile(file.path)}
|
||||
>
|
||||
<strong>{file.title}</strong>
|
||||
<code>{file.path}</code>
|
||||
<p>{file.snippet}</p>
|
||||
<span>{file.tags.join(" · ")}</span>
|
||||
</button>
|
||||
))}
|
||||
{!results.length && <p>{t("demoNoResults")}</p>}
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
21
reme_studio/demo/DemoTags.tsx
Normal file
21
reme_studio/demo/DemoTags.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { memoryTags } from "./workspace";
|
||||
import styles from "./demo.module.css";
|
||||
|
||||
export const DEMO_SEARCH = "reme-demo-search";
|
||||
export default function DemoTags({ content }: { content: string }) {
|
||||
return (
|
||||
<span className={styles.tags}>
|
||||
{memoryTags(content).map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
className={styles.tag}
|
||||
onClick={() =>
|
||||
window.dispatchEvent(new CustomEvent(DEMO_SEARCH, { detail: tag }))
|
||||
}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
131
reme_studio/demo/api.ts
Normal file
131
reme_studio/demo/api.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { translate, useLanguageStore } from "../app/i18n";
|
||||
import { WORKSPACE_CHANGED } from "../app/studio-mode";
|
||||
import type { AppConfig, StreamChunk } from "../app/types";
|
||||
import { createDemoWorkspace, DEMO_STORAGE_KEY } from "./workspace";
|
||||
import { scenarios } from "./scenarios";
|
||||
|
||||
const content = import.meta.glob<string>("./content/**/*.md", {
|
||||
eager: true,
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
});
|
||||
const workspaces = new Map<string, ReturnType<typeof createDemoWorkspace>>();
|
||||
export function demoWorkspace() {
|
||||
const language = useLanguageStore.getState().language;
|
||||
if (!workspaces.has(language)) {
|
||||
const prefix = `./content/${language}/`;
|
||||
const seeds = Object.fromEntries(
|
||||
Object.entries(content)
|
||||
.filter(([path]) => path.startsWith(prefix))
|
||||
.map(([path, text]) => [path.slice(prefix.length), text]),
|
||||
);
|
||||
let storage: Storage | undefined;
|
||||
try {
|
||||
storage = localStorage;
|
||||
} catch {
|
||||
/* Editing remains available in memory. */
|
||||
}
|
||||
workspaces.set(
|
||||
language,
|
||||
createDemoWorkspace(seeds, storage, `${DEMO_STORAGE_KEY}-${language}`),
|
||||
);
|
||||
}
|
||||
return workspaces.get(language)!;
|
||||
}
|
||||
export const REME_API_ENDPOINT = translate(
|
||||
useLanguageStore.getState().language,
|
||||
"demoTitle",
|
||||
);
|
||||
export async function getAppConfig(): Promise<AppConfig> {
|
||||
return {
|
||||
app_name: "ReMe Studio Demo",
|
||||
workspace_dir: "/demo",
|
||||
daily_dir: "daily",
|
||||
digest_dir: "digest",
|
||||
resource_dir: "resources",
|
||||
};
|
||||
}
|
||||
export async function listWorkspaceFiles(extensions: string[]) {
|
||||
return {
|
||||
paths: Object.keys(demoWorkspace().files()).filter((path) =>
|
||||
extensions.includes(path.split(".").pop()!),
|
||||
),
|
||||
limited: false,
|
||||
};
|
||||
}
|
||||
export async function readWorkspaceFile(path: string) {
|
||||
return demoWorkspace().read(path);
|
||||
}
|
||||
export async function saveWorkspaceFile(
|
||||
path: string,
|
||||
content: string,
|
||||
expectedMtime?: string,
|
||||
) {
|
||||
const stat = demoWorkspace().save(path, content, expectedMtime);
|
||||
window.dispatchEvent(new Event(WORKSPACE_CHANGED));
|
||||
return stat;
|
||||
}
|
||||
export async function getGraphSnapshot() {
|
||||
return demoWorkspace().graph();
|
||||
}
|
||||
export async function streamChat(
|
||||
query: string,
|
||||
_sessionId: string | undefined,
|
||||
signal: AbortSignal,
|
||||
onChunk: (chunk: StreamChunk) => void,
|
||||
) {
|
||||
const language = useLanguageStore.getState().language;
|
||||
const scenario = scenarios[language].find((item) => item.question === query);
|
||||
const emit = (
|
||||
chunk_type: StreamChunk["chunk_type"],
|
||||
chunk: StreamChunk["chunk"],
|
||||
extra = {},
|
||||
) => {
|
||||
signal.throwIfAborted();
|
||||
onChunk({ chunk_type, chunk, done: chunk_type === "done", ...extra });
|
||||
};
|
||||
emit("reply_start", "");
|
||||
if (scenario) {
|
||||
emit(
|
||||
"tool_call",
|
||||
{ query: scenario.tag, tags: [scenario.tag] },
|
||||
{ tool_call_id: "search", tool_call_name: "search" },
|
||||
);
|
||||
emit("tool_result", demoWorkspace().search(scenario.tag, [scenario.tag]), {
|
||||
tool_call_id: "search",
|
||||
tool_call_name: "search",
|
||||
});
|
||||
for (const path of scenario.paths) {
|
||||
emit(
|
||||
"tool_call",
|
||||
{ path },
|
||||
{ tool_call_id: path, tool_call_name: "read" },
|
||||
);
|
||||
emit("tool_result", demoWorkspace().read(path).content, {
|
||||
tool_call_id: path,
|
||||
tool_call_name: "read",
|
||||
});
|
||||
}
|
||||
}
|
||||
const answer =
|
||||
scenario?.answer ||
|
||||
(language === "zh"
|
||||
? "这是预设对话演示,未运行模型。请选择上方的示例问题,或使用「搜索 / 标签」检索和编辑工作区。"
|
||||
: "This is a scripted conversation without a model. Choose a sample question above, or use Search / Tags to explore and edit the workspace.");
|
||||
for (const part of answer.match(/[\s\S]{1,24}/gu) || [answer]) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
signal.removeEventListener("abort", abort);
|
||||
resolve();
|
||||
}, 20);
|
||||
function abort() {
|
||||
clearTimeout(timer);
|
||||
reject(signal.reason);
|
||||
}
|
||||
signal.addEventListener("abort", abort, { once: true });
|
||||
});
|
||||
emit("content", part);
|
||||
}
|
||||
emit("reply_end", "");
|
||||
emit("done", "");
|
||||
}
|
||||
29
reme_studio/demo/content/en/START_HERE.md
Normal file
29
reme_studio/demo/content/en/START_HERE.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: From one note to reusable memory
|
||||
description: Explore Aurora resources, daily notes, durable knowledge, tags, and links.
|
||||
memory_tags: ["aurora", "memory", "guide"]
|
||||
---
|
||||
|
||||
# Try ReMe Studio
|
||||
|
||||
This fictional **Aurora team knowledge assistant** workspace turns scattered meetings, decisions, and experience into traceable, reusable memory. Open, edit, save, and download Markdown directly in your browser.
|
||||
|
||||
## 1. Follow a memory to its source
|
||||
|
||||
Open [[digest/wiki/local-first.md|the local-first decision]]. Its sources are [[resources/offline-feedback.md|field feedback]] and [[daily/2026-09-15/decision.md|the decision diary]]. The earlier [[resources/requirements.md|requirements draft]] still describes a cloud-first plan, so you can compare the old proposal with the current conclusion.
|
||||
|
||||
## 2. Find a topic across directories
|
||||
|
||||
Choose **Search / Tags** above and select `local_first`. Resources, daily notes, knowledge, and procedures share this topic. Add the keyword “offline” to narrow the results. Tags come from each file's `memory_tags` front matter, rather than a separate hand-maintained category list.
|
||||
|
||||
## 3. Edit your browser copy
|
||||
|
||||
Open [[digest/personal/communication.md|communication preferences]] and switch to Edit. Replace the tag `communication` with `writing`, save, and check the tag counts and results. Download exports the file; Reset demo restores the starting workspace.
|
||||
|
||||
## 4. Explore links and answers
|
||||
|
||||
Choose Graph beside a Knowledge category. Select a node to inspect its incoming and outgoing links, then open the source note. Start a Chat, send a sample question, and expand `search` and `read` to inspect the actual example files.
|
||||
|
||||
> Graphs and keyword filters run in your browser. Chat answers are scripted; editing a document does not generate a new model answer. Changes stay in this browser and disappear if site data is cleared.
|
||||
|
||||
To use your own workspace, follow the documentation above, run `reme start`, and open your local Studio.
|
||||
14
reme_studio/demo/content/en/daily/2026-09-14.md
Normal file
14
reme_studio/demo/content/en/daily/2026-09-14.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: September 14 daily index
|
||||
description: Find the kickoff by date.
|
||||
memory_tags: ["aurora", "planning", "daily"]
|
||||
---
|
||||
|
||||
# 2026-09-14
|
||||
|
||||
Today the team created the Aurora workspace and completed the kickoff. This daily index provides chronological navigation; individual notes retain the detailed facts.
|
||||
|
||||
- [[daily/2026-09-14/kickoff.md|Kickoff diary]]: goals and owners.
|
||||
- [[resources/kickoff.md|Original meeting record]]: the full agreement.
|
||||
|
||||
See [[digest/wiki/aurora.md|the Aurora overview]] for durable context. The `planning` tag also finds planning notes on other dates.
|
||||
15
reme_studio/demo/content/en/daily/2026-09-14/kickoff.md
Normal file
15
reme_studio/demo/content/en/daily/2026-09-14/kickoff.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Kickoff and open assumptions
|
||||
description: Record observations and retain unresolved proposals.
|
||||
memory_tags: ["aurora", "planning", "daily"]
|
||||
---
|
||||
|
||||
# Kickoff diary · 2026-09-14
|
||||
|
||||
The team agreed to make files and source links useful before expanding other features. Central cloud storage is still a candidate and has not passed field validation.
|
||||
|
||||
Zhou created resources, daily, digest/wiki, digest/personal, and digest/procedure. Lin stressed that stopping the service must not remove access to memory.
|
||||
|
||||
He will investigate connectivity and data restrictions tomorrow. We did not turn today's candidate into an established conclusion, so future readers can distinguish a proposal from a fact.
|
||||
|
||||
Sources: [[resources/kickoff.md|kickoff]] and [[resources/requirements.md|requirements]]. See [[digest/wiki/local-first.md|the architecture decision]] for the current conclusion.
|
||||
15
reme_studio/demo/content/en/daily/2026-09-15/decision.md
Normal file
15
reme_studio/demo/content/en/daily/2026-09-15/decision.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Changing to local-first
|
||||
description: New evidence updates durable memory.
|
||||
memory_tags: ["aurora", "local_first", "decision"]
|
||||
---
|
||||
|
||||
# Decision diary · 2026-09-15
|
||||
|
||||
After [[resources/offline-feedback.md|field feedback]], Lin and Zhou approved local-first operation: Markdown files are durable sources, and indexes and graphs are rebuilt from files.
|
||||
|
||||
This updates yesterday's cloud assumption. We retain [[resources/requirements.md|the original draft]] and write the current conclusion, reasons, and sources into [[digest/wiki/local-first.md|architecture knowledge]].
|
||||
|
||||
Cross-device synchronization moves beyond the pilot; see [[digest/wiki/sync.md|sync scope]]. Model reasoning still depends on the configured provider. Local-first does not mean every model runs on the device.
|
||||
|
||||
Next: Zhou prepares offline reading and index recovery checks, while He observes field usability.
|
||||
15
reme_studio/demo/content/en/daily/2026-09-17/validation.md
Normal file
15
reme_studio/demo/content/en/daily/2026-09-17/validation.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Offline and tag validation
|
||||
description: Check file ownership, topic filtering, and recovery.
|
||||
memory_tags: ["aurora", "local_first", "release"]
|
||||
---
|
||||
|
||||
# Validation diary · 2026-09-17
|
||||
|
||||
Zhou stopped the local service and confirmed that Markdown files remained readable and copyable. After restarting, he checked derived-index recovery from existing files. Source files were not deleted to repair an index.
|
||||
|
||||
Lin filtered by `local_first` and found field resources, the decision diary, and architecture knowledge on the same topic. Adding “offline” narrowed irrelevant results.
|
||||
|
||||
He suggested explaining the distinction between tag filters and wikilink graphs. Tags collect a topic; links express specific relationships. Both are derived from file content.
|
||||
|
||||
We added [[digest/wiki/memory-tags.md|the tag guide]] and [[digest/procedure/recovery.md|recovery steps]]. Before release, check [[digest/procedure/release-checklist.md|the checklist]].
|
||||
15
reme_studio/demo/content/en/daily/2026-09-18.md
Normal file
15
reme_studio/demo/content/en/daily/2026-09-18.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: September 18 daily index
|
||||
description: Navigate by date to acceptance and review.
|
||||
memory_tags: ["aurora", "release", "daily"]
|
||||
---
|
||||
|
||||
# 2026-09-18
|
||||
|
||||
Today the pilot passed acceptance and moved to a five-person internal trial.
|
||||
|
||||
- [[daily/2026-09-18/review.md|Pilot review]]: experience worth retaining.
|
||||
- [[resources/pilot-results.md|Original results]]: passed and untested checks.
|
||||
- [[digest/wiki/release.md|Release status]]: the current conclusion and next steps.
|
||||
|
||||
This index preserves chronological access. Use the `release` tag for a view by topic.
|
||||
15
reme_studio/demo/content/en/daily/2026-09-18/review.md
Normal file
15
reme_studio/demo/content/en/daily/2026-09-18/review.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Turn pilot experience into procedures
|
||||
description: Capture repeated checks as reusable operating knowledge.
|
||||
memory_tags: ["aurora", "release", "procedure"]
|
||||
---
|
||||
|
||||
# Pilot review · 2026-09-18
|
||||
|
||||
[[resources/pilot-results.md|Acceptance results]] support an internal trial, but do not establish readiness for multi-device sync or large-scale retrieval. Conclusions must retain the scope of evidence.
|
||||
|
||||
The review found repeated release checks: file backups, source links, tags, and offline reading. We captured these in [[digest/procedure/release-checklist.md|a reusable release checklist]].
|
||||
|
||||
Index troubleshooting became a separate [[digest/procedure/recovery.md|recovery procedure]], with a clear rule: protect source files before repairing derived state.
|
||||
|
||||
Lin asked future updates to lead with conclusions and evidence, followed by action items. This also updates [[digest/personal/communication.md|communication preferences]].
|
||||
20
reme_studio/demo/content/en/digest/personal/communication.md
Normal file
20
reme_studio/demo/content/en/digest/personal/communication.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Lin’s communication preferences
|
||||
description: Traceable preferences support inspectable personal memory.
|
||||
memory_tags: ["aurora", "personal", "communication"]
|
||||
---
|
||||
|
||||
# Communication preferences
|
||||
|
||||
Lin wants project updates to lead with a conclusion, followed by sources and next steps. Do not describe a plan as completed work; mark unknowns explicitly.
|
||||
|
||||
## Reusable rules
|
||||
|
||||
- Keep an update to three paragraphs where possible.
|
||||
- Attach an original-file link to significant conclusions.
|
||||
- Name an owner and deadline for each action.
|
||||
- State the verified scope without expanding performance claims.
|
||||
|
||||
Source: [[daily/2026-09-18/review.md|the pilot review]]. See [[digest/wiki/release.md|release status]] for an application and [[digest/wiki/people.md|owners]] for roles.
|
||||
|
||||
Try replacing `communication` with `writing` in the front matter. Save and inspect the new tag in the panel. Your edits belong only to this browser copy.
|
||||
15
reme_studio/demo/content/en/digest/personal/work-style.md
Normal file
15
reme_studio/demo/content/en/digest/personal/work-style.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Working habits and data boundaries
|
||||
description: Retain long-term habits that users can inspect and change.
|
||||
memory_tags: ["aurora", "personal", "local_first"]
|
||||
---
|
||||
|
||||
# Working habits
|
||||
|
||||
The team writes original records to files before summarizing them into durable knowledge. Internal material stays in the controlled workspace by default; sending it to an external model depends on the actual deployment and provider settings.
|
||||
|
||||
Lin records conclusions on the day of a meeting. Zhou checks recovery before releases. He uses field feedback to correct untested assumptions.
|
||||
|
||||
These habits come from [[resources/kickoff.md|kickoff agreements]] and [[resources/offline-feedback.md|field constraints]]. They help an agent understand context, while remaining inspectable, editable, and removable when outdated.
|
||||
|
||||
Related knowledge: [[digest/wiki/local-first.md|local-first]] and [[digest/personal/communication.md|communication style]].
|
||||
19
reme_studio/demo/content/en/digest/procedure/recovery.md
Normal file
19
reme_studio/demo/content/en/digest/procedure/recovery.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Protect files and recover derived state
|
||||
description: Protect user memory before repairing indexes and links.
|
||||
memory_tags: ["aurora", "local_first", "procedure"]
|
||||
---
|
||||
|
||||
# Recovery procedure
|
||||
|
||||
When retrieval results are missing, first check that source files exist, then inspect derived indexes. Do not delete or rewrite user memory to repair an index.
|
||||
|
||||
1. Preserve a workspace copy and verify paths and file content.
|
||||
2. Check that the local ReMe service uses the expected workspace.
|
||||
3. Inspect ingestion, search indexes, and wikilink graphs according to the issue.
|
||||
4. `reindex` rebuilds search and tag indexes from ingested chunks. It does not scan the workspace or rebuild the graph; new or changed files still need normal ingestion.
|
||||
5. Verify results with queries that expose their sources.
|
||||
|
||||
Source: [[daily/2026-09-17/validation.md|the validation diary]]. Principle: [[digest/wiki/local-first.md|files are the source of truth]].
|
||||
|
||||
This browser demo has no backend index. Its graph and tags are derived directly from current example files. Reset demo restores only this demo copy; it is not a production recovery tool.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Reusable release checklist
|
||||
description: Turn acceptance experience into documented operating steps.
|
||||
memory_tags: ["aurora", "release", "procedure"]
|
||||
---
|
||||
|
||||
# Release checklist
|
||||
|
||||
For Aurora's internal pilot. [[digest/wiki/people.md|Zhou]] performs the checks and Lin confirms scope. Source: [[daily/2026-09-18/review.md|the review]].
|
||||
|
||||
- [ ] Preserve a workspace file copy and check direct Markdown access.
|
||||
- [ ] Check source links in [[digest/wiki/local-first.md|the architecture decision]].
|
||||
- [ ] Test cross-directory retrieval with `local_first` and `release` tags.
|
||||
- [ ] Stop the service and open existing files offline.
|
||||
- [ ] Check derived-data recovery using [[digest/procedure/recovery.md|the recovery steps]].
|
||||
- [ ] Record passed, failed, and untested checks in the acceptance record.
|
||||
- [ ] Update [[digest/wiki/release.md|release status]] with original evidence.
|
||||
|
||||
The checklist captures reusable experience. Checking an item does not start a background task. See [[resources/pilot-results.md|the acceptance record]] for current results.
|
||||
21
reme_studio/demo/content/en/digest/wiki/aurora.md
Normal file
21
reme_studio/demo/content/en/digest/wiki/aurora.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: Aurora project overview
|
||||
description: Current goals, decisions, owners, and release progress.
|
||||
memory_tags: ["aurora", "planning", "memory"]
|
||||
---
|
||||
|
||||
# Aurora
|
||||
|
||||
Aurora is a five-person team's knowledge-assistant pilot. It helps members recover project context, retain reasons behind decisions, and reuse successful procedures.
|
||||
|
||||
## Current agreements
|
||||
|
||||
- Durable data lives in user-owned Markdown files; see [[digest/wiki/local-first.md|the local-first decision]].
|
||||
- The first pilot excludes multi-device synchronization; see [[digest/wiki/sync.md|scope boundaries]].
|
||||
- [[digest/wiki/memory-tags.md|Memory tags]] collect topics, while wikilinks express explicit file relationships.
|
||||
|
||||
## Owners and status
|
||||
|
||||
[[digest/wiki/people.md|Lin, Zhou, and He]] own product, engineering, and the field pilot. Internal trials began September 18; see [[digest/wiki/release.md|release status]] for the accepted scope.
|
||||
|
||||
This overview captures stable knowledge. Specific events remain in daily and original records in resources. Source: [[resources/kickoff.md|the kickoff]].
|
||||
19
reme_studio/demo/content/en/digest/wiki/local-first.md
Normal file
19
reme_studio/demo/content/en/digest/wiki/local-first.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Local-first architecture decision
|
||||
description: The current decision, reasons for changing it, and traceable sources.
|
||||
memory_tags: ["aurora", "local_first", "decision"]
|
||||
---
|
||||
|
||||
# Local-first
|
||||
|
||||
**Current decision: workspace Markdown is the durable source; indexes, catalogs, and graphs are rebuildable derived data.**
|
||||
|
||||
## Why the plan changed
|
||||
|
||||
[[resources/requirements.md|The September 14 draft]] proposed central cloud storage. [[resources/offline-feedback.md|September 15 field feedback]] revealed unreliable connectivity and restrictions on external sharing. [[daily/2026-09-15/decision.md|That day's decision]] approved local-first operation.
|
||||
|
||||
## What this means for users
|
||||
|
||||
Stopping the service does not prevent direct file reading. A damaged index should be rebuilt from source files; see [[digest/procedure/recovery.md|the recovery guide]]. If an online model is unavailable, model answers may be unavailable too. File ownership is unaffected.
|
||||
|
||||
Synchronization is planned separately in [[digest/wiki/sync.md|sync scope]]. This conclusion follows explicit evidence; do not mistake the early draft for the current plan.
|
||||
26
reme_studio/demo/content/en/digest/wiki/memory-tags.md
Normal file
26
reme_studio/demo/content/en/digest/wiki/memory-tags.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: Find a topic with memory tags
|
||||
description: The tag field, normalization, file counts, and combined filtering.
|
||||
memory_tags: ["aurora", "memory", "guide"]
|
||||
---
|
||||
|
||||
# Memory tags
|
||||
|
||||
Tags live in the Markdown front matter's `memory_tags` list. ReMe derives its rebuildable tag index from files.
|
||||
|
||||
```yaml
|
||||
memory_tags: [aurora, local_first, decision]
|
||||
```
|
||||
|
||||
## Tags, directories, and links
|
||||
|
||||
Directories express file purpose. The `local_first` tag collects a topic across resources, daily, and digest. A [[digest/wiki/local-first.md|wikilink]] expresses a specific source or relationship.
|
||||
|
||||
## Try it
|
||||
|
||||
1. Select `local_first` in the search panel and inspect matches across directories and the file count.
|
||||
2. Also select `release`: multiple tags match **any** selected tag, like the real search Job's tags filter.
|
||||
3. Add “offline”: keywords and tags jointly constrain the results.
|
||||
4. Edit the tags in [[digest/personal/communication.md|communication preferences]] and save; counts update immediately.
|
||||
|
||||
The demo accepts inline and indented lists, replaces whitespace with underscores, lowercases and deduplicates tags, and keeps up to three valid tags per file. It runs keyword filtering; ReMe's hybrid vector and BM25 retrieval requires a service.
|
||||
17
reme_studio/demo/content/en/digest/wiki/people.md
Normal file
17
reme_studio/demo/content/en/digest/wiki/people.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Project roles and owners
|
||||
description: Give tasks and decisions explicit owners.
|
||||
memory_tags: ["aurora", "planning", "memory"]
|
||||
---
|
||||
|
||||
# Owners
|
||||
|
||||
| Member | Role | Responsibility |
|
||||
| --------- | ----------- | ----------------------------------- |
|
||||
| Lin Cheng | Product | Requirements, scope, and acceptance |
|
||||
| Zhou Ning | Engineering | Files, indexes, and recovery |
|
||||
| He Yue | Pilot | Field checks and feedback |
|
||||
|
||||
Source: [[resources/kickoff.md|the kickoff]]. See [[digest/wiki/aurora.md|the overview]] for context and [[digest/procedure/release-checklist.md|release procedures]] for upcoming checks.
|
||||
|
||||
Lin prefers brief updates with evidence; see [[digest/personal/communication.md|communication preferences]]. Roles belong in durable knowledge, while daily actions remain in the diary.
|
||||
19
reme_studio/demo/content/en/digest/wiki/release.md
Normal file
19
reme_studio/demo/content/en/digest/wiki/release.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Pilot release status
|
||||
description: Keep current status with original evidence and remaining work.
|
||||
memory_tags: ["aurora", "release", "memory"]
|
||||
---
|
||||
|
||||
# Release status
|
||||
|
||||
As of 2026-09-18, Aurora is approved for a five-person internal trial. Evidence: [[resources/pilot-results.md|acceptance results]] and [[daily/2026-09-18/review.md|the review]].
|
||||
|
||||
## Verified
|
||||
|
||||
Offline reading, tags across directories, source traceability, and restoring derived data from files. Each release continues to use [[digest/procedure/release-checklist.md|the checklist]].
|
||||
|
||||
## Not verified
|
||||
|
||||
Multi-device synchronization, large-scale performance, and long-term effectiveness. He collects next week's feedback, Zhou handles file and index issues, and Lin determines the next acceptance scope.
|
||||
|
||||
Updates follow [[digest/personal/communication.md|communication preferences]]: lead with the conclusion, then evidence, and distinguish passed checks from checks not performed.
|
||||
19
reme_studio/demo/content/en/digest/wiki/sync.md
Normal file
19
reme_studio/demo/content/en/digest/wiki/sync.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Synchronization scope
|
||||
description: Distinguish deferred work from the accepted pilot scope.
|
||||
memory_tags: ["aurora", "local_first", "planning"]
|
||||
---
|
||||
|
||||
# Synchronization scope
|
||||
|
||||
Aurora's first pilot promises a single-device workspace, without automatic multi-device sync. [[daily/2026-09-15/decision.md|The local-first decision]] confirms this boundary.
|
||||
|
||||
## Future questions
|
||||
|
||||
Concurrent saves across devices, duplicate records, sensitive-data sync scope, and restoring previous versions still need designs. The current pilot did not validate these problems.
|
||||
|
||||
## Current operation
|
||||
|
||||
Users can download Markdown copies, but manual export is not a synchronization mechanism. Saving in this browser demo also belongs only to this browser and is not a backup of a real workspace.
|
||||
|
||||
See [[digest/wiki/release.md|release status]] for acceptance and [[digest/wiki/local-first.md|architecture knowledge]] for data-ownership principles.
|
||||
25
reme_studio/demo/content/en/resources/kickoff.md
Normal file
25
reme_studio/demo/content/en/resources/kickoff.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
name: Kickoff meeting
|
||||
description: Roles, pilot scope, and open questions.
|
||||
memory_tags: ["aurora", "planning", "resource"]
|
||||
---
|
||||
|
||||
# Kickoff · 2026-09-14
|
||||
|
||||
Attendees: Lin Cheng (product), Zhou Ning (engineering), and He Yue (pilot). Lin owns requirements and acceptance, Zhou owns files and indexes, and He collects field feedback.
|
||||
|
||||
## Confirmed
|
||||
|
||||
The pilot uses a Markdown workspace with resources, daily, and digest directories. The team needs three views: events by time, conclusions by topic, and experience as reusable procedures.
|
||||
|
||||
## Open questions
|
||||
|
||||
He will check network availability at two sites. Zhou will verify that files remain readable when the service stops. Lin will prepare ten questions with known original sources to test traceability.
|
||||
|
||||
## Next actions
|
||||
|
||||
- Confirm deployment after field feedback on September 15.
|
||||
- Test offline reading, tag filters, and recovery on September 17.
|
||||
- Decide whether to proceed after the September 18 review.
|
||||
|
||||
References: [[resources/requirements.md|requirements]], [[digest/wiki/people.md|owners]], and [[daily/2026-09-14/kickoff.md|the daily record]].
|
||||
19
reme_studio/demo/content/en/resources/offline-feedback.md
Normal file
19
reme_studio/demo/content/en/resources/offline-feedback.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Offline field feedback
|
||||
description: Unreliable site connectivity changes the deployment assumptions.
|
||||
memory_tags: ["aurora", "local_first", "resource"]
|
||||
---
|
||||
|
||||
# Field feedback · 2026-09-15
|
||||
|
||||
He visited two pilot sites. Site A lost connectivity in a meeting room for about 20 minutes. Site B cannot send internal material to an external service. Both need to read existing records while offline.
|
||||
|
||||
## Observations and constraints
|
||||
|
||||
An online model may still perform reasoning tasks, but basic file reading, editing, and retrieval must not assume public internet access. Users need to inspect exactly which material they have saved.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Use local workspace files as durable sources and treat indexes as rebuildable data. Make cross-device synchronization a later, explicit option so it does not block this pilot.
|
||||
|
||||
This recommendation was approved in [[daily/2026-09-15/decision.md|the decision diary]] and integrated into [[digest/wiki/local-first.md|durable architecture knowledge]]. It revises [[resources/requirements.md|the draft's cloud assumption]].
|
||||
21
reme_studio/demo/content/en/resources/pilot-results.md
Normal file
21
reme_studio/demo/content/en/resources/pilot-results.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: Pilot acceptance results
|
||||
description: Record passed checks, remaining issues, and the scope of evidence.
|
||||
memory_tags: ["aurora", "release", "resource"]
|
||||
---
|
||||
|
||||
# Acceptance results · 2026-09-18
|
||||
|
||||
These are fictional Aurora pilot results, not a ReMe performance benchmark.
|
||||
|
||||
| Check | Result | Evidence |
|
||||
| ------------------------ | ---------- | ------------------------------------------------------- |
|
||||
| Offline Markdown reading | Passed | An editor opens files while the service is stopped |
|
||||
| Tags across directories | Passed | `local_first` finds feedback, decisions, and procedures |
|
||||
| Source traceability | Passed | Architecture conclusions link to field feedback |
|
||||
| Index recovery | Passed | Derived data is restored from existing files |
|
||||
| Multi-device sync | Not tested | Outside the first pilot |
|
||||
|
||||
Lin approved an internal five-person trial. He will collect feedback for a week. Cross-device conflicts and interaction hints when online models are unavailable remain follow-up work.
|
||||
|
||||
The conclusion appears in [[digest/wiki/release.md|release status]]. See [[digest/procedure/release-checklist.md|the release checklist]] for steps and [[daily/2026-09-18/review.md|the review diary]] for the process.
|
||||
22
reme_studio/demo/content/en/resources/requirements.md
Normal file
22
reme_studio/demo/content/en/resources/requirements.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: Aurora requirements draft
|
||||
description: Preserve the early cloud-first proposal for later comparison.
|
||||
memory_tags: ["aurora", "planning", "resource"]
|
||||
---
|
||||
|
||||
# Requirements draft · 2026-09-14
|
||||
|
||||
Aurora helps a five-person product team recover project context, meeting conclusions, and operating experience. The pilot supports Markdown; users own the files and can read them with an ordinary editor.
|
||||
|
||||
## Initial proposal
|
||||
|
||||
The first proposal used a central cloud database: upload meetings, generate knowledge centrally, and require an online connection for every query. This is a discussion draft, not an approved architecture. Field conditions still need validation.
|
||||
|
||||
## Acceptance goals
|
||||
|
||||
- Every significant conclusion leads back to an original record.
|
||||
- A topic can be found across resources, daily notes, and durable knowledge.
|
||||
- Users can export their memory without a proprietary database format.
|
||||
- Later decisions retain the previous plan and the reason for changing it.
|
||||
|
||||
[[resources/kickoff.md|The kickoff]] records ownership. Field feedback changed the architecture; see [[digest/wiki/local-first.md|the current local-first decision]]. This draft preserves history, not the current architecture.
|
||||
29
reme_studio/demo/content/zh/START_HERE.md
Normal file
29
reme_studio/demo/content/zh/START_HERE.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: 从一条记录到可复用的记忆
|
||||
description: 探索 Aurora 的资料、日记、长期知识、标签与关联图谱。
|
||||
memory_tags: ["aurora", "memory", "guide"]
|
||||
---
|
||||
|
||||
# 欢迎体验 ReMe Studio
|
||||
|
||||
这里是虚构的 **Aurora 团队知识助手**工作区。团队希望把散落的会议、决策和经验变成可追溯、可复用的记忆。你可以在浏览器里打开、编辑、保存和下载这些 Markdown,无需启动服务。
|
||||
|
||||
## 1. 跟随一条记忆的来源
|
||||
|
||||
打开 [[digest/wiki/local-first.md|本地优先决策]]。这篇长期知识来自 [[resources/offline-feedback.md|现场反馈]] 与 [[daily/2026-09-15/decision.md|决策日记]]。早期 [[resources/requirements.md|需求草案]] 仍保留云端方案,因此你可以看到旧计划和新结论的区别。
|
||||
|
||||
## 2. 用标签跨目录查找
|
||||
|
||||
点击顶部 **搜索 / 标签**,选择 `local_first`。同一个主题会同时找到资料、日记、知识和流程;再输入「离线」,缩小到包含这个词的文件。标签来自每篇文件 front matter 中的 `memory_tags`,不是另一份手工维护的分类。
|
||||
|
||||
## 3. 修改自己的浏览器副本
|
||||
|
||||
打开 [[digest/personal/communication.md|沟通偏好]],切换到 Edit。将标签 `communication` 改为 `writing`,保存,再查看标签数量和筛选结果。下载按钮可以导出文件;「重置示例」恢复最初的内容。
|
||||
|
||||
## 4. 探索关系与回答
|
||||
|
||||
在 Knowledge 分类旁点击 Graph,选中节点查看入链、出链,再打开源文件。创建 Chat,发送预设问题,展开 `search` 和 `read` 查看实际示例文件。
|
||||
|
||||
> 图谱和关键词筛选在浏览器里计算。对话回答是预设演示,修改文档不会自动生成新的模型回答。数据仅保存在当前浏览器;清除网站数据会清除修改。
|
||||
|
||||
安装真实 ReMe:打开顶部文档,运行 `reme start`,在本机 Studio 使用自己的工作区。
|
||||
14
reme_studio/demo/content/zh/daily/2026-09-14.md
Normal file
14
reme_studio/demo/content/zh/daily/2026-09-14.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: 9 月 14 日日记索引
|
||||
description: 按日期找到启动讨论。
|
||||
memory_tags: ["aurora", "planning", "daily"]
|
||||
---
|
||||
|
||||
# 2026-09-14
|
||||
|
||||
今天建立 Aurora 工作区并完成启动讨论。日记索引负责按时间导航,详细事实保留在独立记录中。
|
||||
|
||||
- [[daily/2026-09-14/kickoff.md|启动记录]]:确定首期目标与负责人。
|
||||
- [[resources/kickoff.md|会议原始资料]]:查看完整约定。
|
||||
|
||||
长期项目背景见 [[digest/wiki/aurora.md|Aurora 概览]]。用 `planning` 标签也能找到其他日期的计划事项。
|
||||
15
reme_studio/demo/content/zh/daily/2026-09-14/kickoff.md
Normal file
15
reme_studio/demo/content/zh/daily/2026-09-14/kickoff.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: 启动与未决假设
|
||||
description: 记录当天观察,保留尚未确定的方案。
|
||||
memory_tags: ["aurora", "planning", "daily"]
|
||||
---
|
||||
|
||||
# 启动记录 · 2026-09-14
|
||||
|
||||
团队确认:先把文件和来源关联做好,再扩展更多能力。云端集中存储仍是候选方案,尚未通过现场验证。
|
||||
|
||||
周宁创建 resources、daily、digest/wiki、digest/personal 和 digest/procedure。林澄强调,用户不能因为停止服务就失去对记忆的访问。
|
||||
|
||||
何悦将在明天调查网络与资料使用限制。今天没有把候选方案写成确定结论,避免以后把讨论误当成事实。
|
||||
|
||||
来源:[[resources/kickoff.md|启动会议]]、[[resources/requirements.md|需求草案]]。最新结论见 [[digest/wiki/local-first.md|架构决策]]。
|
||||
15
reme_studio/demo/content/zh/daily/2026-09-15/decision.md
Normal file
15
reme_studio/demo/content/zh/daily/2026-09-15/decision.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: 从云端假设转向本地优先
|
||||
description: 新证据推动更新长期记忆。
|
||||
memory_tags: ["aurora", "local_first", "decision"]
|
||||
---
|
||||
|
||||
# 决策日记 · 2026-09-15
|
||||
|
||||
收到 [[resources/offline-feedback.md|现场反馈]] 后,林澄与周宁确认本地优先:Markdown 文件是持久来源,索引与图谱均可从文件重建。
|
||||
|
||||
这更新了昨天的云端假设。我们保留 [[resources/requirements.md|原始草案]],将当前结论写入 [[digest/wiki/local-first.md|架构知识]],并标明原因与来源。
|
||||
|
||||
跨设备同步推迟到试点之后,见 [[digest/wiki/sync.md|同步范围]]。模型推理仍取决于配置的提供商;本地优先并不意味着所有模型都在本机运行。
|
||||
|
||||
下一步:准备离线读取与索引恢复检查,由周宁执行,何悦观察现场可用性。
|
||||
15
reme_studio/demo/content/zh/daily/2026-09-17/validation.md
Normal file
15
reme_studio/demo/content/zh/daily/2026-09-17/validation.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: 离线与标签验证
|
||||
description: 验证文件所有权、主题筛选与恢复流程。
|
||||
memory_tags: ["aurora", "local_first", "release"]
|
||||
---
|
||||
|
||||
# 验证记录 · 2026-09-17
|
||||
|
||||
周宁停止本地服务,确认 Markdown 仍可打开和复制。重启后,根据已有文件检查派生索引恢复。没有通过删除源文件来解决索引问题。
|
||||
|
||||
林澄用 `local_first` 标签筛选,发现现场资料、决策日记和架构知识都属于同一主题。再加关键词「离线」,能减少无关结果。
|
||||
|
||||
何悦建议:解释标签筛选和 wikilink 图谱的区别。标签用于主题集合,链接用于明确关系;两者都来自文件内容。
|
||||
|
||||
新增 [[digest/wiki/memory-tags.md|标签说明]] 与 [[digest/procedure/recovery.md|恢复流程]]。发布前还需核对 [[digest/procedure/release-checklist.md|检查清单]]。
|
||||
15
reme_studio/demo/content/zh/daily/2026-09-18.md
Normal file
15
reme_studio/demo/content/zh/daily/2026-09-18.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: 9 月 18 日日记索引
|
||||
description: 按日期导航到验收与复盘。
|
||||
memory_tags: ["aurora", "release", "daily"]
|
||||
---
|
||||
|
||||
# 2026-09-18
|
||||
|
||||
今天完成试点验收,决定进入五人内部试用。
|
||||
|
||||
- [[daily/2026-09-18/review.md|试点复盘]]:哪些经验值得长期保留。
|
||||
- [[resources/pilot-results.md|验收原始结果]]:哪些检查已通过,哪些尚未做。
|
||||
- [[digest/wiki/release.md|发布状态]]:最新结论与下一阶段安排。
|
||||
|
||||
此索引保留时间入口;主题入口可以使用 `release` 标签。
|
||||
15
reme_studio/demo/content/zh/daily/2026-09-18/review.md
Normal file
15
reme_studio/demo/content/zh/daily/2026-09-18/review.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: 把试点经验沉淀为流程
|
||||
description: 将重复检查沉淀为可复用的操作知识。
|
||||
memory_tags: ["aurora", "release", "procedure"]
|
||||
---
|
||||
|
||||
# 试点复盘 · 2026-09-18
|
||||
|
||||
[[resources/pilot-results.md|验收结果]] 支持内部试用,但不证明多设备同步或大规模检索已经就绪。结论必须保留证据范围。
|
||||
|
||||
复盘中发现,每次发布都需要重复核对文件备份、来源链接、标签和离线读取。因此将这些步骤沉淀为 [[digest/procedure/release-checklist.md|发布检查清单]]。
|
||||
|
||||
索引故障处理被单独整理到 [[digest/procedure/recovery.md|恢复流程]],核心原则是先保护源文件,再修复派生状态。
|
||||
|
||||
林澄要求后续汇报先写结论与证据,再写行动项。这也更新了 [[digest/personal/communication.md|沟通偏好]]。
|
||||
20
reme_studio/demo/content/zh/digest/personal/communication.md
Normal file
20
reme_studio/demo/content/zh/digest/personal/communication.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: 林澄的沟通偏好
|
||||
description: 明确偏好来源,支持可检查的个性化记忆。
|
||||
memory_tags: ["aurora", "personal", "communication"]
|
||||
---
|
||||
|
||||
# 沟通偏好
|
||||
|
||||
林澄希望项目汇报先给结论,再给来源与下一步。避免把计划写成已完成事实;未知项应明确标注。
|
||||
|
||||
## 可复用规则
|
||||
|
||||
- 每次汇报尽量控制在三段以内。
|
||||
- 重要结论附一个原始文件链接。
|
||||
- 行动项明确负责人和截止日期。
|
||||
- 给出已验证的范围,不扩大性能结论。
|
||||
|
||||
来源:[[daily/2026-09-18/review.md|试点复盘]]。实际应用见 [[digest/wiki/release.md|发布状态]],角色信息见 [[digest/wiki/people.md|负责人]]。
|
||||
|
||||
体验任务:将 front matter 中 `communication` 改为 `writing`,保存后在标签面板检查新标签。修改只属于当前浏览器副本。
|
||||
15
reme_studio/demo/content/zh/digest/personal/work-style.md
Normal file
15
reme_studio/demo/content/zh/digest/personal/work-style.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: 工作习惯与资料边界
|
||||
description: 保留用户可理解和可修改的长期习惯。
|
||||
memory_tags: ["aurora", "personal", "local_first"]
|
||||
---
|
||||
|
||||
# 工作习惯
|
||||
|
||||
团队习惯先把原始记录写入文件,再总结为长期知识。内部资料默认保留在受控工作区,发送外部模型前需要确认实际部署与提供商设置。
|
||||
|
||||
林澄倾向在会议后当天记录结论;周宁在发布前检查恢复能力;何悦用现场反馈修正未验证的假设。
|
||||
|
||||
这些习惯来自 [[resources/kickoff.md|启动约定]] 和 [[resources/offline-feedback.md|现场限制]]。它们可以帮助 Agent 理解上下文,但用户应能检查、修改或删除过时的偏好。
|
||||
|
||||
相关知识:[[digest/wiki/local-first.md|本地优先]]、[[digest/personal/communication.md|沟通方式]]。
|
||||
19
reme_studio/demo/content/zh/digest/procedure/recovery.md
Normal file
19
reme_studio/demo/content/zh/digest/procedure/recovery.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: 保护源文件并恢复派生状态
|
||||
description: 先保护用户记忆,再修复索引与关联状态。
|
||||
memory_tags: ["aurora", "local_first", "procedure"]
|
||||
---
|
||||
|
||||
# 恢复流程
|
||||
|
||||
出现搜索结果缺失时,先判断源文件是否存在,再检查派生索引;不要为了修复索引删除或重写用户记忆。
|
||||
|
||||
1. 保留工作区文件副本,核对路径与文件内容。
|
||||
2. 确认本地 ReMe 服务使用正确的工作区。
|
||||
3. 根据具体问题检查摄取、搜索索引与 wikilink 图谱。
|
||||
4. `reindex` 从已摄取 chunk 重建搜索和标签索引;它不扫描工作区或重建图谱。新增或改动文件还需正常摄取流程。
|
||||
5. 用带来源的查询验证恢复结果。
|
||||
|
||||
此流程来自 [[daily/2026-09-17/validation.md|验证记录]],遵循 [[digest/wiki/local-first.md|文件作为事实来源]] 的原则。
|
||||
|
||||
浏览器演示没有后端索引。其图谱与标签直接从当前示例文件计算;「重置示例」只恢复演示副本,不是生产恢复工具。
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: 可复用的发布检查清单
|
||||
description: 将验收经验写成有证据的操作步骤。
|
||||
memory_tags: ["aurora", "release", "procedure"]
|
||||
---
|
||||
|
||||
# 发布检查清单
|
||||
|
||||
适用于 Aurora 内部试点,由 [[digest/wiki/people.md|周宁]] 执行,林澄确认范围。来源:[[daily/2026-09-18/review.md|复盘]]。
|
||||
|
||||
- [ ] 保留工作区文件副本,确认 Markdown 可直接读取。
|
||||
- [ ] 检查 [[digest/wiki/local-first.md|架构结论]] 的来源链接。
|
||||
- [ ] 用 `local_first` 和 `release` 标签检查跨目录检索。
|
||||
- [ ] 停止服务后验证已有文件仍可离线打开。
|
||||
- [ ] 根据 [[digest/procedure/recovery.md|恢复流程]] 检查派生数据可恢复。
|
||||
- [ ] 将通过项、失败项和未做项写入验收记录。
|
||||
- [ ] 更新 [[digest/wiki/release.md|发布状态]],附原始结果。
|
||||
|
||||
清单描述可复用经验,不表示勾选动作已触发后台任务。最新原始结果见 [[resources/pilot-results.md|验收记录]]。
|
||||
21
reme_studio/demo/content/zh/digest/wiki/aurora.md
Normal file
21
reme_studio/demo/content/zh/digest/wiki/aurora.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: Aurora 项目概览
|
||||
description: 汇总当前目标、决策、负责人和发布进展。
|
||||
memory_tags: ["aurora", "planning", "memory"]
|
||||
---
|
||||
|
||||
# Aurora
|
||||
|
||||
Aurora 是五人团队的知识助手试点。目标是让成员找回项目背景、保留决策原因,并复用成功的操作经验。
|
||||
|
||||
## 当前约定
|
||||
|
||||
- 持久数据采用用户拥有的 Markdown 文件,见 [[digest/wiki/local-first.md|本地优先决策]]。
|
||||
- 首期不包含多设备同步,见 [[digest/wiki/sync.md|范围边界]]。
|
||||
- 用 [[digest/wiki/memory-tags.md|memory tags]] 聚合主题,用 wikilink 明确文件关系。
|
||||
|
||||
## 负责人和状态
|
||||
|
||||
[[digest/wiki/people.md|林澄、周宁与何悦]] 分别负责产品、工程和现场试点。9 月 18 日进入内部试用,验收范围见 [[digest/wiki/release.md|发布状态]]。
|
||||
|
||||
这篇概览汇总稳定知识;具体事件留在 daily,原始记录留在 resources。来源:[[resources/kickoff.md|启动会议]]。
|
||||
19
reme_studio/demo/content/zh/digest/wiki/local-first.md
Normal file
19
reme_studio/demo/content/zh/digest/wiki/local-first.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: 本地优先架构决策
|
||||
description: 记录当前结论、变更原因与可追溯来源。
|
||||
memory_tags: ["aurora", "local_first", "decision"]
|
||||
---
|
||||
|
||||
# 本地优先
|
||||
|
||||
**当前结论:工作区 Markdown 是持久来源;索引、目录和图谱是可重建的派生数据。**
|
||||
|
||||
## 为什么改变方案
|
||||
|
||||
[[resources/requirements.md|9 月 14 日草案]] 提出云端集中存储。[[resources/offline-feedback.md|9 月 15 日现场反馈]] 发现弱网与资料外发限制,[[daily/2026-09-15/decision.md|当天决策]] 因此确认本地优先。
|
||||
|
||||
## 对用户意味着什么
|
||||
|
||||
停止服务不影响直接读取文件。索引损坏时应从源文件恢复,流程见 [[digest/procedure/recovery.md|恢复指南]]。联网模型不可用时,模型回答可能不可用;这不改变文件所有权。
|
||||
|
||||
同步单独规划,见 [[digest/wiki/sync.md|同步范围]]。此结论来源于明确证据,不应把早期草案误当成当前方案。
|
||||
26
reme_studio/demo/content/zh/digest/wiki/memory-tags.md
Normal file
26
reme_studio/demo/content/zh/digest/wiki/memory-tags.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: 用 memory tags 找到同一主题
|
||||
description: 演示真实标签字段、规范化、文件计数和组合筛选。
|
||||
memory_tags: ["aurora", "memory", "guide"]
|
||||
---
|
||||
|
||||
# Memory tags
|
||||
|
||||
标签保存在 Markdown front matter 的 `memory_tags` 列表中。ReMe 的标签索引来自文件,可以重新构建。
|
||||
|
||||
```yaml
|
||||
memory_tags: [aurora, local_first, decision]
|
||||
```
|
||||
|
||||
## 标签和目录、链接的区别
|
||||
|
||||
目录表达文件用途;`local_first` 标签跨 resources、daily 和 digest 聚合主题;[[digest/wiki/local-first.md|wikilink]] 表达具体来源或关系。
|
||||
|
||||
## 试一试
|
||||
|
||||
1. 在顶部搜索面板选择 `local_first`,观察各目录的命中与标签文件数。
|
||||
2. 同时选择 `release`:多标签为「任意一个匹配」,与真实 search Job 的 tags 筛选一致。
|
||||
3. 输入「离线」:关键词与标签共同约束结果。
|
||||
4. 编辑 [[digest/personal/communication.md|沟通偏好]] 的标签并保存,计数立即变化。
|
||||
|
||||
演示支持行内列表和缩进列表。空格转为下划线、大小写统一、重复项合并,每篇默认最多三个有效标签。这里运行关键词筛选;真实 ReMe 的混合向量与 BM25 检索需要服务。
|
||||
17
reme_studio/demo/content/zh/digest/wiki/people.md
Normal file
17
reme_studio/demo/content/zh/digest/wiki/people.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: 项目角色与负责人
|
||||
description: 让任务和决策有明确的负责人。
|
||||
memory_tags: ["aurora", "planning", "memory"]
|
||||
---
|
||||
|
||||
# 负责人
|
||||
|
||||
| 成员 | 角色 | 责任 |
|
||||
| ---- | ---- | -------------------- |
|
||||
| 林澄 | 产品 | 需求、范围与验收 |
|
||||
| 周宁 | 工程 | 文件操作、索引与恢复 |
|
||||
| 何悦 | 试点 | 现场验证、反馈收集 |
|
||||
|
||||
来源是 [[resources/kickoff.md|启动会议]]。当前项目背景见 [[digest/wiki/aurora.md|概览]],待执行检查见 [[digest/procedure/release-checklist.md|发布流程]]。
|
||||
|
||||
林澄偏好简短且附证据的进展汇报,见 [[digest/personal/communication.md|沟通偏好]]。角色知识适合长期保留;每天的具体行动留在日记中。
|
||||
19
reme_studio/demo/content/zh/digest/wiki/release.md
Normal file
19
reme_studio/demo/content/zh/digest/wiki/release.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: 试点发布状态
|
||||
description: 维护最新状态,并保留结果来源与未完成项。
|
||||
memory_tags: ["aurora", "release", "memory"]
|
||||
---
|
||||
|
||||
# 发布状态
|
||||
|
||||
截至 2026-09-18,Aurora 获准进入五人内部试用。依据为 [[resources/pilot-results.md|验收结果]] 和 [[daily/2026-09-18/review.md|复盘记录]]。
|
||||
|
||||
## 已验证
|
||||
|
||||
离线读取、标签跨目录筛选、结论来源追溯,以及从文件恢复派生数据。每次发布继续执行 [[digest/procedure/release-checklist.md|检查清单]]。
|
||||
|
||||
## 未验证
|
||||
|
||||
多设备同步、大规模性能和长期使用效果。何悦将在下一周收集反馈;周宁处理文件和索引问题;林澄决定下一阶段验收范围。
|
||||
|
||||
汇报格式遵循 [[digest/personal/communication.md|沟通偏好]]:先结论、后证据,明确区分通过项和未做项。
|
||||
19
reme_studio/demo/content/zh/digest/wiki/sync.md
Normal file
19
reme_studio/demo/content/zh/digest/wiki/sync.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: 同步范围与边界
|
||||
description: 把延期事项与当前支持的能力区分清楚。
|
||||
memory_tags: ["aurora", "local_first", "planning"]
|
||||
---
|
||||
|
||||
# 同步范围
|
||||
|
||||
首期 Aurora 只承诺单机工作区,不包含自动多设备同步。此边界由 [[daily/2026-09-15/decision.md|本地优先决策]] 确认。
|
||||
|
||||
## 后续需要解决
|
||||
|
||||
跨设备保存冲突、重复记录合并、敏感资料的同步范围,以及用户如何恢复旧版本。当前试点结果未验证这些问题。
|
||||
|
||||
## 现阶段操作
|
||||
|
||||
用户可以下载 Markdown 副本,但手工导出不等于同步机制。浏览器演示中的保存仅属于当前浏览器,也不能当作真实工作区备份。
|
||||
|
||||
发布条件见 [[digest/wiki/release.md|发布状态]]。数据所有权原则见 [[digest/wiki/local-first.md|架构知识]]。
|
||||
25
reme_studio/demo/content/zh/resources/kickoff.md
Normal file
25
reme_studio/demo/content/zh/resources/kickoff.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
name: 启动会议记录
|
||||
description: 角色分工、试点范围与待确认问题。
|
||||
memory_tags: ["aurora", "planning", "resource"]
|
||||
---
|
||||
|
||||
# 启动会议 · 2026-09-14
|
||||
|
||||
参与者:林澄(产品)、周宁(工程)、何悦(试点)。林澄负责需求与验收,周宁负责文件与索引,何悦收集真实使用反馈。
|
||||
|
||||
## 已确认
|
||||
|
||||
首期使用 Markdown 工作区,包含 resources、daily 和 digest。团队需要三个不同视角:按时间找事件、按主题找结论、按流程复用经验。
|
||||
|
||||
## 待确认
|
||||
|
||||
何悦将在两处现场检查网络条件。周宁验证文件能否在服务停止时继续读取;林澄准备十个带原始出处的问题,检验回答可追溯性。
|
||||
|
||||
## 下一步
|
||||
|
||||
- 9 月 15 日回收现场反馈后确认部署方式。
|
||||
- 9 月 17 日验证离线读取、标签筛选和文件恢复。
|
||||
- 9 月 18 日根据结果决定是否进入下一阶段。
|
||||
|
||||
相关资料:[[resources/requirements.md|需求草案]]、[[digest/wiki/people.md|负责人]]、[[daily/2026-09-14/kickoff.md|当天记录]]。
|
||||
19
reme_studio/demo/content/zh/resources/offline-feedback.md
Normal file
19
reme_studio/demo/content/zh/resources/offline-feedback.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: 现场离线反馈
|
||||
description: 弱网现场改变了部署假设。
|
||||
memory_tags: ["aurora", "local_first", "resource"]
|
||||
---
|
||||
|
||||
# 现场反馈 · 2026-09-15
|
||||
|
||||
何悦访问了两处试点现场。A 现场会议室网络中断约 20 分钟;B 现场的内部资料不允许发送到外部服务。两处都需要在断网时读取已有记录。
|
||||
|
||||
## 观察与约束
|
||||
|
||||
联网模型仍可用于需要推理的任务,但基本文件阅读、编辑和已有记忆的检索不能把公网连接当作前提。用户应能直接检查自己保存了哪些资料。
|
||||
|
||||
## 建议
|
||||
|
||||
先以本地工作区文件作为持久来源,索引作为可重建数据。跨设备同步作为后续显式开启的能力,不阻塞本次试点。
|
||||
|
||||
此建议在 [[daily/2026-09-15/decision.md|决策日记]] 中获确认,并整合到 [[digest/wiki/local-first.md|长期架构知识]]。它修正了 [[resources/requirements.md|草案中的云端假设]]。
|
||||
21
reme_studio/demo/content/zh/resources/pilot-results.md
Normal file
21
reme_studio/demo/content/zh/resources/pilot-results.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: 试点验收结果
|
||||
description: 记录成功项、剩余问题和证据范围。
|
||||
memory_tags: ["aurora", "release", "resource"]
|
||||
---
|
||||
|
||||
# 验收结果 · 2026-09-18
|
||||
|
||||
以下是虚构 Aurora 试点的示例结果,不是 ReMe 的性能评测。
|
||||
|
||||
| 检查 | 结果 | 证据 |
|
||||
| ----------------- | ---- | ---------------------------------- |
|
||||
| 离线读取 Markdown | 通过 | 停止服务后仍可用文本编辑器打开 |
|
||||
| 标签跨目录筛选 | 通过 | `local_first` 找到反馈、决策与流程 |
|
||||
| 关联来源追溯 | 通过 | 架构结论链接回现场反馈 |
|
||||
| 索引恢复 | 通过 | 从已有文件恢复派生数据 |
|
||||
| 多设备同步 | 未做 | 不在首期范围 |
|
||||
|
||||
林澄批准五人内部试用;何悦负责收集一周反馈。跨设备冲突和联网模型不可用时的交互提示仍是后续事项。
|
||||
|
||||
结论已写入 [[digest/wiki/release.md|发布状态]]。操作步骤见 [[digest/procedure/release-checklist.md|发布检查清单]],过程见 [[daily/2026-09-18/review.md|复盘日记]]。
|
||||
22
reme_studio/demo/content/zh/resources/requirements.md
Normal file
22
reme_studio/demo/content/zh/resources/requirements.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: Aurora 需求草案
|
||||
description: 保留早期云端方案,供后续决策追溯。
|
||||
memory_tags: ["aurora", "planning", "resource"]
|
||||
---
|
||||
|
||||
# 需求草案 · 2026-09-14
|
||||
|
||||
Aurora 面向五人产品团队,帮助成员找回项目背景、会议结论与操作经验。首期只支持 Markdown,文件由用户拥有,并应能使用普通编辑器读取。
|
||||
|
||||
## 初始方案
|
||||
|
||||
最初提出集中式云端数据库:所有会议资料上传后统一生成知识,客户端必须联网查询。这是讨论草案,尚未确认;现场使用条件仍需验证。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 每个重要结论都能回到原始记录。
|
||||
- 同一主题跨资料、日记和长期知识可以被发现。
|
||||
- 用户能导出完整记忆,不被专用数据库格式锁定。
|
||||
- 后续决定必须保留原计划与变更理由。
|
||||
|
||||
[[resources/kickoff.md|启动会议]] 明确了负责人。现场反馈导致方案调整,当前决策见 [[digest/wiki/local-first.md|本地优先]]。这份草案保留历史,不代表最新架构。
|
||||
176
reme_studio/demo/demo.module.css
Normal file
176
reme_studio/demo/demo.module.css
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
.bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px 16px;
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--subtle);
|
||||
flex: none;
|
||||
}
|
||||
.description {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 12px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.description strong {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
.description span {
|
||||
color: var(--muted);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
.bar button,
|
||||
.dialog header button,
|
||||
.tagHeading button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dialog {
|
||||
width: min(800px, calc(100vw - 32px));
|
||||
max-height: 85vh;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 20px 80px var(--shadow-strong);
|
||||
}
|
||||
.dialog::backdrop {
|
||||
background: var(--overlay);
|
||||
}
|
||||
.dialog header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.dialog h2 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
}
|
||||
.dialog header p {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.search input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
.search:focus-within {
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
.tagHeading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin: 18px 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tagHeading span {
|
||||
color: var(--muted);
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
padding: 5px 10px;
|
||||
background: var(--subtle);
|
||||
color: var(--ink);
|
||||
font: 11px var(--mono);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag[aria-pressed="true"],
|
||||
.tag:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--soft);
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
.tag span {
|
||||
color: var(--muted);
|
||||
}
|
||||
.count {
|
||||
margin: 16px 0 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.results {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.result {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--hover);
|
||||
}
|
||||
.result code {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.result p {
|
||||
margin: 3px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.result > span {
|
||||
font: 11px var(--mono);
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.bar {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.dialog {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
62
reme_studio/demo/scenarios.ts
Normal file
62
reme_studio/demo/scenarios.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
export const scenarios = {
|
||||
zh: [
|
||||
{
|
||||
question: "Aurora 为什么从云端方案改为本地优先?",
|
||||
tag: "local_first",
|
||||
paths: [
|
||||
"digest/wiki/local-first.md",
|
||||
"resources/offline-feedback.md",
|
||||
"daily/2026-09-15/decision.md",
|
||||
],
|
||||
answer:
|
||||
"**以下是基于初始示例文件的预设回答,未运行模型。**\n\nAurora 最初考虑云端集中存储。现场反馈发现断网和内部资料外发限制,因此 9 月 15 日确认本地优先:Markdown 是持久来源,索引和图谱可重建。\n\n来源:[[resources/offline-feedback.md|现场反馈]]、[[daily/2026-09-15/decision.md|决策记录]]、[[digest/wiki/local-first.md|长期架构知识]]。\n\n模型推理仍取决于配置的提供商,跨设备同步不在首期范围。展开上面的工具块,可以检查当前浏览器中的文件内容。",
|
||||
},
|
||||
{
|
||||
question: "用 memory tags 查找发布相关的记忆。",
|
||||
tag: "release",
|
||||
paths: [
|
||||
"digest/wiki/release.md",
|
||||
"digest/procedure/release-checklist.md",
|
||||
],
|
||||
answer:
|
||||
"**这是预设对话演示。**\n\n`release` 标签把验收资料、日记、发布状态和操作清单聚在一起。标签来自文件的 `memory_tags`,因此不受目录边界限制。\n\n[[digest/wiki/release.md|发布状态]] 记录当前结论;[[digest/procedure/release-checklist.md|检查清单]] 保存可复用经验;[[resources/pilot-results.md|原始结果]] 提供证据。\n\n试着打开「搜索 / 标签」,选择 `release`,再编辑某篇文件的标签并保存。计数与匹配结果会更新。",
|
||||
},
|
||||
{
|
||||
question: "林澄喜欢怎样的项目汇报?",
|
||||
tag: "personal",
|
||||
paths: ["digest/personal/communication.md", "daily/2026-09-18/review.md"],
|
||||
answer:
|
||||
"**这是基于初始示例的预设回答。**\n\n林澄希望先结论、后证据,尽量三段以内;行动项带负责人和截止日期,未验证项明确标注。\n\n这条偏好来自 [[daily/2026-09-18/review.md|试点复盘]],保存于 [[digest/personal/communication.md|个人记忆]]。用户可以直接检查和修改它。\n\n演示中的工具结果读取当前文件,预设回答不会随编辑自动重新生成。",
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
question: "Why did Aurora switch from cloud-first to local-first?",
|
||||
tag: "local_first",
|
||||
paths: [
|
||||
"digest/wiki/local-first.md",
|
||||
"resources/offline-feedback.md",
|
||||
"daily/2026-09-15/decision.md",
|
||||
],
|
||||
answer:
|
||||
"**Scripted answer based on the initial example files. No model is running.**\n\nAurora initially considered central cloud storage. Field feedback revealed connectivity failures and restrictions on external sharing. The September 15 decision made Markdown the durable source and indexes and graphs rebuildable.\n\nSources: [[resources/offline-feedback.md|field feedback]], [[daily/2026-09-15/decision.md|the decision]], and [[digest/wiki/local-first.md|durable architecture knowledge]].\n\nModel reasoning still depends on the configured provider; multi-device sync is outside the pilot. Expand the tool blocks to inspect the current browser files.",
|
||||
},
|
||||
{
|
||||
question: "Find release memories using memory tags.",
|
||||
tag: "release",
|
||||
paths: [
|
||||
"digest/wiki/release.md",
|
||||
"digest/procedure/release-checklist.md",
|
||||
],
|
||||
answer:
|
||||
"**This is a scripted conversation.**\n\nThe `release` tag connects acceptance resources, daily notes, release status, and the operating checklist. It comes from each file's `memory_tags`, so it crosses directory boundaries.\n\n[[digest/wiki/release.md|Release status]] captures the conclusion, [[digest/procedure/release-checklist.md|the checklist]] preserves reusable experience, and [[resources/pilot-results.md|original results]] supply evidence.\n\nOpen Search / Tags, select `release`, then edit a file's tags and save. Counts and matches update.",
|
||||
},
|
||||
{
|
||||
question: "How does Lin prefer project updates?",
|
||||
tag: "personal",
|
||||
paths: ["digest/personal/communication.md", "daily/2026-09-18/review.md"],
|
||||
answer:
|
||||
"**Scripted answer based on the initial examples.**\n\nLin prefers conclusions before evidence, usually within three paragraphs. Actions need owners and deadlines; unverified claims must be marked.\n\nThe preference comes from [[daily/2026-09-18/review.md|the pilot review]] and lives in [[digest/personal/communication.md|personal memory]], where users can inspect and edit it.\n\nTool results read the current files. This scripted answer does not regenerate when you edit them.",
|
||||
},
|
||||
],
|
||||
};
|
||||
213
reme_studio/demo/workspace.ts
Normal file
213
reme_studio/demo/workspace.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { parseDocument } from "yaml";
|
||||
import type { FileStat, GraphSnapshot } from "../app/types.ts";
|
||||
import { parseMarkdownFrontmatter } from "../app/files-workspace/markdown.ts";
|
||||
|
||||
export const DEMO_STORAGE_KEY = "reme-demo-files-v1";
|
||||
export const WELCOME_PATH = "START_HERE.md";
|
||||
const INITIAL_MTIME = "2026-09-18T09:00:00.000Z";
|
||||
|
||||
type SavedFile = { content: string; mtime: string };
|
||||
type StoragePort = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
||||
|
||||
/** Tags are derived from YAML source files, with ReMe's default list/count limits. */
|
||||
export function memoryTags(content: string): string[] {
|
||||
const block = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
|
||||
if (!block) return [];
|
||||
let value: unknown;
|
||||
try {
|
||||
const document = parseDocument(block);
|
||||
if (document.errors.length) return [];
|
||||
value = document.toJS({ maxAliasCount: 50 })?.memory_tags;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(value)) return [];
|
||||
const tags = value.flatMap((item: unknown) => {
|
||||
if (
|
||||
typeof item !== "string" &&
|
||||
!(typeof item === "number" && Number.isInteger(item))
|
||||
)
|
||||
return [];
|
||||
const raw = String(item).trim().replace(/\s+/g, "_");
|
||||
if (!raw || [...raw].length > 64 || !/[\p{L}\p{N}]/u.test(raw)) return [];
|
||||
return [raw.toLowerCase()];
|
||||
});
|
||||
return [...new Set(tags)].slice(0, 3);
|
||||
}
|
||||
|
||||
export function wikilinks(content: string) {
|
||||
return [
|
||||
...content.matchAll(
|
||||
/!?\[\[([^[\]|#\n]+?)(?:#([^[\]|\n]+))?(?:\|[^[\]\n]+)?\]\]/g,
|
||||
),
|
||||
].map((match) => ({
|
||||
target: match[1].trim(),
|
||||
anchor: match[2]?.trim() || null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function createDemoWorkspace(
|
||||
seeds: Record<string, string>,
|
||||
storage?: StoragePort,
|
||||
key = DEMO_STORAGE_KEY,
|
||||
) {
|
||||
function savedFiles(): Record<string, SavedFile> {
|
||||
try {
|
||||
const saved: unknown = JSON.parse(storage?.getItem(key) || "{}");
|
||||
if (!saved || typeof saved !== "object" || Array.isArray(saved))
|
||||
return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(saved).filter(
|
||||
([path, file]) =>
|
||||
Object.hasOwn(seeds, path) &&
|
||||
file &&
|
||||
typeof file.content === "string" &&
|
||||
typeof file.mtime === "string" &&
|
||||
Number.isFinite(Date.parse(file.mtime)),
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
let changes = savedFiles();
|
||||
const files = () => {
|
||||
if (storage) changes = savedFiles();
|
||||
return Object.fromEntries(
|
||||
Object.entries(seeds).map(([path, content]) => [
|
||||
path,
|
||||
changes[path]?.content ?? content,
|
||||
]),
|
||||
);
|
||||
};
|
||||
const stat = (path: string, content: string): FileStat => ({
|
||||
path,
|
||||
exists: true,
|
||||
type: "file",
|
||||
mtime: changes[path]?.mtime || INITIAL_MTIME,
|
||||
size: new TextEncoder().encode(content).length,
|
||||
});
|
||||
const read = (path: string) => {
|
||||
if (!Object.hasOwn(seeds, path)) throw new Error(`File not found: ${path}`);
|
||||
const content = files()[path];
|
||||
return { content, stat: stat(path, content) };
|
||||
};
|
||||
const search = (query = "", tags: string[] = []) => {
|
||||
const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
||||
return Object.entries(files()).flatMap(([path, content]) => {
|
||||
const parsed = parseMarkdownFrontmatter(content);
|
||||
const fileTags = memoryTags(content);
|
||||
const title =
|
||||
parsed.entries.find((entry) => entry.key === "name")?.value || path;
|
||||
const haystack = `${path}\n${content}\n${fileTags.join(
|
||||
" ",
|
||||
)}`.toLowerCase();
|
||||
if (
|
||||
(tags.length && !tags.some((tag) => fileTags.includes(tag))) ||
|
||||
!terms.every((term) => haystack.includes(term))
|
||||
)
|
||||
return [];
|
||||
const lines = parsed.body
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.trim() && !line.startsWith("#"));
|
||||
const snippet =
|
||||
lines.find(
|
||||
(line) =>
|
||||
terms.length &&
|
||||
terms.some((term) => line.toLowerCase().includes(term)),
|
||||
) ||
|
||||
lines[0] ||
|
||||
"";
|
||||
return [{ path, title, tags: fileTags, snippet: snippet.slice(0, 180) }];
|
||||
});
|
||||
};
|
||||
return {
|
||||
files,
|
||||
read,
|
||||
search,
|
||||
save(path: string, content: string, expectedMtime?: string) {
|
||||
const previous = read(path).stat.mtime!;
|
||||
if (expectedMtime && expectedMtime !== previous)
|
||||
throw new Error(
|
||||
"This file changed in another tab. Reopen it before saving. / 文件已在其他标签页修改,请重新打开后保存。",
|
||||
);
|
||||
if (new TextEncoder().encode(content).length > 512_000)
|
||||
throw new Error("Demo files must be smaller than 512 KB.");
|
||||
const mtime = new Date(
|
||||
Math.max(Date.now(), Date.parse(previous) + 1),
|
||||
).toISOString();
|
||||
const next = { ...changes, [path]: { content, mtime } };
|
||||
storage?.setItem(key, JSON.stringify(next));
|
||||
changes = next;
|
||||
return stat(path, content);
|
||||
},
|
||||
reset() {
|
||||
storage?.removeItem(key);
|
||||
changes = {};
|
||||
},
|
||||
tags() {
|
||||
const counts = new Map<string, number>();
|
||||
search().forEach((file) =>
|
||||
file.tags.forEach((tag) => counts.set(tag, (counts.get(tag) || 0) + 1)),
|
||||
);
|
||||
return [...counts]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([tag, count]) => ({ tag, count }));
|
||||
},
|
||||
graph(): GraphSnapshot {
|
||||
const entries = Object.entries(files());
|
||||
const nodes: GraphSnapshot["nodes"] = entries.map(([path, content]) => {
|
||||
const { entries: metadata } = parseMarkdownFrontmatter(content);
|
||||
return {
|
||||
id: path,
|
||||
path,
|
||||
name:
|
||||
metadata.find((entry) => entry.key === "name")?.value ||
|
||||
path.split("/").pop()!,
|
||||
description:
|
||||
metadata.find((entry) => entry.key === "description")?.value || "",
|
||||
indexed: true,
|
||||
virtual: false,
|
||||
};
|
||||
});
|
||||
const edges: GraphSnapshot["edges"] = [];
|
||||
const known = new Set(nodes.map((node) => node.id));
|
||||
const seen = new Set<string>();
|
||||
for (const [source, content] of entries)
|
||||
for (const { target, anchor } of wikilinks(content)) {
|
||||
const key = `${source}\0${target}\0${anchor}`;
|
||||
if (!target || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
if (!known.has(target)) {
|
||||
known.add(target);
|
||||
nodes.push({
|
||||
id: target,
|
||||
path: target,
|
||||
name: target,
|
||||
description: "",
|
||||
indexed: false,
|
||||
virtual: false,
|
||||
});
|
||||
}
|
||||
edges.push({ source, target, target_anchor: anchor });
|
||||
}
|
||||
for (const root of ["wiki", "personal", "procedure"]) {
|
||||
const id = `virtual:${root}`;
|
||||
nodes.push({
|
||||
id,
|
||||
path: "",
|
||||
name: root,
|
||||
description: "",
|
||||
indexed: false,
|
||||
virtual: true,
|
||||
});
|
||||
entries
|
||||
.filter(([path]) => path.startsWith(`digest/${root}/`))
|
||||
.forEach(([path]) =>
|
||||
edges.push({ source: id, target: path, target_anchor: null }),
|
||||
);
|
||||
}
|
||||
return { version: 1, nodes, edges };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
name="description"
|
||||
content="Browse, edit, and discuss your local-first ReMe memory workspace."
|
||||
/>
|
||||
<link rel="icon" href="/reme-icon.svg" />
|
||||
<link rel="icon" href="./reme-icon.svg" />
|
||||
<title>ReMe Workspace</title>
|
||||
<script>
|
||||
(function () {
|
||||
|
|
|
|||
17
reme_studio/package-lock.json
generated
17
reme_studio/package-lock.json
generated
|
|
@ -42,6 +42,7 @@
|
|||
"vinext": "1.0.0-beta.2",
|
||||
"vite": "8.2.1",
|
||||
"wrangler": "4.121.0",
|
||||
"yaml": "2.9.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -11906,6 +11907,22 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -28,19 +28,21 @@
|
|||
"build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build",
|
||||
"build:static": "vite build --config vite.static.config.ts",
|
||||
"start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start",
|
||||
"test": "npm run build && npm run build:static && node --test tests/*.test.mjs",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern dist-static --ignore-pattern .next",
|
||||
"test": "npm run build && npm run build:static && npm run build:demo && node --test tests/*.test.mjs",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern dist-static --ignore-pattern dist-demo --ignore-pattern .next",
|
||||
"format": "tsc --noEmit && prettier --write .",
|
||||
"format:check": "tsc --noEmit && prettier --check .",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"prepack": "npm run build:static"
|
||||
"prepack": "npm run build:static",
|
||||
"build:demo": "vite build --config vite.static.config.ts --mode demo",
|
||||
"dev:demo": "vite --config vite.static.config.ts --mode demo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@agentscope-ai/icons": "^1.0.67",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@cloudflare/vite-plugin": "1.51.3",
|
||||
"@cloudflare/workers-types": "^5.20260804.1",
|
||||
"@eslint/js": "9.39.4",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@next/eslint-plugin-next": "16.2.6",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@types/node": "22.19.19",
|
||||
|
|
@ -48,8 +50,8 @@
|
|||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "6.0.2",
|
||||
"@vitejs/plugin-rsc": "0.5.26",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-kit": "0.31.10",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
|
|
@ -69,6 +71,7 @@
|
|||
"vinext": "1.0.0-beta.2",
|
||||
"vite": "8.2.1",
|
||||
"wrangler": "4.121.0",
|
||||
"yaml": "2.9.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
|
|||
26
reme_studio/tests/demo-build.test.mjs
Normal file
26
reme_studio/tests/demo-build.test.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { access, readFile, readdir } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
test("the browser demo builds for a subdirectory with no service API or live settings", async () => {
|
||||
const directory = new URL("../dist-demo/", import.meta.url);
|
||||
const html = await readFile(new URL("index.html", directory), "utf8");
|
||||
assert.match(html, /<title>Try ReMe Studio<\/title>/);
|
||||
assert.match(html, /src="\.\/assets\//);
|
||||
for (const match of html.matchAll(/(?:src|href)="(\.\/[^"?#]+)"/g))
|
||||
await access(new URL(match[1], directory));
|
||||
const assets = await readdir(new URL("assets/", directory));
|
||||
assert.ok(
|
||||
!assets.some(
|
||||
(name) => name.startsWith("settings-center") || name.endsWith(".map"),
|
||||
),
|
||||
);
|
||||
const scripts = (
|
||||
await Promise.all(
|
||||
assets
|
||||
.filter((name) => name.endsWith(".js"))
|
||||
.map((name) => readFile(new URL(`assets/${name}`, directory), "utf8")),
|
||||
)
|
||||
).join("\n");
|
||||
assert.doesNotMatch(scripts, /127\.0\.0\.1:2333|expected_mtime|health_check/);
|
||||
});
|
||||
217
reme_studio/tests/demo-workspace.test.mjs
Normal file
217
reme_studio/tests/demo-workspace.test.mjs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
createDemoWorkspace,
|
||||
memoryTags,
|
||||
wikilinks,
|
||||
DEMO_STORAGE_KEY,
|
||||
} from "../demo/workspace.ts";
|
||||
import { scenarios } from "../demo/scenarios.ts";
|
||||
|
||||
function storage() {
|
||||
const values = new Map();
|
||||
return {
|
||||
getItem: (key) => values.get(key) ?? null,
|
||||
setItem: (key, value) => values.set(key, value),
|
||||
removeItem: (key) => values.delete(key),
|
||||
};
|
||||
}
|
||||
const note = (tags, body = "Offline release") =>
|
||||
`---\nname: Example\nmemory_tags: ${tags}\n---\n${body}`;
|
||||
const seeds = {
|
||||
"digest/wiki/a.md": note(
|
||||
'["aurora", "local_first"]',
|
||||
"Offline plan [[digest/wiki/b.md#Scope|Release]]",
|
||||
),
|
||||
"digest/wiki/b.md": note('["release"]'),
|
||||
};
|
||||
|
||||
function content(language) {
|
||||
const directory = new URL(`../demo/content/${language}/`, import.meta.url);
|
||||
return Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(directory, { recursive: true })
|
||||
.filter((file) => file.endsWith(".md"))
|
||||
.map((file) => [
|
||||
file.split(path.sep).join("/"),
|
||||
fs.readFileSync(new URL(file, directory), "utf8"),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
test("memory_tags respects strict lists, normalization, deduplication, and the default three-tag limit", () => {
|
||||
assert.deepEqual(
|
||||
memoryTags(
|
||||
note(
|
||||
'[true, 1.5, {}, [], " Local First ", "LOCAL FIRST", "中文", 42, "extra"]',
|
||||
),
|
||||
),
|
||||
["local_first", "中文", "42"],
|
||||
);
|
||||
assert.deepEqual(memoryTags(note("[aurora, 'Release', personal]")), [
|
||||
"aurora",
|
||||
"release",
|
||||
"personal",
|
||||
]);
|
||||
assert.deepEqual(memoryTags(note('["tag,with,commas", "release"]')), [
|
||||
"tag,with,commas",
|
||||
"release",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
memoryTags(
|
||||
note(`\n - Local First\n - RELEASE\n - local first\n - personal`),
|
||||
),
|
||||
["local_first", "release", "personal"],
|
||||
);
|
||||
assert.deepEqual(memoryTags(note("[aurora, release] # shared topic")), [
|
||||
"aurora",
|
||||
"release",
|
||||
]);
|
||||
assert.deepEqual(memoryTags(note("[broken")), []);
|
||||
assert.deepEqual(memoryTags(note("aurora")), []);
|
||||
assert.deepEqual(memoryTags(note(`["${"a".repeat(65)}", "---", false]`)), []);
|
||||
assert.deepEqual(memoryTags("# Body\nmemory_tags: [aurora]"), []);
|
||||
});
|
||||
|
||||
test("saving updates tags, counts, search and graph from files and survives a reload", () => {
|
||||
const saved = storage();
|
||||
const workspace = createDemoWorkspace(seeds, saved);
|
||||
const initial = workspace.read("digest/wiki/a.md");
|
||||
assert.equal(workspace.search("offline", ["local_first"]).length, 1);
|
||||
workspace.save(
|
||||
"digest/wiki/a.md",
|
||||
note('["writing"]', "Updated [[missing.md]]"),
|
||||
initial.stat.mtime,
|
||||
);
|
||||
const restored = createDemoWorkspace(seeds, saved);
|
||||
assert.equal(restored.search("offline", ["local_first"]).length, 0);
|
||||
assert.equal(
|
||||
restored.search("updated", ["writing"])[0].path,
|
||||
"digest/wiki/a.md",
|
||||
);
|
||||
assert.deepEqual(restored.tags(), [
|
||||
{ tag: "release", count: 1 },
|
||||
{ tag: "writing", count: 1 },
|
||||
]);
|
||||
assert.ok(
|
||||
restored.graph().edges.some((edge) => edge.target === "missing.md"),
|
||||
);
|
||||
assert.equal(
|
||||
restored.graph().nodes.find((node) => node.id === "missing.md").indexed,
|
||||
false,
|
||||
);
|
||||
assert.ok(
|
||||
!restored
|
||||
.graph()
|
||||
.edges.some(
|
||||
(edge) =>
|
||||
edge.source === "digest/wiki/a.md" &&
|
||||
edge.target === "digest/wiki/b.md",
|
||||
),
|
||||
);
|
||||
assert.match(seeds["digest/wiki/a.md"], /local_first/);
|
||||
restored.reset();
|
||||
assert.equal(restored.read("digest/wiki/a.md").content, initial.content);
|
||||
});
|
||||
|
||||
test("tag filters match any selected tag, while keywords constrain every result", () => {
|
||||
const workspace = createDemoWorkspace(seeds);
|
||||
assert.equal(workspace.search("", ["local_first", "release"]).length, 2);
|
||||
assert.equal(
|
||||
workspace.search("offline plan", ["local_first", "release"]).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(workspace.search("", ["nonexistent"]).length, 0);
|
||||
const normalized = createDemoWorkspace({
|
||||
"note.md": note('["Local First"]'),
|
||||
});
|
||||
assert.equal(normalized.search("local_first", ["local_first"]).length, 1);
|
||||
});
|
||||
|
||||
test("stale browser writes and unknown paths are rejected without losing the newer file", () => {
|
||||
const saved = storage();
|
||||
const one = createDemoWorkspace(seeds, saved);
|
||||
const two = createDemoWorkspace(seeds, saved);
|
||||
const initial = one.read("digest/wiki/a.md");
|
||||
two.save("digest/wiki/a.md", "Newer content", initial.stat.mtime);
|
||||
assert.throws(
|
||||
() => one.save("digest/wiki/a.md", "Stale content", initial.stat.mtime),
|
||||
/changed/,
|
||||
);
|
||||
assert.equal(one.read("digest/wiki/a.md").content, "Newer content");
|
||||
for (const file of ["../escape.md", "toString", "__proto__", "new.md"]) {
|
||||
assert.throws(() => one.read(file), /not found/);
|
||||
assert.throws(() => one.save(file, "content"), /not found/);
|
||||
}
|
||||
});
|
||||
|
||||
test("failed persistence does not report success or replace the source", () => {
|
||||
const workspace = createDemoWorkspace(seeds, {
|
||||
getItem: () => null,
|
||||
setItem: () => {
|
||||
throw new Error("Quota exceeded");
|
||||
},
|
||||
removeItem: () => {},
|
||||
});
|
||||
assert.throws(() => workspace.save("digest/wiki/a.md", "not saved"), /Quota/);
|
||||
assert.equal(
|
||||
workspace.read("digest/wiki/a.md").content,
|
||||
seeds["digest/wiki/a.md"],
|
||||
);
|
||||
});
|
||||
|
||||
test("corrupt persisted content falls back to examples, and languages stay isolated", () => {
|
||||
const saved = storage();
|
||||
saved.setItem(DEMO_STORAGE_KEY, "broken JSON");
|
||||
assert.equal(
|
||||
createDemoWorkspace(seeds, saved).read("digest/wiki/a.md").content,
|
||||
seeds["digest/wiki/a.md"],
|
||||
);
|
||||
saved.setItem(
|
||||
DEMO_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
"digest/wiki/a.md": { content: "bad", mtime: "invalid" },
|
||||
}),
|
||||
);
|
||||
assert.equal(
|
||||
createDemoWorkspace(seeds, saved).read("digest/wiki/a.md").content,
|
||||
seeds["digest/wiki/a.md"],
|
||||
);
|
||||
createDemoWorkspace(seeds, saved, "zh").save("digest/wiki/a.md", "中文修改");
|
||||
assert.notEqual(
|
||||
createDemoWorkspace(seeds, saved, "en").read("digest/wiki/a.md").content,
|
||||
"中文修改",
|
||||
);
|
||||
});
|
||||
|
||||
for (const language of ["zh", "en"])
|
||||
test(`${language} examples form a complete source-to-memory chain with valid tags and scripted citations`, () => {
|
||||
const files = content(language);
|
||||
assert.equal(Object.keys(files).length, 21);
|
||||
assert.deepEqual(
|
||||
Object.keys(files).sort(),
|
||||
Object.keys(content(language === "zh" ? "en" : "zh")).sort(),
|
||||
);
|
||||
for (const [file, text] of Object.entries(files)) {
|
||||
assert.ok(text.length > 150, `${file}: complete content`);
|
||||
assert.equal(memoryTags(text).length, 3, `${file}: three valid tags`);
|
||||
for (const { target } of wikilinks(text))
|
||||
assert.ok(
|
||||
Object.hasOwn(files, target),
|
||||
`${file}: missing target ${target}`,
|
||||
);
|
||||
}
|
||||
for (const scenario of scenarios[language]) {
|
||||
assert.ok(createDemoWorkspace(files).search("", [scenario.tag]).length);
|
||||
for (const file of scenario.paths) assert.ok(Object.hasOwn(files, file));
|
||||
for (const { target } of wikilinks(scenario.answer))
|
||||
assert.ok(Object.hasOwn(files, target));
|
||||
}
|
||||
const graph = createDemoWorkspace(files).graph();
|
||||
for (const category of ["wiki", "personal", "procedure"])
|
||||
assert.ok(
|
||||
graph.edges.some((edge) => edge.source === `virtual:${category}`),
|
||||
);
|
||||
});
|
||||
139
reme_studio/tests/wikilinks.test.mjs
Normal file
139
reme_studio/tests/wikilinks.test.mjs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
remarkWikilinks,
|
||||
workspaceLinkTarget,
|
||||
} from "../app/files-workspace/wikilinks.ts";
|
||||
|
||||
test("workspace link fragments tolerate malformed Markdown URLs", () => {
|
||||
assert.equal(workspaceLinkTarget("#reme-file=digest%2Fa.md"), "digest/a.md");
|
||||
assert.equal(workspaceLinkTarget("#reme-file=%E4%B8%AD.md"), "中.md");
|
||||
for (const href of [
|
||||
undefined,
|
||||
"https://example.com",
|
||||
"#reme-file=",
|
||||
"#reme-file=%",
|
||||
"#reme-file=%FF",
|
||||
])
|
||||
assert.equal(workspaceLinkTarget(href), undefined);
|
||||
});
|
||||
|
||||
function parse(content, wikilinks = true) {
|
||||
const processor = unified().use(remarkParse).use(remarkGfm);
|
||||
if (wikilinks) processor.use(remarkWikilinks);
|
||||
return processor.runSync(processor.parse(content));
|
||||
}
|
||||
|
||||
function render(content, wikilinks = true) {
|
||||
return renderToStaticMarkup(
|
||||
createElement(
|
||||
ReactMarkdown,
|
||||
{ remarkPlugins: wikilinks ? [remarkGfm, remarkWikilinks] : [remarkGfm] },
|
||||
content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function links(tree) {
|
||||
return (
|
||||
tree.children?.flatMap((node) =>
|
||||
node.type === "link" ? [node] : links(node),
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
test("wikilinks open literal workspace targets and preserve aliases", () => {
|
||||
const result = links(
|
||||
parse("See [[digest/a.md#Section|Decision]] and [[b]] today."),
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.map((node) => node.url),
|
||||
["#reme-file=digest%2Fa.md", "#reme-file=b"],
|
||||
);
|
||||
assert.equal(result[0].children[0].value, "Decision");
|
||||
});
|
||||
|
||||
test("Markdown punctuation in targets and aliases stays literal", () => {
|
||||
for (const [target, alias] of [
|
||||
["resources/release~draft~.md", "**发布状态**"],
|
||||
["notes/a*b*.md", "a `code` label"],
|
||||
["notes/a©.md", "https://example.com"],
|
||||
["notes/a\\b.md", "~~old~~"],
|
||||
]) {
|
||||
const result = links(parse(`Before [[${target}#Scope|${alias}]] after.`));
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(workspaceLinkTarget(result[0].url), target);
|
||||
assert.deepEqual(result[0].children, [{ type: "text", value: alias }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("code examples and existing Markdown links are not rewritten", () => {
|
||||
const content = [
|
||||
"```md",
|
||||
"[[sample.md]]",
|
||||
"```",
|
||||
"",
|
||||
"`[[sample.md]]` and [See [[sample.md]]](https://example.com)",
|
||||
"![See [[sample.md]]](image.png)",
|
||||
].join("\n");
|
||||
assert.equal(render(content), render(content, false));
|
||||
});
|
||||
|
||||
test("malformed wikilinks leave normal Markdown parsing unchanged", () => {
|
||||
for (const content of [
|
||||
"[[ ]]",
|
||||
"[[a.md",
|
||||
"[[a.md] text",
|
||||
"[[a.md\nb.md]]",
|
||||
"[[a.md#]]",
|
||||
"[[a.md|]]",
|
||||
"[ordinary](https://example.com)",
|
||||
])
|
||||
assert.deepEqual(parse(content), parse(content, false));
|
||||
});
|
||||
|
||||
test("wikilinks work beside ordinary emphasis and consecutive links", () => {
|
||||
const tree = parse("**Before [[a.md|first]]** [[b.md]][[c.md]] *after*");
|
||||
assert.deepEqual(
|
||||
links(tree).map((node) => workspaceLinkTarget(node.url)),
|
||||
["a.md", "b.md", "c.md"],
|
||||
);
|
||||
assert.equal(tree.children[0].children[0].type, "strong");
|
||||
assert.equal(tree.children[0].children.at(-1).type, "emphasis");
|
||||
});
|
||||
|
||||
test("reference links and images preserve literal wikilinks in their labels", () => {
|
||||
for (const content of [
|
||||
"[See [[sample.md]]][ref]\n\n[ref]: https://example.com",
|
||||
"[See [[sample.md]]][REF]\n\n[ref]: https://example.com",
|
||||
"![See [[sample.md]]][ref]\n\n[ref]: image.png",
|
||||
]) {
|
||||
assert.equal(render(content), render(content, false));
|
||||
assert.doesNotMatch(render(content), /#reme-file=/);
|
||||
}
|
||||
});
|
||||
|
||||
test("escaped brackets and character entities remain literal text", () => {
|
||||
for (const content of [
|
||||
String.raw`\[\[example.md\]\]`,
|
||||
"[[example.md]]",
|
||||
"[[example.md]]",
|
||||
String.raw`\[[example.md]]`,
|
||||
]) {
|
||||
assert.equal(render(content), render(content, false));
|
||||
assert.equal(links(parse(content)).length, 0);
|
||||
}
|
||||
const mixed = String.raw`\[\[literal.md\]\] [[real.md]] [[entity.md]]`;
|
||||
assert.deepEqual(
|
||||
links(parse(mixed)).map((node) => node.url),
|
||||
["#reme-file=real.md"],
|
||||
);
|
||||
assert.match(render(mixed), /\[\[literal\.md\]\]/);
|
||||
assert.match(render(mixed), /\[\[entity\.md\]\]/);
|
||||
});
|
||||
|
|
@ -1,32 +1,65 @@
|
|||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import { defineConfig, loadEnv, type Plugin } from "vite";
|
||||
|
||||
const staticDir = fileURLToPath(new URL("./static/", import.meta.url));
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
const demo = mode === "demo";
|
||||
const demoApi = fileURLToPath(new URL("./demo/api.ts", import.meta.url));
|
||||
const appApi = fileURLToPath(new URL("./app/api", import.meta.url));
|
||||
const hiddenPanels = new Set(
|
||||
demo
|
||||
? [fileURLToPath(new URL("./app/settings-center", import.meta.url))]
|
||||
: ["DemoBar", "DemoTags"].map((name) =>
|
||||
fileURLToPath(new URL(`./demo/${name}`, import.meta.url)),
|
||||
),
|
||||
);
|
||||
const browserApi: Plugin = {
|
||||
name: "reme-demo-api",
|
||||
enforce: "pre",
|
||||
transformIndexHtml(html) {
|
||||
return demo
|
||||
? html.replace(
|
||||
"<title>ReMe Workspace</title>",
|
||||
"<title>Try ReMe Studio</title>",
|
||||
)
|
||||
: html;
|
||||
},
|
||||
resolveId(id, importer) {
|
||||
if (!importer) return;
|
||||
const resolved = path.resolve(path.dirname(importer), id);
|
||||
if (hiddenPanels.has(resolved)) return "\0reme-hidden-panel";
|
||||
if (demo && resolved === appApi) return demoApi;
|
||||
},
|
||||
load(id) {
|
||||
if (id === "\0reme-hidden-panel") return "export default () => null";
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
base: demo ? "./" : "/",
|
||||
plugins: [browserApi, react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"next/dynamic": path.resolve(staticDir, "next-dynamic.tsx"),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
"process.env.NEXT_PUBLIC_REME_DEMO": JSON.stringify(String(demo)),
|
||||
"process.env.NEXT_PUBLIC_REME_API_URL": JSON.stringify(
|
||||
env.VITE_REME_API_URL || "/",
|
||||
),
|
||||
"process.env.NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS": JSON.stringify(
|
||||
env.VITE_REME_WORKSPACE_EXTENSIONS ?? "",
|
||||
demo ? "md" : env.VITE_REME_WORKSPACE_EXTENSIONS ?? "",
|
||||
),
|
||||
},
|
||||
build: {
|
||||
outDir: "dist-static",
|
||||
outDir: demo ? "dist-demo" : "dist-static",
|
||||
emptyOutDir: true,
|
||||
sourcemap: mode !== "production",
|
||||
sourcemap: mode !== "production" && !demo,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue