1
0
Fork 0
This commit is contained in:
koplenov 2026-04-04 21:23:18 +03:00
commit 5bf668e029
46 changed files with 4480 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
README.md Normal file
View file

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

16
index.html Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>my-react-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1000
package-lock.json generated Normal file
View file

@ -0,0 +1,1000 @@
{
"name": "my-react-app",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-react-app",
"version": "0.0.0",
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "~5.9.3",
"vite": "^8.0.1"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@oxc-project/types": {
"version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
"integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
"integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
"integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
"integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
"integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
"integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
"integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
"integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
"integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
"integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
"integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.7",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "24.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.7"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"@rolldown/plugin-babel": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
}
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.4"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
"integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.122.0",
"@rolldown/pluginutils": "1.0.0-rc.12"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.12",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
"@rolldown/binding-darwin-x64": "1.0.0-rc.12",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
"integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.12",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"esbuild": "^0.27.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}
}
}

23
package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "my-react-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "~5.9.3",
"vite": "^8.0.1"
}
}

1
public/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
public/images/windows_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

116
public/recorderWorker.js Normal file
View file

@ -0,0 +1,116 @@
/* Recorder.js worker — https://github.com/mattdiamond/Recorderjs */
var recLength = 0,
recBuffers = [],
sampleRate,
numChannels;
this.onmessage = function(e) {
switch (e.data.command) {
case 'init': init(e.data.config); break;
case 'record': record(e.data.buffer); break;
case 'exportWAV': exportWAV(e.data.type); break;
case 'getBuffer': getBuffer(); break;
case 'clear': clear(); break;
}
};
function init(config) {
sampleRate = config.sampleRate;
numChannels = config.numChannels;
initBuffers();
}
function record(inputBuffer) {
for (var channel = 0; channel < numChannels; channel++) {
recBuffers[channel].push(inputBuffer[channel]);
}
recLength += inputBuffer[0].length;
}
function exportWAV(type) {
var buffers = [];
for (var channel = 0; channel < numChannels; channel++) {
buffers.push(mergeBuffers(recBuffers[channel], recLength));
}
var interleaved = numChannels === 2
? interleave(buffers[0], buffers[1])
: buffers[0];
var dataview = encodeWAV(interleaved);
var audioBlob = new Blob([dataview], { type: type });
this.postMessage({ command: 'exportWAV', data: audioBlob });
}
function getBuffer() {
var buffers = [];
for (var channel = 0; channel < numChannels; channel++) {
buffers.push(mergeBuffers(recBuffers[channel], recLength));
}
this.postMessage({ command: 'getBuffer', data: buffers });
}
function clear() {
recLength = 0;
recBuffers = [];
initBuffers();
}
function initBuffers() {
for (var channel = 0; channel < numChannels; channel++) {
recBuffers[channel] = [];
}
}
function mergeBuffers(recBuffers, recLength) {
var result = new Float32Array(recLength);
var offset = 0;
for (var i = 0; i < recBuffers.length; i++) {
result.set(recBuffers[i], offset);
offset += recBuffers[i].length;
}
return result;
}
function interleave(inputL, inputR) {
var length = inputL.length + inputR.length;
var result = new Float32Array(length);
var index = 0, inputIndex = 0;
while (inputIndex < inputL.length) {
result[index++] = inputL[inputIndex];
result[index++] = inputR[inputIndex];
inputIndex++;
}
return result;
}
function floatTo16BitPCM(output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
function writeString(view, offset, string) {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function encodeWAV(samples) {
var buffer = new ArrayBuffer(44 + samples.length * 2);
var view = new DataView(buffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + samples.length * 2, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * numChannels * 2, true);
view.setUint16(32, numChannels * 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, samples.length * 2, true);
floatTo16BitPCM(view, 44, samples);
return view;
}

28
public/recorderWorklet.js Normal file
View file

@ -0,0 +1,28 @@
/**
* AudioWorkletProcessor для записи PCM-данных.
* Отправляет Float32Array-чанки на главный поток через port.
*/
class RecorderProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._active = true;
this.port.onmessage = (e) => {
if (e.data === 'stop') this._active = false;
};
}
process(inputs) {
if (!this._active) return false; // выгружаем процессор
const input = inputs[0];
if (input && input.length > 0) {
// Копируем каналы — данные валидны только внутри process()
const channels = input.map(ch => new Float32Array(ch));
// Transferable — избегаем лишних копий
this.port.postMessage(channels, channels.map(c => c.buffer));
}
return true;
}
}
registerProcessor('recorder-processor', RecorderProcessor);

BIN
public/yandex_search.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

267
src/App.css Normal file
View file

@ -0,0 +1,267 @@
.day2-desktop {
background-color: #1a1a1a;
}
.quest-panel {
position: fixed;
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px;
llotext-align: center;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.quest-panel.day2 {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
}
.quest-header {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
color: white;
font-weight: bold;
}
.quest-day {
font-size: 18px;
}
.quest-task {
background: rgba(255,255,255,0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
}
.update-panel-day1 {
position: fixed;
top: 80px;
right: 20px;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 15px;
color: white;
z-index: 1000;
min-width: 280px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.2);
}
.system-info {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.version-icon {
font-size: 32px;
}
.version-details strong {
display: block;
margin-bottom: 5px;
}
.update-status {
font-size: 12px;
color: #4caf50;
margin-top: 5px;
}
.panel-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.update-btn, .end-day-btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.update-btn {
background: #4caf50;
color: white;
}
.update-btn:hover {
background: #45a049;
transform: scale(1.02);
}
.end-day-btn {
background: #ff9800;
color: white;
}
.end-day-btn:hover {
background: #f57c00;
transform: scale(1.02);
}
.update-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.9);
color: white;
padding: 20px 40px;
border-radius: 15px;
font-size: 20px;
font-weight: bold;
z-index: 2000;
animation: fadeInOut 2s ease-in-out;
white-space: nowrap;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
15% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
85% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}
.day1-decoration {
position: fixed;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
pointer-events: none;
}
.watermark {
color: rgba(255,255,255,0.3);
font-size: 14px;
padding: 10px;
background: rgba(0,0,0,0.3);
display: inline-block;
border-radius: 20px;
}
.taskbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
padding: 0 10px;
z-index: 1000;
color: white;
border-top: 1px solid rgba(255,255,255,0.2);
}
.start-button {
background: linear-gradient(135deg, #4caf50, #45a049);
padding: 8px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
margin-right: 20px;
transition: all 0.3s;
}
.start-button:hover {
transform: scale(1.05);
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.taskbar-icons {
flex: 1;
display: flex;
gap: 5px;
overflow-x: auto;
}
.taskbar-item {
padding: 8px 15px;
background: rgba(255,255,255,0.1);
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
white-space: nowrap;
}
.taskbar-item:hover {
background: rgba(255,255,255,0.2);
}
.taskbar-item.minimized {
opacity: 0.6;
}
.system-tray {
display: flex;
align-items: center;
gap: 15px;
margin-left: 20px;
}
.version-badge {
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
}
.clock {
font-family: monospace;
font-size: 14px;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
}
.icons-container {
position: absolute;
top: 80px;
left: 20px;
display: flex;
flex-direction: column;
gap: 20px;
z-index: 10;
}
.quest-panel.day2 {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
}
.icons-container {
position: absolute;
top: 80px;
left: 20px;
display: flex;
flex-direction: column;
gap: 20px;
z-index: 10;
}
.desktop {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}

18
src/App.tsx Normal file
View file

@ -0,0 +1,18 @@
import React, { useState } from 'react';
// import Day1Desktop from './Day1Desktop';
import MainApp from './MainApp';
import ChatterboxTTS from './components/deepfake/ChatterboxTTS';
export type WallpaperType = 'xp' | 'win7' | 'win10';
const App: React.FC = () => {
// const [day1Complete, setDay1Complete] = useState(false);
// if (!day1Complete) {
// return <Day1Desktop onComplete={() => setDay1Complete(true)} />;
// }
return <ChatterboxTTS/>
return <MainApp />;
};
export default App;

126
src/Day1Desktop.css Normal file
View file

@ -0,0 +1,126 @@
.day1-container {
width: 100%;
height: 100%;
font-family: "Raleway", sans-serif;
}
.day1-wallpaper {
width: 100%;
height:100%;
object-fit: cover;
}
.day1-header {
position: absolute;
top: 0;
left: 0;
right: 0;
font-family: "Raleway", sans-serif;
background: rgba(47, 37, 191, 0.4);
padding: 15px;
color: white;
text-align: center;
}
.day1-header-content {
display: flex;
font-family: Arial, Helvetica, sans-serif;
justify-content: space-around;
}
.day1-panel {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(47, 37, 191, 0.4);
padding: 20px;
border-radius: 10px;
color: white;
text-align: center;
min-width: 300px;
}
.day1-version-info {
margin-bottom: 15px;
}
.day1-buttons {
display: flex;
gap: 10px;
justify-content: center;
}
.day1-update-btn {
padding: 10px 20px;
background: #2a0c84;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.day1-update-btn:hover:not(:disabled) {
background: #4313a3;
}
.day1-update-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.day1-end-btn {
padding: 10px 20px;
background: #e0af0f;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.day1-end-btn:hover {
background: #f38518;
}
.day1-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.day1-modal {
background: white;
padding: 20px;
border-radius: 10px;
text-align: center;
max-width: 400px;
}
.day1-modal-title {
color: #f44336;
margin-bottom: 10px;
}
.day1-modal-text {
margin-bottom: 20px;
}
.day1-modal-btn {
padding: 10px 20px;
background: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.day1-modal-btn:hover {
background: #45a049;
}

100
src/Day1Desktop.tsx Normal file
View file

@ -0,0 +1,100 @@
import React, { useState } from 'react';
import type { WallpaperType } from './App';
import './Day1Desktop.css';
interface Day1DesktopProps {
onComplete: (type: WallpaperType) => void;
}
const Day1Desktop: React.FC<Day1DesktopProps> = ({ onComplete }) => {
const [version, setVersion] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const getImage = () => {
if (version === 1) return '/images/windows_xp.jpg';
if (version === 2) return '/images/windows_7.jpg';
return '/images/windows_10.jpg';
};
const getVersionName = () => {
if (version === 1) return 'Windows XP';
if (version === 2) return 'Windows 7';
return 'Windows 10';
};
const getWallpaperType = (): WallpaperType => {
if (version === 1) return 'xp';
if (version === 2) return 'win7';
return 'win10';
};
const updateSystem = () => {
if (version === 1) {
setLoading(true);
setTimeout(() => {
setVersion(2);
setLoading(false);
}, 2000);
} else if (version === 2) {
setLoading(true);
setTimeout(() => {
setVersion(3);
setLoading(false);
}, 2000);
}
};
const endDay = () => {
if (version === 3) {
onComplete(getWallpaperType());
} else {
setError(`Ошибка! Нельзя завершить день на старой версии ${getVersionName()}. Нужно обновиться до Windows 10`);
}
};
return (
<div className="day1-container">
<img src={getImage()} alt="wallpaper" className="day1-wallpaper" />
<div className="day1-header">
<div className="day1-header-content">
<span>Кейс 1: Обновление системы</span>
<span>Выберите наиболее подходящую версию Windows</span>
</div>
</div>
<div className="day1-panel">
<div className="day1-version-info">
<strong>Текущая версия:</strong> {getVersionName()}
</div>
<div className="day1-buttons">
{version !== 3 && (
<button
className="day1-update-btn"
onClick={updateSystem}
disabled={loading}
>
{loading ? 'Обновление...' : 'Обновить до Windows 10'}
</button>
)}
<button className="day1-end-btn" onClick={endDay}>
Закончить день 1
</button>
</div>
</div>
{error && (
<div className="day1-modal-overlay">
<div className="day1-modal">
<h3 className="day1-modal-title">Ошибка</h3>
<p className="day1-modal-text">{error}</p>
<button className="day1-modal-btn" onClick={() => setError('')}>OK</button>
</div>
</div>
)}
</div>
);
};
export default Day1Desktop;

98
src/MainApp.tsx Normal file
View file

@ -0,0 +1,98 @@
import React, { useState } from 'react';
import DesktopIcon from './components/DesktopIcon';
import Window from './components/Window';
import Sidebar from './components/Sidebar';
import './App.css';
import VScodeApp from './apps/vscode/vscode';
import YandexApp from './apps/yandex/Yandex';
import TerminalApp from './apps/terminal/Terminal';
export interface WindowType {
id: string;
title: string;
content: any;
icon: string;
isMinimized: boolean;
url: string;
}
const MainApp: React.FC = () => {
const [windows, setWindows] = useState<WindowType[]>([]);
const [nextId, setNextId] = useState(1);
const openWindow = (title: string, content: any, icon: string, url: string) => {
const newWindow: WindowType = {
id: `window-${nextId}`,
title,
content,
icon,
isMinimized: false,
url,
};
setWindows([...windows, newWindow]);
setNextId(nextId + 1);
};
const closeWindow = (id: string) => {
setWindows(windows.filter(w => w.id !== id));
};
const minimizeWindow = (id: string) => {
setWindows(windows.map(w =>
w.id === id ? { ...w, isMinimized: !w.isMinimized } : w
));
};
const restoreWindow = (id: string) => {
setWindows(windows.map(w =>
w.id === id ? { ...w, isMinimized: false } : w
));
};
const openVS = () => {
openWindow('VS Code', <VScodeApp />, '💻', 'https://code.visualstudio.com');
};
const openYandex = () => {
openWindow('Yandex', <YandexApp />, '🌐', 'https://yandex.ru');
};
const openTerminal = () => {
openWindow('Terminal', <TerminalApp />, 'T', 'https://oop.com');
};
return (
<div className="desktop" style={{
backgroundImage: 'url(/images/windows_10.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '100%',
height: '100%',
position: 'relative',
overflow: 'hidden'
}}>
<div className="icons-container">
<DesktopIcon icon="💻" name="VS Code" onClick={openVS} />
<DesktopIcon icon="🌐" name="Yandex" onClick={openYandex} />
<DesktopIcon icon="T" name="Terminal" onClick={openTerminal} />
</div>
{windows.map(window => !window.isMinimized && (
<Window
key={window.id}
window={window}
onClose={closeWindow}
onMinimize={minimizeWindow}
/>
))}
<Sidebar
windows={windows}
onRestore={restoreWindow}
onClose={closeWindow}
/>
</div>
);
};
export default MainApp;

View file

@ -0,0 +1,101 @@
import { useState, useRef, useEffect } from 'react';
import type { KeyboardEvent } from 'react';
const TerminalApp = () => {
const [currentTaskState, setTaskState] = useState(false);
const [is_updated_libraries_state, set_updated_libraries_state] = useState(false);
const [input, setInput] = useState('');
const [history, setHistory] = useState([
{ type: 'system', text: 'Добро пожаловать в мини-терминал. Введите "help" для списка команд.' }
]);
const bottomRef = useRef<HTMLDivElement>(null);
// Автопрокрутка вниз при новых сообщениях
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [history]);
const commands = {
help: () => 'Доступные команды: help, clear, apt',
"apt": () => "apt is package manager. Use 'apt help' for more info",
"apt help": () => "apt avalible commands: apt update; apt upgrade;",
"apt update": () => {
set_updated_libraries_state(true);
return "Yup, package list update, you can update system"
},
"apt upgrade": () => {
if(is_updated_libraries_state) {
setTaskState(true) // mark as done
alert("Done!")
}
return "Yup, system and libraries updated for latest version"
},
clear: () => {
setHistory([]);
return null;
}
};
const handleCommand = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const cleanInput = input.trim().toLowerCase();
const newHistory = [...history, { type: 'user', text: `> ${input}` }];
if (cleanInput) {
if (cleanInput in commands) {
const result = commands[cleanInput as keyof typeof commands]();
if (result){
newHistory.push({ type: 'bot', text: result });
setHistory(newHistory);
}
} else {
newHistory.push({ type: 'error', text: `Команда "${cleanInput}" не найдена. Введите "help".` });
setHistory(newHistory);
}
}
setInput('');
}
};
return (
<div style={styles.container}>
<div style={styles.terminal}>
{history.map((line, i) => (
<div key={i} style={{ ...styles.line, ...styles[line.type as keyof typeof styles] }}>
{line.text}
</div>
))}
<div ref={bottomRef} />
</div>
<div style={styles.inputLine}>
<span style={styles.prompt}>$</span>
<input
style={styles.input}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleCommand}
autoFocus
/>
</div>
{currentTaskState}
</div>
);
};
const styles = {
container: { backgroundColor: '#1e1e1e', color: '#00ff00', padding: '20px', fontFamily: 'monospace', height: '400px', display: 'flex', flexDirection: 'column' as const, borderRadius: '8px' },
terminal: { flex: 1, overflowY: 'auto' as const, marginBottom: '10px' },
line: { marginBottom: '4px', whiteSpace: 'pre-wrap' as const, textAlign: 'justify' as const },
user: { color: '#fff' },
error: { color: '#ff5555' },
system: { color: '#aaa' },
inputLine: { display: 'flex', alignItems: 'center' },
prompt: { marginRight: '8px', fontWeight: 'bold' },
input: { background: 'transparent', border: 'none', color: '#00ff00', outline: 'none', flex: 1, fontFamily: 'monospace', fontSize: '16px' }
};
export default TerminalApp;

View file

@ -0,0 +1,3 @@
.vscode_main_window {
background: blue;
}

View file

@ -0,0 +1,14 @@
import React from 'react';
import './vscode.css';
const VScodeApp: React.FC = () => {
return (
<div className='vscode_main_window'>
<h1>Vscode</h1>
<button>1</button>
<button>22</button>
</div>
);
};
export default VScodeApp;

View file

@ -0,0 +1,5 @@
.yandex {
width: 100%;
height: 100%;
background: url("/yandex_search.jpg");
}

View file

@ -0,0 +1,14 @@
import React from 'react';
import './Yandex.css';
const YandexApp: React.FC = () => {
return (
<div className='yandex'>
{/* <h1>Vscode</h1>
<button>1</button>
<button>22</button> */}
</div>
);
};
export default YandexApp;

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

1
src/assets/react.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

1
src/assets/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -0,0 +1,29 @@
.desktop-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
padding: 10px;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s;
color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
.desktop-icon:hover {
background: rgba(255,255,255,0.2);
transform: scale(1.05);
}
.icon-image {
font-size: 48px;
margin-bottom: 8px;
}
.icon-name {
font-size: 12px;
text-align: center;
font-weight: 500;
}

View file

@ -0,0 +1,54 @@
import React from 'react';
interface DesktopIconProps {
icon: string;
name: string;
onClick: () => void;
}
const DesktopIcon: React.FC<DesktopIconProps> = ({ icon, name, onClick }) => {
const styles = {
desktopIcon: {
display: 'flex' as const,
flexDirection: 'column' as const,
alignItems: 'center',
justifyContent: 'center',
width: '80px',
padding: '10px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'all 0.2s',
color: 'white',
textShadow: '0 1px 2px rgba(0,0,0,0.5)',
},
iconImage: {
fontSize: '48px',
marginBottom: '8px',
},
iconName: {
fontSize: '12px',
textAlign: 'center' as const,
fontWeight: 500,
},
};
return (
<div
style={styles.desktopIcon}
onClick={onClick}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
e.currentTarget.style.transform = 'scale(1.05)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.transform = 'scale(1)';
}}
>
<div style={styles.iconImage}>{icon}</div>
<span style={styles.iconName}>{name}</span>
</div>
);
};
export default DesktopIcon;

View file

@ -0,0 +1,66 @@
.sidebar-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(32, 32, 32, 0.95);
backdrop-filter: blur(10px);
display: flex;
gap: 4px;
padding: 8px 16px;
z-index: 1000;
border-top: 1px solid rgba(255,255,255,0.2);
overflow-x: auto;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(255,255,255,0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: white;
font-size: 13px;
min-width: 120px;
justify-content: space-between;
}
.sidebar-item:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-2px);
}
.sidebar-item.minimized {
background: rgba(255,255,255,0.05);
opacity: 0.7;
}
.sidebar-icon {
font-size: 16px;
}
.sidebar-title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-close {
background: transparent;
border: none;
color: white;
cursor: pointer;
font-size: 14px;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
}
.sidebar-close:hover {
background: rgba(255,255,255,0.2);
color: #ff4444;
}

125
src/components/Sidebar.tsx Normal file
View file

@ -0,0 +1,125 @@
import React from 'react';
interface WindowData {
id: string;
title: string;
content: string;
icon: string;
isMinimized: boolean;
url: string;
}
interface SidebarProps {
windows: WindowData[];
onRestore: (id: string) => void;
onClose: (id: string) => void;
}
const Sidebar: React.FC<SidebarProps> = ({ windows, onRestore, onClose }) => {
const styles = {
sidebar: {
position: 'fixed' as const,
bottom: 0,
left: 0,
right: 0,
background: 'rgba(32, 32, 32, 0.95)',
backdropFilter: 'blur(10px)',
display: 'flex',
gap: '4px',
padding: '8px 16px',
zIndex: 1000,
borderTop: '1px solid rgba(255,255,255,0.2)',
overflowX: 'auto' as const,
},
item: {
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 12px',
background: 'rgba(255,255,255,0.1)',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s',
color: 'white',
fontSize: '13px',
minWidth: '120px',
justifyContent: 'space-between' as const,
},
itemMinimized: {
background: 'rgba(255,255,255,0.05)',
opacity: 0.7,
},
icon: {
fontSize: '16px',
},
title: {
flex: 1,
whiteSpace: 'nowrap' as const,
overflow: 'hidden' as const,
textOverflow: 'ellipsis',
},
closeBtn: {
background: 'transparent',
border: 'none',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
padding: '2px 6px',
borderRadius: '4px',
transition: 'all 0.2s',
},
};
if (windows.length === 0) return null;
return (
<div style={styles.sidebar}>
{windows.map((window) => (
<div
key={window.id}
style={{
...styles.item,
...(window.isMinimized ? styles.itemMinimized : {})
}}
onClick={() => {
if (window.isMinimized) {
onRestore(window.id);
}
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = window.isMinimized
? 'rgba(255,255,255,0.05)'
: 'rgba(255,255,255,0.1)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<span style={styles.icon}>{window.icon}</span>
<span style={styles.title}>{window.title}</span>
<button
style={styles.closeBtn}
onClick={(e) => {
e.stopPropagation();
onClose(window.id);
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
e.currentTarget.style.color = '#ff4444';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = 'white';
}}
>
</button>
</div>
))}
</div>
);
};
export default Sidebar;

View file

@ -0,0 +1,117 @@
.wallpaper-selector {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.selector-container {
background: rgba(255, 255, 255, 0.98);
border-radius: 20px;
padding: 50px;
text-align: center;
box-shadow: 0 30px 80px rgba(0,0,0,0.3);
max-width: 1200px;
width: 90%;
}
.quest-badge {
display: inline-block;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
margin-bottom: 20px;
}
.windows-logo {
font-size: 60px;
margin-bottom: 20px;
}
.selector-container h1 {
font-size: 32px;
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 16px;
margin-bottom: 40px;
}
.wallpaper-options {
display: flex;
gap: 30px;
justify-content: center;
margin-bottom: 50px;
flex-wrap: wrap;
}
.wallpaper-option {
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
border-radius: 12px;
overflow: hidden;
background: #f5f5f5;
}
.wallpaper-option:hover {
transform: translateY(-10px);
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
}
.wallpaper-preview {
width: 280px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
font-size: 60px;
}
.xp-preview {
background: linear-gradient(135deg, #3a8c3a 0%, #1e5c1e 100%);
}
.win7-preview {
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
}
.win10-preview {
background: linear-gradient(135deg, #0078d7 0%, #005a9e 100%);
}
.option-info {
padding: 15px;
background: white;
}
.option-info h3 {
margin: 0 0 5px 0;
color: #333;
font-size: 18px;
}
.option-info p {
margin: 0;
color: #666;
font-size: 14px;
}
.selector-footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #999;
font-size: 14px;
}

View file

@ -0,0 +1,52 @@
import React from 'react';
import type { WallpaperType } from '../App';
import './WallpaperSelector.css';
interface WallpaperSelectorProps {
onSelect: (type: WallpaperType) => void;
}
const WallpaperSelector: React.FC<WallpaperSelectorProps> = ({ onSelect }) => {
return (
<div className="wallpaper-selector">
<div className="selector-container">
<div className="quest-badge">День 1</div>
<div className="windows-logo">🪟</div>
<h1>Квест: Выбор темы</h1>
<p className="subtitle">Попробуй разные версии Windows и почувствуй разницу!</p>
<div className="wallpaper-options">
<div className="wallpaper-option" onClick={() => onSelect('xp')}>
<div className="wallpaper-preview xp-preview">🏔</div>
<div className="option-info">
<h3>Windows XP</h3>
<p>Классический стиль 2001 года</p>
</div>
</div>
<div className="wallpaper-option" onClick={() => onSelect('win7')}>
<div className="wallpaper-preview win7-preview">🐟</div>
<div className="option-info">
<h3>Windows 7</h3>
<p>Современная классика 2009 года</p>
</div>
</div>
<div className="wallpaper-option" onClick={() => onSelect('win10')}>
<div className="wallpaper-preview win10-preview">🪟</div>
<div className="option-info">
<h3>Windows 10</h3>
<p>Современный дизайн 2015 года</p>
</div>
</div>
</div>
<div className="selector-footer">
<p>💡 Подсказка: Выберите любую тему, чтобы продолжить квест</p>
</div>
</div>
</div>
);
};
export default WallpaperSelector;

91
src/components/Window.css Normal file
View file

@ -0,0 +1,91 @@
.window {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-width: 600px;
min-height: 400px;
background: rgb(255, 255, 255);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden;
animation: fadeIn 0.2s ease;
z-index: 100;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.window-header {
background: #1a0000;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #ddd;
cursor: move;
}
.window-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
color: #333;
}
.window-icon {
font-size: 18px;
}
.window-controls {
display: flex;
gap: 8px;
}
.window-btn {
width: 28px;
height: 28px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.window-btn.minimize:hover {
background: #e0e0e0;
}
.window-btn.close:hover {
background: #e81123;
color: white;
}
.window-content {
flex: 1;
overflow: hidden;
}
.window-iframe {
width: 100%;
height: 100%;
border: none;
}

49
src/components/Window.tsx Normal file
View file

@ -0,0 +1,49 @@
import React from 'react';
import './Window.css';
import type { WindowType } from '../MainApp';
interface WindowProps {
window: WindowType;
onClose: (id: string) => void;
onMinimize: (id: string) => void;
}
const Window: React.FC<WindowProps> = ({ window, onClose, onMinimize }) => {
return (
<div className="window">
<div className="window-header">
<div className="window-title">
<span className="window-icon">{window.icon}</span>
<span>{window.title}</span>
</div>
<div className="window-controls">
<button
className="window-btn minimize"
onClick={() => onMinimize(window.id)}
title="Minimize"
>
</button>
<button
className="window-btn close"
onClick={() => onClose(window.id)}
title="Close"
>
</button>
</div>
</div>
<div className="window-content">
{window.content}
{/* <iframe
src={window.url}
title={window.title}
className="window-iframe"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals"
/> */}
</div>
</div>
);
};
export default Window;

View file

@ -0,0 +1,348 @@
.chatterbox-container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.chatterbox-container h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: border-color 0.3s;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.char-counter {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 5px;
}
/* Voice section */
.voice-section {
background: #f8f9ff;
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #e0e0e0;
}
.voice-section.recording {
border-color: #e74c3c;
background: #fdf2f2;
}
.voice-section.has-recording {
border-color: #4caf50;
background: #f1f8f4;
}
.voice-label {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.check-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: #4caf50;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
margin-left: 10px;
}
.record-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
font-size: 28px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.record-btn:hover {
transform: scale(1.1);
}
.record-btn.recording {
background: #e74c3c;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.record-status {
text-align: center;
margin-top: 15px;
color: #666;
font-size: 14px;
}
.record-timer {
text-align: center;
font-size: 24px;
font-weight: 700;
color: #667eea;
margin-top: 10px;
font-family: monospace;
}
.voice-text {
background: white;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #333;
border: 2px dashed #667eea;
}
.voice-preview {
margin-top: 15px;
}
.voice-preview audio {
width: 100%;
}
.voice-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.voice-actions button {
flex: 1;
padding: 10px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-retry {
background: #e0e0e0;
color: #333;
}
.btn-use {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* File upload */
.or-divider {
text-align: center;
margin: 20px 0;
color: #999;
font-size: 14px;
position: relative;
}
.or-divider::before,
.or-divider::after {
content: '';
position: absolute;
top: 50%;
width: 40%;
height: 1px;
background: #e0e0e0;
}
.or-divider::before { left: 0; }
.or-divider::after { right: 0; }
.file-input-wrapper {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.file-input-wrapper:hover {
border-color: #667eea;
background: #f8f9ff;
}
.file-input-wrapper.has-file {
border-color: #4caf50;
background: #f1f8f4;
}
.file-name {
margin-top: 10px;
font-weight: 600;
color: #667eea;
}
/* Sliders */
.slider-group {
display: flex;
align-items: center;
gap: 15px;
}
.slider-group input[type="range"] {
flex: 1;
}
.slider-value {
min-width: 50px;
text-align: center;
font-weight: 600;
color: #667eea;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
/* Submit */
.submit-btn {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.submit-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Loading */
.loading-block {
text-align: center;
margin-top: 20px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error */
.error-block {
color: #e74c3c;
background: #fdf2f2;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
}
/* Result */
.result-block {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
}
.result-block h3 {
margin-bottom: 15px;
color: #333;
}
.result-block audio {
width: 100%;
margin-top: 15px;
}
.download-btn {
display: inline-block;
margin-top: 15px;
padding: 10px 20px;
background: #4caf50;
color: white;
text-decoration: none;
border-radius: 8px;
font-size: 14px;
}

View file

@ -0,0 +1,356 @@
import { useState, useRef, useEffect } from 'react';
import { Recorder } from '../../lib/recorder';
import './ChatterboxTTS.css';
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
const ChatterboxTTS = () => {
// Recording state
const [isRecording, setIsRecording] = useState(false);
const [hasRecording, setHasRecording] = useState(false);
const [isUsingRecordedVoice, setIsUsingRecordedVoice] = useState(false);
const [recordStatus, setRecordStatus] = useState('Нажмите для записи');
const [recordTime, setRecordTime] = useState('');
const [showVoiceText, setShowVoiceText] = useState(false);
const [showVoicePreview, setShowVoicePreview] = useState(false);
const [recordedAudioUrl, setRecordedAudioUrl] = useState('');
// File state
const [fileName, setFileName] = useState('');
const [isDragOver, setIsDragOver] = useState(false);
// Form state
const [text, setText] = useState('В прошлом месяце мы достигли нового рубежа.');
const [language, setLanguage] = useState('ru');
const [exaggeration, setExaggeration] = useState(0.5);
const [temperature, setTemperature] = useState(0.8);
const [cfgWeight, setCfgWeight] = useState(0.5);
const [seed, setSeed] = useState(0);
// UI state
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [resultUrl, setResultUrl] = useState('');
// Refs (не тригерят ре-рендер)
const recorderRef = useRef<Recorder | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const recordedBlobRef = useRef<Blob | null>(null);
const recordingStartRef = useRef<number>(0);
const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const fileRef = useRef<File | null>(null);
// Запрос микрофона при монтировании
useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => stream.getTracks().forEach(t => t.stop()))
.catch(() => {});
}, []);
const updateTimer = () => {
const elapsed = Math.floor((Date.now() - recordingStartRef.current) / 1000);
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
const secs = (elapsed % 60).toString().padStart(2, '0');
setRecordTime(`${mins}:${secs}`);
};
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
streamRef.current = stream;
const audioContext = new AudioContext();
audioContextRef.current = audioContext;
const source = audioContext.createMediaStreamSource(stream);
const recorder = await Recorder.create(source, { numChannels: 1 });
recorderRef.current = recorder;
setIsRecording(true);
setHasRecording(false);
setShowVoiceText(true);
setShowVoicePreview(false);
setIsUsingRecordedVoice(false);
setRecordStatus('Идёт запись...');
setRecordTime('00:00');
recordingStartRef.current = Date.now();
updateTimer();
timerIntervalRef.current = setInterval(updateTimer, 1000);
} catch (err) {
alert('Не удалось получить доступ к микрофону: ' + (err as Error).message);
}
};
const stopRecording = () => {
const recorder = recorderRef.current;
if (!recorder) return;
recorder.stop();
// Сразу обновляем UI — не ждём окончания экспорта
setIsRecording(false);
setRecordStatus('Обработка...');
if (timerIntervalRef.current) {
clearInterval(timerIntervalRef.current);
timerIntervalRef.current = null;
}
setRecordTime('');
recorder.exportWAV((blob) => {
recorder.clear();
recorder.destroy();
recorderRef.current = null;
streamRef.current?.getTracks().forEach(t => t.stop());
streamRef.current = null;
audioContextRef.current?.close();
audioContextRef.current = null;
recordedBlobRef.current = blob;
setRecordedAudioUrl(URL.createObjectURL(blob));
setShowVoicePreview(true);
setShowVoiceText(false);
setRecordStatus('Запись завершена');
setHasRecording(true);
});
};
const handleRecordClick = () => {
if (isRecording) stopRecording();
else startRecording();
};
const handleRetry = () => {
setShowVoicePreview(false);
setHasRecording(false);
setIsUsingRecordedVoice(false);
setRecordStatus('Нажмите для записи');
recordedBlobRef.current = null;
};
const handleUseRecording = () => {
setIsUsingRecordedVoice(true);
setRecordStatus('✅ Голосовое сообщение сохранено');
// Сбрасываем файл
fileRef.current = null;
setFileName('');
if (fileInputRef.current) fileInputRef.current.value = '';
};
const updateFileName = (file: File) => {
fileRef.current = file;
setFileName(file.name);
setIsUsingRecordedVoice(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
const file = e.dataTransfer.files[0];
if (file) updateFileName(file);
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) updateFileName(file);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setResultUrl('');
setError('');
setLoading(true);
try {
const formData = new FormData();
if (isUsingRecordedVoice) {
if (!recordedBlobRef.current) {
throw new Error('Запись голоса потеряна — запишите снова');
}
formData.append('audio_file', recordedBlobRef.current, 'voice_recording.wav');
} else if (fileRef.current) {
formData.append('audio_file', fileRef.current);
} else {
throw new Error('Выберите аудио файл или запишите голос');
}
formData.append('text', text);
formData.append('language_id', language);
formData.append('exaggeration', String(exaggeration));
formData.append('temperature', String(temperature));
formData.append('seed_num', String(seed));
formData.append('cfg_weight', String(cfgWeight));
console.log('[ChatterboxTTS] отправка:', {
audio: isUsingRecordedVoice ? 'recorded blob' : fileRef.current?.name,
text, language, exaggeration, temperature, cfgWeight, seed,
});
const response = await fetch(API_URL, { method: 'POST', body: formData });
const contentType = response.headers.get('Content-Type') ?? '';
console.log('[ChatterboxTTS] ответ:', response.status, contentType);
if (!response.ok || contentType.includes('application/json')) {
const errData = await response.json().catch(() => ({}));
const msg = (errData as { error?: string }).error;
if (msg) throw new Error(msg);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
throw new Error('Сервер вернул JSON вместо аудио — возможно audio_file не принят');
}
const blob = await response.blob();
setResultUrl(URL.createObjectURL(blob));
} catch (err) {
setError('❌ Ошибка: ' + (err as Error).message);
} finally {
setLoading(false);
}
};
const voiceSectionClass = [
'voice-section',
isRecording ? 'recording' : '',
hasRecording && !isRecording ? 'has-recording' : '',
].filter(Boolean).join(' ');
return (
<div className="chatterbox-container">
<h1>🎙 Chatterbox TTS</h1>
<form onSubmit={handleSubmit}>
{/* Запись голоса */}
<div className={voiceSectionClass}>
<div className="voice-label">
🎤 Запишите голосовое сообщение для проверки на бота
{isUsingRecordedVoice && <span className="check-badge"> Готово</span>}
</div>
<button type="button" className={`record-btn${isRecording ? ' recording' : ''}`} onClick={handleRecordClick}>
{isRecording ? '⏹' : '⏺'}
</button>
<div className="record-status">{recordStatus}</div>
{recordTime && <div className="record-timer">{recordTime}</div>}
{showVoiceText && (
<div className="voice-text">
📢 Произнесите: <span style={{ color: '#667eea' }}>"Хакатон 2026 французский стиль"</span>
</div>
)}
{showVoicePreview && (
<div className="voice-preview">
<audio src={recordedAudioUrl} controls />
<div className="voice-actions">
<button type="button" className="btn-retry" onClick={handleRetry}>🔄 Перезаписать</button>
<button type="button" className="btn-use" onClick={handleUseRecording}> Использовать</button>
</div>
</div>
)}
</div>
{/* Загрузка файла */}
<div className="or-divider">или загрузите файл</div>
<div className="form-group">
<div
className={`file-input-wrapper${fileName ? ' has-file' : ''}`}
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDrop}
style={isDragOver ? { borderColor: '#667eea' } : undefined}
>
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>
{fileName && <div className="file-name">{fileName}</div>}
</div>
</div>
{/* Текст */}
<div className="form-group">
<label>Текст для синтеза (макс 300 символов)</label>
<textarea maxLength={300} value={text} onChange={e => setText(e.target.value)} placeholder="Введите текст..." />
<div className="char-counter"><span>{text.length}</span>/300</div>
</div>
{/* Язык */}
<div className="form-group">
<label>Язык</label>
<select value={language} onChange={e => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="ru">Russian</option>
<option value="de">German</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="it">Italian</option>
<option value="pt">Portuguese</option>
<option value="hi">Hindi</option>
</select>
</div>
{/* Слайдеры */}
<div className="form-group">
<label>Exaggeration (экспрессивность)</label>
<div className="slider-group">
<input type="range" min="0.25" max="2" step="0.05" value={exaggeration} onChange={e => setExaggeration(Number(e.target.value))} />
<span className="slider-value">{exaggeration}</span>
</div>
</div>
<div className="form-group">
<label>Temperature (разнообразие)</label>
<div className="slider-group">
<input type="range" min="0.05" max="5" step="0.05" value={temperature} onChange={e => setTemperature(Number(e.target.value))} />
<span className="slider-value">{temperature}</span>
</div>
</div>
<div className="form-group">
<label>CFG Weight</label>
<div className="slider-group">
<input type="range" min="0.2" max="1" step="0.05" value={cfgWeight} onChange={e => setCfgWeight(Number(e.target.value))} />
<span className="slider-value">{cfgWeight}</span>
</div>
</div>
<div className="two-col">
<div className="form-group">
<label>Random Seed (0 = random)</label>
<input type="number" value={seed} min={0} onChange={e => setSeed(Number(e.target.value))} />
</div>
</div>
<button type="submit" className="submit-btn" disabled={loading}>
🚀 Сгенерировать аудио
</button>
</form>
{loading && (
<div className="loading-block">
<div className="spinner" />
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
</div>
)}
{error && <div className="error-block">{error}</div>}
{resultUrl && (
<div className="result-block">
<h3> Готово!</h3>
<audio src={resultUrl} controls />
<br />
<a href={resultUrl} className="download-btn" download="output.wav">💾 Скачать WAV</a>
</div>
)}
</div>
);
};
export default ChatterboxTTS;

0
src/index.css Normal file
View file

78
src/lib/recorder.ts Normal file
View file

@ -0,0 +1,78 @@
/**
* Recorder AudioWorklet-based replacement for Recorderjs
* https://github.com/mattdiamond/Recorderjs
*
* AudioWorkletNode заменяет устаревший ScriptProcessorNode.
* WAV-кодирование остаётся в отдельном Worker (recorderWorker.js).
*/
export class Recorder {
private node: AudioWorkletNode;
private worker: Worker;
private currCallback: ((blob: Blob) => void) | null = null;
private constructor(node: AudioWorkletNode, worker: Worker) {
this.node = node;
this.worker = worker;
// Пробрасываем PCM-чанки из воркл'ета в WAV-воркер
node.port.onmessage = (e: MessageEvent<Float32Array[]>) => {
this.worker.postMessage({ command: 'record', buffer: e.data });
};
this.worker.onmessage = (e: MessageEvent<{ command: string; data: Blob }>) => {
if (e.data.command === 'exportWAV' && this.currCallback) {
this.currCallback(e.data.data);
this.currCallback = null;
}
};
}
/** Фабричный метод — async из-за audioWorklet.addModule */
static async create(
source: MediaStreamAudioSourceNode,
cfg?: { numChannels?: number; workletPath?: string; workerPath?: string },
): Promise<Recorder> {
const context = source.context as AudioContext;
const numChannels = cfg?.numChannels ?? 1;
await context.audioWorklet.addModule(cfg?.workletPath ?? '/recorderWorklet.js');
const node = new AudioWorkletNode(context, 'recorder-processor', {
numberOfInputs: 1,
numberOfOutputs: 0,
channelCount: numChannels,
});
const worker = new Worker(cfg?.workerPath ?? '/recorderWorker.js');
worker.postMessage({
command: 'init',
config: { sampleRate: context.sampleRate, numChannels },
});
source.connect(node);
return new Recorder(node, worker);
}
record() { /* данные уже идут через node.port.onmessage */ }
stop() {
// Сообщаем воркл'ету завершить process() и отключиться
this.node.port.postMessage('stop');
}
clear() {
this.worker.postMessage({ command: 'clear' });
}
exportWAV(cb: (blob: Blob) => void, type = 'audio/wav') {
this.currCallback = cb;
this.worker.postMessage({ command: 'exportWAV', type });
}
destroy() {
this.node.disconnect();
this.worker.terminate();
}
}

13
src/main.tsx Normal file
View file

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

197
src/styles/shared.css Normal file
View file

@ -0,0 +1,197 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.wallpaper-selector {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.selector-container {
background: rgba(255, 255, 255, 0.98);
border-radius: 20px;
padding: 50px;
text-align: center;
box-shadow: 0 30px 80px rgba(0,0,0,0.3);
max-width: 1200px;
width: 90%;
}
.quest-badge {
display: inline-block;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
margin-bottom: 20px;
}
.windows-logo {
margin-bottom: 20px;
}
.logo-icon {
font-size: 60px;
}
.selector-container h1 {
font-size: 32px;
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 16px;
margin-bottom: 10px;
}
.quest-description {
color: #999;
font-size: 14px;
margin-bottom: 40px;
}
.wallpaper-options {
display: flex;
gap: 30px;
justify-content: center;
margin-bottom: 50px;
flex-wrap: wrap;
}
.wallpaper-option {
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
border-radius: 12px;
overflow: hidden;
background: #f5f5f5;
}
.wallpaper-option:hover {
transform: translateY(-10px);
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
}
.wallpaper-preview {
width: 280px;
height: 200px;
position: relative;
overflow: hidden;
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.wallpaper-option:hover .preview-overlay {
opacity: 1;
}
.preview-overlay span {
color: white;
font-size: 20px;
font-weight: bold;
}
.xp-preview {
background: linear-gradient(135deg, #3a8c3a 0%, #1e5c1e 100%);
position: relative;
}
.xp-preview::before {
content: "🏔️";
position: absolute;
font-size: 60px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.win7-preview {
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
position: relative;
}
.win7-preview::before {
content: "🐟";
position: absolute;
font-size: 60px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.win10-preview {
background: linear-gradient(135deg, #0078d7 0%, #005a9e 100%);
position: relative;
}
.win10-preview::before {
content: "🪟";
position: absolute;
font-size: 60px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.option-info {
padding: 15px;
background: white;
}
.option-info h3 {
margin: 0 0 5px 0;
color: #333;
font-size: 18px;
}
.option-info p {
margin: 0;
color: #666;
font-size: 14px;
}
.selector-footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #999;
font-size: 14px;
}

26
src/types.ts Normal file
View file

@ -0,0 +1,26 @@
export interface WindowData {
id: string;
title: string;
content: string;
icon: string;
isMinimized: boolean;
url: string;
}
export interface DesktopIconProps {
icon: string;
name: string;
onClick: () => void;
}
export interface WindowProps {
window: WindowData;
onClose: (id: string) => void;
onMinimize: (id: string) => void;
}
export interface SidebarProps {
windows: WindowData[];
onRestore: (id: string) => void;
onClose: (id: string) => void;
}

758
test.html Normal file
View file

@ -0,0 +1,758 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chatterbox TTS Test</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: border-color 0.3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
/* Voice Recording Section */
.voice-section {
background: #f8f9ff;
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
border: 2px solid #e0e0e0;
}
.voice-section.recording {
border-color: #e74c3c;
background: #fdf2f2;
}
.voice-section.has-recording {
border-color: #4caf50;
background: #f1f8f4;
}
.voice-label {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.record-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
font-size: 28px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.record-btn:hover {
transform: scale(1.1);
}
.record-btn.recording {
background: #e74c3c;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.record-status {
text-align: center;
margin-top: 15px;
color: #666;
font-size: 14px;
}
.record-timer {
text-align: center;
font-size: 24px;
font-weight: 700;
color: #667eea;
margin-top: 10px;
font-family: monospace;
}
.voice-text {
background: white;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #333;
border: 2px dashed #667eea;
display: none;
}
.voice-text.show {
display: block;
}
.voice-preview {
margin-top: 15px;
display: none;
}
.voice-preview.show {
display: block;
}
.voice-preview audio {
width: 100%;
}
.voice-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.voice-actions button {
flex: 1;
padding: 10px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-retry {
background: #e0e0e0;
color: #333;
}
.btn-use {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* File upload alternative */
.or-divider {
text-align: center;
margin: 20px 0;
color: #999;
font-size: 14px;
position: relative;
}
.or-divider::before,
.or-divider::after {
content: '';
position: absolute;
top: 50%;
width: 40%;
height: 1px;
background: #e0e0e0;
}
.or-divider::before { left: 0; }
.or-divider::after { right: 0; }
.file-input-wrapper {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.file-input-wrapper:hover {
border-color: #667eea;
background: #f8f9ff;
}
.file-input-wrapper.has-file {
border-color: #4caf50;
background: #f1f8f4;
}
input[type="file"] {
display: none;
}
.slider-group {
display: flex;
align-items: center;
gap: 15px;
}
input[type="range"] {
flex: 1;
}
.slider-value {
min-width: 50px;
text-align: center;
font-weight: 600;
color: #667eea;
}
button[type="submit"] {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button[type="submit"]:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
button[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.result {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
display: none;
}
.result.show {
display: block;
}
.result audio {
width: 100%;
margin-top: 15px;
}
.download-btn {
display: inline-block;
margin-top: 15px;
padding: 10px 20px;
background: #4caf50;
color: white;
text-decoration: none;
border-radius: 8px;
font-size: 14px;
}
.loading {
display: none;
text-align: center;
margin-top: 20px;
}
.loading.show {
display: block;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #e74c3c;
background: #fdf2f2;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
display: none;
}
.error.show {
display: block;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.check-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: #4caf50;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎙️ Chatterbox TTS</h1>
<form id="ttsForm">
<!-- Голосовая запись -->
<div class="voice-section" id="voiceSection">
<div class="voice-label">
🎤 Запишите голосовое сообщение для проверки на бота
<span class="check-badge" id="checkBadge" style="display: none;">✓ Готово</span>
</div>
<button type="button" class="record-btn" id="recordBtn"></button>
<div class="record-status" id="recordStatus">Нажмите для записи</div>
<div class="record-timer" id="recordTimer" style="display: none;">00:00</div>
<div class="voice-text" id="voiceText">
📢 Произнесите: <span style="color: #667eea;">"Хакатон 2026 французский стиль"</span>
</div>
<div class="voice-preview" id="voicePreview">
<audio id="recordedAudio" controls></audio>
<div class="voice-actions">
<button type="button" class="btn-retry" id="retryBtn">🔄 Перезаписать</button>
<button type="button" class="btn-use" id="useBtn">✅ Использовать</button>
</div>
</div>
</div>
<!-- Или загрузить файл -->
<div class="or-divider">или загрузите файл</div>
<div class="form-group">
<div class="file-input-wrapper" id="dropZone">
<input type="file" id="audioFile" accept="audio/*">
<div id="fileLabel">📁 Кликни или перетащи аудио файл</div>
<div id="fileName" style="margin-top: 10px; font-weight: 600; color: #667eea;"></div>
</div>
</div>
<!-- Текст для синтеза -->
<div class="form-group">
<label for="text">Текст для синтеза (макс 300 символов)</label>
<textarea id="text" maxlength="300" placeholder="Введите текст...">В прошлом месяце мы достигли нового рубежа.</textarea>
<div style="text-align: right; font-size: 12px; color: #999; margin-top: 5px;">
<span id="charCount">0</span>/300
</div>
</div>
<!-- Язык -->
<div class="form-group">
<label for="language">Язык</label>
<select id="language">
<option value="en">English</option>
<option value="ru" selected>Russian</option>
<option value="de">German</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="it">Italian</option>
<option value="pt">Portuguese</option>
<option value="hi">Hindi</option>
</select>
</div>
<!-- Слайдеры -->
<div class="form-group">
<label>Exaggeration (экспрессивность)</label>
<div class="slider-group">
<input type="range" id="exaggeration" min="0.25" max="2" step="0.05" value="0.5">
<span class="slider-value" id="exaggerationVal">0.5</span>
</div>
</div>
<div class="form-group">
<label>Temperature (разнообразие)</label>
<div class="slider-group">
<input type="range" id="temperature" min="0.05" max="5" step="0.05" value="0.8">
<span class="slider-value" id="temperatureVal">0.8</span>
</div>
</div>
<div class="form-group">
<label>CFG Weight</label>
<div class="slider-group">
<input type="range" id="cfgWeight" min="0.2" max="1" step="0.05" value="0.5">
<span class="slider-value" id="cfgWeightVal">0.5</span>
</div>
</div>
<div class="two-col">
<div class="form-group">
<label for="seed">Random Seed (0 = random)</label>
<input type="number" id="seed" value="0" min="0">
</div>
</div>
<button type="submit" id="submitBtn">🚀 Сгенерировать аудио</button>
</form>
<!-- Лоадер -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
</div>
<!-- Ошибка -->
<div class="error" id="error"></div>
<!-- Результат -->
<div class="result" id="result">
<h3 style="margin-bottom: 15px; color: #333;">✅ Готово!</h3>
<audio id="audioPlayer" controls></audio>
<br>
<a href="#" class="download-btn" id="downloadBtn" download="output.wav">💾 Скачать WAV</a>
</div>
</div>
<script>
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
// Элементы
const form = document.getElementById('ttsForm');
const voiceSection = document.getElementById('voiceSection');
const recordBtn = document.getElementById('recordBtn');
const recordStatus = document.getElementById('recordStatus');
const recordTimer = document.getElementById('recordTimer');
const voiceText = document.getElementById('voiceText');
const voicePreview = document.getElementById('voicePreview');
const recordedAudio = document.getElementById('recordedAudio');
const retryBtn = document.getElementById('retryBtn');
const useBtn = document.getElementById('useBtn');
const checkBadge = document.getElementById('checkBadge');
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('audioFile');
const fileLabel = document.getElementById('fileLabel');
const fileName = document.getElementById('fileName');
const text = document.getElementById('text');
const charCount = document.getElementById('charCount');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const error = document.getElementById('error');
const audioPlayer = document.getElementById('audioPlayer');
const downloadBtn = document.getElementById('downloadBtn');
// Переменные для записи
let mediaRecorder = null;
let recordedChunks = [];
let recordingStartTime = null;
let recordingTimer = null;
let recordedBlob = null;
let isUsingRecordedVoice = false;
// Запрос разрешения на микрофон при загрузке
async function initMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => track.stop());
} catch (err) {
console.log('Микрофон не доступен:', err);
}
}
initMicrophone();
// Начать/остановить запись
recordBtn.addEventListener('click', async () => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
stopRecording();
} else {
startRecording();
}
});
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType =
MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' :
MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' :
MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') ? 'audio/ogg;codecs=opus' : '';
mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : {});
recordedChunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
recordedChunks.push(e.data);
}
};
mediaRecorder.onstop = () => {
const actualType = mediaRecorder.mimeType || 'audio/webm';
recordedBlob = new Blob(recordedChunks, { type: actualType });
const url = URL.createObjectURL(recordedBlob);
recordedAudio.src = url;
voicePreview.classList.add('show');
recordStatus.textContent = 'Запись завершена';
recordBtn.textContent = '⏺';
recordBtn.classList.remove('recording');
voiceSection.classList.remove('recording');
voiceSection.classList.add('has-recording');
// Останавливаем все треки
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
// UI обновления
recordBtn.textContent = '⏹';
recordBtn.classList.add('recording');
voiceSection.classList.add('recording');
recordStatus.textContent = 'Идёт запись...';
voiceText.classList.add('show');
recordTimer.style.display = 'block';
voicePreview.classList.remove('show');
isUsingRecordedVoice = false;
checkBadge.style.display = 'none';
// Таймер
recordingStartTime = Date.now();
updateTimer();
recordingTimer = setInterval(updateTimer, 1000);
} catch (err) {
alert('Не удалось получить доступ к микрофону: ' + err.message);
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
clearInterval(recordingTimer);
recordTimer.style.display = 'none';
voiceText.classList.remove('show');
}
}
function updateTimer() {
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
const secs = (elapsed % 60).toString().padStart(2, '0');
recordTimer.textContent = `${mins}:${secs}`;
}
// Перезаписать
retryBtn.addEventListener('click', () => {
voicePreview.classList.remove('show');
voiceSection.classList.remove('has-recording');
recordStatus.textContent = 'Нажмите для записи';
recordedBlob = null;
isUsingRecordedVoice = false;
checkBadge.style.display = 'none';
});
// Использовать запись
useBtn.addEventListener('click', () => {
isUsingRecordedVoice = true;
checkBadge.style.display = 'inline-flex';
recordStatus.textContent = '✅ Голосовое сообщение сохранено';
// Сбрасываем файл если был
fileInput.value = '';
fileLabel.textContent = '📁 Кликни или перетащи аудио файл';
fileName.textContent = '';
dropZone.classList.remove('has-file');
});
// File upload (drag & drop)
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#667eea';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.borderColor = '#ccc';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#ccc';
const files = e.dataTransfer.files;
if (files.length) {
fileInput.files = files;
updateFileName(files[0].name);
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
updateFileName(fileInput.files[0].name);
// Сбрасываем голосовую запись
isUsingRecordedVoice = false;
checkBadge.style.display = 'none';
}
});
function updateFileName(name) {
fileLabel.textContent = '✅ Файл выбран';
fileName.textContent = name;
dropZone.classList.add('has-file');
}
// Character counter
text.addEventListener('input', () => {
charCount.textContent = text.value.length;
});
charCount.textContent = text.value.length;
// Slider updates
document.getElementById('exaggeration').addEventListener('input', (e) => {
document.getElementById('exaggerationVal').textContent = e.target.value;
});
document.getElementById('temperature').addEventListener('input', (e) => {
document.getElementById('temperatureVal').textContent = e.target.value;
});
document.getElementById('cfgWeight').addEventListener('input', (e) => {
document.getElementById('cfgWeightVal').textContent = e.target.value;
});
// Form submit
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Проверяем что есть источник аудио
if (!isUsingRecordedVoice && !fileInput.files.length) {
alert('Пожалуйста, запишите голосовое сообщение или загрузите аудио файл');
return;
}
result.classList.remove('show');
error.classList.remove('show');
loading.classList.add('show');
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
try {
const formData = new FormData();
// Добавляем аудио (голосовое или файл)
if (isUsingRecordedVoice && recordedBlob) {
const ext = recordedBlob.type.includes('ogg') ? 'ogg' : 'webm';
formData.append('audio_file', recordedBlob, `voice_recording.${ext}`);
} else if (fileInput.files.length > 0) {
formData.append('audio_file', fileInput.files[0]);
}
formData.append('text', text.value);
formData.append('language_id', document.getElementById('language').value);
formData.append('exaggeration', document.getElementById('exaggeration').value);
formData.append('temperature', document.getElementById('temperature').value);
formData.append('seed_num', document.getElementById('seed').value);
formData.append('cfg_weight', document.getElementById('cfgWeight').value);
const response = await fetch(API_URL, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.error || `HTTP ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
audioPlayer.src = url;
downloadBtn.href = url;
result.classList.add('show');
} catch (err) {
error.textContent = '❌ Ошибка: ' + err.message;
error.classList.add('show');
console.error(err);
} finally {
loading.classList.remove('show');
submitBtn.disabled = false;
}
});
</script>
</body>
</html>

28
tsconfig.app.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2023",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
// "strict": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "erasableSyntaxOnly": true,
// "noFallthroughCasesInSwitch": true,
// "noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
// "strict": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "erasableSyntaxOnly": true,
// "noFallthroughCasesInSwitch": true,
// "noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})