xinyu-pro 0.21.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/.env.example +21 -0
- package/README.md +36 -0
- package/app/api/chat/route.ts +84 -0
- package/app/api/generate-svg/route.ts +171 -0
- package/app/api/generate-theme/route.ts +137 -0
- package/app/api/plugins/bindings/route.ts +173 -0
- package/app/api/plugins/export/route.ts +122 -0
- package/app/api/plugins/export-xye/route.ts +156 -0
- package/app/api/plugins/files/route.ts +146 -0
- package/app/api/plugins/files-list/route.ts +168 -0
- package/app/api/plugins/files-upload/route.ts +101 -0
- package/app/api/plugins/files-write/route.ts +272 -0
- package/app/api/plugins/import/route.ts +140 -0
- package/app/api/plugins/import-package/route.ts +231 -0
- package/app/api/plugins/resources/route.ts +109 -0
- package/app/api/plugins/route.ts +308 -0
- package/app/api/plugins/scan/route.ts +280 -0
- package/app/api/plugins/storage/route.ts +146 -0
- package/app/api/sessions/route.ts +165 -0
- package/app/api/settings/route.ts +40 -0
- package/app/api/suggest-fields/route.ts +129 -0
- package/app/api/templates/route.ts +159 -0
- package/app/api/test-api/route.ts +63 -0
- package/app/editor/page.tsx +1466 -0
- package/app/extensions/create/page.tsx +1422 -0
- package/app/extensions/edit/[id]/page.tsx +2342 -0
- package/app/extensions/page.tsx +1572 -0
- package/app/extensions/tutorial/page.tsx +4258 -0
- package/app/favicon.ico +0 -0
- package/app/fonts/GeistMonoVF.woff +0 -0
- package/app/fonts/GeistVF.woff +0 -0
- package/app/game/[id]/page.tsx +996 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +26 -0
- package/app/loading.tsx +26 -0
- package/app/page.tsx +345 -0
- package/app/settings/page.tsx +1490 -0
- package/bin/cli.js +262 -0
- package/components/ChatInput.tsx +106 -0
- package/components/ChatWindow.tsx +52 -0
- package/components/FullPageLoader.tsx +107 -0
- package/components/LoadingDots.tsx +20 -0
- package/components/MathCurveLoader.tsx +173 -0
- package/components/MessageBubble.tsx +147 -0
- package/components/WorldCardPreview.tsx +98 -0
- package/components/WorldCardUploader.tsx +58 -0
- package/components/ui/ConfirmDialog.tsx +135 -0
- package/components/ui/PageHeader.tsx +99 -0
- package/components/ui/PermissionConflictDialog.tsx +206 -0
- package/components/ui/PluginConfigForm.tsx +192 -0
- package/components/ui/PluginFloatingLayer.tsx +52 -0
- package/components/ui/PluginIcon.tsx +53 -0
- package/components/ui/PluginModalRenderer.tsx +185 -0
- package/components/ui/PluginProvider.tsx +1038 -0
- package/components/ui/PluginSlotRenderer.tsx +76 -0
- package/components/ui/ThemeCustomizer.tsx +174 -0
- package/components/ui/ThemeProvider.tsx +125 -0
- package/components/ui/ThemeSwitcher.tsx +140 -0
- package/components/ui/ToastProvider.tsx +141 -0
- package/lib/builtin-plugins.ts +11 -0
- package/lib/db-init.ts +35 -0
- package/lib/db.ts +244 -0
- package/lib/manifest-parser.ts +185 -0
- package/lib/parseWorldCard.ts +110 -0
- package/lib/plugin-dom-sandbox.ts +327 -0
- package/lib/plugin-events.ts +88 -0
- package/lib/plugin-files.ts +186 -0
- package/lib/plugin-html-sanitizer.ts +79 -0
- package/lib/plugin-resource-tracker.ts +175 -0
- package/lib/plugin-runtime.ts +2287 -0
- package/lib/plugin-security.ts +151 -0
- package/lib/plugin-types.ts +416 -0
- package/lib/prompt-builder.ts +55 -0
- package/lib/router-history.ts +119 -0
- package/lib/storage.ts +381 -0
- package/lib/themes.ts +129 -0
- package/lib/types.ts +117 -0
- package/lib/version.ts +55 -0
- package/next.config.mjs +43 -0
- package/package.json +56 -0
- package/plugins/xinyu.bag-system.xye +0 -0
- package/plugins/xinyu.cache-optimizer.xye +0 -0
- package/plugins/xinyu.dice-arbiter.xye +0 -0
- package/plugins/xinyu.game-auto-start-choices.xye +0 -0
- package/plugins/xinyu.markdown-render.xye +0 -0
- package/plugins/xinyu.slot-ui-beautify.xye +0 -0
- package/plugins/xinyu.world-info.xye +0 -0
- package/postcss.config.mjs +8 -0
- package/public/templates/atlantis.svg +63 -0
- package/public/templates/cyber-city.svg +68 -0
- package/public/templates/jianghu.svg +69 -0
- package/public/templates//351/255/224/346/263/225/344/270/226/347/225/214.svg +137 -0
- package/styles/themes.css +111 -0
- package/tailwind.config.ts +18 -0
- package/tsconfig.json +26 -0
- package/version.json +6 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } = require('fs');
|
|
4
|
+
const { join, resolve } = require('path');
|
|
5
|
+
const { createInterface } = require('readline');
|
|
6
|
+
const { execSync, spawn } = require('child_process');
|
|
7
|
+
const { homedir } = require('os');
|
|
8
|
+
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const APP_DIR = __dirname.includes('node_modules') ? resolve(__dirname, '..') : cwd;
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const HELP_FLAG = args.includes('--help') || args.includes('-h');
|
|
14
|
+
const DEV_FLAG = args.includes('--dev');
|
|
15
|
+
const PORT_INDEX = args.indexOf('--port');
|
|
16
|
+
const CUSTOM_PORT = PORT_INDEX !== -1 ? args[PORT_INDEX + 1] : '3000';
|
|
17
|
+
const BUILD_FLAG = args.includes('--build');
|
|
18
|
+
|
|
19
|
+
function echo(msg, type = 'info') {
|
|
20
|
+
const colors = {
|
|
21
|
+
info: '\x1b[36m',
|
|
22
|
+
success: '\x1b[32m',
|
|
23
|
+
warn: '\x1b[33m',
|
|
24
|
+
error: '\x1b[31m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
reset: '\x1b[0m',
|
|
27
|
+
bold: '\x1b[1m',
|
|
28
|
+
};
|
|
29
|
+
const color = colors[type] || colors.info;
|
|
30
|
+
console.log(`${color}${msg}${colors.reset}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ask(question) {
|
|
34
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
rl.question(question, (answer) => {
|
|
37
|
+
rl.close();
|
|
38
|
+
resolve(answer.trim());
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function showHelp() {
|
|
44
|
+
echo('');
|
|
45
|
+
echo(' ╔══════════════════════════════════════╗', 'bold');
|
|
46
|
+
echo(' ║ 星语 Pro (xinyu-pro) ║', 'bold');
|
|
47
|
+
echo(' ║ AI 驱动的互动叙事平台 ║', 'bold');
|
|
48
|
+
echo(' ╚══════════════════════════════════════╝', 'bold');
|
|
49
|
+
echo('');
|
|
50
|
+
echo(' 用法:', 'bold');
|
|
51
|
+
echo(' $ xinyu 启动服务器(自动构建)');
|
|
52
|
+
echo(' $ xinyu --dev 开发模式(热更新)');
|
|
53
|
+
echo(' $ xinyu --port 4000 指定端口');
|
|
54
|
+
echo(' $ xinyu --build 仅构建不启动');
|
|
55
|
+
echo(' $ xinyu --help 显示帮助');
|
|
56
|
+
echo('');
|
|
57
|
+
echo(' 环境要求:', 'bold');
|
|
58
|
+
echo(' • Node.js >= 18');
|
|
59
|
+
echo(' • MySQL 数据库(需提前运行)');
|
|
60
|
+
echo('');
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkNodeVersion() {
|
|
65
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
66
|
+
if (major < 18) {
|
|
67
|
+
echo(` ✗ Node.js 版本过低: ${process.versions.node},需要 >= 18`, 'error');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectPackageManager() {
|
|
73
|
+
if (existsSync(join(APP_DIR, 'yarn.lock'))) return 'yarn';
|
|
74
|
+
if (existsSync(join(APP_DIR, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
75
|
+
return 'npm';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getNextCliPath() {
|
|
79
|
+
const possiblePaths = [
|
|
80
|
+
join(APP_DIR, 'node_modules', '.bin', 'next'),
|
|
81
|
+
join(APP_DIR, 'node_modules', 'next', 'dist', 'bin', 'next'),
|
|
82
|
+
join(cwd, 'node_modules', '.bin', 'next'),
|
|
83
|
+
];
|
|
84
|
+
for (const p of possiblePaths) {
|
|
85
|
+
if (existsSync(p)) return p;
|
|
86
|
+
}
|
|
87
|
+
return 'next';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkDependencies() {
|
|
91
|
+
if (!existsSync(join(APP_DIR, 'node_modules'))) {
|
|
92
|
+
echo(' ⚠ 未检测到 node_modules,正在安装依赖...', 'warn');
|
|
93
|
+
const pm = detectPackageManager();
|
|
94
|
+
const cmd = pm === 'yarn' ? 'yarn install' : pm === 'pnpm' ? 'pnpm install' : 'npm install';
|
|
95
|
+
echo(` $ ${cmd}`, 'dim');
|
|
96
|
+
execSync(cmd, { cwd: APP_DIR, stdio: 'inherit' });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setupEnvFile() {
|
|
103
|
+
const envPath = join(APP_DIR, '.env.local');
|
|
104
|
+
const examplePath = join(APP_DIR, '.env.example');
|
|
105
|
+
|
|
106
|
+
if (existsSync(envPath)) {
|
|
107
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
108
|
+
if (content.includes('your-api-key') || content.includes('你的密码')) {
|
|
109
|
+
echo(' ⚠ .env.local 中存在占位值,建议修改为真实配置', 'warn');
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
echo(' ⚠ 未检测到 .env.local 配置文件', 'warn');
|
|
115
|
+
echo('');
|
|
116
|
+
|
|
117
|
+
if (!existsSync(examplePath)) {
|
|
118
|
+
echo(' ✗ 未找到 .env.example,请手动创建 .env.local', 'error');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const example = readFileSync(examplePath, 'utf-8');
|
|
123
|
+
writeFileSync(envPath, example, 'utf-8');
|
|
124
|
+
echo(' ✓ 已从 .env.example 创建 .env.local', 'success');
|
|
125
|
+
echo('');
|
|
126
|
+
|
|
127
|
+
const setupInteractive = async () => {
|
|
128
|
+
echo(' ── 请填写以下必要配置(直接回车可跳过) ──', 'bold');
|
|
129
|
+
echo('');
|
|
130
|
+
|
|
131
|
+
const apiKey = await ask(' AI API 密钥 (AI_API_KEY): ');
|
|
132
|
+
if (apiKey) {
|
|
133
|
+
let content = readFileSync(envPath, 'utf-8');
|
|
134
|
+
content = content.replace(/AI_API_KEY=.*/, `AI_API_KEY=${apiKey}`);
|
|
135
|
+
writeFileSync(envPath, content, 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const dbPassword = await ask(' 数据库密码 (DB_PASSWORD,默认空): ');
|
|
139
|
+
if (dbPassword) {
|
|
140
|
+
let content = readFileSync(envPath, 'utf-8');
|
|
141
|
+
content = content.replace(/DB_PASSWORD=.*/, `DB_PASSWORD=${dbPassword}`);
|
|
142
|
+
writeFileSync(envPath, content, 'utf-8');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const dbName = await ask(' 数据库名称 (DB_NAME,默认 xinyu): ');
|
|
146
|
+
if (dbName) {
|
|
147
|
+
let content = readFileSync(envPath, 'utf-8');
|
|
148
|
+
content = content.replace(/DB_NAME=.*/, `DB_NAME=${dbName}`);
|
|
149
|
+
writeFileSync(envPath, content, 'utf-8');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
echo('');
|
|
153
|
+
echo(' ✓ .env.local 配置完成', 'success');
|
|
154
|
+
echo(' ✎ 如需修改,请编辑 ' + envPath, 'dim');
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return setupInteractive();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function checkBuildExists() {
|
|
161
|
+
return existsSync(join(APP_DIR, '.next', 'BUILD_ID'));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function runBuild() {
|
|
165
|
+
echo(' ■ 正在构建应用,首次构建可能需要 1-2 分钟...', 'info');
|
|
166
|
+
const nextCli = getNextCliPath();
|
|
167
|
+
try {
|
|
168
|
+
execSync(`"${process.execPath}" "${nextCli}" build`, {
|
|
169
|
+
cwd: APP_DIR,
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
});
|
|
172
|
+
echo(' ✓ 构建完成', 'success');
|
|
173
|
+
} catch (e) {
|
|
174
|
+
echo(' ✗ 构建失败,请检查代码错误', 'error');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function startServer(port, devMode) {
|
|
180
|
+
const nextCli = getNextCliPath();
|
|
181
|
+
|
|
182
|
+
echo('');
|
|
183
|
+
echo(` ╔══════════════════════════════════════╗`, 'success');
|
|
184
|
+
echo(` ║ 星语 Pro 服务器已启动 ║`, 'success');
|
|
185
|
+
echo(` ║ 访问地址: http://localhost:${port} ║`, 'success');
|
|
186
|
+
echo(` ║ 按 Ctrl+C 停止 ║`, 'success');
|
|
187
|
+
echo(` ╚══════════════════════════════════════╝`, 'success');
|
|
188
|
+
echo('');
|
|
189
|
+
|
|
190
|
+
const cmd = devMode ? 'dev' : 'start';
|
|
191
|
+
const child = spawn(process.execPath, [nextCli, cmd, '-p', port], {
|
|
192
|
+
cwd: APP_DIR,
|
|
193
|
+
stdio: 'inherit',
|
|
194
|
+
env: {
|
|
195
|
+
...process.env,
|
|
196
|
+
PORT: port,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
child.on('error', (err) => {
|
|
201
|
+
echo(` ✗ 启动失败: ${err.message}`, 'error');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
child.on('exit', (code) => {
|
|
206
|
+
if (code !== 0 && code !== null) {
|
|
207
|
+
echo(` ✗ 进程异常退出 (code: ${code})`, 'error');
|
|
208
|
+
}
|
|
209
|
+
process.exit(code || 0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
process.on('SIGINT', () => {
|
|
213
|
+
echo('');
|
|
214
|
+
echo(' ■ 正在关闭服务器...', 'info');
|
|
215
|
+
child.kill('SIGINT');
|
|
216
|
+
process.exit(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
process.on('SIGTERM', () => {
|
|
220
|
+
child.kill('SIGTERM');
|
|
221
|
+
process.exit(0);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function main() {
|
|
226
|
+
echo('');
|
|
227
|
+
echo(' ╔══════════════════════════════════════╗', 'bold');
|
|
228
|
+
echo(' ║ 星语 Pro (xinyu-pro) ║', 'bold');
|
|
229
|
+
echo(' ║ 启动中... ║', 'bold');
|
|
230
|
+
echo(' ╚══════════════════════════════════════╝', 'bold');
|
|
231
|
+
echo('');
|
|
232
|
+
|
|
233
|
+
if (HELP_FLAG) {
|
|
234
|
+
showHelp();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
checkNodeVersion();
|
|
239
|
+
checkDependencies();
|
|
240
|
+
|
|
241
|
+
await setupEnvFile();
|
|
242
|
+
|
|
243
|
+
if (BUILD_FLAG) {
|
|
244
|
+
runBuild();
|
|
245
|
+
echo(' ✓ 构建完成,可使用 xinyu 启动服务器', 'success');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!DEV_FLAG && !checkBuildExists()) {
|
|
250
|
+
echo(' ⚠ 未检测到构建产物,将自动执行构建', 'warn');
|
|
251
|
+
echo(' (首次构建需要下载依赖并编译 TypeScript,请耐心等待)', 'dim');
|
|
252
|
+
echo('');
|
|
253
|
+
runBuild();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
startServer(CUSTOM_PORT, DEV_FLAG);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
main().catch((e) => {
|
|
260
|
+
echo(` ✗ 启动失败: ${e.message}`, 'error');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ChatInputProps {
|
|
6
|
+
onSend: (message: string) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** 可选的外部 ref,允许外部获取 textarea 元素引用 */
|
|
10
|
+
inputRef?: React.Ref<HTMLTextAreaElement>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(function ChatInput({
|
|
14
|
+
onSend,
|
|
15
|
+
disabled = false,
|
|
16
|
+
placeholder = '描述你想做的事、说的话...',
|
|
17
|
+
inputRef,
|
|
18
|
+
}, ref) {
|
|
19
|
+
const [input, setInput] = useState('');
|
|
20
|
+
const internalRef = useRef<HTMLTextAreaElement>(null);
|
|
21
|
+
|
|
22
|
+
// 合并内部 ref 和外部 ref,同时支持 forwardRef 和 inputRef
|
|
23
|
+
useImperativeHandle(ref, () => internalRef.current!, []);
|
|
24
|
+
|
|
25
|
+
// 同步 inputRef
|
|
26
|
+
const setTextareaRef = (el: HTMLTextAreaElement | null) => {
|
|
27
|
+
(internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = el;
|
|
28
|
+
if (typeof inputRef === 'function') {
|
|
29
|
+
inputRef(el);
|
|
30
|
+
} else if (inputRef) {
|
|
31
|
+
(inputRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = el;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
const trimmed = input.trim();
|
|
38
|
+
if (!trimmed || disabled) return;
|
|
39
|
+
onSend(trimmed);
|
|
40
|
+
setInput('');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
44
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
handleSubmit(e);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<form
|
|
52
|
+
onSubmit={handleSubmit}
|
|
53
|
+
className="flex items-center gap-2 px-4 py-3 border-t"
|
|
54
|
+
style={{
|
|
55
|
+
borderColor: 'var(--color-border)',
|
|
56
|
+
backgroundColor: 'var(--color-bg-secondary)',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<textarea
|
|
60
|
+
ref={setTextareaRef}
|
|
61
|
+
id="xinyu-chat-input"
|
|
62
|
+
value={input}
|
|
63
|
+
onChange={(e) => setInput(e.target.value)}
|
|
64
|
+
onKeyDown={handleKeyDown}
|
|
65
|
+
placeholder={placeholder}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
rows={1}
|
|
68
|
+
className="flex-1 resize-none px-4 py-2.5 rounded-xl border text-sm outline-none transition-colors"
|
|
69
|
+
style={{
|
|
70
|
+
backgroundColor: 'var(--color-bg-tertiary)',
|
|
71
|
+
borderColor: 'var(--color-border)',
|
|
72
|
+
color: 'var(--color-text-primary)',
|
|
73
|
+
maxHeight: '120px',
|
|
74
|
+
minHeight: '42px',
|
|
75
|
+
}}
|
|
76
|
+
onFocus={(e) => {
|
|
77
|
+
e.target.style.borderColor = 'var(--color-accent)';
|
|
78
|
+
}}
|
|
79
|
+
onBlur={(e) => {
|
|
80
|
+
e.target.style.borderColor = 'var(--color-border)';
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<button
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={disabled || !input.trim()}
|
|
86
|
+
className="flex items-center justify-center w-10 h-10 rounded-xl transition-all shrink-0"
|
|
87
|
+
style={{
|
|
88
|
+
backgroundColor:
|
|
89
|
+
disabled || !input.trim()
|
|
90
|
+
? 'var(--color-bg-tertiary)'
|
|
91
|
+
: 'var(--color-accent)',
|
|
92
|
+
color:
|
|
93
|
+
disabled || !input.trim()
|
|
94
|
+
? 'var(--color-text-muted)'
|
|
95
|
+
: '#fff',
|
|
96
|
+
cursor: disabled || !input.trim() ? 'not-allowed' : 'pointer',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
100
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
101
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
102
|
+
</svg>
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
);
|
|
106
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
|
+
import { ChatMessage } from '@/lib/types';
|
|
5
|
+
import { MessageBubble } from './MessageBubble';
|
|
6
|
+
import { LoadingDots } from './LoadingDots';
|
|
7
|
+
|
|
8
|
+
interface ChatWindowProps {
|
|
9
|
+
messages: ChatMessage[];
|
|
10
|
+
isLoading?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChatWindow({ messages, isLoading = false }: ChatWindowProps) {
|
|
14
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
// 自动滚动到最新消息
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
19
|
+
}, [messages, isLoading]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
id="xinyu-chat-window"
|
|
24
|
+
className="flex-1 overflow-y-auto"
|
|
25
|
+
style={{ backgroundColor: 'var(--color-bg-primary)' }}
|
|
26
|
+
>
|
|
27
|
+
{messages.length === 0 && !isLoading && (
|
|
28
|
+
<div className="flex flex-col items-center justify-center h-full gap-3 px-4">
|
|
29
|
+
<div
|
|
30
|
+
className="text-4xl"
|
|
31
|
+
style={{ color: 'var(--color-text-muted)' }}
|
|
32
|
+
>
|
|
33
|
+
🌟
|
|
34
|
+
</div>
|
|
35
|
+
<p className="text-sm text-center" style={{ color: 'var(--color-text-muted)' }}>
|
|
36
|
+
你的冒险即将开始...
|
|
37
|
+
<br />
|
|
38
|
+
描述你想做的第一件事吧
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
{messages.map((msg, idx) => (
|
|
44
|
+
<MessageBubble key={idx} message={msg} />
|
|
45
|
+
))}
|
|
46
|
+
|
|
47
|
+
{isLoading && <LoadingDots />}
|
|
48
|
+
|
|
49
|
+
<div ref={bottomRef} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import MathCurveLoader from './MathCurveLoader';
|
|
5
|
+
|
|
6
|
+
interface FullPageLoaderProps {
|
|
7
|
+
/** 提示文字,默认 "加载中..." */
|
|
8
|
+
text?: string;
|
|
9
|
+
/** 自定义 className */
|
|
10
|
+
className?: string;
|
|
11
|
+
/** 最小显示时间(ms),默认 500。挂载后低于此时间被卸载则不显示内容 */
|
|
12
|
+
minShowMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function FullPageLoader({ text = '加载中...', className, minShowMs = 500 }: FullPageLoaderProps) {
|
|
16
|
+
const [visible, setVisible] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const timer = setTimeout(() => setVisible(true), minShowMs);
|
|
20
|
+
return () => clearTimeout(timer);
|
|
21
|
+
}, [minShowMs]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={`flex flex-col items-center justify-center gap-6 ${className || ''}`}
|
|
26
|
+
style={{
|
|
27
|
+
minHeight: '100vh',
|
|
28
|
+
backgroundColor: 'var(--color-bg-primary)',
|
|
29
|
+
opacity: visible ? 1 : 0,
|
|
30
|
+
transition: 'opacity 0.3s ease',
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
{/* Logo 符号 - 非线性旋转 */}
|
|
34
|
+
<div className="xinyu-logo-spin" style={{
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
width: '56px',
|
|
39
|
+
height: '56px',
|
|
40
|
+
filter: 'drop-shadow(0 0 20px var(--color-accent))',
|
|
41
|
+
}}>
|
|
42
|
+
<span
|
|
43
|
+
className="select-none"
|
|
44
|
+
style={{
|
|
45
|
+
fontSize: '48px',
|
|
46
|
+
lineHeight: 1,
|
|
47
|
+
color: 'var(--color-accent)',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
✦
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* 艺术字体标题 - 覆盖光效滑动 */}
|
|
55
|
+
<h1
|
|
56
|
+
className="select-none xinyu-shimmer"
|
|
57
|
+
style={{
|
|
58
|
+
fontFamily: "'Georgia', 'Times New Roman', serif",
|
|
59
|
+
fontSize: '28px',
|
|
60
|
+
fontWeight: 300,
|
|
61
|
+
letterSpacing: '0.3em',
|
|
62
|
+
color: 'var(--color-text-primary)',
|
|
63
|
+
margin: 0,
|
|
64
|
+
background: 'linear-gradient(90deg, var(--color-text-primary) 0%, var(--color-text-primary) 30%, var(--color-accent) 50%, var(--color-text-primary) 70%, var(--color-text-primary) 100%)',
|
|
65
|
+
backgroundSize: '300% 100%',
|
|
66
|
+
WebkitBackgroundClip: 'text',
|
|
67
|
+
backgroundClip: 'text',
|
|
68
|
+
WebkitTextFillColor: 'transparent',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
XIN · YU
|
|
72
|
+
</h1>
|
|
73
|
+
|
|
74
|
+
{/* 数学曲线动画 */}
|
|
75
|
+
<MathCurveLoader size={160} strokeWidthScale={1.2} />
|
|
76
|
+
|
|
77
|
+
{/* 提示文字 */}
|
|
78
|
+
<p
|
|
79
|
+
className="text-sm select-none"
|
|
80
|
+
style={{ color: 'var(--color-text-muted)', marginTop: '-8px' }}
|
|
81
|
+
>
|
|
82
|
+
{text}
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
{/* 动画样式 */}
|
|
86
|
+
<style>{`
|
|
87
|
+
@keyframes xinyu-logo-spin {
|
|
88
|
+
0% { transform: rotate(0deg) scale(1); }
|
|
89
|
+
25% { transform: rotate(180deg) scale(1.1); }
|
|
90
|
+
50% { transform: rotate(360deg) scale(1); }
|
|
91
|
+
75% { transform: rotate(540deg) scale(1.1); }
|
|
92
|
+
100% { transform: rotate(720deg) scale(1); }
|
|
93
|
+
}
|
|
94
|
+
.xinyu-logo-spin {
|
|
95
|
+
animation: xinyu-logo-spin 6s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
|
|
96
|
+
}
|
|
97
|
+
@keyframes xinyu-shimmer {
|
|
98
|
+
0% { background-position: 200% 0; }
|
|
99
|
+
100% { background-position: -200% 0; }
|
|
100
|
+
}
|
|
101
|
+
.xinyu-shimmer {
|
|
102
|
+
animation: xinyu-shimmer 5s ease-in-out infinite;
|
|
103
|
+
}
|
|
104
|
+
`}</style>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import MathCurveLoader from './MathCurveLoader';
|
|
5
|
+
|
|
6
|
+
interface LoadingDotsProps {
|
|
7
|
+
/** 提示文字,默认"思考中..." */
|
|
8
|
+
text?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function LoadingDots({ text = '思考中...' }: LoadingDotsProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex items-center gap-2.5 px-4 py-3">
|
|
14
|
+
<MathCurveLoader size={28} strokeWidthScale={0.6} />
|
|
15
|
+
<span className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
|
|
16
|
+
{text}
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|