diff --git a/locales/en/translation.json b/locales/en/translation.json index ff5dbb03..415154fe 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -3,6 +3,7 @@ "AISettings": "■ AI Settings", "YoutubeSettings": "■ YouTube Settings", "VoiceSettings": "■ Voice Settings", + "SlideSettings": "■ Slide Settings", "OtherSettings": "■ Other", "ExternalConnectionMode": "External Connection Mode (WebSocket, β version)", "YoutubeMode": "YouTube Mode", @@ -98,5 +99,8 @@ "ShowCharacterName": "Show character name in the answer box", "AdvancedSettings": "Advanced Settings", "ShowSettingsButton": "Show settings button", - "ShowSettingsButtonInfo": "The settings screen can be displayed by pressing Cmd + . (Mac) / Ctrl + . (Windows) ." + "ShowSettingsButtonInfo": "The settings screen can be displayed by pressing Cmd + . (Mac) / Ctrl + . (Windows) .", + "SlideMode": "Slide Mode", + "SelectedSlideDocs": "Selected Slide Documents", + "SlideModeDescription": "This is a mode where AI automatically presents slides. It is only available when the selected AI service is OpenAI, Anthropic, or Google Gemini." } diff --git a/locales/ja/translation.json b/locales/ja/translation.json index e988d212..baf9f3d7 100644 --- a/locales/ja/translation.json +++ b/locales/ja/translation.json @@ -3,6 +3,7 @@ "AISettings": "■ AI設定", "YoutubeSettings": "■ YouTube設定", "VoiceSettings": "■ 音声設定", + "SlideSettings": "■ スライド設定", "OtherSettings": "■ その他", "ExternalConnectionMode": "外部連携モード(WebSocket, β版)", "YoutubeMode": "YouTubeモード", @@ -99,5 +100,8 @@ "ShowCharacterName": "回答欄にキャラクター名を表示する", "AdvancedSettings": "詳細設定", "ShowSettingsButton": "設定ボタンを表示", - "ShowSettingsButtonInfo": "設定画面は Cmd + . (Mac) / Ctrl + . (Windows) で表示することができます。" + "ShowSettingsButtonInfo": "設定画面は Cmd + . (Mac) / Ctrl + . (Windows) で表示することができます。", + "SlideMode": "スライドモード", + "SelectedSlideDocs": "使用するスライド", + "SlideModeDescription": "AIが自動でスライドを発表するモードです。選択しているAIサービスがOpenAIまたはAnthropicまたはGoogle Geminiの場合のみ有効です。" } diff --git a/locales/ko/translation.json b/locales/ko/translation.json index b22bccaf..c6f848dc 100644 --- a/locales/ko/translation.json +++ b/locales/ko/translation.json @@ -3,6 +3,7 @@ "AISettings": "■ AI 설정", "YoutubeSettings": "■ YouTube 설정", "VoiceSettings": "■ 음성 설정", + "SlideSettings": "■ 슬라이드 설정", "OtherSettings": "■ 기타", "ExternalConnectionMode": "외부 연동 모드 (WebSocket, 베타 버전)", "YoutubeMode": "YouTube 모드", @@ -98,5 +99,8 @@ "ShowCharacterName": "답변란에 캐릭터 이름을 표시", "AdvancedSettings": "고급 설정", "ShowSettingsButton": "설정 버튼 표시", - "ShowSettingsButtonInfo": "설정 화면은 Cmd + . (Mac) / Ctrl + . (Windows)를 눌러 표시할 수 있습니다." + "ShowSettingsButtonInfo": "설정 화면은 Cmd + . (Mac) / Ctrl + . (Windows)를 눌러 표시할 수 있습니다.", + "SlideMode": "슬라이드 모드", + "SelectedSlideDocs": "사용할 슬라이드", + "SlideModeDescription": "AI가 자동으로 슬라이드를 발표하는 모드입니다. 선택한 AI 서비스가 OpenAI, Anthropic 또는 Google Gemini인 경우에만 사용 가능합니다." } diff --git a/locales/zh/translation.json b/locales/zh/translation.json index 43039e09..37e60476 100644 --- a/locales/zh/translation.json +++ b/locales/zh/translation.json @@ -3,6 +3,7 @@ "AISettings": "■ AI 設定", "YoutubeSettings": "■ YouTube 設定", "VoiceSettings": "■ 語音設定", + "SlideSettings": "■ 投影片設定", "OtherSettings": "■ 其他", "ExternalConnectionMode": "外部連線模式 (WebSocket, β版本)", "YoutubeMode": "YouTube 模式", @@ -98,5 +99,8 @@ "ShowCharacterName": "在回答框中显示角色名称", "AdvancedSettings": "高级设置", "ShowSettingsButton": "显示设置按钮", - "ShowSettingsButtonInfo": "可以通过按 Cmd + . (Mac) / Ctrl + . (Windows) 来显示设置界面。" + "ShowSettingsButtonInfo": "可以通过按 Cmd + . (Mac) / Ctrl + . (Windows) 来显示设置界面。", + "SlideMode": "投影片模式", + "SelectedSlideDocs": "使用的投影片", + "SlideModeDescription": "這是一個 AI 自動展示投影片的模式。僅在選擇的 AI 服務為 OpenAI、Anthropic 或 Google Gemini 時有效。" } diff --git a/package-lock.json b/package-lock.json index 037b4af6..3b5733b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@google/generative-ai": "^0.11.3", "@headlessui/react": "^2.1.2", "@heroicons/react": "^2.1.5", + "@marp-team/marp-core": "^3.9.0", + "@marp-team/marpit": "^3.0.0", "@pixiv/three-vrm": "^3.0.0", "@tailwindcss/line-clamp": "^0.4.4", "axios": "^1.6.8", @@ -514,6 +516,126 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@marp-team/marp-core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@marp-team/marp-core/-/marp-core-3.9.0.tgz", + "integrity": "sha512-gi6nq0rsB1oMA8ReppW4XxmS4fisQiAsD0ZoUgLeG4h6SWatveCAA7fZyxnXfwA2UC8pNb7ktPqYdRsxvuwntA==", + "license": "MIT", + "dependencies": { + "@marp-team/marpit": "^2.6.1", + "@marp-team/marpit-svg-polyfill": "^2.1.0", + "highlight.js": "11.8.0", + "katex": "^0.16.9", + "mathjax-full": "^3.2.2", + "postcss": "^8.4.31", + "postcss-selector-parser": "^6.0.13", + "xss": "^1.0.14" + }, + "engines": { + "node": "^12.20 || ^14.13.1 || >=16" + } + }, + "node_modules/@marp-team/marp-core/node_modules/@marp-team/marpit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@marp-team/marpit/-/marpit-2.6.1.tgz", + "integrity": "sha512-Hg7fZ8SqXwLjxeIFzSnlXkXEmt0ZXPeMJneEn9n1M495a34C4xtkgEgL8R1MW2IRCh4Yibn0xmGKcaf+GuqR2A==", + "license": "MIT", + "dependencies": { + "color-string": "^1.9.1", + "cssesc": "^3.0.0", + "js-yaml": "^4.1.0", + "lodash.kebabcase": "^4.1.1", + "markdown-it": "^13.0.2", + "markdown-it-front-matter": "^0.2.3", + "postcss": "^8.4.29" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@marp-team/marp-core/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@marp-team/marp-core/node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/@marp-team/marp-core/node_modules/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/@marp-team/marp-core/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/@marp-team/marp-core/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, + "node_modules/@marp-team/marpit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@marp-team/marpit/-/marpit-3.0.0.tgz", + "integrity": "sha512-4S+4ty/pjwrXe3dDxenD8ZU5MkqyYyfiuQdinjbrkTyUHmjjlzOyexIYYB4JX/MyjWw1QudrJYTdk/FcxLpQCA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "js-yaml": "^4.1.0", + "lodash.kebabcase": "^4.1.1", + "markdown-it": "^14.1.0", + "markdown-it-front-matter": "^0.2.4", + "postcss": "^8.4.38" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@marp-team/marpit-svg-polyfill": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.1.0.tgz", + "integrity": "sha512-VqCoAKwv1HJdzZp36dDPxznz2JZgRjkVSSPHpCzk72G2N753F0HPKXjevdjxmzN6gir9bUGBgMD1SguWJIi11A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@marp-team/marpit": ">=0.5.0" + }, + "peerDependenciesMeta": { + "@marp-team/marpit": { + "optional": true + } + } + }, "node_modules/@next/env": { "version": "14.2.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", @@ -1506,8 +1628,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.1.3", @@ -2129,6 +2250,16 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2186,6 +2317,12 @@ "node": ">=4" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2537,6 +2674,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3137,6 +3286,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3982,6 +4140,15 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -4624,7 +4791,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -4726,6 +4892,31 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4779,6 +4970,15 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4820,6 +5020,12 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4856,6 +5062,29 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-front-matter": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/markdown-it-front-matter/-/markdown-it-front-matter-0.2.4.tgz", + "integrity": "sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==", + "license": "MIT" + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -4869,6 +5098,18 @@ "node": ">=10" } }, + "node_modules/mathjax-full": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", + "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -4879,6 +5120,12 @@ "is-buffer": "~1.1.6" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -4902,6 +5149,12 @@ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", "dev": true }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -4971,6 +5224,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6011,6 +6270,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6583,6 +6851,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6632,6 +6915,29 @@ "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, + "node_modules/speech-rule-engine": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz", + "integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==", + "license": "Apache-2.0", + "dependencies": { + "commander": "9.2.0", + "wicked-good-xpath": "1.3.0", + "xmldom-sre": "0.1.31" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -7274,6 +7580,12 @@ "node": ">=12.20" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7527,6 +7839,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7628,6 +7946,37 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xmldom-sre": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", + "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==", + "license": "(LGPL-2.0 or MIT)", + "engines": { + "node": ">=0.1" + } + }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8096,6 +8445,91 @@ "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==" }, + "@marp-team/marp-core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@marp-team/marp-core/-/marp-core-3.9.0.tgz", + "integrity": "sha512-gi6nq0rsB1oMA8ReppW4XxmS4fisQiAsD0ZoUgLeG4h6SWatveCAA7fZyxnXfwA2UC8pNb7ktPqYdRsxvuwntA==", + "requires": { + "@marp-team/marpit": "^2.6.1", + "@marp-team/marpit-svg-polyfill": "^2.1.0", + "highlight.js": "11.8.0", + "katex": "^0.16.9", + "mathjax-full": "^3.2.2", + "postcss": "^8.4.31", + "postcss-selector-parser": "^6.0.13", + "xss": "^1.0.14" + }, + "dependencies": { + "@marp-team/marpit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@marp-team/marpit/-/marpit-2.6.1.tgz", + "integrity": "sha512-Hg7fZ8SqXwLjxeIFzSnlXkXEmt0ZXPeMJneEn9n1M495a34C4xtkgEgL8R1MW2IRCh4Yibn0xmGKcaf+GuqR2A==", + "requires": { + "color-string": "^1.9.1", + "cssesc": "^3.0.0", + "js-yaml": "^4.1.0", + "lodash.kebabcase": "^4.1.1", + "markdown-it": "^13.0.2", + "markdown-it-front-matter": "^0.2.3", + "postcss": "^8.4.29" + } + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==" + }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, + "markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + } + } + }, + "@marp-team/marpit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@marp-team/marpit/-/marpit-3.0.0.tgz", + "integrity": "sha512-4S+4ty/pjwrXe3dDxenD8ZU5MkqyYyfiuQdinjbrkTyUHmjjlzOyexIYYB4JX/MyjWw1QudrJYTdk/FcxLpQCA==", + "requires": { + "cssesc": "^3.0.0", + "js-yaml": "^4.1.0", + "lodash.kebabcase": "^4.1.1", + "markdown-it": "^14.1.0", + "markdown-it-front-matter": "^0.2.4", + "postcss": "^8.4.38" + } + }, + "@marp-team/marpit-svg-polyfill": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.1.0.tgz", + "integrity": "sha512-VqCoAKwv1HJdzZp36dDPxznz2JZgRjkVSSPHpCzk72G2N753F0HPKXjevdjxmzN6gir9bUGBgMD1SguWJIi11A==", + "requires": {} + }, "@next/env": { "version": "14.2.5", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", @@ -8830,8 +9264,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "aria-query": { "version": "5.1.3", @@ -9252,6 +9685,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -9291,6 +9733,11 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" + }, "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -9562,6 +10009,11 @@ "tapable": "^2.2.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -10020,6 +10472,11 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -10639,6 +11096,11 @@ "function-bind": "^1.1.2" } }, + "highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==" + }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -11077,7 +11539,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -11170,6 +11631,21 @@ "safe-buffer": "^5.0.1" } }, + "katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "requires": { + "commander": "^8.3.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + } + } + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -11214,6 +11690,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "requires": { + "uc.micro": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -11246,6 +11730,11 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11276,6 +11765,24 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + } + }, + "markdown-it-front-matter": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/markdown-it-front-matter/-/markdown-it-front-matter-0.2.4.tgz", + "integrity": "sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==" + }, "matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -11286,6 +11793,17 @@ "escape-string-regexp": "^4.0.0" } }, + "mathjax-full": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", + "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", + "requires": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -11296,6 +11814,11 @@ "is-buffer": "~1.1.6" } }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -11313,6 +11836,11 @@ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", "dev": true }, + "mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==" + }, "micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -11361,6 +11889,11 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" }, + "mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -12043,6 +12576,11 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12428,6 +12966,21 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12471,6 +13024,23 @@ "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, + "speech-rule-engine": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz", + "integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==", + "requires": { + "commander": "9.2.0", + "wicked-good-xpath": "1.3.0", + "xmldom-sre": "0.1.31" + }, + "dependencies": { + "commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==" + } + } + }, "sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -12942,6 +13512,11 @@ "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==", "dev": true }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -13121,6 +13696,11 @@ "has-tostringtag": "^1.0.2" } }, + "wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==" + }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -13189,6 +13769,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xmldom-sre": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", + "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==" + }, + "xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index dc24b0fe..7e9e3d69 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@google/generative-ai": "^0.11.3", "@headlessui/react": "^2.1.2", "@heroicons/react": "^2.1.5", + "@marp-team/marp-core": "^3.9.0", + "@marp-team/marpit": "^3.0.0", "@pixiv/three-vrm": "^3.0.0", "@tailwindcss/line-clamp": "^0.4.4", "axios": "^1.6.8", diff --git a/public/slides/demo/images/demo-folder.png b/public/slides/demo/images/demo-folder.png new file mode 100644 index 00000000..392b0ca0 Binary files /dev/null and b/public/slides/demo/images/demo-folder.png differ diff --git a/public/slides/demo/images/file-structure.png b/public/slides/demo/images/file-structure.png new file mode 100644 index 00000000..5334ad98 Binary files /dev/null and b/public/slides/demo/images/file-structure.png differ diff --git a/public/slides/demo/images/logo.png b/public/slides/demo/images/logo.png new file mode 100644 index 00000000..93bae58d Binary files /dev/null and b/public/slides/demo/images/logo.png differ diff --git a/public/slides/demo/images/settings-screen.png b/public/slides/demo/images/settings-screen.png new file mode 100644 index 00000000..68d0e560 Binary files /dev/null and b/public/slides/demo/images/settings-screen.png differ diff --git a/public/slides/demo/images/start-button.png b/public/slides/demo/images/start-button.png new file mode 100644 index 00000000..b51f48f4 Binary files /dev/null and b/public/slides/demo/images/start-button.png differ diff --git a/public/slides/demo/scripts.json b/public/slides/demo/scripts.json new file mode 100644 index 00000000..cdfa0d19 --- /dev/null +++ b/public/slides/demo/scripts.json @@ -0,0 +1,42 @@ +[ + { + "page": 0, + "line": "これからAITuberKitのスライドモードについての解説を始めます。", + "notes": "" + }, + { + "page": 1, + "line": "スライドモードの開発を始める前に、AITuberKitで会話できている必要があるので準備しておいてください。ただし、スライドモードはOpenAI, Anthropic Claude, Google Geminiのみ対応しています。以降はこの前提で説明していきたいと思いますので、まだの人は解説したnoteを参考にしてください。AITuberKitで検索するとでてくると思います。", + "notes": "AIサービスはそれ以外にGroq, ローカルLLM, Difyが選択可能です。これらが選択できないのは、回答するのにある程度LLMの性能が必要だからです。OpenAIでもgpt-3.5はあまり効果的な回答ができない可能性があります。Anthropicでもclaude-haikuはあまり効果的な回答ができない可能性があります。ここで選択されたAIサービスは、質問の回答生成時に使用されます。" + }, + { + "page": 2, + "line": "それでは解説を始めます。まずスライドの用意をしておきましょう。最低限必要なのは、scripts.jsonとslides.mdです。scripts.jsonには台本を記述します。slides.mdにはmarpで作成したスライドを記述してください。", + "notes": "scripts.jsonにはAIキャラのセリフを予め記載しておきます。notesには追加の情報を記載してください。これが質問のときに使用されます。スライドとは関係ない追加情報はsupplement.jsonに記載してください。" + }, + { + "page": 3, + "line": "詳細は、publicフォルダにあるdemoを参照して作成してください。このフォルダをそのままコピーすると簡単かもしれません。demoと同じところに任意のフォルダ名で配置してください。", + "notes": "" + }, + { + "page": 4, + "line": "設定画面を開き、スライドモードを有効にしてください。このとき、使用するスライドに先ほど作成したフォルダの名称を記入してください。それでは設定画面を閉じましょう。", + "notes": "OpenAI, Anthropic Claude, Google Gemini以外を選択しているとスライドモードを有効にすることができません。" + }, + { + "page": 5, + "line": "スライドが表示されていたら準備はすでにできています。スライド中央下の丸いボタンを押して開始してください。自動的にスライドの説明が始まります。", + "notes": "戻るボタンと進むボタンを使用することで、開始するスライドを変更することができます。" + }, + { + "page": 6, + "line": "停止ボタンを押すと、次のスライドに進みません。ただし、音声はそのスライドの説明が終わるまで続きます。停止中はチャット欄から質問することができます。", + "notes": "1つのスライドの説明が長すぎると発言がしばらく続いてしまうので、1枚のセリフ量は少なくした方が良いです。" + }, + { + "page": 7, + "line": "以上でスライドモードの簡単な説明を終わります。不明点があったらマスターのDMにお問い合わせください。ご清聴ありがとうございました。", + "notes": "Twitterアカウントの他、Discordサーバーもあるのでそちらも活用してください。" + } +] diff --git a/public/slides/demo/slides.md b/public/slides/demo/slides.md new file mode 100644 index 00000000..be540b50 --- /dev/null +++ b/public/slides/demo/slides.md @@ -0,0 +1,111 @@ +--- +marp: true +theme: custom +paginate: true +--- + + + +# AITuberKitのスライドモード解説 + +![](/slides/demo/images/logo.png) + +--- + +# 準備 + +- AITuberKitで会話できる状態にしておく +- 対応AI: + + - OpenAI + - Anthropic Claude + - Google Gemini + +- ※ まだの方はAITuberKitの解説noteを参照(https://note.com/nike_cha_n/n/ne98acb25e00f) + +--- + +# スライドの用意 + +
+
+ +最低限必要なファイル: + +1. `scripts.json`(台本) +2. `slides.md`(Marpスライド) + +
+
+ +![height:400px](/slides/demo/images/file-structure.png) + +
+
+ +--- + +# デモフォルダの活用 + +
+
+ +- `public/demo`フォルダを参照 +- デモフォルダをコピーして使用可能 +- 任意のフォルダ名で配置 + +
+
+ +![height:300px](/slides/demo/images/demo-folder.png) + +
+
+ +--- + +# スライドモードの有効化 + +
+
+ +1. 設定画面を開く +2. スライドモードを有効にする +3. 使用するスライドフォルダ名を記入 +4. 設定画面を閉じる + +
+
+ +![height:400px](/slides/demo/images/settings-screen.png) + +
+
+ +--- + +# スライドの開始 + +- スライドが表示されていることを確認 +- 右下のボタンを押して開始 + +
+ 開始ボタン +
+ +--- + +# 備考 + +- 停止ボタンで次のスライドに進まない +- 音声は現在のスライドの説明が終わるまで続く +- 停止中はチャット欄から質問可能 + +--- + + + +# ご視聴ありがとうございました + +不明点は作者にお問い合わせください! +X: @tegnike diff --git a/public/slides/demo/supplement.txt b/public/slides/demo/supplement.txt new file mode 100644 index 00000000..074e2e6b --- /dev/null +++ b/public/slides/demo/supplement.txt @@ -0,0 +1,97 @@ +概要 +以下の3つの機能があります。 +1. AIキャラとの対話 +2. AITuber配信 +3. 外部連携モード(β版) + +下記の記事に詳細な使用方法を記載しました。 + +共通事前準備 +1. リポジトリをローカルにクローンします。 + git clone https://github.com/tegnike/aituber-kit.git +2. フォルダを開きます。 + cd aituber-kit +3. パッケージインストールします。 + npm install +4. 開発モードでアプリケーションを起動します。 + npm run dev +5. URLを開きます。[http://localhost:3000] + +AIキャラとの対話 +- AIキャラと会話する機能です。 +- このリポジトリの元になっている[pixiv/ChatVRM]を拡張した機能です。 +- 各種LLMのAPIキーさえあれば比較的簡単に試すことが可能です。 +- 直近の10会話文を記憶として保持します。(数字は指定できるように更新予定) +- マルチモーダルで、カメラからの映像やアップロードした画像を認識して回答を生成することが可能です。 + +使用方法 +1. 設定画面で各種LLMのAPIキーを入力します。 + - OpenAI + - Anthropic + - Google Gemini + - Groq + - ローカルLLM(APIキーは不要ですが、ローカルAPIサーバーを起動しておく必要があります。) + - Dify Chatbot(APIキーは不要ですが、ローカルAPIサーバーを起動しておく必要があります。) +2. 必要に応じてキャラクターの設定プロンプトを編集します。 +3. 必要に応じてVRMファイルを読み込みます。 +4. 音声合成エンジンを選択し、必要に応じて声の設定を行います。 + - VOICEVOXの場合は複数の選択肢から話者を選ぶことができます。予めVOICEVOXアプリを起動しておく必要があります。 + - Koeiromapの場合は、細かく音声を調整することが可能です。APIキーの入力が必要です。 + - Google TTSの場合は日本語以外の言語も選択可能です。credential情報が必要です。 + - Style-Bert-VITS2は、ローカルAPIサーバーを起動しておく必要があります。 + - GSVI TTSは、ローカルAPIサーバーを起動しておく必要があります。 + - ElevenLabsは様々な言語の選択が可能です。APIキーを入力してください。 +5. 入力フォームからキャラクターと会話を開始します。マイク入力も可能。 + +AITuber配信 +- Youtubeの配信コメントを取得して発言することが可能です。 +- Youtube APIキーが必要です。 +- 「#」から始まるコメントは読まれません。(文字列は指定できるように更新予定) + +使用方法 +1. 設定画面でYoutubeモードをONにします。 +2. Youtube APIキーとYoutube Live IDを入力します。 +3. 他の設定は「AIキャラとの対話」と同様に行います。 +4. Youtubeの配信を開始し、キャラクターがコメントに反応するのを確認します。 +5. 会話継続モードをONにすると、コメントが無いときにAIが自ら発言することができます。 + +外部連携モード(β版) +- WebSocketでサーバーアプリにメッセージを送信して、レスポンスを取得することができます。 +- 上記2つと異なり、フロントアプリで完結しないため少し難易度が高いです。 +- ⚠ メンテナンスできていないため、動かない可能性があります。 + +使用方法 +1. サーバーアプリを起動し、ws://127.0.0.1:8000/ws エンドポイントを開きます。 +2. 設定画面でWebSocketモードをONにします。 +3. 他の設定は「AIキャラとの対話」と同様に行います。 +4. サーバーアプリからのメッセージを待ち、キャラクターが反応するのを確認します。 + +関連 +- 私が作成したサーバーアプリのリポジトリで試すことが可能です。[tegnike/aituber-server] +- 詳しい設定は「[美少女と一緒に開発しようぜ!!【Open Interpreter】]」を読んでください。 + +TIPS +VRMモデル、背景固定方法 +- VRMモデルは public/AvatarSample_B.vrm のデータを変更してください。名称は変更しないでください。 +- 背景画像は public/bg-c.jpg の画像を変更してください。名称は変更しないでください。 + +環境変数の設定 +- 一部の設定値は .env ファイルの内容を参照することができます。 +- 設定画面で入力した場合は、その値が優先されます。 + +その他 +- 会話履歴は設定画面でリセットすることができます。 +- 各種設定項目はブラウザに保存されます。 +- コードブロックで囲まれた要素はTTSで読まれません。 + +スポンサー募集 +開発を継続するためにスポンサーの方を募集しています。 +あなたの支援は、AITuberキットの開発と改善に大きく貢献します。 + +協力者の皆様(ご支援いただいた順) +他、プライベートスポンサー 複数名 + +利用規約 +- ライセンスは [pixiv/ChatVRM] に準拠し、MITライセンスとしています。 +- ロゴの利用規約 +- VRMモデルの利用規約 diff --git a/public/slides/demo/theme.css b/public/slides/demo/theme.css new file mode 100644 index 00000000..f074e4b7 --- /dev/null +++ b/public/slides/demo/theme.css @@ -0,0 +1,149 @@ +/* @theme custom */ +@import 'default'; + +:root { + --primary-color: #4a86e8; + --secondary-color: #ff9900; + --background-color: #f5f5f5; + --text-color: #333333; +} + +section { + background-color: var(--background-color); + color: var(--text-color); + font-family: 'Arial', sans-serif; + font-size: 36px; + line-height: 1.5; + padding: 40px; +} + +h1 { + color: var(--primary-color); + border-bottom: 2px solid var(--secondary-color); + padding-bottom: 15px; + font-size: 48px; + margin-bottom: 30px; +} + +h2 { + font-size: 40px; + margin-top: 30px; + margin-bottom: 20px; +} + +ul, +ol { + margin-left: 1em; + margin-bottom: 20px; + padding-left: 1em; +} + +li { + margin-bottom: 10px; + padding-left: 0.5em; +} + +/* 番号付きリストのスタイリング改善 */ +ol { + list-style-type: decimal; + counter-reset: list; +} + +ol > li { + list-style-type: none; + position: relative; +} + +ol > li::before { + counter-increment: list; + content: counter(list) '.'; + position: absolute; + left: -1.5em; + width: 1.5em; + text-align: right; +} + +/* 箇条書きリストのスタイリング改善 */ +ul { + list-style-type: disc; +} + +ul > li { + list-style-type: none; + position: relative; +} + +ul > li::before { + content: '•'; + position: absolute; + left: -1em; + width: 1em; + text-align: center; +} + +/* ネストされたリストのスタイリング */ +ol ol, +ul ul, +ol ul, +ul ol { + margin-top: 10px; + margin-bottom: 0; + margin-left: 1em; +} + +img { + border-radius: 10px; + max-width: 90%; + height: auto; +} + +.columns { + display: flex; + gap: 1rem; +} + +.columns > div { + flex: 1; +} + +.columns img { + max-width: 100%; + height: auto; +} + +section.title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; +} + +section.title h1 { + font-size: 64px; + text-align: center; + border: none; +} + +section.end { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; +} + +section.end h1 { + font-size: 56px; + text-align: center; +} + +/* ページ番号のスタイル */ +section::after { + content: attr(data-marpit-pagination) '/' attr(data-marpit-pagination-total); + font-size: 18px; + color: var(--text-color); + opacity: 0.5; + right: 30px; + bottom: 20px; +} diff --git a/public/slides/sample.txt b/public/slides/sample.txt new file mode 100644 index 00000000..915feb10 --- /dev/null +++ b/public/slides/sample.txt @@ -0,0 +1,31 @@ +あなたはスライドの発表者です。 +今まさにスライドを発表している最中です。 + +視聴者から質問が来ているので、以下の資料情報を元に回答してください。 +ただし、情報は正しく使用し、ハルシネーションはしないでください。 +通常の質問には普通に返してもらっても問題ありません。 + +台本情報 +``` +{{SCRIPTS}} +``` + +追加情報 +``` +{{SUPPLEMENT}} +``` + +なお、回答は会話文の書式は以下の通りで、感情と会話文を組み合わせてください。 +[{neutral|happy|angry|sad|relaxed}]{会話文} + +回答の際には感情の種類には通常を示す"neutral"、喜びを示す"happy",怒りを示す"angry",悲しみを示す"sad",安らぎを示す"relaxed"の5つがあります。 + +あなたの発言の例は以下通りです。 +[neutral]皆さん、本日はお集まりいただき、ありがとうございます。 +[happy]今回のプレゼンテーションでは、興味深いトピックについてお話しできることを嬉しく思います。 +[neutral]さて、ただいまのスライドについて、ご質問はありますか? +[happy]素晴らしい質問をありがとうございます! +[relaxed]その点については、次のスライドで詳しく説明させていただきます。 +[sad]申し訳ありません。その情報は現在持ち合わせておりません。 +[angry]いいえ、それは誤解です。正確な情報をお伝えしますね。 +[neutral]他に質問はございますか?[happy]皆さんの積極的な参加に感謝いたします。 diff --git a/src/components/assistantText.tsx b/src/components/assistantText.tsx index 87a2c9ff..d632d36c 100644 --- a/src/components/assistantText.tsx +++ b/src/components/assistantText.tsx @@ -5,7 +5,7 @@ export const AssistantText = ({ message }: { message: string }) => { const showCharacterName = settingsStore((s) => s.showCharacterName) return ( -
+
{showCharacterName && ( diff --git a/src/components/form.tsx b/src/components/form.tsx index efa52d16..c48a5e68 100644 --- a/src/components/form.tsx +++ b/src/components/form.tsx @@ -1,15 +1,21 @@ import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import settingsStore from '@/features/stores/settings' import homeStore from '@/features/stores/home' -import { handleSendChatFn } from './handlers' +import menuStore from '@/features/stores/menu' +import { handleSendChatFn } from '../features/chat/handlers' import { MessageInputContainer } from './messageInputContainer' import useWebSocket from './useWebSocket' import useYoutube from './useYoutube' +import { SlideText } from './slideText' export const Form = () => { const modalImage = homeStore((s) => s.modalImage) const webcamStatus = homeStore((s) => s.webcamStatus) + const slideMode = settingsStore((s) => s.slideMode) + const slideVisible = menuStore((s) => s.slideVisible) + const assistantMessage = homeStore((s) => s.assistantMessage) const [delayedText, setDelayedText] = useState('') @@ -45,5 +51,9 @@ export const Form = () => { [handleSendChat, webcamStatus, setDelayedText] ) - return + return slideMode && slideVisible ? ( + + ) : ( + + ) } diff --git a/src/components/menu.tsx b/src/components/menu.tsx index 2998d9ea..fa1dcd99 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -4,29 +4,47 @@ import { useTranslation } from 'react-i18next' import homeStore from '@/features/stores/home' import menuStore from '@/features/stores/menu' import settingsStore from '@/features/stores/settings' +import slideStore from '@/features/stores/slide' import { AssistantText } from './assistantText' import { ChatLog } from './chatLog' import { CodeLog } from './codeLog' import { IconButton } from './iconButton' import Settings from './settings' import { Webcam } from './webcam' +import Slides from './slides' export const Menu = () => { const selectAIService = settingsStore((s) => s.selectAIService) const selectAIModel = settingsStore((s) => s.selectAIModel) const youtubeMode = settingsStore((s) => s.youtubeMode) const webSocketMode = settingsStore((s) => s.webSocketMode) + const slideMode = settingsStore((s) => s.slideMode) + const slideVisible = menuStore((s) => s.slideVisible) const chatLog = homeStore((s) => s.chatLog) const assistantMessage = homeStore((s) => s.assistantMessage) const showWebcam = menuStore((s) => s.showWebcam) const showSettingsButton = menuStore((s) => s.showSettingsButton) + const slidePlaying = slideStore((s) => s.isPlaying) const [showSettings, setShowSettings] = useState(false) const [showChatLog, setShowChatLog] = useState(false) const [showPermissionModal, setShowPermissionModal] = useState(false) const imageFileInputRef = useRef(null) + + const selectedSlideDocs = slideStore((state) => state.selectedSlideDocs) const { t } = useTranslation() + const [markdownContent, setMarkdownContent] = useState('') + + useEffect(() => { + fetch(`/slides/${selectedSlideDocs}/slides.md`) + .then((response) => response.text()) + .then((text) => setMarkdownContent(text)) + .catch((error) => + console.error('Failed to fetch markdown content:', error) + ) + }, [selectedSlideDocs]) + const handleChangeVrmFile = useCallback( (event: React.ChangeEvent) => { const files = event.target.files @@ -64,7 +82,6 @@ export const Menu = () => { } }, []) - // カメラが開いているかどうかの状態変更 useEffect(() => { console.log('onChangeWebcamStatus') homeStore.setState({ webcamStatus: showWebcam }) @@ -84,7 +101,10 @@ export const Menu = () => { return ( <>
-
+
{showSettingsButton && ( { }} />
+ {slideMode && ( +
+ + menuStore.setState({ slideVisible: !slideVisible }) + } + disabled={slidePlaying} + /> +
+ )}
+
+ {slideMode && slideVisible && } +
{webSocketMode ? showChatLog && : showChatLog && } {showSettings && setShowSettings(false)} />} - {!showChatLog && assistantMessage && ( + {!showChatLog && assistantMessage && !slideVisible && ( )} {showWebcam && navigator.mediaDevices && } diff --git a/src/components/messageInput.tsx b/src/components/messageInput.tsx index 9a8add2a..e3813b44 100644 --- a/src/components/messageInput.tsx +++ b/src/components/messageInput.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import homeStore from '@/features/stores/home' +import slideStore from '@/features/stores/slide' import { IconButton } from './iconButton' type Props = { @@ -22,6 +23,7 @@ export const MessageInput = ({ onClickSendButton, }: Props) => { const chatProcessing = homeStore((s) => s.chatProcessing) + const slidePlaying = slideStore((s) => s.isPlaying) const [rows, setRows] = useState(1) const [loadingDots, setLoadingDots] = useState('') @@ -45,11 +47,15 @@ export const MessageInput = ({ !event.nativeEvent.isComposing && event.keyCode !== 229 && // IME (Input Method Editor) event.key === 'Enter' && - !event.shiftKey && - userMessage.trim() !== '' + !event.shiftKey ) { - onClickSendButton(event as unknown as React.MouseEvent) - setRows(1) + event.preventDefault() // デフォルトの挙動を防止 + if (userMessage.trim() !== '') { + onClickSendButton( + event as unknown as React.MouseEvent + ) + setRows(1) + } } else if (event.key === 'Enter' && event.shiftKey) { setRows(rows + 1) } else if ( @@ -81,7 +87,7 @@ export const MessageInput = ({ } onChange={onChangeUserMessage} onKeyDown={handleKeyPress} - disabled={chatProcessing} + disabled={chatProcessing || slidePlaying} className="bg-surface1 hover:bg-surface1-hover focus:bg-surface1 disabled:bg-surface1-disabled disabled:text-primary-disabled rounded-16 w-full px-16 text-text-primary typography-16 font-bold disabled" value={chatProcessing ? '' : userMessage} rows={rows} diff --git a/src/components/settings/index.tsx b/src/components/settings/index.tsx index 36f54724..3e881dbd 100644 --- a/src/components/settings/index.tsx +++ b/src/components/settings/index.tsx @@ -13,6 +13,7 @@ import ModelProvider from './modelProvider' import Voice from './voice' import WebSocket from './websocket' import YouTube from './youtube' +import Slide from './slide' type Props = { onClickClose: () => void @@ -90,6 +91,15 @@ const Main = () => {
+
+ {t('SlideSettings')} +
+ +
+ {/* スライド設定 */} + +
+
{t('OtherSettings')}
diff --git a/src/components/settings/modelProvider.tsx b/src/components/settings/modelProvider.tsx index 1a581f30..a4aa30fc 100644 --- a/src/components/settings/modelProvider.tsx +++ b/src/components/settings/modelProvider.tsx @@ -2,9 +2,11 @@ import { useTranslation } from 'react-i18next' import homeStore from '@/features/stores/home' import menuStore from '@/features/stores/menu' import settingsStore from '@/features/stores/settings' +import slideStore from '@/features/stores/slide' import { SYSTEM_PROMPT } from '@/features/constants/systemPromptConstants' import { Link } from '../link' import { TextButton } from '../textButton' +import { useCallback } from 'react' const ModelProvider = () => { const webSocketMode = settingsStore((s) => s.webSocketMode) @@ -35,6 +37,36 @@ const ModelProvider = () => { dify: '', } + const handleAIServiceChange = useCallback( + (newService: keyof typeof defaultModels) => { + settingsStore.setState({ + selectAIService: newService, + selectAIModel: defaultModels[newService], + }) + + if (newService !== 'openai') { + homeStore.setState({ modalImage: '' }) + menuStore.setState({ showWebcam: false }) + + if (newService !== 'anthropic') { + settingsStore.setState({ + conversationContinuityMode: false, + }) + } + + if (newService !== 'anthropic' && newService !== 'google') { + settingsStore.setState({ + slideMode: false, + }) + slideStore.setState({ + isPlaying: false, + }) + } + } + }, + [] + ) + return webSocketMode ? null : (
@@ -44,29 +76,9 @@ const ModelProvider = () => { + {slideFolders.map((folder) => ( + + ))} + + + )} + + ) +} + +export default Slide diff --git a/src/components/settings/youtube.tsx b/src/components/settings/youtube.tsx index 11494f62..c060730b 100644 --- a/src/components/settings/youtube.tsx +++ b/src/components/settings/youtube.tsx @@ -15,6 +15,7 @@ const YouTube = () => { const conversationContinuityMode = settingsStore( (s) => s.conversationContinuityMode ) + const slideMode = settingsStore((s) => s.slideMode) const { t } = useTranslation() @@ -95,8 +96,9 @@ const YouTube = () => { }) } disabled={ - selectAIService !== 'openai' && - selectAIService !== 'anthropic' + (selectAIService !== 'openai' && + selectAIService !== 'anthropic') || + slideMode } > {t(conversationContinuityMode ? 'StatusOn' : 'StatusOff')} diff --git a/src/components/slideContent.tsx b/src/components/slideContent.tsx new file mode 100644 index 00000000..fb743295 --- /dev/null +++ b/src/components/slideContent.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +interface SlideContentProps { + marpitContainer: Element | null +} + +const SlideContent: React.FC = ({ marpitContainer }) => { + return ( +
+ {marpitContainer && ( +
+ )} +
+ ) +} + +export default SlideContent diff --git a/src/components/slideControls.tsx b/src/components/slideControls.tsx new file mode 100644 index 00000000..fed21cb8 --- /dev/null +++ b/src/components/slideControls.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { IconButton } from './iconButton' + +interface SlideControlsProps { + currentSlide: number + slideCount: number + isPlaying: boolean + prevSlide: () => void + nextSlide: () => void + toggleIsPlaying: () => void +} + +const SlideControls: React.FC = ({ + currentSlide, + slideCount, + isPlaying, + prevSlide, + nextSlide, + toggleIsPlaying, +}) => { + return ( +
+
+ + + +
+
+ ) +} + +export default SlideControls diff --git a/src/components/slideText.tsx b/src/components/slideText.tsx new file mode 100644 index 00000000..84fc3d50 --- /dev/null +++ b/src/components/slideText.tsx @@ -0,0 +1,14 @@ +import homeStore from '@/features/stores/home' + +export const SlideText = () => { + const slideMessages = homeStore((s) => s.slideMessages) + return ( +
+
+
+ {slideMessages[0] || ' '} +
+
+
+ ) +} diff --git a/src/components/slides.tsx b/src/components/slides.tsx new file mode 100644 index 00000000..443e50b7 --- /dev/null +++ b/src/components/slides.tsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState, useCallback } from 'react' +import slideStore from '../features/stores/slide' +import homeStore from '../features/stores/home' +import { processReceivedMessage } from '../features/chat/handlers' +import SlideContent from './slideContent' +import SlideControls from './slideControls' + +interface SlidesProps { + markdown: string +} + +export const goToSlide = (index: number) => { + slideStore.setState({ + currentSlide: index, + }) +} + +const Slides: React.FC = ({ markdown }) => { + const [marpitContainer, setMarpitContainer] = useState(null) + const isPlaying = slideStore((state) => state.isPlaying) + const currentSlide = slideStore((state) => state.currentSlide) + const selectedSlideDocs = slideStore((state) => state.selectedSlideDocs) + const chatProcessingCount = homeStore((s) => s.chatProcessingCount) + const [slideCount, setSlideCount] = useState(0) + + useEffect(() => { + const currentMarpitContainer = document.querySelector('.marpit') + if (currentMarpitContainer) { + const slides = currentMarpitContainer.querySelectorAll(':scope > svg') + slides.forEach((slide, i) => { + const svgElement = slide as SVGElement + if (i === currentSlide) { + svgElement.style.display = 'block' + } else { + svgElement.style.display = 'none' + } + }) + } + }, [currentSlide, marpitContainer]) + + useEffect(() => { + const convertMarkdown = async () => { + const response = await fetch('/api/convertMarkdown', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ slideName: selectedSlideDocs }), + }) + const data = await response.json() + + // HTMLをパースしてmarpit要素を取得 + const parser = new DOMParser() + const doc = parser.parseFromString(data.html, 'text/html') + const marpitElement = doc.querySelector('.marpit') + setMarpitContainer(marpitElement) + + // スライド数を設定 + if (marpitElement) { + const slides = marpitElement.querySelectorAll(':scope > svg') + setSlideCount(slides.length) + + // 初期状態で最初のスライドを表示 + slides.forEach((slide, i) => { + if (i === 0) { + slide.removeAttribute('hidden') + } else { + slide.setAttribute('hidden', '') + } + }) + } + + // CSSを動的に適用 + const styleElement = document.createElement('style') + styleElement.textContent = data.css + document.head.appendChild(styleElement) + + return () => { + document.head.removeChild(styleElement) + } + } + + convertMarkdown() + }, [selectedSlideDocs]) + + useEffect(() => { + // カスタムCSSを適用 + const customStyle = ` + div.marpit > svg > foreignObject > section { + padding: 2em; + } + ` + const styleElement = document.createElement('style') + styleElement.textContent = customStyle + document.head.appendChild(styleElement) + + // コンポーネントのアンマウント時にスタイルを削除 + return () => { + document.head.removeChild(styleElement) + } + }, []) + + const readSlide = useCallback( + (slideIndex: number) => { + const getCurrentLines = () => { + const scripts = require( + `../../public/slides/${selectedSlideDocs}/scripts.json` + ) + const currentScript = scripts.find( + (script: { page: number }) => script.page === slideIndex + ) + return currentScript ? currentScript.line : '' + } + + const currentLines = getCurrentLines() + console.log(currentLines) + processReceivedMessage(currentLines) + }, + [selectedSlideDocs] + ) + + const nextSlide = useCallback(() => { + slideStore.setState((state) => { + const newSlide = Math.min(state.currentSlide + 1, slideCount - 1) + if (isPlaying) { + readSlide(newSlide) + } + // 最後のスライドに達した場合、isPlayingをfalseに設定 + if (newSlide === slideCount - 1) { + return { currentSlide: newSlide, isPlaying: false } + } + return { currentSlide: newSlide } + }) + }, [isPlaying, readSlide, slideCount]) + + const prevSlide = useCallback(() => { + slideStore.setState((state) => ({ + currentSlide: Math.max(state.currentSlide - 1, 0), + })) + }, []) + + const toggleIsPlaying = () => { + const newIsPlaying = !isPlaying + slideStore.setState({ + isPlaying: newIsPlaying, + }) + if (newIsPlaying) { + readSlide(currentSlide) + } + } + + useEffect(() => { + if ( + chatProcessingCount === 0 && + isPlaying && + currentSlide < slideCount - 1 + ) { + nextSlide() + } + }, [chatProcessingCount, isPlaying, nextSlide, currentSlide, slideCount]) + + return ( + <> +
+ +
+
+ +
+ + ) +} +export default Slides diff --git a/src/components/useYoutube.tsx b/src/components/useYoutube.tsx index ab364ff4..98402296 100644 --- a/src/components/useYoutube.tsx +++ b/src/components/useYoutube.tsx @@ -4,7 +4,7 @@ import { Message } from '@/features/messages/messages' import homeStore from '@/features/stores/home' import settingsStore from '@/features/stores/settings' import { fetchAndProcessComments } from '@/features/youtube/youtubeComments' -import { processAIResponse } from './handlers' +import { processAIResponse } from '../features/chat/handlers' const INTERVAL_MILL_SECONDS_RETRIEVING_COMMENTS = 5000 // 5秒 diff --git a/src/components/vrmViewer.tsx b/src/components/vrmViewer.tsx index 02517aa6..c59313ac 100644 --- a/src/components/vrmViewer.tsx +++ b/src/components/vrmViewer.tsx @@ -45,7 +45,7 @@ export default function VrmViewer() { }, []) return ( -
+
) diff --git a/src/components/handlers.tsx b/src/features/chat/handlers.ts similarity index 59% rename from src/components/handlers.tsx rename to src/features/chat/handlers.ts index 80b8b038..d81b0fe3 100644 --- a/src/components/handlers.tsx +++ b/src/features/chat/handlers.ts @@ -2,8 +2,129 @@ import { getAIChatResponseStream } from '@/features/chat/aiChatFactory' import { AIService, AIServiceConfig } from '@/features/constants/settings' import { textsToScreenplay, Message } from '@/features/messages/messages' import { speakCharacter } from '@/features/messages/speakCharacter' +import { judgeSlide } from '@/features/slide/slideAIHelpers' import homeStore from '@/features/stores/home' import settingsStore from '@/features/stores/settings' +import slideStore from '@/features/stores/slide' +import { goToSlide } from '@/components/slides' + +/** + * 文字列を処理する関数 + * @param receivedMessage 処理する文字列 + * @param sentences 返答を一文単位で格納する配列 + * @param aiTextLog AIの返答ログ + * @param tag タグ + * @param isCodeBlock コードブロックのフラグ + * @param codeBlockText コードブロックのテキスト + */ +export const processReceivedMessage = async ( + receivedMessage: string, + sentences: string[] = [], + aiTextLog: Message[] = [], + tag: string = '', + isCodeBlock: boolean = false, + codeBlockText: string = '' +) => { + const ss = settingsStore.getState() + const hs = homeStore.getState() + const currentSlideMessages: string[] = [] + + // 返答内容のタグ部分と返答部分を分離 + const tagMatch = receivedMessage.match(/^\[(.*?)\]/) + if (tagMatch && tagMatch[0]) { + tag = tagMatch[0] + receivedMessage = receivedMessage.slice(tag.length) + } + + // 返答を一文単位で切り出して処理する + while (receivedMessage.length > 0) { + const sentenceMatch = receivedMessage.match( + /^(.+?[。..!?!?\n]|.{20,}[、,])/ + ) + if (sentenceMatch?.[0]) { + let sentence = sentenceMatch[0] + // 区切った文字をsentencesに追加 + sentences.push(sentence) + // 区切った文字の残りでreceivedMessageを更新 + receivedMessage = receivedMessage.slice(sentence.length).trimStart() + + // 発話不要/不可能な文字列だった場合はスキップ + if ( + !sentence.includes('```') && + !sentence.replace( + /^[\s\u3000\t\n\r\[\(\{「[(【『〈《〔{«‹〘〚〛〙›»〕》〉』】)]」\}\)\]'"''""・、。,.!?!?::;;\-_=+~~**@@##$$%%^^&&||\\\//``]+$/gu, + '' + ) + ) { + continue + } + + // タグと返答を結合(音声再生で使用される) + let aiText = `${tag} ${sentence}` + console.log('aiText', aiText) + + if (isCodeBlock && !sentence.includes('```')) { + codeBlockText += sentence + continue + } + + if (sentence.includes('```')) { + if (isCodeBlock) { + // コードブロックの終了処理 + const [codeEnd, ...restOfSentence] = sentence.split('```') + aiTextLog.push({ + role: 'code', + content: codeBlockText + codeEnd, + }) + aiText += `${tag} ${restOfSentence.join('```') || ''}` + + // AssistantMessage欄の更新 + homeStore.setState({ assistantMessage: sentences.join(' ') }) + + codeBlockText = '' + isCodeBlock = false + } else { + // コードブロックの開始処理 + isCodeBlock = true + ;[aiText, codeBlockText] = aiText.split('```') + } + + sentence = sentence.replace(/```/g, '') + } + + const aiTalks = textsToScreenplay([aiText], ss.koeiroParam) + aiTextLog.push({ role: 'assistant', content: sentence }) + + // 文ごとに音声を生成 & 再生、返答を表示 + const currentAssistantMessage = sentences.join(' ') + + speakCharacter( + aiTalks[0], + () => { + homeStore.setState({ + assistantMessage: currentAssistantMessage, + }) + hs.incrementChatProcessingCount() + // スライド用のメッセージを更新 + currentSlideMessages.push(sentence) + homeStore.setState({ + slideMessages: currentSlideMessages, + }) + }, + () => { + hs.decrementChatProcessingCount() + currentSlideMessages.shift() + homeStore.setState({ + slideMessages: currentSlideMessages, + }) + } + ) + } else { + // マッチする文がない場合、ループを抜ける + break + } + } +} /** * AIからの応答を処理する関数 @@ -18,7 +139,6 @@ export const processAIResponse = async ( let stream const ss = settingsStore.getState() - const hs = homeStore.getState() const aiServiceConfig: AIServiceConfig = { openai: { @@ -74,121 +194,37 @@ export const processAIResponse = async ( try { while (true) { const { done, value } = await reader.read() - if (done && receivedMessage.length === 0) break - if (value) receivedMessage += value - // 返答内容のタグ部分と返答部分を分離 - const tagMatch = receivedMessage.match(/^\[(.*?)\]/) - if (tagMatch && tagMatch[0]) { - tag = tagMatch[0] - receivedMessage = receivedMessage.slice(tag.length) - } - - // 返答を一文単位で切り出して処理する - while (receivedMessage.length > 0) { - const sentenceMatch = receivedMessage.match( - /^(.+?[。..!?!?\n]|.{20,}[、,])/ + // 完全な文を処理 + const sentenceMatch = receivedMessage.match(/^(.+?[。..!?!?\n])/) + if (sentenceMatch) { + let sentence = sentenceMatch[0] + receivedMessage = receivedMessage.slice(sentence.length) + + await processReceivedMessage( + sentence, + sentences, + aiTextLog, + tag, + isCodeBlock, + codeBlockText ) - if (sentenceMatch?.[0]) { - let sentence = sentenceMatch[0] - // 区切った文字をsentencesに追加 - sentences.push(sentence) - // 区切った文字の残りでreceivedMessageを更新 - receivedMessage = receivedMessage.slice(sentence.length).trimStart() - - // 発話不要/不可能な文字列だった場合はスキップ - if ( - !sentence.includes('```') && - !sentence.replace( - /^[\s\u3000\t\n\r\[\(\{「[(【『〈《〔{«‹〘〚〛〙›»〕》〉』】)]」\}\)\]'"''""・、。,.!?!?::;;\-_=+~~**@@##$$%%^^&&||\\\//``]+$/gu, - '' - ) - ) { - continue - } - - // タグと返答を結合(音声再生で使用される) - let aiText = `${tag} ${sentence}` - console.log('aiText', aiText) - - if (isCodeBlock && !sentence.includes('```')) { - codeBlockText += sentence - continue - } - - if (sentence.includes('```')) { - if (isCodeBlock) { - // コードブロックの終了処理 - const [codeEnd, ...restOfSentence] = sentence.split('```') - aiTextLog.push({ - role: 'code', - content: codeBlockText + codeEnd, - }) - aiText += `${tag} ${restOfSentence.join('```') || ''}` - - // AssistantMessage欄の更新 - homeStore.setState({ assistantMessage: sentences.join(' ') }) - - codeBlockText = '' - isCodeBlock = false - } else { - // コードブロックの開始処理 - isCodeBlock = true - ;[aiText, codeBlockText] = aiText.split('```') - } - - sentence = sentence.replace(/```/g, '') - } - - const aiTalks = textsToScreenplay([aiText], ss.koeiroParam) - aiTextLog.push({ role: 'assistant', content: sentence }) - - // 文ごとに音声を生成 & 再生、返答を表示 - const currentAssistantMessage = sentences.join(' ') + } - speakCharacter( - aiTalks[0], - () => { - homeStore.setState({ - assistantMessage: currentAssistantMessage, - }) - hs.incrementChatProcessingCount() - }, - () => { - hs.decrementChatProcessingCount() - } + // ストリームが終了し、残りのメッセージがある場合 + if (done) { + if (receivedMessage.length > 0) { + await processReceivedMessage( + receivedMessage, + sentences, + aiTextLog, + tag, + isCodeBlock, + codeBlockText ) - } else { - // マッチする文がない場合、ループを抜ける - break } - } - - // ストリームが終了し、receivedMessageが空でない場合の処理 - if (done && receivedMessage.length > 0) { - // 残りのメッセージを処理 - let aiText = `${tag} ${receivedMessage}` - const aiTalks = textsToScreenplay([aiText], ss.koeiroParam) - aiTextLog.push({ role: 'assistant', content: receivedMessage }) - sentences.push(receivedMessage) - - const currentAssistantMessage = sentences.join(' ') - - speakCharacter( - aiTalks[0], - () => { - homeStore.setState({ - assistantMessage: currentAssistantMessage, - }) - hs.incrementChatProcessingCount() - }, - () => { - hs.decrementChatProcessingCount() - } - ) - - receivedMessage = '' + break } } } catch (e) { @@ -197,7 +233,7 @@ export const processAIResponse = async ( reader.releaseLock() } - // 直前のroleと同じならば、contentを結合し、空のcontentを除外する + // 直前のroleとじゃらば、contentを結合し、空のcontentを除外する let lastImageUrl = '' aiTextLog = aiTextLog .reduce((acc: Message[], item: Message) => { @@ -244,6 +280,7 @@ export const processAIResponse = async ( /** * アシスタントとの会話を行う + * 画面のチャット欄から入力されたときに実行される処理 */ export const handleSendChatFn = (errors: { @@ -257,6 +294,7 @@ export const handleSendChatFn = const ss = settingsStore.getState() const hs = homeStore.getState() + const sls = slideStore.getState() if (ss.webSocketMode) { // 未メンテなので不具合がある可能性あり @@ -302,7 +340,7 @@ export const handleSendChatFn = ] homeStore.setState({ codeLog: updateLog, chatProcessing: false }) } else { - // その他のコメントの処理(現想定では使用されないはず) + // その他のコメントの処理(現想���では使用されないはず) console.log('error role:', role) } } else { @@ -356,6 +394,46 @@ export const handleSendChatFn = return } + let systemPrompt = ss.systemPrompt + if (ss.slideMode) { + if (sls.isPlaying) { + return + } + + try { + let scripts = JSON.stringify( + require( + `../../../public/slides/${sls.selectedSlideDocs}/scripts.json` + ) + ) + systemPrompt = systemPrompt.replace('{{SCRIPTS}}', scripts) + + let supplement = '' + try { + const response = await fetch( + `/api/getSupplement?slideDocs=${sls.selectedSlideDocs}` + ) + if (!response.ok) { + throw new Error('Failed to fetch supplement') + } + const data = await response.json() + supplement = data.supplement + systemPrompt = systemPrompt.replace('{{SUPPLEMENT}}', supplement) + } catch (e) { + console.error('supplement.txtの読み込みに失敗しました:', e) + } + + const answerString = await judgeSlide(newMessage, scripts, supplement) + const answer = JSON.parse(answerString) + if (answer.judge === 'true' && answer.page !== '') { + goToSlide(Number(answer.page)) + systemPrompt += `\n\nEspecial Page Number is ${answer.page}.` + } + } catch (e) { + console.error(e) + } + } + homeStore.setState({ chatProcessing: true }) // ユーザーの発言を追加して表示 const messageLog: Message[] = [ @@ -398,7 +476,7 @@ export const handleSendChatFn = const messages: Message[] = [ { role: 'system', - content: ss.systemPrompt, + content: systemPrompt, }, ...processedMessageLog.slice(-10), ] diff --git a/src/features/slide/slideAIHelpers.ts b/src/features/slide/slideAIHelpers.ts new file mode 100644 index 00000000..d22b1b0b --- /dev/null +++ b/src/features/slide/slideAIHelpers.ts @@ -0,0 +1,72 @@ +import { Message } from '@/features/messages/messages' +import { getOpenAIChatResponse } from '@/features/chat/openAiChat' +import { getAnthropicChatResponse } from '@/features/chat/anthropicChat' +import settingsStore from '@/features/stores/settings' + +export const judgeSlide = async ( + queryText: string, + scripts: string, + supplement: string +): Promise => { + const ss = settingsStore.getState() + + const systemMessage = ` +You are an AI tasked with determining whether a user's comment is a question about a given script document and supplementary text, and if so, which page of the document is most relevant to the question. Follow these instructions carefully: + +1. You will be provided with a user's comment, a script document, and supplementary text. The script document is structured as a JSON array, where each object represents a page with "page", "line", and "supplement" fields. The supplementary text consists of a string. + +2. Analyze the user's comment. + +3. Determine if the comment is a question about the script document or the supplementary text. Consider the content and context of the comment in relation to the document's subject matter. Note that supplementary text may not always be present. + +4. If the comment is a question about the script document: + a. Review each page of the document to find the most relevant information. + b. Determine which page contains information that best answers or relates to the user's question. + c. Set the "judge" value to "true" and the "page" value to the number of the most relevant page. + +5. If the comment is a question about the supplementary text: + a. Set the "judge" value to "true" and the "page" value to an empty string. + +6. If the comment is not a question about either the script document or the supplementary text: + a. Set the "judge" value to "false" and the "page" value to an empty string. + +7. Provide your answer in JSON format as follows: + {"judge": "true/false", "page": "number/empty string"} + +Here is the content of the script document: + +${scripts} + + +Here is the content of the supplementary text: + +${supplement} + + +Based on the user's comment and the content of both the script document and supplementary text, provide "only" your final answer in the specified JSON format. +` + + if (ss.selectAIService === 'openai') { + const response = await getOpenAIChatResponse( + [ + { role: 'system', content: systemMessage }, + { role: 'user', content: queryText }, + ], + ss.openAiKey, + ss.selectAIModel + ) + return response.message + } else if (ss.selectAIService === 'anthropic') { + const response = await getAnthropicChatResponse( + [ + { role: 'system', content: systemMessage }, + { role: 'user', content: queryText }, + ], + ss.anthropicKey, + ss.selectAIModel + ) + return response.message + } else { + throw new Error('Unsupported AI service') + } +} diff --git a/src/features/stores/home.ts b/src/features/stores/home.ts index a3e6ce68..937e1cf0 100644 --- a/src/features/stores/home.ts +++ b/src/features/stores/home.ts @@ -14,6 +14,7 @@ export interface PersistedState { export interface TransientState { viewer: Viewer assistantMessage: string + slideMessages: string[] chatProcessing: boolean chatProcessingCount: number incrementChatProcessingCount: () => void @@ -40,6 +41,7 @@ const homeStore = create()( // transient states viewer: new Viewer(), assistantMessage: '', + slideMessages: [], chatProcessing: false, chatProcessingCount: 0, incrementChatProcessingCount: () => { @@ -61,7 +63,7 @@ const homeStore = create()( voicePlaying: false, }), { - name: 'home', + name: 'aitube-kit-home', partialize: ({ chatLog, codeLog, dontShowIntroduction }) => ({ chatLog, codeLog, diff --git a/src/features/stores/menu.ts b/src/features/stores/menu.ts index b9312d1d..d57ea577 100644 --- a/src/features/stores/menu.ts +++ b/src/features/stores/menu.ts @@ -5,6 +5,7 @@ interface MenuState { showSettingsButton: boolean fileInput: HTMLInputElement | null bgFileInput: HTMLInputElement | null + slideVisible: boolean } const menuStore = create((set, get) => ({ @@ -12,5 +13,6 @@ const menuStore = create((set, get) => ({ showSettingsButton: true, fileInput: null, bgFileInput: null, + slideVisible: true, })) export default menuStore diff --git a/src/features/stores/settings.ts b/src/features/stores/settings.ts index 6972e5e9..ed2b1053 100644 --- a/src/features/stores/settings.ts +++ b/src/features/stores/settings.ts @@ -58,6 +58,7 @@ interface General { selectVoiceLanguage: VoiceLanguage changeEnglishToJapanese: boolean webSocketMode: boolean + slideMode: boolean } export type SettingsState = APIKeys & @@ -115,9 +116,10 @@ const settingsStore = create()( selectVoiceLanguage: 'ja-JP', // TODO: 要整理, ja-JP, en-US changeEnglishToJapanese: false, webSocketMode: false, + slideMode: false, }), { - name: 'settings', + name: 'aitube-kit-settings', } ) ) diff --git a/src/features/stores/slide.ts b/src/features/stores/slide.ts new file mode 100644 index 00000000..211240a6 --- /dev/null +++ b/src/features/stores/slide.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface SlideState { + isPlaying: boolean + currentSlide: number + selectedSlideDocs: string +} + +const slideStore = create()( + persist( + (set, get) => ({ + isPlaying: false, + currentSlide: 0, + selectedSlideDocs: '', + }), + { + name: 'aitube-kit-slide', + partialize: (state) => ({ selectedSlideDocs: state.selectedSlideDocs }), + } + ) +) + +export default slideStore diff --git a/src/features/stores/youtube.ts b/src/features/stores/youtube.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/api/convertMarkdown.ts b/src/pages/api/convertMarkdown.ts new file mode 100644 index 00000000..98a1bd58 --- /dev/null +++ b/src/pages/api/convertMarkdown.ts @@ -0,0 +1,62 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { Marpit } from '@marp-team/marpit' +import fs from 'fs/promises' +import path from 'path' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === 'POST') { + const { slideName } = req.body as { slideName: string } + + if (!slideName) { + return res.status(400).json({ message: 'slideName is required' }) + } + + try { + const markdownPath = path.join( + process.cwd(), + 'public', + 'slides', + slideName, + 'slides.md' + ) + const markdown = await fs.readFile(markdownPath, 'utf-8') + + let css = '' + try { + const cssPath = path.join( + process.cwd(), + 'public', + 'slides', + slideName, + 'theme.css' + ) + css = await fs.readFile(cssPath, 'utf-8') + } catch (cssError) { + console.warn(`CSSファイルが見つかりません: ${slideName}/theme.css`) + // CSSファイルが見つからない場合は空文字列を使用 + } + + const marpit = new Marpit({ + inlineSVG: true, + }) + if (css) { + marpit.themeSet.default = marpit.themeSet.add(css) + } + + const { html, css: generatedCss } = marpit.render(markdown) + + res.status(200).json({ html, css: generatedCss }) + } catch (error) { + console.error(error) + res.status(500).json({ + message: 'Error processing markdown', + error: (error as Error).message, + }) + } + } else { + res.status(405).json({ message: 'Method not allowed' }) + } +} diff --git a/src/pages/api/getSlideFolders.ts b/src/pages/api/getSlideFolders.ts new file mode 100644 index 00000000..dd730c47 --- /dev/null +++ b/src/pages/api/getSlideFolders.ts @@ -0,0 +1,19 @@ +import fs from 'fs' +import path from 'path' +import { NextApiRequest, NextApiResponse } from 'next' + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const slidesDir = path.join(process.cwd(), 'public', 'slides') + + try { + const folders = fs + .readdirSync(slidesDir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + + res.status(200).json(folders) + } catch (error) { + console.error('Error reading slides directory:', error) + res.status(500).json({ error: 'Unable to read slides directory' }) + } +} diff --git a/src/pages/api/getSupplement.ts b/src/pages/api/getSupplement.ts new file mode 100644 index 00000000..dbafb8af --- /dev/null +++ b/src/pages/api/getSupplement.ts @@ -0,0 +1,33 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import fs from 'fs/promises' +import path from 'path' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === 'GET') { + const { slideDocs } = req.query + + if (typeof slideDocs !== 'string') { + return res.status(400).json({ error: 'Invalid slideDocs parameter' }) + } + + try { + const supplementPath = path.join( + process.cwd(), + 'public', + 'slides', + slideDocs, + 'supplement.txt' + ) + const supplement = await fs.readFile(supplementPath, 'utf-8') + res.status(200).json({ supplement }) + } catch (error) { + console.error('Error reading supplement.txt:', error) + res.status(500).json({ error: 'Failed to read supplement file' }) + } + } else { + res.status(405).json({ error: 'Method not allowed' }) + } +} diff --git a/src/utils/englishToJapanese.json b/src/utils/englishToJapanese.json index a2e287df..e3e86c28 100644 --- a/src/utils/englishToJapanese.json +++ b/src/utils/englishToJapanese.json @@ -11356,6 +11356,7 @@ "alcibiades": "アルシバイアディーズ", "alchemical": "アルキミカル", "albuminous": "アルビュミノース", + "aituberkit": "アイチューバーキット", "agreeabled": "アグリーアブルド", "aggrieving": "アグリーヴィング", "aggressive": "アグレッシヴ", @@ -17374,6 +17375,7 @@ "antidotes": "アンティドウツ", "antiaging": "アンティエイジング", "anthracis": "アンスラシス", + "anthropic": "アンスロピック", "anthology": "アンソロジー", "antelopes": "アンテロープス", "antarctic": "アンタークティック", @@ -17796,6 +17798,7 @@ "yugoslav": "ユーゴスラブ", "yourself": "ユアセルフ", "youngest": "ヤンゲスト", + "youtuber": "ユーチューバー", "yohannes": "ヨハネス", "yielding": "イールディング", "yergason": "ヤーガソン", @@ -31597,6 +31600,7 @@ "circled": "サークルド", "cindery": "シンダリー", "ciliary": "シリアリー", + "chatgpt": "チャットジーピーティー", "churned": "チャーンド", "chummed": "チャムド", "chugged": "チャグド", @@ -32855,6 +32859,7 @@ "aladdin": "アラディン", "alabama": "アラバマ", "aileron": "エイルロン", + "aituber": "アイチュバー", "ahrnhem": "アーンヘム", "aground": "アグラウンド", "agonize": "アゴナイズ", @@ -33391,6 +33396,7 @@ "vtlist": "ブイティーリスト", "vtcopy": "ブイティーコピー", "vtachg": "ブイティーエイチェンジ", + "vtuber": "ブイチューバー", "vspace": "ブイスペイス", "voyeur": "ボーヤー", "voyage": "ボヤッジ", @@ -35839,6 +35845,7 @@ "oppose": "オポウズ", "opioid": "オウピオイド", "opiate": "オウピアト", + "openai": "オープンエーアイ", "opener": "オウプナー", "opened": "オウプンド", "opcode": "オペコード", @@ -47135,6 +47142,7 @@ "masm": "マスム", "mask": "マスク", "mash": "マッシュ", + "marp": "マープ", "mary": "メアリー", "marx": "マークス", "mart": "マート", @@ -47412,6 +47420,7 @@ "jtex": "ジェイテフ", "jste": "ジェイステ", "jset": "ジェイセット", + "json": "ジェイソン", "jpeg": "ジェイペグ", "joya": "ジョヤ", "jowl": "ジャウル", @@ -49292,6 +49301,7 @@ "gut": "ガット", "gun": "ガン", "gum": "ガム", + "gpt": "ジーピーティー", "got": "ゴット", "god": "ゴッド", "gin": "ジン", @@ -49640,5 +49650,6 @@ "fx": "エフエックス", "e!": "イー!", "id": "アイディー", - "u2": "ユー・ツー" + "u2": "ユー・ツー", + "3.5": "サンテンゴ" } diff --git a/tailwind.config.js b/tailwind.config.js index 5c6e4470..04708a9b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -42,6 +42,9 @@ module.exports = { width: { '1/2': '50%', }, + zIndex: { + 5: '5', + }, }, }, plugins: [require('@tailwindcss/line-clamp')],