duqing2026 commited on
Commit
adb7761
·
0 Parent(s):

代码、编辑和打印网站

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ .git
4
+ .DS_Store
5
+ venv
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Set up a new user named "user" with user ID 1000
4
+ RUN useradd -m -u 1000 user
5
+
6
+ # Switch to the "user" user
7
+ USER user
8
+
9
+ # Set home to the user's home directory
10
+ ENV HOME=/home/user \
11
+ PATH=/home/user/.local/bin:$PATH
12
+
13
+ # Set the working directory to the user's home directory
14
+ WORKDIR $HOME/app
15
+
16
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
17
+ COPY --chown=user . $HOME/app
18
+
19
+ # Install any needed packages specified in requirements.txt
20
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
21
+
22
+ # Expose the port that the app runs on
23
+ EXPOSE 7860
24
+
25
+ # Run the application
26
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code Interview Showcase
2
+
3
+ 这是一个用于积累和练习手写代码题(笔试题)的 Web 应用。
4
+
5
+ ## 特性
6
+
7
+ * **极简流程**:只需在 `problems/` 目录下新建 `.js` 文件,即可自动显示在网页上。
8
+ * **三栏布局**:左侧题目列表,中间 Monaco 代码编辑器,右侧实时控制台输出。
9
+ * **即时运行**:支持直接在浏览器中运行 JavaScript 代码并查看结果。
10
+ * **暗色主题**:美观的 Dark Mode 界面。
11
+
12
+ ## 如何添加题目
13
+
14
+ 1. 在 `problems/` 目录下创建一个新的 JavaScript 文件(例如 `03_my_problem.js`)。
15
+ 2. (可选)在文件开头使用注释添加 `@title` 来定义显示的题目名称。如果未定义,将显示文件名。
16
+ 3. 编写代码和测试用例(使用 `console.log` 输出结果)。
17
+
18
+ **示例文件内容:**
19
+
20
+ ```javascript
21
+ /**
22
+ * @title 3. 我的新题目
23
+ * 描述:这是一个测试题目。
24
+ */
25
+
26
+ function solve() {
27
+ return "Hello World";
28
+ }
29
+
30
+ console.log(solve());
31
+ ```
32
+
33
+ 保存文件后,刷新网页即可看到新题目。
34
+
35
+ ## 本地运行
36
+
37
+ 1. 安装依赖:
38
+ ```bash
39
+ pip install -r requirements.txt
40
+ ```
41
+ 2. 运行应用:
42
+ ```bash
43
+ python app.py
44
+ ```
45
+ 3. 打开浏览器访问:`http://localhost:7860`
46
+
47
+ ## 部署到 Hugging Face Spaces
48
+
49
+ 本项目已配置 Dockerfile,可直接部署到 Hugging Face Spaces。
50
+
51
+ 1. 在 Hugging Face 创建一个新的 Space。
52
+ 2. 选择 SDK 为 **Docker**。
53
+ 3. 上传本项目所有文件。
54
+ 4. 等待构建完成即可。
app.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from flask import Flask, jsonify, send_from_directory, render_template, request
4
+
5
+ app = Flask(__name__)
6
+ PROBLEMS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'problems')
7
+
8
+ def parse_problem_metadata(filename):
9
+ filepath = os.path.join(PROBLEMS_DIR, filename)
10
+ with open(filepath, 'r', encoding='utf-8') as f:
11
+ content = f.read()
12
+
13
+ # 默认标题为文件名
14
+ title = filename
15
+
16
+ # 尝试解析 @title
17
+ # 匹配 /** ... @title 标题 ... */ 或 // @title 标题
18
+ title_match = re.search(r'@title\s+(.+)', content)
19
+ if title_match:
20
+ title = title_match.group(1).strip()
21
+
22
+ return {
23
+ 'id': filename,
24
+ 'title': title,
25
+ 'content': content
26
+ }
27
+
28
+ @app.route('/')
29
+ def index():
30
+ return render_template('index.html')
31
+
32
+ @app.route('/api/problems', methods=['GET'])
33
+ def get_problems():
34
+ files = [f for f in os.listdir(PROBLEMS_DIR) if f.endswith('.js') or f.endswith('.ts')]
35
+ # 按文件名排序
36
+ files.sort()
37
+
38
+ problems = []
39
+ for f in files:
40
+ try:
41
+ problems.append(parse_problem_metadata(f))
42
+ except Exception as e:
43
+ print(f"Error parsing {f}: {e}")
44
+
45
+ return jsonify(problems)
46
+
47
+ @app.route('/api/problems', methods=['POST'])
48
+ def create_problem():
49
+ data = request.json
50
+ filename = data.get('filename')
51
+
52
+ if not filename:
53
+ return jsonify({'error': 'Filename is required'}), 400
54
+
55
+ if not (filename.endswith('.js') or filename.endswith('.ts')):
56
+ filename += '.js'
57
+
58
+ filepath = os.path.join(PROBLEMS_DIR, filename)
59
+
60
+ if os.path.exists(filepath):
61
+ return jsonify({'error': 'File already exists'}), 409
62
+
63
+ # 默认模板
64
+ content = f"""/**
65
+ * @title {filename}
66
+ * @description 在此处添加题目描述
67
+ */
68
+
69
+ function solve() {{
70
+ // TODO: Implement solution
71
+ }}
72
+
73
+ console.log(solve());
74
+ """
75
+
76
+ try:
77
+ with open(filepath, 'w', encoding='utf-8') as f:
78
+ f.write(content)
79
+ return jsonify(parse_problem_metadata(filename)), 201
80
+ except Exception as e:
81
+ return jsonify({'error': str(e)}), 500
82
+
83
+ @app.route('/api/problems/<path:filename>', methods=['GET'])
84
+ def get_problem_content(filename):
85
+ return send_from_directory(PROBLEMS_DIR, filename)
86
+
87
+ @app.route('/api/problems/<path:filename>', methods=['PUT'])
88
+ def update_problem(filename):
89
+ filepath = os.path.join(PROBLEMS_DIR, filename)
90
+
91
+ if not os.path.exists(filepath):
92
+ return jsonify({'error': 'File not found'}), 404
93
+
94
+ content = request.json.get('content')
95
+ if content is None:
96
+ return jsonify({'error': 'Content is required'}), 400
97
+
98
+ try:
99
+ with open(filepath, 'w', encoding='utf-8') as f:
100
+ f.write(content)
101
+ # 返回新的 metadata 以便前端更新标题
102
+ return jsonify(parse_problem_metadata(filename))
103
+ except Exception as e:
104
+ return jsonify({'error': str(e)}), 500
105
+
106
+ if __name__ == '__main__':
107
+ # 确保 problems 目录存在
108
+ if not os.path.exists(PROBLEMS_DIR):
109
+ os.makedirs(PROBLEMS_DIR)
110
+ app.run(host='0.0.0.0', port=7860, debug=True)
problems/01_two_sum.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @title 1. 两数之和 (Two Sum)
3
+ *
4
+ * 题目描述:
5
+ * 给定一个整数数组 nums 和一个目标值 target,
6
+ * 请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
7
+ */
8
+
9
+ function twoSum(nums, target) {
10
+ const map = new Map();
11
+ for (let i = 0; i < nums.length; i++) {
12
+ const complement = target - nums[i];
13
+ if (map.has(complement)) {
14
+ return [map.get(complement), i];
15
+ }
16
+ map.set(nums[i], i);
17
+ }
18
+ return [];
19
+ }
20
+
21
+ // 测试用例
22
+ console.log("测试用例 1: nums = [2, 7, 11, 15], target = 9");
23
+ console.log("结果:", twoSum([2, 7, 11, 15], 9)); // 预期 [0, 1]
24
+
25
+ console.log("测试用例 2: nums = [3, 2, 4], target = 6");
26
+ console.log("结果:", twoSum([3, 2, 4], 6)); // 预期 [1, 2]
problems/02_fibonacci.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @title 2. 斐波那契数列 (Fibonacci)
3
+ *
4
+ * 题目描述:
5
+ * 写一个函数,输入 n,求斐波那契(Fibonacci)数列的第 n 项。
6
+ * F(0) = 0, F(1) = 1
7
+ * F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
8
+ */
9
+
10
+ function fib(n) {
11
+ if (n < 2) return n;
12
+ let p = 0, q = 0, r = 1;
13
+ for (let i = 2; i <= n; i++) {
14
+ p = q;
15
+ q = r;
16
+ r = p + q;
17
+ }
18
+ return r;
19
+ }
20
+
21
+ // 测试用例
22
+ console.log("Fib(2) =", fib(2)); // 1
23
+ console.log("Fib(5) =", fib(5)); // 5
24
+ console.log("Fib(10) =", fib(10)); // 55
problems/map.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @title map.js
3
+ * @description 在此处添加题目描述
4
+ */
5
+
6
+ let a = [1,2,3].map(n => n*2)
7
+ console.log(a)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ flask
templates/index.html ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Code Interview Showcase</title>
7
+ <!-- Split.js for Resizable Layout -->
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.0/split.min.js"></script>
9
+ <!-- JSON Formatter for Console -->
10
+ <script src="https://cdn.jsdelivr.net/npm/json-formatter-js@2.3.4/dist/json-formatter.umd.min.js"></script>
11
+ <style>
12
+ :root {
13
+ --bg-color: #1e1e1e;
14
+ --sidebar-bg: #252526;
15
+ --sidebar-hover: #2a2d2e;
16
+ --sidebar-active: #37373d;
17
+ --border-color: #333;
18
+ --text-color: #cccccc;
19
+ --accent-color: #007acc;
20
+ --console-bg: #1e1e1e;
21
+ --console-text: #e0e0e0;
22
+ }
23
+
24
+ * {
25
+ box-sizing: border-box;
26
+ margin: 0;
27
+ padding: 0;
28
+ }
29
+
30
+ body {
31
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
32
+ background-color: var(--bg-color);
33
+ color: var(--text-color);
34
+ height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ overflow: hidden;
38
+ }
39
+
40
+ header {
41
+ height: 50px;
42
+ background-color: #333333;
43
+ display: flex;
44
+ align-items: center;
45
+ padding: 0 20px;
46
+ border-bottom: 1px solid var(--border-color);
47
+ justify-content: space-between;
48
+ flex-shrink: 0; /* Prevent shrinking */
49
+ }
50
+
51
+ h1 {
52
+ font-size: 18px;
53
+ font-weight: 500;
54
+ color: #fff;
55
+ }
56
+
57
+ .run-btn {
58
+ background-color: #0e639c;
59
+ color: white;
60
+ border: none;
61
+ padding: 6px 16px;
62
+ border-radius: 2px;
63
+ cursor: pointer;
64
+ font-size: 14px;
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 6px;
68
+ }
69
+
70
+ .run-btn:hover {
71
+ background-color: #1177bb;
72
+ }
73
+
74
+ /* Split.js Layout */
75
+ .main-container {
76
+ display: flex;
77
+ flex: 1;
78
+ overflow: hidden;
79
+ height: calc(100vh - 50px);
80
+ }
81
+
82
+ /* Gutter Styles for Split.js */
83
+ .gutter {
84
+ background-color: var(--bg-color);
85
+ background-repeat: no-repeat;
86
+ background-position: 50%;
87
+ cursor: col-resize;
88
+ }
89
+ .gutter:hover {
90
+ background-color: var(--accent-color);
91
+ }
92
+
93
+ /* Left Sidebar: Problem List */
94
+ .sidebar {
95
+ background-color: var(--sidebar-bg);
96
+ display: flex;
97
+ flex-direction: column;
98
+ overflow: hidden;
99
+ /* width is handled by Split.js */
100
+ }
101
+
102
+ .sidebar-header {
103
+ padding: 10px 15px;
104
+ font-size: 12px;
105
+ text-transform: uppercase;
106
+ font-weight: 600;
107
+ color: #888;
108
+ display: flex;
109
+ justify-content: space-between;
110
+ align-items: center;
111
+ border-bottom: 1px solid var(--border-color);
112
+ }
113
+
114
+ .add-btn {
115
+ background: none;
116
+ border: none;
117
+ color: #888;
118
+ cursor: pointer;
119
+ font-size: 18px;
120
+ line-height: 1;
121
+ }
122
+ .add-btn:hover { color: #fff; }
123
+
124
+ .problem-list {
125
+ flex: 1;
126
+ overflow-y: auto;
127
+ list-style: none;
128
+ }
129
+
130
+ .problem-item {
131
+ padding: 8px 15px;
132
+ cursor: pointer;
133
+ font-size: 14px;
134
+ white-space: nowrap;
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
137
+ border-left: 3px solid transparent;
138
+ }
139
+
140
+ .problem-item:hover {
141
+ background-color: var(--sidebar-hover);
142
+ }
143
+
144
+ .problem-item.active {
145
+ background-color: var(--sidebar-active);
146
+ border-left-color: var(--accent-color);
147
+ color: #fff;
148
+ }
149
+
150
+ /* Center: Editor */
151
+ .editor-container {
152
+ display: flex;
153
+ flex-direction: column;
154
+ position: relative;
155
+ overflow: hidden;
156
+ }
157
+
158
+ #monaco-editor {
159
+ width: 100%;
160
+ height: 100%;
161
+ }
162
+
163
+ /* Right: Console Output */
164
+ .output-panel {
165
+ background-color: var(--console-bg);
166
+ display: flex;
167
+ flex-direction: column;
168
+ overflow: hidden;
169
+ }
170
+
171
+ .output-header {
172
+ padding: 10px 15px;
173
+ background-color: #252526;
174
+ border-bottom: 1px solid var(--border-color);
175
+ font-size: 12px;
176
+ text-transform: uppercase;
177
+ font-weight: 600;
178
+ display: flex;
179
+ justify-content: space-between;
180
+ }
181
+
182
+ .clear-btn {
183
+ background: none;
184
+ border: none;
185
+ color: #888;
186
+ cursor: pointer;
187
+ font-size: 12px;
188
+ }
189
+ .clear-btn:hover { color: #fff; }
190
+
191
+ #console-output {
192
+ flex: 1;
193
+ padding: 10px;
194
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
195
+ font-size: 13px;
196
+ overflow-y: auto;
197
+ /* white-space: pre-wrap; Removed to allow JSONFormatter to handle layout */
198
+ color: var(--console-text);
199
+ }
200
+
201
+ .log-line {
202
+ margin-bottom: 4px;
203
+ border-bottom: 1px solid #2a2a2a;
204
+ padding-bottom: 2px;
205
+ display: flex; /* For alignment */
206
+ align-items: flex-start;
207
+ }
208
+ .log-error { color: #f48771; background-color: rgba(255, 0, 0, 0.1); }
209
+ .log-warn { color: #cca700; background-color: rgba(255, 255, 0, 0.1); }
210
+ .log-info { color: #e0e0e0; }
211
+
212
+ /* JSON Formatter Tweaks for Dark Mode */
213
+ .json-formatter-row {
214
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
215
+ }
216
+ .json-formatter-row .json-formatter-key {
217
+ color: #9cdcfe !important;
218
+ }
219
+ .json-formatter-row .json-formatter-string {
220
+ color: #ce9178 !important;
221
+ }
222
+ .json-formatter-row .json-formatter-number {
223
+ color: #b5cea8 !important;
224
+ }
225
+ .json-formatter-row .json-formatter-boolean {
226
+ color: #569cd6 !important;
227
+ }
228
+ .json-formatter-row .json-formatter-null {
229
+ color: #569cd6 !important;
230
+ }
231
+
232
+ .log-content {
233
+ display: flex;
234
+ flex-wrap: wrap;
235
+ gap: 8px;
236
+ width: 100%;
237
+ }
238
+
239
+ /* Loading Overlay */
240
+ .loading {
241
+ display: flex;
242
+ justify-content: center;
243
+ align-items: center;
244
+ height: 100%;
245
+ color: #888;
246
+ }
247
+ </style>
248
+ </head>
249
+ <body>
250
+
251
+ <header>
252
+ <h1>Code Interview Showcase</h1>
253
+ <button class="run-btn" onclick="runCode()">
254
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
255
+ <path d="M4 2.5L13.5 8L4 13.5V2.5Z"/>
256
+ </svg>
257
+ 运行代码 (Run)
258
+ </button>
259
+ </header>
260
+
261
+ <div class="main-container">
262
+ <div id="split-0" class="sidebar">
263
+ <div class="sidebar-header">
264
+ 题目列表
265
+ <button class="add-btn" onclick="createProblem()" title="新建题目">+</button>
266
+ </div>
267
+ <ul class="problem-list" id="problem-list">
268
+ <!-- Items will be injected here -->
269
+ <div class="loading">Loading...</div>
270
+ </ul>
271
+ </div>
272
+
273
+ <div id="split-1" class="editor-container">
274
+ <div id="monaco-editor"></div>
275
+ </div>
276
+
277
+ <div id="split-2" class="output-panel">
278
+ <div class="output-header">
279
+ <span>Console</span>
280
+ <button class="clear-btn" onclick="clearConsole()">Clear</button>
281
+ </div>
282
+ <div id="console-output">
283
+ <div class="log-info">
284
+ <span class="log-content">Click 'Run' to see output...</span>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <!-- Monaco Editor Loader -->
291
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
292
+ <script>
293
+ let editor;
294
+ let currentProblemId = null;
295
+ let autoSaveTimer = null;
296
+ let isInternalUpdate = false;
297
+
298
+ // Initialize Split.js
299
+ Split(['#split-0', '#split-1', '#split-2'], {
300
+ sizes: [20, 50, 30], // Initial percentages
301
+ minSize: [150, 200, 200],
302
+ gutterSize: 6,
303
+ cursor: 'col-resize',
304
+ onDragEnd: function() {
305
+ if (editor) editor.layout(); // Resize editor when layout changes
306
+ }
307
+ });
308
+
309
+ require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
310
+
311
+ require(['vs/editor/editor.main'], function() {
312
+ // Enhance JS/TS IntelliSense
313
+ monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
314
+ target: monaco.languages.typescript.ScriptTarget.ES2020,
315
+ allowNonTsExtensions: true,
316
+ moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
317
+ module: monaco.languages.typescript.ModuleKind.CommonJS,
318
+ noEmit: true,
319
+ // Enable type checking for better suggestions
320
+ checkJs: true
321
+ });
322
+
323
+ // Add some basic lib definitions if needed (optional)
324
+ // monaco.languages.typescript.javascriptDefaults.addExtraLib(...)
325
+
326
+ editor = monaco.editor.create(document.getElementById('monaco-editor'), {
327
+ value: '// Select a problem to start coding...',
328
+ language: 'javascript',
329
+ theme: 'vs-dark',
330
+ automaticLayout: true, // Auto resize with container
331
+ minimap: { enabled: false },
332
+ fontSize: 14,
333
+ scrollBeyondLastLine: false,
334
+ // IntelliSense features
335
+ suggest: {
336
+ showWords: false // Prefer intelligent suggestions over text matches
337
+ },
338
+ quickSuggestions: true,
339
+ parameterHints: { enabled: true }
340
+ });
341
+
342
+ // Initialize
343
+ fetchProblems();
344
+
345
+ // Auto Save Listener
346
+ editor.onDidChangeModelContent(() => {
347
+ if (currentProblemId && !isInternalUpdate) {
348
+ clearTimeout(autoSaveTimer);
349
+ autoSaveTimer = setTimeout(saveProblem, 1000);
350
+ }
351
+ });
352
+ });
353
+
354
+ async function fetchProblems(selectId = null) {
355
+ try {
356
+ const res = await fetch('/api/problems');
357
+ const problems = await res.json();
358
+ renderProblemList(problems);
359
+
360
+ if (problems.length > 0) {
361
+ const target = selectId
362
+ ? problems.find(p => p.id === selectId)
363
+ : (!currentProblemId ? problems[0] : problems.find(p => p.id === currentProblemId));
364
+
365
+ if (target) loadProblem(target);
366
+ else if (problems.length > 0) loadProblem(problems[0]);
367
+ }
368
+ } catch (e) {
369
+ console.error("Failed to fetch problems:", e);
370
+ }
371
+ }
372
+
373
+ function renderProblemList(problems) {
374
+ const listEl = document.getElementById('problem-list');
375
+ listEl.innerHTML = '';
376
+
377
+ problems.forEach(p => {
378
+ const li = document.createElement('li');
379
+ li.className = 'problem-item';
380
+ li.textContent = p.title;
381
+ li.dataset.id = p.id;
382
+ li.onclick = () => loadProblem(p);
383
+ listEl.appendChild(li);
384
+ });
385
+
386
+ if (currentProblemId) {
387
+ const activeEl = listEl.querySelector(`.problem-item[data-id="${currentProblemId}"]`);
388
+ if (activeEl) activeEl.classList.add('active');
389
+ }
390
+ }
391
+
392
+ async function loadProblem(problem) {
393
+ if (currentProblemId === problem.id && editor.getValue() === problem.content) return;
394
+
395
+ currentProblemId = problem.id;
396
+
397
+ document.querySelectorAll('.problem-item').forEach(el => {
398
+ el.classList.toggle('active', el.dataset.id === problem.id);
399
+ });
400
+
401
+ if (editor) {
402
+ isInternalUpdate = true;
403
+ editor.setValue(problem.content);
404
+ isInternalUpdate = false;
405
+ }
406
+
407
+ clearConsole();
408
+ logToConsole(['Loaded:', problem.title], 'info');
409
+ }
410
+
411
+ async function createProblem() {
412
+ const filename = prompt("请输入新题目文件名 (例如: 03_new_problem):", "new_problem");
413
+ if (!filename) return;
414
+
415
+ try {
416
+ const res = await fetch('/api/problems', {
417
+ method: 'POST',
418
+ headers: { 'Content-Type': 'application/json' },
419
+ body: JSON.stringify({ filename })
420
+ });
421
+
422
+ if (res.ok) {
423
+ const newProblem = await res.json();
424
+ await fetchProblems(newProblem.id);
425
+ logToConsole(['Created:', newProblem.title], 'info');
426
+ } else {
427
+ const err = await res.json();
428
+ alert(`Error: ${err.error}`);
429
+ }
430
+ } catch (e) {
431
+ alert(`Error: ${e.message}`);
432
+ }
433
+ }
434
+
435
+ async function saveProblem() {
436
+ if (!currentProblemId || !editor) return;
437
+
438
+ const content = editor.getValue();
439
+
440
+ try {
441
+ const res = await fetch(`/api/problems/${currentProblemId}`, {
442
+ method: 'PUT',
443
+ headers: { 'Content-Type': 'application/json' },
444
+ body: JSON.stringify({ content })
445
+ });
446
+
447
+ if (res.ok) {
448
+ const updatedMeta = await res.json();
449
+ const listItem = document.querySelector(`.problem-item[data-id="${currentProblemId}"]`);
450
+ if (listItem && listItem.textContent !== updatedMeta.title) {
451
+ listItem.textContent = updatedMeta.title;
452
+ }
453
+ }
454
+ } catch (e) {
455
+ console.error("Save failed:", e);
456
+ logToConsole(["Auto-save failed"], 'error');
457
+ }
458
+ }
459
+
460
+ function runCode() {
461
+ if (!editor) return;
462
+ const code = editor.getValue();
463
+
464
+ clearConsole();
465
+ logToConsole(["Running..."], 'info');
466
+
467
+ const originalLog = console.log;
468
+ const originalError = console.error;
469
+ const originalWarn = console.warn;
470
+
471
+ try {
472
+ console.log = (...args) => {
473
+ logToConsole(args, 'info');
474
+ };
475
+ console.error = (...args) => {
476
+ logToConsole(args, 'error');
477
+ };
478
+ console.warn = (...args) => {
479
+ logToConsole(args, 'warn');
480
+ };
481
+
482
+ // Using new Function is safer than eval for scope isolation, but we want quick results
483
+ // Note: Objects printed will be live references if we don't clone them,
484
+ // but for a console that's usually desired or acceptable.
485
+ // If async is needed:
486
+ const wrappedCode = `(async () => { \n${code}\n })();`;
487
+ eval(code);
488
+
489
+ } catch (e) {
490
+ logToConsole([e], 'error');
491
+ } finally {
492
+ console.log = originalLog;
493
+ console.error = originalError;
494
+ console.warn = originalWarn;
495
+ }
496
+ }
497
+
498
+ // New Log Function supporting Objects
499
+ function logToConsole(args, type = 'info') {
500
+ const outputEl = document.getElementById('console-output');
501
+ const lineDiv = document.createElement('div');
502
+ lineDiv.className = `log-line log-${type}`;
503
+
504
+ const contentDiv = document.createElement('div');
505
+ contentDiv.className = 'log-content';
506
+
507
+ args.forEach(arg => {
508
+ if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean' || arg === null || arg === undefined) {
509
+ const span = document.createElement('span');
510
+ span.textContent = String(arg);
511
+ span.style.marginRight = '5px';
512
+ contentDiv.appendChild(span);
513
+ } else {
514
+ // Use JSONFormatter for objects/arrays
515
+ try {
516
+ const formatter = new JSONFormatter(arg, 1, { // 1 = open depth
517
+ theme: 'dark',
518
+ hoverPreviewEnabled: true
519
+ });
520
+ contentDiv.appendChild(formatter.render());
521
+ } catch (e) {
522
+ const span = document.createElement('span');
523
+ span.textContent = String(arg);
524
+ contentDiv.appendChild(span);
525
+ }
526
+ }
527
+ });
528
+
529
+ lineDiv.appendChild(contentDiv);
530
+ outputEl.appendChild(lineDiv);
531
+ outputEl.scrollTop = outputEl.scrollHeight;
532
+ }
533
+
534
+ function clearConsole() {
535
+ const outputEl = document.getElementById('console-output');
536
+ outputEl.innerHTML = '';
537
+ }
538
+ </script>
539
+
540
+ </body>
541
+ </html>