diff --git a/config.ts b/config.ts index ba97c062a7..b6f74f1e7b 100644 --- a/config.ts +++ b/config.ts @@ -10,12 +10,13 @@ import { DigitalOceanSpaces } from './src/utils/cdn/classes/DigitalOceanSpaces'; import { createColorfulComputeImageColorStats15 } from './src/utils/image/palette/15/createColorfulComputeImageColorStats15'; import type { IComputeImageColorStats } from './src/utils/image/utils/IImageColorStats'; import { isRunningInBrowser } from './src/utils/isRunningInWhatever'; -import { string_font_family } from './src/utils/typeAliases'; +import type { string_email, string_font_family, string_name, string_token } from './src/utils/typeAliases'; import { isUrlOnPrivateNetwork } from './src/utils/validators/isUrlOnPrivateNetwork'; import { validateUuid } from './src/utils/validators/validateUuid'; export const APP_VERSION = packageJson.version; export const APP_NAME = 'WebGPT'; +export const ADMIN_EMAIL: string_email = 'pavol@webgpt.cz'; export const USE_DALLE_VERSION: 2 | 3 = 3; @@ -100,16 +101,14 @@ export const PHOTOBANK_SEARCH_IMAGES_COUNT = 4; */ export const OPTIMIZE_PHOTOBANK_MAX_SEARCH_DEPTH = 5; - - export const IS_VERIFIED_EMAIL_REQUIRED = { CREATE: false, EDIT: false, LIKE: false, PUBLISH: true, + SPEECH: false, } as const; - export const NEXT_PUBLIC_SUPABASE_URL = config.get('NEXT_PUBLIC_SUPABASE_URL').url().required().value; export const NEXT_PUBLIC_SUPABASE_ANON_KEY = config.get('NEXT_PUBLIC_SUPABASE_ANON_KEY').required().value; export const SUPABASE_SERVICE_ROLE_KEY = config.get('SUPABASE_SERVICE_ROLE_KEY').value; @@ -125,6 +124,9 @@ export const LIMIT_WALLPAPERS_EXCLUDE = config.get('LIMIT_WALLPAPERS_EXCLUDE').l export const OPENAI_API_KEY = config.get('OPENAI_API_KEY').value; +export const ELEVENLABS_API_KEY = config.get('ELEVENLABS_API_KEY').value; +export const ELEVENLABS_VOICE_IDS: Record = config.get('ELEVENLABS_VOICE_IDS').json().value; + export const AZURE_COMPUTER_VISION_ENDPOINT = config.get('AZURE_COMPUTER_VISION_ENDPOINT').url().value; export const AZURE_COMPUTER_VISION_KEY = config.get('AZURE_COMPUTER_VISION_KEY').value; @@ -1078,8 +1080,7 @@ export const PUBLISH_TO_GITHUB_ORGANIZATION = config.get( ).value; export const GITHUB_TOKEN = config.get('GITHUB_TOKEN', `@see https://github.com/settings/tokens`).value; - /** * TODO: !! Annotate all * TODO: [📙] Every dictionary should look like LikedStatus - */ \ No newline at end of file + */ diff --git a/package-lock.json b/package-lock.json index 0ac284ff5b..8d22ab0d38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,12 +24,14 @@ "@promptbook/types": "0.18.0", "@promptbook/utils": "0.18.0", "@supabase/supabase-js": "2.26.0", + "@types/dom-speech-recognition": "^0.0.1", "@types/file-saver": "2.0.5", "@vercel/og": "0.5.8", "babylonjs": "6.17.0", "configchecker": "1.5.1", "crypto-js": "4.1.1", "destroyable": "0.12.0", + "elevenlabs-node": "^2.0.1", "everstorage": "1.13.0", "express": "4.18.2", "file-saver": "2.0.5", @@ -4218,6 +4220,11 @@ "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", "dev": true }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz", + "integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==" + }, "node_modules/@types/eslint": { "version": "8.44.7", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", @@ -7521,6 +7528,56 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.589.tgz", "integrity": "sha512-zF6y5v/YfoFIgwf2dDfAqVlPPsyQeWNpEWXbAlDUS8Ax4Z2VoiiZpAPC0Jm9hXEkJm2vIZpwB6rc4KnLTQffbQ==" }, + "node_modules/elevenlabs-node": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/elevenlabs-node/-/elevenlabs-node-2.0.1.tgz", + "integrity": "sha512-q6vUznhudS1yxYxTVP3sT3xWzSzoXH+y3T+4GE1X8gjGwJw1Inmko/paQfl4d+CpTQXGNWuACAlFUdIk96xebw==", + "dependencies": { + "axios": "^1.4.0", + "fs-extra": "^11.1.1" + } + }, + "node_modules/elevenlabs-node/node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/elevenlabs-node/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/elevenlabs-node/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/elevenlabs-node/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/email-addresses": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", @@ -11674,7 +11731,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -16171,7 +16227,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -20409,6 +20464,11 @@ "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", "dev": true }, + "@types/dom-speech-recognition": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz", + "integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==" + }, "@types/eslint": { "version": "8.44.7", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", @@ -22964,6 +23024,52 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.589.tgz", "integrity": "sha512-zF6y5v/YfoFIgwf2dDfAqVlPPsyQeWNpEWXbAlDUS8Ax4Z2VoiiZpAPC0Jm9hXEkJm2vIZpwB6rc4KnLTQffbQ==" }, + "elevenlabs-node": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/elevenlabs-node/-/elevenlabs-node-2.0.1.tgz", + "integrity": "sha512-q6vUznhudS1yxYxTVP3sT3xWzSzoXH+y3T+4GE1X8gjGwJw1Inmko/paQfl4d+CpTQXGNWuACAlFUdIk96xebw==", + "requires": { + "axios": "^1.4.0", + "fs-extra": "^11.1.1" + }, + "dependencies": { + "axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + } + } + }, "email-addresses": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", @@ -26082,7 +26188,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" @@ -29398,8 +29503,7 @@ "universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" }, "unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index 51a9cd58b3..457a1745ea 100644 --- a/package.json +++ b/package.json @@ -72,12 +72,14 @@ "@promptbook/types": "0.18.0", "@promptbook/utils": "0.18.0", "@supabase/supabase-js": "2.26.0", + "@types/dom-speech-recognition": "^0.0.1", "@types/file-saver": "2.0.5", "@vercel/og": "0.5.8", "babylonjs": "6.17.0", "configchecker": "1.5.1", "crypto-js": "4.1.1", "destroyable": "0.12.0", + "elevenlabs-node": "^2.0.1", "everstorage": "1.13.0", "express": "4.18.2", "file-saver": "2.0.5", diff --git a/promptbook/write-website-content-cs.ptbk.md b/promptbook/write-website-content-cs.ptbk.md index a2859a8f0c..15e4215f99 100644 --- a/promptbook/write-website-content-cs.ptbk.md +++ b/promptbook/write-website-content-cs.ptbk.md @@ -1,5 +1,8 @@ # 🌍 Vytvoření obsahu webové stránky + + + Instrukce pro vytvoření obsahu webové stránky za pomocí [🌠 Prompt template pipelines](https://github.com/webgptorg/promptbook). - PTBK URL https://ptbk.webgpt.com/cs/write-website-content.ptbk.md@v0.1.0 @@ -12,69 +15,15 @@ Instrukce pro vytvoření obsahu webové stránky za pomocí [🌠 Prompt templa - Output param `{content}` Obsah webu _v Češtině_ - Output param `{wallpaperPrompt}` Prompt pro obrázkový model _v Angličtině_ -## 🖋 Překlad popisu - -- Use completion -- Postprocessing `trim` - - -```text - -English assignment: -> {rawAssignment} - -České zadání: -> -``` - -`-> {rawAssignmentCs}` popis obrázku v češtině - -## 🖋 Účel stránek - -- Use completion -- Postprocessing `unwrapResult` - -```markdown -Navrhni účel webových stránek - -## Pravidla - -- Piš jediný návrh, neříkej více možností -- Navrhni obecnou kategorii, např. "Autoservis" ne "Autoservis Pod Ohradou" -- Návrh je v češtině -- Návrh je stručný, maximálně 3 slova - -## Příklady - -- "Kavárna" -- "Autoservis" -- "Dětská herna" -- "Svatba" -- "Osobní stránka fotografa" - -## Podklady - -- Idea: {idea} -- Zadání: {rawAssignmentCs} - -## Účel webu - -> -``` - -`-> {draftedPurpose}`Návrh účelu webu - -## 👤 Upřesnění účelu uživatelem +## Mapping -Je toto účelem vašeho webu? - -- Prompt dialog +- Execute simple template ```text -{draftedPurpose} +{idea} ``` -`-> {purpose}` Účel webu +-> {purpose} ## 🖋 Návrh zadání @@ -91,21 +40,16 @@ Vytvoř zadání reálného webu pro {purpose} z čistého popisu co se nacház - Zadání obsahuje konkrétní čísla, odrážky a je přesné - Stručně, maximálně 4 body zadání, každý bod je maximálně 2 věty -## Podklady - -- {idea} -- {rawAssignmentCs} - ## Zadání webu v Češtině ``` `-> {draftedAssignment}` Zadání webu v Češtině -## 👤 Upřesnění zadání uživatelem +## Mapping Popište cíl vašeho webu -- Prompt dialog +- Simple template ```text {draftedAssignment} diff --git a/public/avatars/bot.jpeg b/public/avatars/bot.jpeg new file mode 100644 index 0000000000..ebcab08b9a Binary files /dev/null and b/public/avatars/bot.jpeg differ diff --git a/public/avatars/teacher.jpeg b/public/avatars/teacher.jpeg new file mode 100644 index 0000000000..3ccb34cbe6 Binary files /dev/null and b/public/avatars/teacher.jpeg differ diff --git a/public/people/tomas-studenik.transparent.png b/public/people/tomas-studenik.transparent.png new file mode 100644 index 0000000000..c54ac23bb7 Binary files /dev/null and b/public/people/tomas-studenik.transparent.png differ diff --git a/scripts/upload-wallpapers/10-upload-wallpapers-images.ts b/scripts/upload-wallpapers/10-upload-wallpapers-images.ts index ce6fcea809..5ffbd1ec22 100644 --- a/scripts/upload-wallpapers/10-upload-wallpapers-images.ts +++ b/scripts/upload-wallpapers/10-upload-wallpapers-images.ts @@ -10,7 +10,7 @@ import { readFile } from 'fs/promises'; import { basename, join } from 'path'; import { forTime } from 'waitasecond'; import { CDN, MIDJOURNEY_WHOLE_GALLERY_PATH } from '../../config'; -import { generatePreparedWallpaperCdnKey } from '../../src/utils/cdn/utils/generateWallpaperCdnKey'; +import { getPreparedWallpaperCdnKey } from '../../src/utils/cdn/utils/getPreparedWallpaperCdnKey'; import { getSupabaseForServer } from '../../src/utils/supabase/getSupabaseForServer'; import { getHardcodedWallpapers } from '../utils/hardcoded-wallpaper/getHardcodedWallpapers'; @@ -48,7 +48,7 @@ async function uploadWallpapersImages() { const wallpaper = selectResult.data[0]!; - const key = generatePreparedWallpaperCdnKey(wallpaper); + const key = getPreparedWallpaperCdnKey(wallpaper); const file = await CDN.getItem(key); if (file) { diff --git a/speech/README.md b/speech/README.md new file mode 100644 index 0000000000..3136f3d61b --- /dev/null +++ b/speech/README.md @@ -0,0 +1,7 @@ +# 🗣 Speech library + +> Note: [🧆] Now unused, but will be used in the future. + +In this folder you can find the library of spoken text. + +This is kinda cache BUT persistent and commited to the repository. diff --git a/speech/pavol/ahoj.mp3 b/speech/pavol/ahoj.mp3 new file mode 100644 index 0000000000..db44bd65b0 Binary files /dev/null and b/speech/pavol/ahoj.mp3 differ diff --git a/speech/pavol/jaky-web-chcete-vytvorit.mp3 b/speech/pavol/jaky-web-chcete-vytvorit.mp3 new file mode 100644 index 0000000000..88d957e627 Binary files /dev/null and b/speech/pavol/jaky-web-chcete-vytvorit.mp3 differ diff --git a/speech/pavol/je-toto-ucelem-vaseho-webu-kavarna-idealni-jmeno-pro-web-kavarna-relax.mp3 b/speech/pavol/je-toto-ucelem-vaseho-webu-kavarna-idealni-jmeno-pro-web-kavarna-relax.mp3 new file mode 100644 index 0000000000..155bff350d Binary files /dev/null and b/speech/pavol/je-toto-ucelem-vaseho-webu-kavarna-idealni-jmeno-pro-web-kavarna-relax.mp3 differ diff --git a/src/ai/text-to-image/dalle/DalleImageGenerator.ts b/src/ai/text-to-image/dalle/DalleImageGenerator.ts index 025d1137ca..0b2cf853d7 100644 --- a/src/ai/text-to-image/dalle/DalleImageGenerator.ts +++ b/src/ai/text-to-image/dalle/DalleImageGenerator.ts @@ -6,7 +6,7 @@ import { Writable } from 'type-fest'; import { Vector } from 'xyzt'; import { CDN } from '../../../../config'; import { WebgptTaskProgress } from '../../../components/TaskInProgress/task/WebgptTaskProgress'; -import { generateDalleCdnKey } from '../../../utils/cdn/utils/generateDalleCdnKey'; +import { getDalleCdnKey } from '../../../utils/cdn/utils/getDalleCdnKey'; import { isRunningInNode } from '../../../utils/isRunningInWhatever'; import type { ImageGenerator } from '../0-interfaces/ImageGenerator'; import type { ImagePromptResult } from '../0-interfaces/ImagePromptResult'; @@ -93,7 +93,7 @@ export class DalleImageGenerator implements ImageGenerator { const imageArrayBuffer = await fetch(imageSrc).then((response) => response.arrayBuffer()); const imageBuffer = Buffer.from(imageArrayBuffer); - const key = generateDalleCdnKey(prompt, imageBuffer); + const key = getDalleCdnKey(prompt, imageBuffer); await CDN.setItem(key, { type: 'image/png', // <- TODO: Is Dalle always creating PNGs? data: imageBuffer, diff --git a/src/components/Chat/Chat/Chat.module.css b/src/components/Chat/Chat/Chat.module.css new file mode 100644 index 0000000000..e46faa879b --- /dev/null +++ b/src/components/Chat/Chat/Chat.module.css @@ -0,0 +1,152 @@ +.Chat { + /*/ + outline: 1px dotted rgb(255, 38, 38); + /**/ + + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 20px; + /*background-color: #f2f2f2;*/ +} + +/* Chat messages area */ +.chatMessages { + /*/ + outline: 1px dotted rgb(38, 194, 255); + /**/ + + height: 100%; + overflow-y: scroll; + margin-bottom: 10px; +} + +.chatMessages::-webkit-scrollbar { + width: 10px; +} + +.chatMessages::-webkit-scrollbar-track { + background: #00000000; + border-radius: 5px; +} + +.chatMessages::-webkit-scrollbar-thumb { + background: #77777731; + border-radius: 5px; +} + +.chatMessages::-webkit-scrollbar-thumb:hover { + background: auto; +} + +.scrollToBottom { + position: absolute; + top: calc(100% - 170px); + left: 50%; + transform: translate(-50%, 0); + + width: 50px; + height: 50px; + + display: flex; + justify-content: center; + align-items: center; + padding-bottom: 13px; + + border: none; + outline: none; + box-shadow: 0 0 20px #00000077; + background-color: #008cba; + border-radius: 100%; + + font-weight: bold; + font-size: 30px; + color: white; + + cursor: pointer; +} + +/* Individual chat message */ +.chatMessage { + /*/ + outline: 1px dotted rgb(38, 96, 255); + /**/ + + display: flex; + margin-bottom: 10px; + width: 100%; + + align-items: flex-start; + flex-direction: row; +} + +.chatMessage.sender { + align-items: flex-end; + flex-direction: row-reverse; +} + +/* Sender Avatar */ +.chatMessage .avatar { + margin-left: 10px; + margin-right: 10px; +} + +.chatMessage .avatar img { + width: 50px; + height: 50px; + border-radius: 50%; + margin-right: 10px; + + object-fit: cover; + background-color: #B8B8B8; + +} + +/* Message text */ +.chatMessage .messageText { + background-color: #4caf50; + color: white; + padding: 10px; + border-radius: 10px; + max-width: 70%; + text-align: left; +} + +/* Sender message text */ +.chatMessage.sender .messageText { + background-color: #008cba; +} + +/* Chat input area */ +.chatInput { + /*/ + outline: 1px dotted rgb(38, 255, 172); + /**/ + + display: flex; + flex-direction: row; +} + +/* Chat input field */ +.chatInput textarea { + flex: 1; + padding: 10px; + border: none; + border-radius: 5px; + + resize: none; +} + +/* Chat send button */ +.chatInput button { + background-color: #008cba; + color: white; + border: none; + padding: 10px; + border-radius: 5px; + margin-left: 5px; + cursor: pointer; +} diff --git a/src/components/Chat/Chat/Chat.tsx b/src/components/Chat/Chat/Chat.tsx new file mode 100644 index 0000000000..4459d22cb7 --- /dev/null +++ b/src/components/Chat/Chat/Chat.tsx @@ -0,0 +1,211 @@ +import Image from 'next/image'; +import { CSSProperties, useEffect, useRef, useState } from 'react'; +import spaceTrim from 'spacetrim'; +import { Promisable } from 'type-fest'; +import simpleChatAvatar from '../../../../public/avatars/bot.jpeg'; /* <- TODO: Use ACRY Import attributes> with { type: "???json" }; */ +import teacherAvatar from '../../../../public/avatars/teacher.jpeg'; +import pavolHejnyImage from '../../../../public/people/pavol-hejny.transparent.png'; +import tomasStudenikImage from '../../../../public/people/tomas-studenik.transparent.png'; +import { classNames } from '../../../utils/classNames'; +import { focusRef } from '../../../utils/focusRef'; +import { string_css_class, string_translate_language } from '../../../utils/typeAliases'; +import { MarkdownContent } from '../../Content/MarkdownContent'; +import { ChatMessage } from '../interfaces/ChatMessage'; +import { VoiceRecognitionButton } from '../VoiceRecognitionButton/VoiceRecognitionButton'; +import styles from './Chat.module.css'; + +interface ChatProps { + /** + * Messages to render - they are rendered as they are + */ + messages: Array; + + /** + * Called when user sends a message + * + * Note: You must handle the message yourself and add it to the `messages` array + */ + onMessage(messageContent: string /* <- TODO: [🍗] Pass here the message object NOT just text */): Promisable; + + /** + * Determines whether the voice recognition button is rendered + */ + isVoiceRecognitionButtonShown?: true; + + /** + * The language code to use for voice recognition (e.g. "en"). + */ + voiceLanguage?: string_translate_language; + + /** + * Optional CSS class name which will be added to root
element + */ + className?: string_css_class; + + /** + * Optional CSS style which will be added to root
element + */ + style?: CSSProperties; +} + +/** + * Renders a chat with messages and input for new messages + * + * Note: There are two components: + * - renders chat as it is without any logic - messages you pass as props are rendered as they are + * - renders a chat with some logic - it manages messages, optionally speaks them, etc. + * - renders a chat which runs a async (worker) function on background and user interacts with it + * + * Use or in most cases. + */ +export function Chat(props: ChatProps) { + const { messages, onMessage, isVoiceRecognitionButtonShown, voiceLanguage = 'en', className, style } = props; + + const [isAutoScrolling, setAutoScrolling] = useState(true); + const textareaRef = useRef(null); + const buttonSendRef = useRef(null); + + useEffect( + (/* Focus textarea on page load */) => { + if (!textareaRef.current) { + return; + } + textareaRef.current.focus(); + }, + [textareaRef], + ); + + const handleSend = async () => { + const textareaElement = textareaRef.current; + const buttonSendElement = buttonSendRef.current; + + if (!textareaElement) { + throw new Error(`Can not find textarea`); + } + if (!buttonSendElement) { + throw new Error(`Can not find textarea`); + } + + textareaElement.disabled = true; + buttonSendElement.disabled = true; + + try { + if (spaceTrim(textareaElement.value) === '') { + throw new Error(`You need to write some text`); + } + + await onMessage(textareaElement.value); + + textareaElement.value = ''; + textareaElement.focus(); + } catch (error) { + if (!(error instanceof Error)) { + throw error; + } + + console.error(error); + alert(error.message); + } finally { + textareaElement.disabled = false; + buttonSendElement.disabled = false; + focusRef(textareaElement); + } + }; + + return ( +
+
{ + if (!element) { + return; + } + + if (!isAutoScrolling) { + return; + } + + element.scrollBy(0, 10000); + }} + onScroll={(event) => { + const element = event.target; + + if (!(element instanceof HTMLDivElement)) { + return; + } + + setAutoScrolling(element.scrollTop + element.clientHeight === element.scrollHeight); + }} + > + {messages.map((message, i) => ( +
console.info(message)} + > +
+ {`AI +
+ +
+ +
+
+ ))} +
+ + {!isAutoScrolling && ( + + )} + +
+