@game_ryo/lsji 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/node.yml +46 -0
- package/AGENTS.md +143 -0
- package/LICENSE +185 -0
- package/PROPOSAL.md +18 -0
- package/README.md +102 -0
- package/bin/lsji.js +8 -0
- package/docs/README.md +43 -0
- package/docs/blog/2019-05-28-first-blog-post.mdx +12 -0
- package/docs/blog/2019-05-29-long-blog-post.mdx +44 -0
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +24 -0
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.mdx +29 -0
- package/docs/blog/authors.yml +25 -0
- package/docs/blog/tags.yml +19 -0
- package/docs/docs/api/agent.md +151 -0
- package/docs/docs/api/env.md +133 -0
- package/docs/docs/api/environments.md +102 -0
- package/docs/docs/api/qlearning.md +138 -0
- package/docs/docs/api/storage.md +168 -0
- package/docs/docs/architecture.md +155 -0
- package/docs/docs/cli.md +210 -0
- package/docs/docs/contributing.md +162 -0
- package/docs/docs/core-concepts.md +152 -0
- package/docs/docs/examples/advanced-training.md +244 -0
- package/docs/docs/examples/custom-environment.md +198 -0
- package/docs/docs/examples/custom-storage.md +251 -0
- package/docs/docs/getting-started.md +91 -0
- package/docs/docusaurus.config.ts +149 -0
- package/docs/package-lock.json +19522 -0
- package/docs/package.json +49 -0
- package/docs/sidebars.ts +33 -0
- package/docs/src/components/HomepageFeatures/index.tsx +71 -0
- package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
- package/docs/src/css/custom.css +79 -0
- package/docs/src/pages/index.module.css +23 -0
- package/docs/src/pages/index.tsx +44 -0
- package/docs/src/pages/markdown-page.mdx +7 -0
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.png +0 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docs/static/img/undraw_docusaurus_react.svg +170 -0
- package/docs/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docs/tsconfig.json +12 -0
- package/legacy/worker.js +166 -0
- package/legacy/wrangler.toml +11 -0
- package/package.json +26 -0
- package/src/cli.js +232 -0
- package/src/core/agent.js +239 -0
- package/src/core/env.js +86 -0
- package/src/core/qlearning.js +197 -0
- package/src/envs/rps.js +168 -0
- package/src/index.js +22 -0
- package/src/storage/better-sqlite.js +133 -0
- package/src/storage/index.js +146 -0
- package/src/storage/memory.js +98 -0
- package/src/storage/sqlite.js +123 -0
- package/test/core/qlearning.test.js +150 -0
- package/test/storage/memory.test.js +81 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Custom Storage Backend
|
|
3
|
+
description: Implement your own storage backend
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Custom Storage Backend
|
|
7
|
+
|
|
8
|
+
Create a custom storage backend by extending the abstract `Storage` class.
|
|
9
|
+
|
|
10
|
+
## Interface to Implement
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { Storage } from 'lsji';
|
|
14
|
+
|
|
15
|
+
abstract class Storage {
|
|
16
|
+
abstract initialize(): Promise<void>;
|
|
17
|
+
abstract close(): Promise<void>;
|
|
18
|
+
abstract getSetting(key: string): Promise<{key: string, value: string} | null>;
|
|
19
|
+
abstract setSetting(key: string, value: string | number): Promise<void>;
|
|
20
|
+
abstract getQTable(): Promise<Array<{state: string, action: number, q_value: number}>>;
|
|
21
|
+
abstract updateQ(state: string, action: number, qValue: number): Promise<void>;
|
|
22
|
+
abstract addBattle(record: BattleRecord): Promise<void>;
|
|
23
|
+
abstract getTodayBattleCount(): Promise<number>;
|
|
24
|
+
abstract getPerformanceStats(): Promise<Array<{mode: string, total: number, win_rate: number}>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface BattleRecord {
|
|
28
|
+
mode: 'train' | 'test';
|
|
29
|
+
handA: number;
|
|
30
|
+
handB: number;
|
|
31
|
+
reward: number;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Example: Redis Storage
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Storage } from 'lsji';
|
|
40
|
+
import Redis from 'ioredis';
|
|
41
|
+
|
|
42
|
+
class RedisStorage extends Storage {
|
|
43
|
+
constructor(redisUrl = 'redis://localhost:6379') {
|
|
44
|
+
super();
|
|
45
|
+
this.redis = new Redis(redisUrl);
|
|
46
|
+
this.keyPrefix = 'lsji:';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async initialize() {
|
|
50
|
+
// Set default settings
|
|
51
|
+
await this.setSetting('is_active', 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async close() {
|
|
55
|
+
await this.redis.quit();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getSetting(key) {
|
|
59
|
+
const value = await this.redis.get(this.keyPrefix + 'setting:' + key);
|
|
60
|
+
return value ? { key, value } : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async setSetting(key, value) {
|
|
64
|
+
await this.redis.set(this.keyPrefix + 'setting:' + key, String(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getQTable() {
|
|
68
|
+
const keys = await this.redis.keys(this.keyPrefix + 'q:*');
|
|
69
|
+
const results = [];
|
|
70
|
+
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
const value = await this.redis.get(key);
|
|
73
|
+
const parts = key.replace(this.keyPrefix + 'q:', '').split(':');
|
|
74
|
+
results.push({
|
|
75
|
+
state: parts[0],
|
|
76
|
+
action: parseInt(parts[1], 10),
|
|
77
|
+
q_value: parseFloat(value)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return results.sort((a, b) => a.state.localeCompare(b.state) || a.action - b.action);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async updateQ(state, action, qValue) {
|
|
85
|
+
await this.redis.set(
|
|
86
|
+
this.keyPrefix + `q:${state}:${action}`,
|
|
87
|
+
qValue.toString()
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async addBattle(record) {
|
|
92
|
+
const battleKey = this.keyPrefix + `battle:${Date.now()}:${Math.random()}`;
|
|
93
|
+
await this.redis.hset(battleKey, {
|
|
94
|
+
mode: record.mode,
|
|
95
|
+
hand_a: record.handA.toString(),
|
|
96
|
+
hand_b: record.handB.toString(),
|
|
97
|
+
reward: record.reward.toString(),
|
|
98
|
+
created_at: record.createdAt
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Add to sorted set for date queries
|
|
102
|
+
await this.redis.zadd(
|
|
103
|
+
this.keyPrefix + 'battles:by_date',
|
|
104
|
+
new Date(record.createdAt).getTime(),
|
|
105
|
+
battleKey
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getTodayBattleCount() {
|
|
110
|
+
const today = new Date();
|
|
111
|
+
today.setHours(0, 0, 0, 0);
|
|
112
|
+
const tomorrow = new Date(today);
|
|
113
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
114
|
+
|
|
115
|
+
return this.redis.zcount(
|
|
116
|
+
this.keyPrefix + 'battles:by_date',
|
|
117
|
+
today.getTime(),
|
|
118
|
+
tomorrow.getTime()
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getPerformanceStats() {
|
|
123
|
+
const keys = await this.redis.zrange(this.keyPrefix + 'battles:by_date', 0, -1);
|
|
124
|
+
const stats = new Map();
|
|
125
|
+
|
|
126
|
+
for (const key of keys) {
|
|
127
|
+
const battle = await this.redis.hgetall(key);
|
|
128
|
+
const mode = battle.mode;
|
|
129
|
+
|
|
130
|
+
if (!stats.has(mode)) {
|
|
131
|
+
stats.set(mode, { total: 0, wins: 0 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const stat = stats.get(mode);
|
|
135
|
+
stat.total++;
|
|
136
|
+
if (parseInt(battle.reward) > 0) stat.wins++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Array.from(stats.entries()).map(([mode, stat]) => ({
|
|
140
|
+
mode,
|
|
141
|
+
total: stat.total,
|
|
142
|
+
win_rate: stat.total > 0 ? Math.round((stat.wins / stat.total) * 1000) / 10 : 0
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Register Custom Storage
|
|
149
|
+
|
|
150
|
+
Add to `createStorage` factory (or use directly):
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { createStorage } from 'lsji';
|
|
154
|
+
|
|
155
|
+
// Option 1: Use directly
|
|
156
|
+
const storage = new RedisStorage('redis://localhost:6379');
|
|
157
|
+
await storage.initialize();
|
|
158
|
+
|
|
159
|
+
// Option 2: Extend createStorage (modify src/storage/index.js)
|
|
160
|
+
import { RedisStorage } from './redis-storage';
|
|
161
|
+
|
|
162
|
+
// In your code:
|
|
163
|
+
const storage = new RedisStorage();
|
|
164
|
+
await storage.initialize();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Example: PostgreSQL Storage
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { Pool } from 'pg';
|
|
171
|
+
|
|
172
|
+
class PostgresStorage extends Storage {
|
|
173
|
+
constructor(connectionString) {
|
|
174
|
+
super();
|
|
175
|
+
this.pool = new Pool({ connectionString });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async initialize() {
|
|
179
|
+
await this.pool.query(`
|
|
180
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
181
|
+
key TEXT PRIMARY KEY,
|
|
182
|
+
value TEXT NOT NULL
|
|
183
|
+
);
|
|
184
|
+
CREATE TABLE IF NOT EXISTS battle_history (
|
|
185
|
+
id SERIAL PRIMARY KEY,
|
|
186
|
+
mode TEXT NOT NULL,
|
|
187
|
+
hand_a INTEGER NOT NULL,
|
|
188
|
+
hand_b INTEGER NOT NULL,
|
|
189
|
+
reward INTEGER NOT NULL,
|
|
190
|
+
created_at TIMESTAMP NOT NULL
|
|
191
|
+
);
|
|
192
|
+
CREATE TABLE IF NOT EXISTS q_table (
|
|
193
|
+
state TEXT NOT NULL,
|
|
194
|
+
action INTEGER NOT NULL,
|
|
195
|
+
q_value REAL NOT NULL DEFAULT 0,
|
|
196
|
+
PRIMARY KEY (state, action)
|
|
197
|
+
);
|
|
198
|
+
`);
|
|
199
|
+
await this.setSetting('is_active', 1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async close() {
|
|
203
|
+
await this.pool.end();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async getSetting(key) {
|
|
207
|
+
const res = await this.pool.query('SELECT key, value FROM settings WHERE key = $1', [key]);
|
|
208
|
+
return res.rows[0] || null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async setSetting(key, value) {
|
|
212
|
+
await this.pool.query(
|
|
213
|
+
`INSERT INTO settings (key, value) VALUES ($1, $2)
|
|
214
|
+
ON CONFLICT (key) DO UPDATE SET value = $2`,
|
|
215
|
+
[key, String(value)]
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ... implement other methods similarly
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Testing Custom Storage
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { MemoryStorage } from 'lsji';
|
|
227
|
+
|
|
228
|
+
// Use MemoryStorage as reference implementation for testing
|
|
229
|
+
async function testStorageImplementation(StorageClass) {
|
|
230
|
+
const storage = new StorageClass();
|
|
231
|
+
await storage.initialize();
|
|
232
|
+
|
|
233
|
+
// Test settings
|
|
234
|
+
await storage.setSetting('test', 'value');
|
|
235
|
+
const setting = await storage.getSetting('test');
|
|
236
|
+
assert(setting.value === 'value');
|
|
237
|
+
|
|
238
|
+
// Test Q-table
|
|
239
|
+
await storage.updateQ('state1', 0, 0.5);
|
|
240
|
+
const qTable = await storage.getQTable();
|
|
241
|
+
assert(qTable[0].q_value === 0.5);
|
|
242
|
+
|
|
243
|
+
// Test battles
|
|
244
|
+
await storage.addBattle({ mode: 'train', handA: 0, handB: 1, reward: 1, createdAt: new Date().toISOString() });
|
|
245
|
+
const count = await storage.getTodayBattleCount();
|
|
246
|
+
assert(count === 1);
|
|
247
|
+
|
|
248
|
+
await storage.close();
|
|
249
|
+
console.log('All tests passed!');
|
|
250
|
+
}
|
|
251
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting Started
|
|
3
|
+
description: Install LSJI and run your first RL agent
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Getting Started
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- **Node.js 22+** (required for built-in `node:sqlite`)
|
|
11
|
+
- npm, yarn, or pnpm
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install as a library
|
|
17
|
+
npm install lsji
|
|
18
|
+
|
|
19
|
+
# Or use CLI directly with npx
|
|
20
|
+
npx lsji --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Train an Agent
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Train with default settings (200 episodes, random pattern)
|
|
29
|
+
npx lsji train --episodes 500
|
|
30
|
+
|
|
31
|
+
# Train against specific opponent
|
|
32
|
+
npx lsji train --episodes 1000 --opponent counter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Play Against the Agent
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Play Rock (0)
|
|
39
|
+
npx lsji play --hand 0
|
|
40
|
+
|
|
41
|
+
# Play Paper (2)
|
|
42
|
+
npx lsji play --hand 2
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Check Status
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx lsji status --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Using as a Library
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import { Agent, QLearning, createStorage, RockPaperScissorsEnv } from 'lsji';
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
// Create storage (SQLite recommended for persistence)
|
|
58
|
+
const storage = await createStorage('sqlite', { path: './my-agent.db' });
|
|
59
|
+
|
|
60
|
+
// Create Q-Learning engine
|
|
61
|
+
const qlearning = new QLearning({
|
|
62
|
+
alpha: 0.1, // learning rate
|
|
63
|
+
gamma: 0.9, // discount factor
|
|
64
|
+
epsilon: 0.1, // exploration rate
|
|
65
|
+
storage
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Create environment
|
|
69
|
+
const env = new RockPaperScissorsEnv({ opponent: 'random' });
|
|
70
|
+
|
|
71
|
+
// Create agent
|
|
72
|
+
const agent = new Agent({ qlearning, storage, env });
|
|
73
|
+
|
|
74
|
+
// Train
|
|
75
|
+
await agent.train({ episodes: 1000 });
|
|
76
|
+
|
|
77
|
+
// Play
|
|
78
|
+
const result = await agent.play(0); // 0 = Rock
|
|
79
|
+
console.log(result);
|
|
80
|
+
|
|
81
|
+
await storage.close();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch(console.error);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Next Steps
|
|
88
|
+
|
|
89
|
+
- Read [Core Concepts](/docs/core-concepts) to understand the architecture
|
|
90
|
+
- Explore [API Reference](/docs/api/agent) for detailed class documentation
|
|
91
|
+
- Try [Examples](/docs/examples/custom-environment) for custom environments
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {themes as prismThemes} from 'prism-react-renderer';
|
|
2
|
+
import type {Config} from '@docusaurus/types';
|
|
3
|
+
import type * as Preset from '@docusaurus/preset-classic';
|
|
4
|
+
|
|
5
|
+
const config: Config = {
|
|
6
|
+
title: 'LSJI',
|
|
7
|
+
tagline: 'Learning System for JavaScript Intelligence',
|
|
8
|
+
favicon: 'img/favicon.ico',
|
|
9
|
+
|
|
10
|
+
future: {
|
|
11
|
+
v4: true,
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
url: 'https://lsji.ryopc.org',
|
|
15
|
+
baseUrl: '/',
|
|
16
|
+
|
|
17
|
+
organizationName: 'ryotagtagtag-wq',
|
|
18
|
+
projectName: 'LSJI',
|
|
19
|
+
|
|
20
|
+
onBrokenLinks: 'throw',
|
|
21
|
+
onBrokenMarkdownLinks: 'warn',
|
|
22
|
+
|
|
23
|
+
i18n: {
|
|
24
|
+
defaultLocale: 'en',
|
|
25
|
+
locales: ['en'],
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
presets: [
|
|
29
|
+
[
|
|
30
|
+
'classic',
|
|
31
|
+
{
|
|
32
|
+
docs: {
|
|
33
|
+
sidebarPath: './sidebars.ts',
|
|
34
|
+
editUrl: 'https://github.com/ryotagtagtag-wq/LSJI/tree/main/docs/',
|
|
35
|
+
},
|
|
36
|
+
blog: {
|
|
37
|
+
showReadingTime: true,
|
|
38
|
+
feedOptions: {
|
|
39
|
+
type: ['rss', 'atom'],
|
|
40
|
+
xslt: true,
|
|
41
|
+
},
|
|
42
|
+
editUrl: 'https://github.com/ryotagtagtag-wq/LSJI/tree/main/docs/',
|
|
43
|
+
onInlineTags: 'warn',
|
|
44
|
+
onInlineAuthors: 'warn',
|
|
45
|
+
onUntruncatedBlogPosts: 'warn',
|
|
46
|
+
},
|
|
47
|
+
theme: {
|
|
48
|
+
customCss: './src/css/custom.css',
|
|
49
|
+
},
|
|
50
|
+
} satisfies Preset.Options,
|
|
51
|
+
],
|
|
52
|
+
],
|
|
53
|
+
|
|
54
|
+
themeConfig: {
|
|
55
|
+
image: 'img/lsji-social-card.jpg',
|
|
56
|
+
colorMode: {
|
|
57
|
+
respectPrefersColorScheme: true,
|
|
58
|
+
},
|
|
59
|
+
navbar: {
|
|
60
|
+
title: 'LSJI',
|
|
61
|
+
logo: {
|
|
62
|
+
alt: 'LSJI Logo',
|
|
63
|
+
src: 'img/logo.png',
|
|
64
|
+
},
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
type: 'docSidebar',
|
|
68
|
+
sidebarId: 'tutorialSidebar',
|
|
69
|
+
position: 'left',
|
|
70
|
+
label: 'Docs',
|
|
71
|
+
},
|
|
72
|
+
{to: '/blog', label: 'Blog', position: 'left'},
|
|
73
|
+
{
|
|
74
|
+
href: 'https://github.com/ryotagtagtag-wq/LSJI',
|
|
75
|
+
label: 'GitHub',
|
|
76
|
+
position: 'right',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
href: 'https://www.npmjs.com/package/lsji',
|
|
80
|
+
label: 'npm',
|
|
81
|
+
position: 'right',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
footer: {
|
|
86
|
+
style: 'dark',
|
|
87
|
+
links: [
|
|
88
|
+
{
|
|
89
|
+
title: 'Documentation',
|
|
90
|
+
items: [
|
|
91
|
+
{
|
|
92
|
+
label: 'Getting Started',
|
|
93
|
+
to: '/docs/getting-started',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: 'Core Concepts',
|
|
97
|
+
to: '/docs/core-concepts',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: 'API Reference',
|
|
101
|
+
to: '/docs/api/agent',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: 'CLI Reference',
|
|
105
|
+
to: '/docs/cli',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: 'Community',
|
|
111
|
+
items: [
|
|
112
|
+
{
|
|
113
|
+
label: 'GitHub Issues',
|
|
114
|
+
href: 'https://github.com/ryotagtagtag-wq/LSJI/issues',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: 'GitHub Discussions',
|
|
118
|
+
href: 'https://github.com/ryotagtagtag-wq/LSJI/discussions',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: 'More',
|
|
124
|
+
items: [
|
|
125
|
+
{
|
|
126
|
+
label: 'Blog',
|
|
127
|
+
to: '/blog',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
label: 'GitHub',
|
|
131
|
+
href: 'https://github.com/ryotagtagtag-wq/LSJI',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: 'npm',
|
|
135
|
+
href: 'https://www.npmjs.com/package/lsji',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
copyright: `Copyright © ${new Date().getFullYear()} LSJI Contributors. Built with Docusaurus.`,
|
|
141
|
+
},
|
|
142
|
+
prism: {
|
|
143
|
+
theme: prismThemes.github,
|
|
144
|
+
darkTheme: prismThemes.dracula,
|
|
145
|
+
},
|
|
146
|
+
} satisfies Preset.ThemeConfig,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default config;
|