diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833f679..9945262 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: with: python-version: "3.11" + - name: Install Python test dependencies + run: python -m pip install pytest + - name: Install dependencies run: npm ci diff --git a/README.en.md b/README.en.md index 9ca60e0..aaa3a06 100644 --- a/README.en.md +++ b/README.en.md @@ -65,6 +65,7 @@ flowchart TD | Goal | Command | | --- | --- | | Run locally | `npm install && npm run dev` | +| Seed a playable demo | `npm run demo:seed && npm run dev` | | Verify locally | `npm run lint && npm test && npm run build && pytest tests/python -q` | | Export one remembered entry skill for Codex | `npm run skills:export -- --target codex --mode aggregator-only` | | Export full bundle for Codex | `npm run skills:export -- --target codex --mode full-bundle` | @@ -78,6 +79,26 @@ Defaults: - Codex installs to `~/.codex/skills` unless `CODEX_HOME` is set - Claude installs to `~/.claude/skills` unless `CLAUDE_HOME` is set +## Quick Start + +```bash +npm install +npm run dev +``` + +Open `http://localhost:3000`, or the local port printed by Next.js. + +To see the studio with useful first-run content: + +```bash +npm run demo:seed +npm run dev +``` + +The seed command creates one idempotent local demo project, reference sample, +style card, and queued planning job. Running it again will not duplicate the +same demo. + ## Top-level Skill If you only want to remember one skill name, use: diff --git a/README.md b/README.md index b3c8030..c675b10 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ flowchart TD | 目标 | 命令 | | --- | --- | | 本地开发运行 | `npm install && npm run dev` | +| 预置一个可试玩 demo | `npm run demo:seed && npm run dev` | | 本地完整验证 | `npm run lint && npm test && npm run build && pytest tests/python -q` | | 为 Codex 导出一个总入口 skill | `npm run skills:export -- --target codex --mode aggregator-only` | | 为 Codex 导出完整 skill bundle | `npm run skills:export -- --target codex --mode full-bundle` | @@ -96,6 +97,15 @@ npm run dev 打开 `http://localhost:3000`,或者 Next.js 启动时显示的本地端口。 +想先看一个有内容的仪表盘,可以运行: + +```bash +npm run demo:seed +npm run dev +``` + +这个命令会创建一个幂等的本地示例项目、参考素材、风格卡和排队任务。重复运行不会重复插入同一个 demo。 + 推荐第一轮使用顺序: 1. 打开 `/wizard/new-novel` diff --git a/package.json b/package.json index 2838fc6..042d591 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "next build", "start": "next start", "lint": "eslint", + "demo:seed": "tsx scripts/seed-demo.ts", "skills:export": "tsx scripts/export-skills.ts", "skills:install": "tsx scripts/install-skills.ts", "test": "vitest run", diff --git a/scripts/seed-demo.ts b/scripts/seed-demo.ts new file mode 100644 index 0000000..e94b6fb --- /dev/null +++ b/scripts/seed-demo.ts @@ -0,0 +1,9 @@ +import { seedDemoData } from "../src/lib/server/demo-seed"; + +function main() { + const result = seedDemoData(); + + console.log(JSON.stringify(result, null, 2)); +} + +main(); diff --git a/src/lib/server/demo-seed.ts b/src/lib/server/demo-seed.ts new file mode 100644 index 0000000..8c60f29 --- /dev/null +++ b/src/lib/server/demo-seed.ts @@ -0,0 +1,78 @@ +import { + createDraftJob, + createProject, + createReferenceWork, + createStyleProfile, + listProjects, +} from "./db"; + +const DEMO_PROJECT_TITLE = "雾港债火"; + +type ProjectRow = { + id: number; + title: string; +}; + +export function seedDemoData() { + const existingProject = (listProjects() as ProjectRow[]).find( + (project) => project.title === DEMO_PROJECT_TITLE, + ); + + if (existingProject) { + return { + created: false, + message: "Demo data already exists.", + projectId: existingProject.id, + title: existingProject.title, + }; + } + + const project = createProject({ + genre: "都市奇幻 / 悬疑", + premise: + "负债的夜班修表师在雾港听见旧钟里的亡者留言,被迫用七天查清一场债火案。", + title: DEMO_PROJECT_TITLE, + }); + + const reference = createReferenceWork({ + creatorLabel: "demo sample", + projectId: project.id, + sourceLabel: "demo://fog-harbor-opening", + sourceType: "excerpt", + title: "雾港开场节奏样本", + }); + + const styleProfile = createStyleProfile({ + antiPatterns: ["解释动机过早", "段尾连续抽象总结", "对白缺少动作承接"], + metrics: { + averageSentenceLength: 15.8, + dialogueRatio: 0.31, + hookDensity: 0.42, + }, + name: "雾港悬压推进卡", + referenceWorkId: reference.id, + summary: + "短场景切换、物件线索推进、对白后接动作反应,章末保留一个可验证谜面。", + }); + + const job = createDraftJob({ + jobType: "plan", + payload: { + chapter: 1, + focus: "建立债火案、旧钟留言和主角七天倒计时。", + styleProfileId: styleProfile.id, + }, + projectId: project.id, + status: "queued", + }); + + return { + created: true, + jobId: job.id, + message: "Demo data created.", + projectId: project.id, + referenceId: reference.id, + styleProfileId: styleProfile.id, + title: project.title, + }; +} diff --git a/tests/ts/demo-seed.test.ts b/tests/ts/demo-seed.test.ts new file mode 100644 index 0000000..2eb2914 --- /dev/null +++ b/tests/ts/demo-seed.test.ts @@ -0,0 +1,48 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +async function loadDemoModules() { + vi.resetModules(); + return { + dashboard: await import("@/lib/server/dashboard"), + db: await import("@/lib/server/db"), + demoSeed: await import("@/lib/server/demo-seed"), + }; +} + +describe("demo seed data", () => { + afterEach(async () => { + const { db } = await loadDemoModules(); + db.resetDatabaseForTests(); + delete process.env.XIAOSHUO_HOME; + }); + + test("creates an idempotent first-run project, style card, and queued job", async () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "xiaoshuo-demo-seed-")); + process.env.XIAOSHUO_HOME = home; + + const { dashboard, demoSeed } = await loadDemoModules(); + + const first = demoSeed.seedDemoData(); + const second = demoSeed.seedDemoData(); + const snapshot = dashboard.getDashboardSnapshot(); + + expect(first).toMatchObject({ + created: true, + title: "雾港债火", + }); + expect(second).toMatchObject({ + created: false, + projectId: first.projectId, + }); + expect(snapshot.metrics).toMatchObject({ + projects: 1, + queuedJobs: 1, + references: 1, + styleProfiles: 1, + }); + expect(snapshot.projects[0]?.title).toBe("雾港债火"); + expect(snapshot.styleProfiles[0]?.name).toBe("雾港悬压推进卡"); + }); +});