summaryrefslogtreecommitdiff
path: root/packages/prosody-ui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/prosody-ui')
-rw-r--r--packages/prosody-ui/.gitignore175
-rw-r--r--packages/prosody-ui/README.md15
-rw-r--r--packages/prosody-ui/bun.lock318
-rw-r--r--packages/prosody-ui/index.ts6
-rw-r--r--packages/prosody-ui/package.json22
-rw-r--r--packages/prosody-ui/src/LangText.tsx78
-rw-r--r--packages/prosody-ui/src/Paragraph.tsx56
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/BeiShiDaJiaGuWenZiTi-1.ttfbin0 -> 1984156 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/GaiLiangShouJinTi-2.ttfbin0 -> 4827080 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/GuDianMingChaoKai-2.ttfbin0 -> 30564660 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/Iansui-Regular.ttfbin0 -> 9396328 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/LiuJianMaoCao-Regular.ttfbin0 -> 4940224 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/LongCang-Regular.ttfbin0 -> 5151180 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/Meirenzhuan.ttfbin0 -> 7336648 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/No.300-ShangShouGuHuangTi-2.ttfbin0 -> 4890264 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Black.ttfbin0 -> 7082052 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Bold.ttfbin0 -> 7088136 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraBold.ttfbin0 -> 7084512 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraLight.ttfbin0 -> 7095176 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Light.ttfbin0 -> 7095576 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Medium.ttfbin0 -> 7089444 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Regular.ttfbin0 -> 7093428 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-SemiBold.ttfbin0 -> 7086788 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Thin.ttfbin0 -> 7092016 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-VariableFont_wght.ttfbin0 -> 11909304 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/YeZiGongChangZuiHanJiangXingCao-2.ttfbin0 -> 5486956 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/YiShanBeiZhuanTi.ttfbin0 -> 2314256 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/ZhiMangXing-Regular.ttfbin0 -> 4052388 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttfbin0 -> 5359608 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/style.css223
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/字魂天龙行楷.ttfbin0 -> 3344412 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/IPA/Judson-Bold.ttfbin0 -> 192452 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/IPA/Judson-Italic.ttfbin0 -> 209028 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/IPA/Judson-Regular.ttfbin0 -> 217096 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/IPA/NotoSans-Italic-VariableFont_wdth,wght.ttfbin0 -> 2637272 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/IPA/NotoSans-VariableFont_wdth,wght.ttfbin0 -> 2490816 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/IPA/Voces-Regular.ttfbin0 -> 119992 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/IPA/style.css33
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/DelaGothicOne-Regular.ttfbin0 -> 2487960 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Bold.ttfbin0 -> 4256124 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Medium.ttfbin0 -> 4265764 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Regular.ttfbin0 -> 4472380 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/ZenAntiqueSoft-Regular.ttfbin0 -> 7095584 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/ZenKurenaido-Regular.ttfbin0 -> 4303064 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Jpan/style.css223
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/OFL.txt93
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Black.ttfbin0 -> 151396 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BlackItalic.ttfbin0 -> 171604 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Bold.ttfbin0 -> 153944 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BoldItalic.ttfbin0 -> 176588 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBold.ttfbin0 -> 152764 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBoldItalic.ttfbin0 -> 173916 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLight.ttfbin0 -> 161456 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLightItalic.ttfbin0 -> 186168 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Italic.ttfbin0 -> 182012 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Light.ttfbin0 -> 159892 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-LightItalic.ttfbin0 -> 184460 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Medium.ttfbin0 -> 156520 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-MediumItalic.ttfbin0 -> 180444 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Regular.ttfbin0 -> 158240 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBold.ttfbin0 -> 155232 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBoldItalic.ttfbin0 -> 178584 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Thin.ttfbin0 -> 161652 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ThinItalic.ttfbin0 -> 187044 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Latn/style.css11
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Charm-Bold.ttfbin0 -> 135416 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Charm-Regular.ttfbin0 -> 134652 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Chonburi-Regular.ttfbin0 -> 168404 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Bold.ttfbin0 -> 113924 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-ExtraLight.ttfbin0 -> 117248 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Light.ttfbin0 -> 115728 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Medium.ttfbin0 -> 113640 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Regular.ttfbin0 -> 113392 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-SemiBold.ttfbin0 -> 113856 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Thin.ttfbin0 -> 118256 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Black.ttfbin0 -> 173492 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-BlackItalic.ttfbin0 -> 182076 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Bold.ttfbin0 -> 172876 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-BoldItalic.ttfbin0 -> 180308 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBold.ttfbin0 -> 174464 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBoldItalic.ttfbin0 -> 184928 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLight.ttfbin0 -> 160796 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLightItalic.ttfbin0 -> 164908 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Italic.ttfbin0 -> 171876 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Light.ttfbin0 -> 168036 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-LightItalic.ttfbin0 -> 171596 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Medium.ttfbin0 -> 171336 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-MediumItalic.ttfbin0 -> 172360 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Regular.ttfbin0 -> 169744 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBold.ttfbin0 -> 171548 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBoldItalic.ttfbin0 -> 172244 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-Thin.ttfbin0 -> 155788 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kanit-ThinItalic.ttfbin0 -> 161688 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Bold.ttfbin0 -> 98600 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-BoldItalic.ttfbin0 -> 104048 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLight.ttfbin0 -> 96992 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLightItalic.ttfbin0 -> 101308 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Italic.ttfbin0 -> 103088 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Light.ttfbin0 -> 98168 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-LightItalic.ttfbin0 -> 102364 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Medium.ttfbin0 -> 98740 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-MediumItalic.ttfbin0 -> 103552 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Regular.ttfbin0 -> 98392 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBold.ttfbin0 -> 98824 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBoldItalic.ttfbin0 -> 104024 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-Bold.ttfbin0 -> 105372 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-BoldItalic.ttfbin0 -> 108796 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLight.ttfbin0 -> 105668 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLightItalic.ttfbin0 -> 108572 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-Italic.ttfbin0 -> 108960 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-Light.ttfbin0 -> 105864 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-LightItalic.ttfbin0 -> 108732 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-Medium.ttfbin0 -> 105988 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-MediumItalic.ttfbin0 -> 109008 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-Regular.ttfbin0 -> 105896 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBold.ttfbin0 -> 106032 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBoldItalic.ttfbin0 -> 109000 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-Bold.ttfbin0 -> 82592 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-BoldItalic.ttfbin0 -> 85636 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBold.ttfbin0 -> 82632 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBoldItalic.ttfbin0 -> 85448 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLight.ttfbin0 -> 83276 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLightItalic.ttfbin0 -> 86368 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-Italic.ttfbin0 -> 86024 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-Light.ttfbin0 -> 83224 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-LightItalic.ttfbin0 -> 86072 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-Medium.ttfbin0 -> 83080 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-MediumItalic.ttfbin0 -> 86136 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-Regular.ttfbin0 -> 83080 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBold.ttfbin0 -> 82952 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBoldItalic.ttfbin0 -> 86020 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-Thin.ttfbin0 -> 83408 bytes
-rwxr-xr-xpackages/prosody-ui/src/assets/fonts/Thai/Sarabun-ThinItalic.ttfbin0 -> 86344 bytes
-rw-r--r--packages/prosody-ui/src/assets/fonts/Thai/style.css76
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/bookmark.svg4
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/font.svg2
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/heart.svg4
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/play.svg4
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/quote.svg28
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/react.svg1
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/share.svg28
-rw-r--r--packages/prosody-ui/src/assets/icons/speaker.svg32
-rwxr-xr-xpackages/prosody-ui/src/assets/icons/spinner.svg1
-rw-r--r--packages/prosody-ui/src/components/Sentence.tsx57
-rw-r--r--packages/prosody-ui/src/components/Word.tsx119
-rw-r--r--packages/prosody-ui/src/components/sentence.css272
-rw-r--r--packages/prosody-ui/src/components/word.css0
-rw-r--r--packages/prosody-ui/src/files.d.ts4
-rw-r--r--packages/prosody-ui/src/fonts/FontChanger.tsx64
-rw-r--r--packages/prosody-ui/src/fonts/Hani.tsx14
-rw-r--r--packages/prosody-ui/src/fonts/Thai.tsx8
-rw-r--r--packages/prosody-ui/src/fonts/useLangFont.tsx44
-rw-r--r--packages/prosody-ui/src/hooks/useLang.tsx184
-rw-r--r--packages/prosody-ui/src/hooks/useModal.tsx53
-rw-r--r--packages/prosody-ui/src/hooks/useTTS.tsx3
-rw-r--r--packages/prosody-ui/src/latin/LatinText.tsx77
-rw-r--r--packages/prosody-ui/src/logic/iso6393to1.ts186
-rw-r--r--packages/prosody-ui/src/logic/stanza.ts86
-rw-r--r--packages/prosody-ui/src/logic/types.ts48
-rw-r--r--packages/prosody-ui/src/logic/utils.ts66
-rw-r--r--packages/prosody-ui/src/logic/wiki.ts138
-rw-r--r--packages/prosody-ui/src/sortug.css248
-rw-r--r--packages/prosody-ui/src/styles/styles.css281
-rw-r--r--packages/prosody-ui/src/thai/ThaiText.tsx49
-rw-r--r--packages/prosody-ui/src/thai/logic/thainlp.ts90
-rw-r--r--packages/prosody-ui/src/zoom/FullText.tsx60
-rw-r--r--packages/prosody-ui/src/zoom/Paragraph.tsx60
-rw-r--r--packages/prosody-ui/src/zoom/Sentence.tsx46
-rw-r--r--packages/prosody-ui/src/zoom/SpacyClause.tsx125
-rw-r--r--packages/prosody-ui/src/zoom/animations.ts199
-rw-r--r--packages/prosody-ui/src/zoom/hooks/useZoom.tsx135
-rw-r--r--packages/prosody-ui/src/zoom/index.ts8
-rw-r--r--packages/prosody-ui/src/zoom/logic/types.ts53
-rw-r--r--packages/prosody-ui/src/zoom/spacy.css39
-rw-r--r--packages/prosody-ui/tsconfig.json27
175 files changed, 4276 insertions, 0 deletions
diff --git a/packages/prosody-ui/.gitignore b/packages/prosody-ui/.gitignore
new file mode 100644
index 0000000..9b1ee42
--- /dev/null
+++ b/packages/prosody-ui/.gitignore
@@ -0,0 +1,175 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/packages/prosody-ui/README.md b/packages/prosody-ui/README.md
new file mode 100644
index 0000000..e38a0b1
--- /dev/null
+++ b/packages/prosody-ui/README.md
@@ -0,0 +1,15 @@
+# prosody-ui
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/packages/prosody-ui/bun.lock b/packages/prosody-ui/bun.lock
new file mode 100644
index 0000000..c276ddd
--- /dev/null
+++ b/packages/prosody-ui/bun.lock
@@ -0,0 +1,318 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "prosody-ui",
+ "dependencies": {
+ "franc-all": "^7.2.0",
+ "glotscript": "file:../glotscript",
+ "motion": "^12.11.3",
+ "sortug": "file:../sortug",
+ "sortug-ai": "file:../models",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.2",
+ "@types/react": "^19.2.6",
+ },
+ "peerDependencies": {
+ "react": ">=19.0.0",
+ "typescript": "^5.0.0",
+ },
+ },
+ },
+ "packages": {
+ "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.36.3", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-+c0mMLxL/17yFZ4P5+U6bTWiCSFZUKJddrv01ud2aFBWnTPLdRncYV76D3q1tqfnL7aCnhRtykFnoCFzvr4U3Q=="],
+
+ "@google/genai": ["@google/genai@0.13.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.4" } }, "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ=="],
+
+ "@google/generative-ai": ["@google/generative-ai@0.21.0", "", {}, "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg=="],
+
+ "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="],
+
+ "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
+
+ "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
+
+ "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
+
+ "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
+
+ "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
+
+ "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
+
+ "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
+
+ "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
+
+ "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
+
+ "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
+
+ "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
+
+ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
+
+ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
+
+ "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
+
+ "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
+
+ "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
+
+ "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="],
+
+ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
+
+ "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
+
+ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="],
+
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="],
+
+ "bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="],
+
+ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+ "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
+
+ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "file-type": ["file-type@18.7.0", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.2", "strtok3": "^7.0.0", "token-types": "^5.0.1" } }, "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw=="],
+
+ "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
+
+ "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
+
+ "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
+
+ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
+
+ "framer-motion": ["framer-motion@12.11.3", "", { "dependencies": { "motion-dom": "^12.11.2", "motion-utils": "^12.9.4", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ksUtDFBZtrbQFt4bEMFrFgo7camhmXcLeuylKQxEYSd9czkZ4tZmFROxWczWeu51WqC2m91ifpvgGCBLd0uviQ=="],
+
+ "franc-all": ["franc-all@7.2.0", "", { "dependencies": { "trigram-utils": "^2.0.0" } }, "sha512-ZR6ciLQTDBaOvBdkOd8+vqDzaLtmIXRa9GCzcAlaBpqNAKg9QrtClPmqiKac5/xZXfCZGMo1d8dIu1T0BLhHEg=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
+
+ "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "glotscript": ["glotscript@file:../glotscript", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.0.0" } }],
+
+ "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
+
+ "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "groq-sdk": ["groq-sdk@0.15.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-aYDEdr4qczx3cLCRRe+Beb37I7g/9bD5kHF+EEDxcrREWw1vKoRcfP3vHEkJB7Ud/8oOuF0scRwDpwWostTWuQ=="],
+
+ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
+
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+
+ "iso-639-3": ["iso-639-3@3.0.1", "", {}, "sha512-SdljCYXOexv/JmbQ0tvigHN43yECoscVpe2y2hlEqy/CStXQlroPhZLj7zKLRiGqLJfw8k7B973UAMDoQczVgQ=="],
+
+ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
+
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
+
+ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
+
+ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "motion": ["motion@12.11.3", "", { "dependencies": { "framer-motion": "^12.11.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-R9t8IYJ5hSl+Ao5rj6XGS4lJN+fXQstcpwKOcFA5aWjlwjf3IHcHr8DUjPV0My6T/5ZCQ1jqh0pmjggO4zUpEA=="],
+
+ "motion-dom": ["motion-dom@12.11.2", "", { "dependencies": { "motion-utils": "^12.9.4" } }, "sha512-wZ396XNNTI9GOkyrr80wFSbZc1JbIHSHTbLdririSbkEgahWWKmsHzsxyxqBBvuBU/iaQWVu1YCjdpXYNfo2yQ=="],
+
+ "motion-utils": ["motion-utils@12.9.4", "", {}, "sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "n-gram": ["n-gram@2.0.2", "", {}, "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ=="],
+
+ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
+
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
+
+ "openai": ["openai@4.98.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-TmDKur1WjxxMPQAtLG5sgBSCJmX7ynTsGmewKzoDwl1fRxtbLOsiR0FA/AOAAtYUmP6azal+MYQuOENfdU+7yg=="],
+
+ "peek-readable": ["peek-readable@5.4.2", "", {}, "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg=="],
+
+ "playht": ["playht@0.16.0", "", { "dependencies": { "@grpc/grpc-js": "^1.9.4", "axios": "^1.4.0", "cross-fetch": "^4.0.0", "file-type": "^18.5.0", "protobufjs": "^7.2.5", "tslib": "^2.1.0" } }, "sha512-gwKqGcmUwrd3NaG6B2z5RZCjxPM0CI915Bmej+GXWZU2PSdN2g4hXsDMnjts+uakLaqGEY8YaIqNokyYH7SnvQ=="],
+
+ "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
+
+ "protobufjs": ["protobufjs@7.5.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w=="],
+
+ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+
+ "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
+
+ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+
+ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
+
+ "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
+
+ "replicate": ["replicate@1.0.1", "", { "optionalDependencies": { "readable-stream": ">=4.0.0" } }, "sha512-EY+rK1YR5bKHcM9pd6WyaIbv6m2aRIvHfHDh51j/LahlHTLKemTYXF6ptif2sLa+YospupAsIoxw8Ndt5nI3vg=="],
+
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "sortug": ["sortug@file:../sortug", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" } }],
+
+ "sortug-ai": ["models@file:../models", { "dependencies": { "@anthropic-ai/sdk": "^0.36.3", "@google/genai": "^0.13.0", "@google/generative-ai": "^0.21.0", "bcp-47": "^2.1.0", "franc-all": "^7.2.0", "groq-sdk": "^0.15.0", "iso-639-3": "^3.0.1", "openai": "^4.84.0", "playht": "^0.16.0", "replicate": "^1.0.1", "sortug": "file://home/y/code/npm/sortug" }, "devDependencies": { "@types/bun": "^1.2.12" }, "peerDependencies": { "typescript": "^5.7.3" } }],
+
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strtok3": ["strtok3@7.1.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.1.3" } }, "sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg=="],
+
+ "token-types": ["token-types@5.0.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg=="],
+
+ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+
+ "trigram-utils": ["trigram-utils@2.0.1", "", { "dependencies": { "collapse-white-space": "^2.0.0", "n-gram": "^2.0.0" } }, "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
+
+ "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
+
+ "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
+
+ "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
+
+ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+
+ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
+
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
+
+ "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
+
+ "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="],
+
+ "groq-sdk/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="],
+
+ "openai/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="],
+
+ "sortug-ai/@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
+
+ "sortug-ai/sortug": ["sortug@file:../../../npm/sortug", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" } }],
+
+ "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+
+ "groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+
+ "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+
+ "sortug-ai/@types/bun/bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
+ }
+}
diff --git a/packages/prosody-ui/index.ts b/packages/prosody-ui/index.ts
new file mode 100644
index 0000000..f7339a2
--- /dev/null
+++ b/packages/prosody-ui/index.ts
@@ -0,0 +1,6 @@
+import LangText from "./src/LangText.tsx";
+import FontChanger from "./src/fonts/FontChanger.tsx";
+import Paragraph from "./src/Paragraph.tsx";
+import * as Zoom from "./src/zoom";
+
+export { LangText, FontChanger, Paragraph, Zoom };
diff --git a/packages/prosody-ui/package.json b/packages/prosody-ui/package.json
new file mode 100644
index 0000000..cf9ad48
--- /dev/null
+++ b/packages/prosody-ui/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@sortug/prosody-ui",
+ "version": "0.1.0",
+ "module": "index.ts",
+ "type": "module",
+ "peerDependencies": {
+ "typescript": "^5.0.0",
+ "react": ">=19.0.0"
+ },
+ "dependencies": {
+ "franc-all": "^7.2.0",
+ "motion": "^12.11.3",
+ "@sortug/lib": "workspace:*",
+ "@sortug/langlib": "workspace:*",
+ "@sortug/ai": "workspace:*"
+ },
+ "private": true,
+ "devDependencies": {
+ "@types/bun": "^1.3.2",
+ "@types/react": "^19.2.6"
+ }
+}
diff --git a/packages/prosody-ui/src/LangText.tsx b/packages/prosody-ui/src/LangText.tsx
new file mode 100644
index 0000000..790c499
--- /dev/null
+++ b/packages/prosody-ui/src/LangText.tsx
@@ -0,0 +1,78 @@
+import { franc } from "franc-all";
+import React, { useEffect, useState } from "react";
+import ThaiText from "./thai/ThaiText";
+import { ColoredText } from "./components/Sentence";
+import type { AnalyzeRes, WordData } from "./logic/types";
+import { detectScript, scriptFromLang } from "./logic/utils";
+import LatinText from "./latin/LatinText";
+import { buildWiktionaryURL, parseWiktionary } from "./logic/wiki";
+import type { Result } from "sortug";
+
+export default function LangText({
+ text,
+ lang,
+ theme,
+ fetchWiki,
+ handleWord,
+}: {
+ text: string;
+ lang?: string;
+ theme?: string;
+ fetchWiki?: (url: string) => Promise<string>;
+ handleWord?: (wd: Result<WordData>) => any;
+}) {
+ const [llang, setLang] = useState("");
+ const [script, setScript] = useState(scriptFromLang(lang || "", text));
+ useEffect(() => {
+ if (!lang) {
+ const res = franc(text);
+ setLang(res);
+ } else setLang(lang);
+ }, [text]);
+ console.log("langtext", { text, llang, script });
+
+ async function openWord(word: string) {
+ // console.log("looking up", word);
+ // const url = buildWiktionaryURL(word);
+ // const html = await fetchWiki(url);
+ // const parsed = parseWiktionary(html, url);
+ // console.log({ parsed });
+ // if ("error" in parsed) handleWord(parsed);
+ // else {
+ // const wd: WordData = {
+ // spelling: word,
+ // lang: llang,
+ // ipa: parsed.ok.ipa[0],
+ // meanings: parsed.ok.meanings,
+ // references: { url: parsed.ok.url },
+ // };
+ // handleWord({ ok: wd });
+ // }
+ // // const d = data[s];
+ // // setModal(d);
+ // // setModal(<WordModal data={d} lang={lang} />);
+ }
+
+ return (
+ <div className="lang-text-container">
+ {script === "Thai" ? (
+ <ThaiText text={text} openWord={openWord} />
+ ) : script === "Latin" ? (
+ <LatinText text={text} lang={llang} openWord={openWord} />
+ ) : (
+ <Generic text={text} lang={llang} />
+ )}
+ </div>
+ );
+}
+function Generic({ text, lang }: { text: string; lang: string }) {
+ const [data, setData] = useState<AnalyzeRes>();
+ useEffect(() => {
+ // segmentate(text, lang)
+ }, [text, lang]);
+ console.log({ lang }, "generic");
+ // <p className="lang-lang">{lang}</p>
+ // <p className="lang-text">{text}</p>
+ // {data && <ColoredText frags={Object.keys(data)} />}
+ return <div className="lang-text-div"></div>;
+}
diff --git a/packages/prosody-ui/src/Paragraph.tsx b/packages/prosody-ui/src/Paragraph.tsx
new file mode 100644
index 0000000..72c43a7
--- /dev/null
+++ b/packages/prosody-ui/src/Paragraph.tsx
@@ -0,0 +1,56 @@
+import { franc } from "franc-all";
+import React, { useCallback, useEffect, useState } from "react";
+import ThaiText from "./thai/ThaiText";
+import { ColoredText } from "./components/Sentence";
+import type { AnalyzeRes, WordData } from "./logic/types";
+import { detectScript, langFromScript } from "./logic/utils";
+import LatinText from "./latin/LatinText";
+import { buildWiktionaryURL, parseWiktionary } from "./logic/wiki";
+import type { Result } from "sortug";
+import * as Stanza from "./logic/stanza";
+import { iso6393To1 } from "./logic/iso6393to1";
+
+export default function Paragraph({
+ text,
+}: {
+ text: string;
+ handleWord?: (wd: Result<WordData>) => any;
+}) {
+ useEffect(() => {
+ segmentString();
+ }, [text]);
+ const [lang, setLang] = useState("");
+ const [script, setScript] = useState("");
+ const [segs, setSegs] = useState<Stanza.StanzaSegment[]>([]);
+ useEffect(() => {
+ const res = franc(text);
+ console.log();
+ console.log({ res, text });
+ if (res === "und") detectLanguage();
+ else {
+ const smol = iso6393To1[res];
+ if (!smol) console.log("lang", res);
+ else setLang(smol);
+ }
+ }, [text]);
+
+ const segmentString = useCallback(async () => {
+ if (lang) {
+ const res = await Stanza.segmenter(text, lang);
+ if ("ok" in res) setSegs(res.ok.segments);
+ console.log("stanza", res);
+ }
+ }, [text, lang]);
+ const detectLanguage = useCallback(async () => {
+ const script = detectScript(text);
+ if ("error" in script) console.log("ded");
+ else {
+ setScript(script.ok);
+ const lng = langFromScript(script.ok);
+ if ("error" in lng) console.log("ded again!");
+ else setLang(lng.ok);
+ }
+ }, [text]);
+
+ return <div className="segmented-text">{text}</div>;
+}
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/BeiShiDaJiaGuWenZiTi-1.ttf b/packages/prosody-ui/src/assets/fonts/Hani/BeiShiDaJiaGuWenZiTi-1.ttf
new file mode 100644
index 0000000..5ada19c
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/BeiShiDaJiaGuWenZiTi-1.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/GaiLiangShouJinTi-2.ttf b/packages/prosody-ui/src/assets/fonts/Hani/GaiLiangShouJinTi-2.ttf
new file mode 100644
index 0000000..2606d1c
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/GaiLiangShouJinTi-2.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/GuDianMingChaoKai-2.ttf b/packages/prosody-ui/src/assets/fonts/Hani/GuDianMingChaoKai-2.ttf
new file mode 100644
index 0000000..49f5405
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/GuDianMingChaoKai-2.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/Iansui-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Hani/Iansui-Regular.ttf
new file mode 100644
index 0000000..5da538b
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/Iansui-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/LiuJianMaoCao-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Hani/LiuJianMaoCao-Regular.ttf
new file mode 100644
index 0000000..4b7e9bc
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/LiuJianMaoCao-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/LongCang-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Hani/LongCang-Regular.ttf
new file mode 100644
index 0000000..44f6a9f
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/LongCang-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/Meirenzhuan.ttf b/packages/prosody-ui/src/assets/fonts/Hani/Meirenzhuan.ttf
new file mode 100644
index 0000000..8eb1209
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/Meirenzhuan.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/No.300-ShangShouGuHuangTi-2.ttf b/packages/prosody-ui/src/assets/fonts/Hani/No.300-ShangShouGuHuangTi-2.ttf
new file mode 100644
index 0000000..1f16ba8
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/No.300-ShangShouGuHuangTi-2.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Black.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Black.ttf
new file mode 100644
index 0000000..e86fb96
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Black.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Bold.ttf
new file mode 100644
index 0000000..76812d7
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraBold.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraBold.ttf
new file mode 100644
index 0000000..db381ed
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraLight.ttf
new file mode 100644
index 0000000..e30cdd8
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Light.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Light.ttf
new file mode 100644
index 0000000..8be49fc
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Medium.ttf
new file mode 100644
index 0000000..f45d4ec
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Regular.ttf
new file mode 100644
index 0000000..2a62b97
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-SemiBold.ttf
new file mode 100644
index 0000000..bc3e266
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Thin.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Thin.ttf
new file mode 100644
index 0000000..832c29f
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-Thin.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-VariableFont_wght.ttf b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-VariableFont_wght.ttf
new file mode 100644
index 0000000..47f2c84
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/NotoSansHK-VariableFont_wght.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/YeZiGongChangZuiHanJiangXingCao-2.ttf b/packages/prosody-ui/src/assets/fonts/Hani/YeZiGongChangZuiHanJiangXingCao-2.ttf
new file mode 100644
index 0000000..b59a399
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/YeZiGongChangZuiHanJiangXingCao-2.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/YiShanBeiZhuanTi.ttf b/packages/prosody-ui/src/assets/fonts/Hani/YiShanBeiZhuanTi.ttf
new file mode 100644
index 0000000..9364f2d
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/YiShanBeiZhuanTi.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/ZhiMangXing-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Hani/ZhiMangXing-Regular.ttf
new file mode 100644
index 0000000..e2d4fad
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/ZhiMangXing-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf b/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf
new file mode 100644
index 0000000..b387fc5
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/style.css b/packages/prosody-ui/src/assets/fonts/Hani/style.css
new file mode 100644
index 0000000..2048617
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/style.css
@@ -0,0 +1,223 @@
+/* https://www.fonts.net.cn/fonts-zh/tag-yankai-1.html */
+
+
+@font-face {
+ font-family: "BeiShiDaJiaGuWen";
+ src: url(./BeiShiDaJiaGuWenZiTi-1.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "ShanShouGuHuangTi";
+ src: url(./No.300-ShangShouGuHuangTi-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "DinglieZhuhai";
+ src: url(./dingliezhuhaifont-20240831GengXinBan-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "ShouJin";
+ src: url(./GaiLiangShouJinTi-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "GudianMingChaoKai";
+ src: url(./GuDianMingChaoKai-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-SemiBold.ttf);
+ font-weight: 500;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-Black.ttf);
+ font-weight: 900;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-Thin.ttf);
+ font-weight: 200;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-Bold.ttf);
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-VariableFont_wght.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-ExtraBold.ttf);
+ font-weight: 800;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-ExtraLight.ttf);
+ font-weight: 100;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-Light.ttf);
+ font-weight: 200;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(./NotoSansHK-Medium.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "Iansui";
+ src: url(./Iansui-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "YeZiGongXingCao";
+ src: url(./YeZiGongChangZuiHanJiangXingCao-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "LiuJianMaoCao";
+ src: url(./LiuJianMaoCao-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "YiShanBeiZhuanti";
+ src: url(./YiShanBeiZhuanTi.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "LongCang";
+ src: url(./LongCang-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "ZhiMangXing";
+ src: url(./ZhiMangXing-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "Meirenzhuan";
+ src: url(./Meirenzhuan.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "TianlongXingkai";
+ src: url(./字魂天龙行楷.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+.font-Hani-0 {
+ font-family: "ShouJin";
+}
+
+.font-Hani-1 {
+ font-family: "DinglieZhuhai";
+}
+
+.font-Hani-2 {
+ font-family: "GudianMingChaokai";
+}
+
+.font-Hani-3 {
+ font-family: "NotoSansHK";
+}
+
+.font-Hani-4 {
+ font-family: "ShanShouGuHuangTi";
+}
+
+.font-Hani-5 {
+ font-family: "Iansui";
+}
+
+.font-Hani-6 {
+ font-family: "YeZiGongXingCao";
+}
+
+.font-Hani-7 {
+ font-family: "LiuJianMaoCao";
+}
+
+.font-Hani-8 {
+ font-family: "YiShanBeiZhuanti";
+}
+
+.font-Hani-9 {
+ font-family: "LongCang";
+}
+
+.font-Hani-10 {
+ font-family: "ZhiMangXing";
+}
+
+.font-Hani-11 {
+ font-family: "Meirenzhuan";
+}
+
+.font-Hani-12 {
+ font-family: "TianlongXingkai";
+} \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/字魂天龙行楷.ttf b/packages/prosody-ui/src/assets/fonts/Hani/字魂天龙行楷.ttf
new file mode 100644
index 0000000..b565e37
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Hani/字魂天龙行楷.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/Judson-Bold.ttf b/packages/prosody-ui/src/assets/fonts/IPA/Judson-Bold.ttf
new file mode 100755
index 0000000..4b977b0
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/Judson-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/Judson-Italic.ttf b/packages/prosody-ui/src/assets/fonts/IPA/Judson-Italic.ttf
new file mode 100755
index 0000000..b2ea190
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/Judson-Italic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/Judson-Regular.ttf b/packages/prosody-ui/src/assets/fonts/IPA/Judson-Regular.ttf
new file mode 100755
index 0000000..a1cbb58
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/Judson-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/NotoSans-Italic-VariableFont_wdth,wght.ttf b/packages/prosody-ui/src/assets/fonts/IPA/NotoSans-Italic-VariableFont_wdth,wght.ttf
new file mode 100755
index 0000000..4e962ee
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/NotoSans-Italic-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/NotoSans-VariableFont_wdth,wght.ttf b/packages/prosody-ui/src/assets/fonts/IPA/NotoSans-VariableFont_wdth,wght.ttf
new file mode 100755
index 0000000..f7d0d78
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/NotoSans-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/Voces-Regular.ttf b/packages/prosody-ui/src/assets/fonts/IPA/Voces-Regular.ttf
new file mode 100755
index 0000000..36aa975
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/Voces-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/IPA/style.css b/packages/prosody-ui/src/assets/fonts/IPA/style.css
new file mode 100644
index 0000000..ecc0a8a
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/IPA/style.css
@@ -0,0 +1,33 @@
+@font-face {
+ font-family: "Voces";
+ src: url(./Voces-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "Judson";
+ src: url(./Judson-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+/* TODO check the specifics of variable fonts */
+@font-face {
+ font-family: "Noto Sans";
+ src: url(./NotoSans-VariableFont_wdth,wght.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+.font-IPA-0 {
+ font-family: "Voces";
+}
+
+.font-IPA-1 {
+ font-family: "Judson";
+}
+
+.font-IPA-2 {
+ font-family: "Noto Sans";
+} \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/DelaGothicOne-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Jpan/DelaGothicOne-Regular.ttf
new file mode 100644
index 0000000..475c4dd
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/DelaGothicOne-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Bold.ttf
new file mode 100644
index 0000000..7d13d05
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Medium.ttf
new file mode 100644
index 0000000..1e35712
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Regular.ttf
new file mode 100644
index 0000000..7d68508
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/KaiseiDecol-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/ZenAntiqueSoft-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Jpan/ZenAntiqueSoft-Regular.ttf
new file mode 100644
index 0000000..c78f7d6
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/ZenAntiqueSoft-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/ZenKurenaido-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Jpan/ZenKurenaido-Regular.ttf
new file mode 100644
index 0000000..cb3044e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/ZenKurenaido-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Jpan/style.css b/packages/prosody-ui/src/assets/fonts/Jpan/style.css
new file mode 100644
index 0000000..cd1b79f
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Jpan/style.css
@@ -0,0 +1,223 @@
+/* https://www.fonts.net.cn/fonts-zh/tag-yankai-1.html */
+
+
+@font-face {
+ font-family: "BeiShiDaJiaGuWen";
+ src: url(../Hani/BeiShiDaJiaGuWenZiTi-1.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "ShanShouGuHuangTi";
+ src: url(../Hani/No.300-ShangShouGuHuangTi-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "DinglieZhuhai";
+ src: url(../Hani/dingliezhuhaifont-20240831GengXinBan-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "ShouJin";
+ src: url(../Hani/GaiLiangShouJinTi-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "GudianMingChaoKai";
+ src: url(../Hani/GuDianMingChaoKai-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-SemiBold.ttf);
+ font-weight: 500;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-Black.ttf);
+ font-weight: 900;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-Thin.ttf);
+ font-weight: 200;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-Bold.ttf);
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-VariableFont_wght.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-ExtraBold.ttf);
+ font-weight: 800;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-ExtraLight.ttf);
+ font-weight: 100;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-Light.ttf);
+ font-weight: 200;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "NotoSansHK";
+ src: url(../Hani/NotoSansHK-Medium.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "Iansui";
+ src: url(../Hani/Iansui-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "YeZiGongXingCao";
+ src: url(../Hani/YeZiGongChangZuiHanJiangXingCao-2.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "LiuJianMaoCao";
+ src: url(../Hani/LiuJianMaoCao-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "YiShanBeiZhuanti";
+ src: url(../Hani/YiShanBeiZhuanTi.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "LongCang";
+ src: url(../Hani/LongCang-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "ZhiMangXing";
+ src: url(../Hani/ZhiMangXing-Regular.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: "Meirenzhuan";
+ src: url(../Hani/Meirenzhuan.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: "TianlongXingkai";
+ src: url(../Hani/字魂天龙行楷.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+.font-Hani-0 {
+ font-family: "ShouJin";
+}
+
+.font-Hani-1 {
+ font-family: "DinglieZhuhai";
+}
+
+.font-Hani-2 {
+ font-family: "GudianMingChaokai";
+}
+
+.font-Hani-3 {
+ font-family: "NotoSansHK";
+}
+
+.font-Hani-4 {
+ font-family: "ShanShouGuHuangTi";
+}
+
+.font-Hani-5 {
+ font-family: "Iansui";
+}
+
+.font-Hani-6 {
+ font-family: "YeZiGongXingCao";
+}
+
+.font-Hani-7 {
+ font-family: "LiuJianMaoCao";
+}
+
+.font-Hani-8 {
+ font-family: "YiShanBeiZhuanti";
+}
+
+.font-Hani-9 {
+ font-family: "LongCang";
+}
+
+.font-Hani-10 {
+ font-family: "ZhiMangXing";
+}
+
+.font-Hani-11 {
+ font-family: "Meirenzhuan";
+}
+
+.font-Hani-12 {
+ font-family: "TianlongXingkai";
+} \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/OFL.txt b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/OFL.txt
new file mode 100644
index 0000000..3e92931
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Black.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Black.ttf
new file mode 100644
index 0000000..71c0f99
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Black.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BlackItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BlackItalic.ttf
new file mode 100644
index 0000000..7aeb58b
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BlackItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Bold.ttf
new file mode 100644
index 0000000..00559ee
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BoldItalic.ttf
new file mode 100644
index 0000000..e61e8e8
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-BoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBold.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBold.ttf
new file mode 100644
index 0000000..df70936
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBoldItalic.ttf
new file mode 100644
index 0000000..14d2b37
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLight.ttf
new file mode 100644
index 0000000..e76ec69
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLightItalic.ttf
new file mode 100644
index 0000000..89513d9
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ExtraLightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Italic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Italic.ttf
new file mode 100644
index 0000000..12b7b3c
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Italic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Light.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Light.ttf
new file mode 100644
index 0000000..bc36bcc
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-LightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-LightItalic.ttf
new file mode 100644
index 0000000..9e70be6
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-LightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Medium.ttf
new file mode 100644
index 0000000..6bcdcc2
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-MediumItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-MediumItalic.ttf
new file mode 100644
index 0000000..be67410
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-MediumItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Regular.ttf
new file mode 100644
index 0000000..9f0c71b
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBold.ttf
new file mode 100644
index 0000000..74c726e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBoldItalic.ttf
new file mode 100644
index 0000000..3e6c942
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-SemiBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Thin.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Thin.ttf
new file mode 100644
index 0000000..03e7366
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-Thin.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ThinItalic.ttf b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ThinItalic.ttf
new file mode 100644
index 0000000..e26db5d
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/Poppins/Poppins-ThinItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Latn/style.css b/packages/prosody-ui/src/assets/fonts/Latn/style.css
new file mode 100644
index 0000000..e2aef9b
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Latn/style.css
@@ -0,0 +1,11 @@
+@font-face {
+ font-family: "Poppins";
+ src: url(./Poppins.ttf);
+ font-weight: 300;
+ font-style: normal;
+}
+
+
+.font-Latn-0 {
+ font-family: "Poppins";
+} \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Charm-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Charm-Bold.ttf
new file mode 100755
index 0000000..d32f072
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Charm-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Charm-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Charm-Regular.ttf
new file mode 100755
index 0000000..02013a4
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Charm-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Chonburi-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Chonburi-Regular.ttf
new file mode 100755
index 0000000..52fedc7
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Chonburi-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Bold.ttf
new file mode 100755
index 0000000..3a8583f
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-ExtraLight.ttf
new file mode 100755
index 0000000..c4161bd
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Light.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Light.ttf
new file mode 100755
index 0000000..a676550
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Medium.ttf
new file mode 100755
index 0000000..75e62df
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Regular.ttf
new file mode 100755
index 0000000..c31b8c2
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-SemiBold.ttf
new file mode 100755
index 0000000..9812b5a
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Thin.ttf b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Thin.ttf
new file mode 100755
index 0000000..2cc968f
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/IBMPlexSansThai-Thin.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Black.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Black.ttf
new file mode 100755
index 0000000..2e37a00
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Black.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-BlackItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-BlackItalic.ttf
new file mode 100755
index 0000000..dc81853
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-BlackItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Bold.ttf
new file mode 100755
index 0000000..4686906
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-BoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-BoldItalic.ttf
new file mode 100755
index 0000000..17eda2a
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-BoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBold.ttf
new file mode 100755
index 0000000..5240d38
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBoldItalic.ttf
new file mode 100755
index 0000000..037187a
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLight.ttf
new file mode 100755
index 0000000..404c1e0
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLightItalic.ttf
new file mode 100755
index 0000000..5d575cc
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ExtraLightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Italic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Italic.ttf
new file mode 100755
index 0000000..e6d868b
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Italic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Light.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Light.ttf
new file mode 100755
index 0000000..3c9d4b5
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-LightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-LightItalic.ttf
new file mode 100755
index 0000000..59a3f2e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-LightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Medium.ttf
new file mode 100755
index 0000000..50413d0
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-MediumItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-MediumItalic.ttf
new file mode 100755
index 0000000..2c6aa14
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-MediumItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Regular.ttf
new file mode 100755
index 0000000..ef204c1
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBold.ttf
new file mode 100755
index 0000000..7501a2e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBoldItalic.ttf
new file mode 100755
index 0000000..2284650
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-SemiBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Thin.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Thin.ttf
new file mode 100755
index 0000000..5c835ad
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-Thin.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ThinItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ThinItalic.ttf
new file mode 100755
index 0000000..25fd1f9
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kanit-ThinItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Bold.ttf
new file mode 100755
index 0000000..cd36ffc
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-BoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-BoldItalic.ttf
new file mode 100755
index 0000000..f86a715
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-BoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLight.ttf
new file mode 100755
index 0000000..af416d2
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLightItalic.ttf
new file mode 100755
index 0000000..3478406
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-ExtraLightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Italic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Italic.ttf
new file mode 100755
index 0000000..3032d77
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Italic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Light.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Light.ttf
new file mode 100755
index 0000000..b3db128
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-LightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-LightItalic.ttf
new file mode 100755
index 0000000..5acfdd5
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-LightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Medium.ttf
new file mode 100755
index 0000000..de8f843
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-MediumItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-MediumItalic.ttf
new file mode 100755
index 0000000..2c0cb1e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-MediumItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Regular.ttf
new file mode 100755
index 0000000..b6cb5d5
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBold.ttf
new file mode 100755
index 0000000..917a988
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBoldItalic.ttf
new file mode 100755
index 0000000..36d3579
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Kodchasan-SemiBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Bold.ttf
new file mode 100755
index 0000000..db8c190
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-BoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-BoldItalic.ttf
new file mode 100755
index 0000000..a67a0ae
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-BoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLight.ttf
new file mode 100755
index 0000000..0e3f955
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLightItalic.ttf
new file mode 100755
index 0000000..6f4d3d3
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-ExtraLightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-Italic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Italic.ttf
new file mode 100755
index 0000000..479a8e8
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Italic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-Light.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Light.ttf
new file mode 100755
index 0000000..aaad3b6
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-LightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-LightItalic.ttf
new file mode 100755
index 0000000..cdfa5c6
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-LightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Medium.ttf
new file mode 100755
index 0000000..658a6a1
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-MediumItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-MediumItalic.ttf
new file mode 100755
index 0000000..4b88574
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-MediumItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Regular.ttf
new file mode 100755
index 0000000..70cd35b
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBold.ttf
new file mode 100755
index 0000000..acdce49
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBoldItalic.ttf
new file mode 100755
index 0000000..fc76e74
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Mali-SemiBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Bold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Bold.ttf
new file mode 100755
index 0000000..4d6bf36
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Bold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-BoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-BoldItalic.ttf
new file mode 100755
index 0000000..42857a5
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-BoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBold.ttf
new file mode 100755
index 0000000..27ba997
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBoldItalic.ttf
new file mode 100755
index 0000000..bc36f97
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLight.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLight.ttf
new file mode 100755
index 0000000..060c6c1
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLight.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLightItalic.ttf
new file mode 100755
index 0000000..beaa024
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ExtraLightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Italic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Italic.ttf
new file mode 100755
index 0000000..51d6dbe
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Italic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Light.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Light.ttf
new file mode 100755
index 0000000..d2f2291
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Light.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-LightItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-LightItalic.ttf
new file mode 100755
index 0000000..75eb8d8
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-LightItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Medium.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Medium.ttf
new file mode 100755
index 0000000..e03148e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Medium.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-MediumItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-MediumItalic.ttf
new file mode 100755
index 0000000..b172a09
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-MediumItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Regular.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Regular.ttf
new file mode 100755
index 0000000..50fa707
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Regular.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBold.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBold.ttf
new file mode 100755
index 0000000..7b760ce
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBold.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBoldItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBoldItalic.ttf
new file mode 100755
index 0000000..ecc5fb6
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-SemiBoldItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Thin.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Thin.ttf
new file mode 100755
index 0000000..cdaabcb
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-Thin.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ThinItalic.ttf b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ThinItalic.ttf
new file mode 100755
index 0000000..e96d17e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/Sarabun-ThinItalic.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/assets/fonts/Thai/style.css b/packages/prosody-ui/src/assets/fonts/Thai/style.css
new file mode 100644
index 0000000..14b828c
--- /dev/null
+++ b/packages/prosody-ui/src/assets/fonts/Thai/style.css
@@ -0,0 +1,76 @@
+@font-face {
+ font-family: "Kanit";
+ src: url(./Kanit-Regular.ttf);
+}
+
+@font-face {
+ font-family: "Sarabun";
+ src: url(./Sarabun-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "IBM Plex Sans Thai";
+ src: url(./IBMPlexSansThai-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "Mali";
+ src: url(./Mali-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "Kodchasan";
+ src: url(./Kodchasan-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "Chonburi";
+ src: url(./Chonburi-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+@font-face {
+ font-family: "Charm";
+ src: url(./Charm-Regular.ttf);
+ font-weight: 300;
+ font-style: normal
+}
+
+
+
+.font-Thai-0 {
+ font-family: "Sarabun";
+}
+
+.font-Thai-1 {
+ font-family: "Kanit";
+}
+
+.font-Thai-2 {
+ font-family: "IBM Plex Sans Thai";
+}
+
+.font-Thai-3 {
+ font-family: "Mali";
+}
+
+.font-Thai-4 {
+ font-family: "Kodchasan";
+}
+
+.font-Thai-5 {
+ font-family: "Charm";
+}
+
+.font-Thai-6 {
+ font-family: "Chonburi";
+} \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/icons/bookmark.svg b/packages/prosody-ui/src/assets/icons/bookmark.svg
new file mode 100755
index 0000000..07c1129
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/bookmark.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 19.6693V4C5 3.44772 5.44772 3 6 3H18C18.5523 3 19 3.44772 19 4V19.6693C19 20.131 18.4277 20.346 18.1237 19.9985L12 13L5.87629 19.9985C5.57227 20.346 5 20.131 5 19.6693Z" stroke="#000000" stroke-linejoin="round"/>
+</svg>
diff --git a/packages/prosody-ui/src/assets/icons/font.svg b/packages/prosody-ui/src/assets/icons/font.svg
new file mode 100755
index 0000000..d4d5c72
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/font.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M4.51 2.6.25 13.67h1.34l1.49-3.86h4l1.52 3.86h1.34L5.68 2.6a.63.63 0 0 0-1.17 0zm-.95 6 1.54-4 1.53 4zm9.35-2.54a2.8 2.8 0 0 0-3 2.08l1.21.31a1.6 1.6 0 0 1 1.78-1.14c.77 0 1.59.26 1.59 1v.75c-.27 0-.63.09-.94.13a9.12 9.12 0 0 0-2.5.52 2.06 2.06 0 0 0-1.41 2.23 1.94 1.94 0 0 0 .88 1.44 3 3 0 0 0 1.62.43 4.39 4.39 0 0 0 1.36-.22 2.92 2.92 0 0 0 1-.52v.61h1.25V8.3c0-1.3-1.14-2.24-2.84-2.24zm.22 6.33a2.4 2.4 0 0 1-1.91-.07.64.64 0 0 1-.32-.52c-.1-.89.82-1.16 2.8-1.38l.76-.1c-.19 1.68-.94 1.94-1.33 2.07z"/></svg> \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/icons/heart.svg b/packages/prosody-ui/src/assets/icons/heart.svg
new file mode 100755
index 0000000..f728e1e
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/heart.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M21 8.99998C21 12.7539 15.7156 17.9757 12.5857 20.5327C12.2416 20.8137 11.7516 20.8225 11.399 20.5523C8.26723 18.1523 3 13.1225 3 8.99998C3 2.00001 12 2.00002 12 8C12 2.00001 21 1.99999 21 8.99998Z" stroke="#000000" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/packages/prosody-ui/src/assets/icons/play.svg b/packages/prosody-ui/src/assets/icons/play.svg
new file mode 100755
index 0000000..9a56073
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/play.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 -2 19 19" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#000000" fill-rule="evenodd" d="M657,246.007484 C657,245.451066 657.450925,245 657.994771,245 L675.005229,245 C675.554626,245 676,245.44892 676,246.007484 L676,258.992516 C676,259.548934 675.549075,260 675.005229,260 L657.994771,260 C657.445374,260 657,259.55108 657,258.992516 L657,246.007484 Z M658,246 L675,246 L675,259 L658,259 L658,246 Z M670.113445,252.056723 C670.603076,252.301538 670.603593,252.698203 670.113445,252.943277 L664.886555,255.556723 C664.396924,255.801538 664,255.562119 664,254.997071 L664,250.002929 C664,249.449027 664.396407,249.198203 664.886555,249.443277 L670.113445,252.056723 Z" transform="translate(-657 -245)"/>
+</svg> \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/icons/quote.svg b/packages/prosody-ui/src/assets/icons/quote.svg
new file mode 100755
index 0000000..34f5de9
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/quote.svg
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
+<g id="Quotemarks-right">
+ <path d="M14.1933422,9.4116497c-7.8260994,0-14.1922989,6.3662004-14.1922989,14.1924
+ c0,7.5498009,5.9247999,13.7420998,13.3690996,14.169899c0.1288996,1.3916016,0.0321999,5.1797028-3.5977001,10.4491997
+ C9.4980431,48.6206512,9.547843,49.1567497,9.888648,49.497551c1.4853945,1.4853973,2.4033947,2.4208984,3.0458946,3.0751991
+ c0.8408995,0.8554993,1.2247,1.2461014,1.7861996,1.7559013c0.1904001,0.1727982,0.4306002,0.259697,0.6719055,0.259697
+ c0.2342949,0,0.4676943-0.0819969,0.6561956-0.2450981c6.3251991-5.5038986,13.3515987-16.8759995,12.3349991-30.8115005
+ C27.7881413,15.3501501,21.820343,9.4116497,14.1933422,9.4116497z M15.4023428,52.2221489
+ c-0.2723999-0.2684975-0.5830002-0.5848999-1.0410004-1.0508003c-0.5565996-0.5672989-1.3203001-1.3446999-2.4784994-2.5067978
+ c4.4053001-6.7881012,3.5731993-11.6230011,3.2089996-12.3164024c-0.1729002-0.3290977-0.5274-0.5507965-0.8985004-0.5507965
+ c-6.7225995,0-12.1922989-5.4697018-12.1922989-12.1933022c0-6.7227001,5.4696999-12.1924,12.1922989-12.1924
+ c6.5489006,0,11.6777992,5.1582012,12.1963062,12.2646008C27.5322418,39.3501511,18.2168427,49.5268517,15.4023428,52.2221489z"/>
+ <path d="M63.9004402,23.5317497v-0.0009995C63.302742,15.3501501,57.3340416,9.4116497,49.7090416,9.4116497
+ c-7.8261986,0-14.1933937,6.3662004-14.1933937,14.1924c0,7.5498009,5.9257927,13.7420998,13.3710938,14.169899
+ c0.1289062,1.3906021,0.0312004,5.1767006-3.5996017,10.4491997c-0.2743988,0.3975029-0.2245979,0.9336014,0.1162033,1.2744026
+ c1.4794998,1.4794998,2.3955002,2.4130974,3.0380974,3.0663986c0.8446999,0.8613014,1.2304993,1.2538986,1.7949028,1.7656021
+ c0.1903992,0.1718979,0.4315987,0.2587967,0.6718979,0.2587967c0.2344055,0,0.4678001-0.0819969,0.6562004-0.2460976
+ C57.8896484,48.8383484,64.9160385,37.4663506,63.9004402,23.5317497z M50.917942,52.2221489
+ c-0.2743988-0.2705002-0.5877991-0.5887985-1.0498009-1.0594978c-0.5565987-0.5665016-1.3172989-1.3418007-2.4706993-2.4981003
+ c4.4053001-6.7891006,3.5742989-11.6230011,3.2109985-12.3164024c-0.1728973-0.3280983-0.5282974-0.5507965-0.8993988-0.5507965
+ c-6.7237015,0-12.1933937-5.4697018-12.1933937-12.1933022c0-6.7227001,5.4696922-12.1924,12.1933937-12.1924
+ c6.5477982,0,11.6777,5.1582012,12.1972008,12.2656002v-0.0009995
+ C63.0478401,39.3481483,53.7324409,49.5268517,50.917942,52.2221489z"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/icons/react.svg b/packages/prosody-ui/src/assets/icons/react.svg
new file mode 100755
index 0000000..6c87de9
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/react.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> \ No newline at end of file
diff --git a/packages/prosody-ui/src/assets/icons/share.svg b/packages/prosody-ui/src/assets/icons/share.svg
new file mode 100755
index 0000000..92c2b94
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/share.svg
@@ -0,0 +1,28 @@
+<!--?xml version="1.0" encoding="utf-8"?-->
+<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="width: 256px; height: 256px; opacity: 1;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#4B4B4B;}
+</style>
+<g>
+ <path class="st0" d="M398.73,227.402c62.563,0,113.27-50.793,113.27-113.359c0-62.656-50.707-113.36-113.27-113.36
+ c-62.656,0-113.364,50.704-113.364,113.36c0,11.587,1.733,22.711,4.926,33.292l-114.914,69.397
+ c-18.512-20.154-44.959-32.739-74.417-32.739C45.146,183.993,0,229.228,0,284.954c0,55.816,45.146,100.962,100.962,100.962
+ c30.736,0,58.278-13.778,76.79-35.482l86.824,45.787c-2.646,8.39-4.106,17.323-4.106,26.63
+ c0.093,48.878,39.673,88.466,88.555,88.466c48.976,0,88.556-39.588,88.556-88.466c0-48.976-39.58-88.554-88.556-88.554
+ c-26.812,0-50.886,11.942-67.122,30.825l-84.726-49.431c3.104-9.672,4.742-19.976,4.742-30.736c0-10.393-1.55-20.43-4.56-29.827
+ l118.013-64.294C335.985,213.268,365.715,227.402,398.73,227.402z M344.282,59.687c14.045-13.956,33.11-22.524,54.448-22.524
+ c21.251,0,40.31,8.567,54.356,22.524c13.862,13.956,22.434,33.016,22.434,54.356c0,21.25-8.572,40.399-22.434,54.354
+ c-14.046,13.956-33.105,22.525-54.356,22.525c-19.059,0-36.298-6.84-49.794-18.419h-0.094c-1.55-1.273-3.099-2.645-4.56-4.106
+ c-10.852-10.946-18.422-24.991-21.246-40.852c-0.824-4.382-1.189-8.942-1.189-13.502C321.846,92.703,330.419,73.644,344.282,59.687
+ z M164.343,296.532c-2.28,13.138-8.661,24.902-17.781,34.022c-0.731,0.73-1.55,1.461-2.373,2.192
+ c-11.49,10.393-26.536,16.69-43.227,16.69c-17.874,0-33.928-7.205-45.6-18.881c-11.676-11.765-18.881-27.725-18.881-45.6
+ c0-17.874,7.205-33.834,18.881-45.6c11.672-11.676,27.726-18.881,45.6-18.881c16.232,0,30.825,5.932,42.225,15.782
+ c1.185,0.997,2.28,2.004,3.376,3.099c9.027,9.12,15.413,20.698,17.781,33.746c0.73,3.83,1.095,7.748,1.095,11.854
+ C165.438,288.873,165.074,292.801,164.343,296.532z M297.773,413.73c1.915-10.767,7.022-20.253,14.499-27.725
+ c0.638-0.641,1.367-1.372,2.098-1.915c9.21-8.39,21.251-13.314,34.654-13.314c14.504,0,27.36,5.745,36.846,15.23
+ c9.485,9.485,15.23,22.346,15.23,36.845c0,14.411-5.745,27.272-15.23,36.748c-9.486,9.486-22.342,15.238-36.846,15.238
+ c-14.406,0-27.266-5.753-36.752-15.238c-9.485-9.476-15.23-22.337-15.322-36.748C296.95,419.751,297.225,416.643,297.773,413.73z"></path>
+</g>
+</svg>
diff --git a/packages/prosody-ui/src/assets/icons/speaker.svg b/packages/prosody-ui/src/assets/icons/speaker.svg
new file mode 100644
index 0000000..a16db51
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/speaker.svg
@@ -0,0 +1,32 @@
+<!--?xml version="1.0" encoding="utf-8"?-->
+<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="width: 256px; height: 256px; opacity: 1;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#4B4B4B;}
+</style>
+<g>
+ <path class="st0" d="M242.908,77.65c-14.745,0-28.695,6.578-41.1,18.231L84.032,170.798H51.761c-4.486,0-8.883,0.893-12.887,2.457
+ c-7.042,2.77-12.814,7.426-17.569,12.941c-7.113,8.319-12.225,18.73-15.808,30.554C1.939,228.592,0,241.889,0,256
+ c0,10.751,1.126,21.037,3.235,30.546c3.19,14.28,8.418,26.855,16.086,36.828c3.861,4.969,8.382,9.312,13.816,12.556
+ c5.407,3.244,11.876,5.29,18.624,5.272h32.271l117.785,74.917c12.396,11.653,26.346,18.231,41.091,18.231
+ c51.529,0,93.3-79.849,93.3-178.35S294.437,77.65,242.908,77.65z M78.732,311.023H51.761c-0.58-0.009-1.116-0.09-1.894-0.394
+ c-1.313-0.491-3.378-1.823-5.684-4.512c-3.468-3.986-7.185-10.84-9.785-19.562c-2.628-8.723-4.219-19.258-4.219-30.555
+ c0-8.615,0.921-16.774,2.52-24.031c2.387-10.884,6.444-19.714,10.492-24.862c2.002-2.601,3.932-4.244,5.38-5.094
+ c1.483-0.858,2.342-1.01,3.19-1.037h26.971c-1.752,3.968-3.297,8.178-4.629,12.61c-3.808,12.646-5.925,27.069-5.925,42.414
+ C68.196,276.742,72.03,295.759,78.732,311.023z M166.32,357.771l-66.372-42.217c-3.915-5.648-7.463-13.101-10.081-21.877
+ c-3.289-10.948-5.228-23.861-5.228-37.677c-0.027-21.046,4.531-40.036,11.386-53.066c1.233-2.359,2.538-4.522,3.878-6.462
+ l66.417-42.244c-10.518,28.866-16.694,63.924-16.694,101.771C149.627,293.838,155.812,328.906,166.32,357.771z M242.908,404.161
+ c-25.719,0-63.102-57.712-63.102-148.161c0-90.448,37.383-148.17,63.102-148.17c25.738,0,63.111,57.722,63.111,148.17
+ C306.018,346.448,268.645,404.161,242.908,404.161z" style="fill: rgb(75, 75, 75);"></path>
+ <path class="st0" d="M243.605,212.684v-0.09c-0.169,0-0.33,0.045-0.491,0.054c-0.233-0.009-0.474-0.054-0.715-0.054v0.134
+ c-11.18,1.322-20.063,21.001-20.063,45.139s8.883,43.816,20.063,45.148v0.125c0.24,0,0.482-0.045,0.715-0.054
+ c0.161,0.009,0.322,0.054,0.491,0.054v-0.09c16.676-0.92,30.01-20.76,30.01-45.184
+ C273.614,233.452,260.281,213.604,243.605,212.684z" style="fill: rgb(75, 75, 75);"></path>
+ <path class="st0" d="M481.696,142.996l-25.085,13.986c17.185,30.859,26.658,65.524,26.666,103.013
+ c-0.008,37.472-9.481,72.137-26.666,102.996l25.085,13.995v-0.009c19.491-34.942,30.313-74.63,30.304-116.981
+ C512.009,217.626,501.187,177.938,481.696,142.996z" style="fill: rgb(75, 75, 75);"></path>
+ <path class="st0" d="M383.026,195.588c9.384,19.456,14.558,41.073,14.567,64.398c-0.009,23.325-5.183,44.952-14.567,64.389
+ l25.862,12.484c11.225-23.244,17.427-49.268,17.418-76.873c0.008-27.614-6.194-53.629-17.418-76.882L383.026,195.588z" style="fill: rgb(75, 75, 75);"></path>
+</g>
+</svg>
diff --git a/packages/prosody-ui/src/assets/icons/spinner.svg b/packages/prosody-ui/src/assets/icons/spinner.svg
new file mode 100755
index 0000000..2a516de
--- /dev/null
+++ b/packages/prosody-ui/src/assets/icons/spinner.svg
@@ -0,0 +1 @@
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_z9k8{transform-origin:center;animation:spinner_StKS .75s infinite linear}@keyframes spinner_StKS{100%{transform:rotate(360deg)}}</style><path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/><path d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z" class="spinner_z9k8"/></svg>
diff --git a/packages/prosody-ui/src/components/Sentence.tsx b/packages/prosody-ui/src/components/Sentence.tsx
new file mode 100644
index 0000000..33144ac
--- /dev/null
+++ b/packages/prosody-ui/src/components/Sentence.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { notRandomFromArray } from "sortug";
+import "./sentence.css";
+
+export function ColoredText({
+ frags,
+ fn,
+ lang,
+}: {
+ frags: string[];
+ fn?: (s: string) => void;
+ lang?: string;
+}) {
+ return (
+ <>
+ {frags.map((s, i) => {
+ const prev = frags[i - 1];
+ const prevC = prev ? notRandomFromArray(prev, colors) : "lol";
+ const color = notRandomFromArray(s, colors);
+ const opacity = prev && prevC === color ? 0.8 : 1;
+ const style = { color, opacity };
+ console.log({ style });
+ return <CTInner lang={lang} key={s + i} s={s} style={style} fn={fn} />;
+ })}
+ </>
+ );
+}
+
+export function CTInner({
+ s,
+ style,
+ fn,
+ lang,
+}: {
+ s: string;
+ style: any;
+ fn?: (s: string) => void;
+ lang?: string;
+}) {
+ function handleClick(e: React.MouseEvent<HTMLSpanElement>) {
+ console.log(!!fn, "fn");
+ if (fn) fn(e.currentTarget.innerText.trim());
+ }
+ return (
+ <span lang={lang} onClick={handleClick} className="word cp" style={style}>
+ {s}
+ </span>
+ );
+}
+export const colors = [
+ "#8c2c2c",
+ "#000000",
+ "#ffd400",
+ "#1513a0",
+ "#7e7e7e",
+ "1eb52d",
+];
diff --git a/packages/prosody-ui/src/components/Word.tsx b/packages/prosody-ui/src/components/Word.tsx
new file mode 100644
index 0000000..82939ce
--- /dev/null
+++ b/packages/prosody-ui/src/components/Word.tsx
@@ -0,0 +1,119 @@
+import React, { useCallback, useEffect, useState } from "react";
+import spinner from "../assets/icons/spinner.svg";
+import likeIcon from "../assets/icons/heart.svg";
+import commentsIcon from "../assets/icons/quote.svg";
+import shareIcon from "../assets/icons/share.svg";
+import fontIcon from "../assets/icons/font.svg";
+import bookmarkIcon from "../assets/icons/bookmark.svg";
+import speakerIcon from "../assets/icons/speaker.svg";
+import type { AnalyzeRes, Meaning } from "../logic/types";
+import { ColoredText } from "./Sentence.tsx";
+import { P, Span, useSpeechSynthesis } from "../hooks/useLang.tsx";
+
+function Word({ data, lang }: { data: AnalyzeRes; lang: string }) {
+ async function load() {
+ // const wiki = await fetchWiki(data.word);
+ // console.log(wiki, "wiki res");
+ // if ("ok" in wiki) setM(wiki.ok.meanings);
+ // else setError(wiki.error);
+ // setLoading(false);
+ }
+ useEffect(() => {
+ load();
+ }, []);
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [meanings, setM] = useState<Meaning[]>([]);
+ const [font, setFont] = useState(0);
+
+ function changeFont() {
+ if (font === 6) setFont(0);
+ else setFont(font + 1);
+ }
+ const { voices, speaking, speak, stop } = useSpeechSynthesis();
+ function playAudio() {
+ console.log({ voices, speaking });
+ console.log("word", data.word);
+ speak(data.word);
+ }
+
+ async function saveW() {}
+
+ return (
+ <div className={`font-${font}`} id="word-modal" title={data.word}>
+ <img className="font-icon cp" onClick={changeFont} src={fontIcon} />
+ <img className="save-icon cp" onClick={saveW} src={bookmarkIcon} />
+ <div className="original">
+ <ColoredText frags={data.syllables} />
+ </div>
+ <div className="pronunciation IPA flex1 flex-center">
+ <P>{`/${data.ipa.replace(/\s/g, "")}/`}</P>
+ <img onClick={playAudio} className="icon cp" src={speakerIcon} />
+ </div>
+ <div className="meanings">
+ {loading ? (
+ <img src={spinner} className="spinner bc" />
+ ) : (
+ meanings.map((m) => (
+ <div key={JSON.stringify(m)} className="meaning">
+ <div className="pos">
+ <Span>{m.pos}</Span>
+ </div>
+ <ol>
+ {m.meaning.map((t, i) => (
+ <li key={t + i} className="translation">
+ <P>{t}</P>
+ </li>
+ ))}
+ </ol>
+ </div>
+ ))
+ )}
+ {error && <div className="error">{error}</div>}
+ </div>
+ </div>
+ );
+}
+
+export default Word;
+
+// function FloatingButtons({
+// tweet,
+// avatar,
+// changeFont,
+// }: {
+// tweet: Tweet;
+// avatar: string;
+// changeFont: any;
+// }) {
+// const { apiKeys, prompts, setModal } = useGlobalState();
+// function openUser() {}
+// function openComments() {}
+// function openShare() {}
+// async function doLike() {
+// const key = apiKeys.openai;
+// // TODO hide button if key not set
+// console.log(tweet.text, "whole text");
+// console.log(tweet.media, "media");
+// if (tweet.media[0] && !tweet.media[0].isVideo) {
+// const res = await vision(tweet.media[0].url, "", key);
+// console.log(res, "vision res");
+// }
+// // const res = await translate(tweet.text, prompts.translate,key);
+// // if ("ok" in res)
+// // setModal(<TranslationModal text={res.ok.choices[0].message.content}/>)
+// }
+// async function doBookmark() {}
+// return (
+// <div id="entry-icons">
+// <div className="avatar">
+// <img className="cp" onClick={openUser} src={avatar} />
+// </div>
+// <img className="cp" onClick={doLike} src={likeIcon} />
+// <img className="cp" onClick={openComments} src={commentsIcon} />
+// <img className="cp" onClick={doBookmark} src={bookmarkIcon} />
+// <img className="cp" onClick={openShare} src={shareIcon} />
+// <img className="cp" onClick={changeFont} src={fontIcon} />
+// </div>
+// );
+// }
diff --git a/packages/prosody-ui/src/components/sentence.css b/packages/prosody-ui/src/components/sentence.css
new file mode 100644
index 0000000..0bd0a49
--- /dev/null
+++ b/packages/prosody-ui/src/components/sentence.css
@@ -0,0 +1,272 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+#root>.spinner {
+ width: 100px;
+ height: 100px;
+}
+
+#entry>.spinner {
+ width: 80px;
+ height: 80px;
+}
+
+
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+#cookies {
+ & .active {
+ background-color: var(--huang);
+ }
+
+ & input {
+ margin-left: 1rem;
+ width: 100%;
+ }
+
+ & textarea {
+ width: 100%;
+ height: 500px;
+ resize: none;
+ outline: none;
+ }
+}
+
+#entry {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ padding: 1rem;
+ /* prov */
+ border: 2px solid black;
+
+ & div[lang="th"] {
+
+ & .tw-text,
+ & .tw-hashtag {
+ font-size: 3rem;
+ }
+ }
+
+ & .text-wrapper {
+ display: block;
+ margin: 0.5rem 0;
+ /* overflow: hidden; */
+ }
+
+ & .word {
+ display: inline-block;
+ transition: transform 0.3s ease;
+ }
+
+ & .word:hover {
+ transform: scale(1.4);
+ background-color: white;
+ }
+
+ & #tw-media {
+ max-width: 100%;
+
+ & img,
+ & video {
+ max-width: 100%;
+ }
+ }
+}
+
+#inner {
+ height: 100%;
+ max-height: 100%;
+ overflow-y: auto;
+}
+
+#entry-icons {
+ position: absolute;
+ bottom: 5%;
+ right: 5%;
+ width: 50px;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ & .avatar {
+ border: 2px solid black;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+
+ & img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+}
+
+#word-modal {
+ position: relative;
+
+ & .font-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 32px;
+ height: 32px;
+ }
+
+ & .save-icon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 32px;
+ height: 32px;
+ }
+
+ & .original {
+ font-size: 4rem;
+ margin-bottom: 1rem;
+ }
+
+ & .syllable {}
+
+ & .IPA {
+ font-size: 1.6rem;
+ line-height: 1.6rem;
+ & img{
+ width: 50px;
+ margin-left: 1rem;
+ }
+ }
+
+ & .meanings {
+
+ & .spinner {
+ width: 80px;
+ height: 80px;
+ }
+
+ & .meaning {
+ margin: 1rem auto;
+ }
+
+ & .pos {
+ font-size: 1.2rem;
+ margin-bottom: 0.3rem;
+ text-align: left;
+ }
+
+ & ol {
+ word-wrap: normal;
+ margin: auto;
+ text-align: left;
+ }
+ }
+}
+
+img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.flex1{
+ width: 100%;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+.flex-center{
+ justify-content: center;
+}
+
+/* p { */
+/* position: absolute; */
+/* top: 50%; */
+/* left: 50%; */
+/* transform: translate(-50%, -50%); */
+/* color: white; */
+/* background-color: rgba(0, 0, 0, 0.5); */
+/* padding: 10px; */
+/* border-radius: 5px; */
+/* } */
+#modal-bg{
+ height: 100vh;
+ width: 100vw;
+ background-color: rgb(0, 0, 0, 0.9);
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 100;
+}
+
+#modal-fg {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ width: 80%;
+ z-index: 101;
+ transform: translate(-50%, -50%);
+ /* background-color: var(--background-color); */
+ background-color: lightgrey;
+ font-size: 1.2rem;
+ padding: 1rem;
+ max-height: 80%;
+ overflow-y: scroll;
+}
+
+
+.text-ipa{
+ font-size: 1.5rem;
+}
+.text-thai{
+ font-size: 1.5rem;
+}
+
+
+/* // new */
+.word.cp{
+ margin: 0 0.5ch;
+}
diff --git a/packages/prosody-ui/src/components/word.css b/packages/prosody-ui/src/components/word.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/prosody-ui/src/components/word.css
diff --git a/packages/prosody-ui/src/files.d.ts b/packages/prosody-ui/src/files.d.ts
new file mode 100644
index 0000000..5e52f80
--- /dev/null
+++ b/packages/prosody-ui/src/files.d.ts
@@ -0,0 +1,4 @@
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
diff --git a/packages/prosody-ui/src/fonts/FontChanger.tsx b/packages/prosody-ui/src/fonts/FontChanger.tsx
new file mode 100644
index 0000000..15c932e
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/FontChanger.tsx
@@ -0,0 +1,64 @@
+import React, { useEffect, useState, type ReactNode } from "react";
+import fontIcon from "../assets/icons/font.svg";
+import { getScriptPredictor } from "glotscript";
+import ThaiFontLoader from "./Thai";
+
+function FontChanger({ text }: { text: string }) {
+ const [script, setScript] = useState<string | null>(null);
+ useEffect(() => {
+ const predictor = getScriptPredictor();
+ const res = predictor(text);
+ console.log("script predicted", res);
+ setScript(res[0]);
+ }, [text]);
+ useEffect(() => {
+ if (script === "Hani") setFontCount(12);
+ else if (script === "Thai") setFontCount(6);
+ else if (script === "Jpan") setFontCount(5);
+ // else if (script === "Latn") setFontCount(6)
+ }, [script]);
+ const [fontIdx, setFont] = useState(0);
+ const [fontCount, setFontCount] = useState(0);
+ function changeFont() {
+ if (fontIdx === fontCount) setFont(0);
+ else setFont((prev) => prev + 1);
+ }
+ return (
+ <div
+ className={`font-changer font-${script}-${fontIdx}`}
+ lang={script || ""}
+ >
+ <img
+ className="font-icon cp"
+ style={{ width: 25 }}
+ onClick={changeFont}
+ src={fontIcon}
+ />
+ {script === "Thai" ? <ThaiFontLoader text={text} /> : null}
+ </div>
+ );
+}
+// function FontChanger({
+// lang,
+// children,
+// }: {
+// lang: string;
+// children: ReactNode;
+// }) {
+// useEffect(() => {}, []);
+// const [script, setScript] = useState("Latn");
+// const [fontIdx, setFont] = useState(0);
+// const fontCount = 6;
+// function changeFont() {
+// if (fontIdx === fontCount) setFont(0);
+// else setFont((prev) => prev + 1);
+// }
+// return (
+// <div className="font-changer" lang={script}>
+// <img className="font-icon cp" onClick={changeFont} src={fontIcon} />
+// {children}
+// </div>
+// );
+// }
+
+export default FontChanger;
diff --git a/packages/prosody-ui/src/fonts/Hani.tsx b/packages/prosody-ui/src/fonts/Hani.tsx
new file mode 100644
index 0000000..f9cc602
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/Hani.tsx
@@ -0,0 +1,14 @@
+import React, { useState, type ReactNode } from "react";
+import "../assets/fonts/Hani/style.css";
+
+function ChineseFontLoader({ children }: { children: ReactNode }) {
+ const [fontIdx, setFont] = useState(0);
+ const fontCount = 12;
+ function changeFont() {
+ if (fontIdx === fontCount) setFont(0);
+ else setFont((prev) => prev + 1);
+ }
+ return <div>{children}</div>;
+}
+
+export default ChineseFontLoader;
diff --git a/packages/prosody-ui/src/fonts/Thai.tsx b/packages/prosody-ui/src/fonts/Thai.tsx
new file mode 100644
index 0000000..0048316
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/Thai.tsx
@@ -0,0 +1,8 @@
+import React, { useState, type ReactNode } from "react";
+import "../assets/fonts/Thai/style.css";
+
+function ThaiFontLoader({ text }: { text: string }) {
+ return <div>{text}</div>;
+}
+
+export default ThaiFontLoader;
diff --git a/packages/prosody-ui/src/fonts/useLangFont.tsx b/packages/prosody-ui/src/fonts/useLangFont.tsx
new file mode 100644
index 0000000..36fa603
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/useLangFont.tsx
@@ -0,0 +1,44 @@
+import React, { useEffect, useState, type ReactNode } from "react";
+import fontIcon from "../assets/icons/font.svg";
+import { getScriptPredictor } from "glotscript";
+
+function useLangFont({ text }: { text: string }) {
+ useEffect(() => {
+ const predictor = getScriptPredictor();
+ const res = predictor(text);
+ console.log("script predicted", res);
+ setScript(res[0]);
+ }, [text]);
+ const [script, setScript] = useState<string | null>(null);
+ const [fontIdx, setFont] = useState(0);
+ const fontCount = 6;
+ function changeFont() {
+ if (fontIdx === fontCount) setFont(0);
+ else setFont((prev) => prev + 1);
+ }
+ // if (script === "Hani") return {}
+}
+// function FontChanger({
+// lang,
+// children,
+// }: {
+// lang: string;
+// children: ReactNode;
+// }) {
+// useEffect(() => {}, []);
+// const [script, setScript] = useState("Latn");
+// const [fontIdx, setFont] = useState(0);
+// const fontCount = 6;
+// function changeFont() {
+// if (fontIdx === fontCount) setFont(0);
+// else setFont((prev) => prev + 1);
+// }
+// return (
+// <div className="font-changer" lang={script}>
+// <img className="font-icon cp" onClick={changeFont} src={fontIcon} />
+// {children}
+// </div>
+// );
+// }
+
+export default useLangFont;
diff --git a/packages/prosody-ui/src/hooks/useLang.tsx b/packages/prosody-ui/src/hooks/useLang.tsx
new file mode 100644
index 0000000..687c81d
--- /dev/null
+++ b/packages/prosody-ui/src/hooks/useLang.tsx
@@ -0,0 +1,184 @@
+import {
+ useState,
+ useEffect,
+ createContext,
+ useContext,
+ useCallback,
+ type ElementType,
+} from "react";
+type ScriptClass =
+ | "text-ipa"
+ | "text-rtl"
+ | "text-cjk"
+ | "text-thai"
+ | "text-cyrillic"
+ | "text-latin";
+interface ScriptContextType {
+ getClass: (text: string) => ScriptClass;
+}
+const ScriptContext = createContext({
+ getClass: (text: string) => "text-latin" as ScriptClass,
+});
+
+const getScriptClass = (text: string): ScriptClass => {
+ // You can combine these with includes() if text has multiple scripts
+ if (/[\u0591-\u07FF\u200F\u202B]/.test(text)) return "text-rtl";
+
+ if (/[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff]/.test(text)) return "text-cjk";
+
+ if (/[\u0E00-\u0E7F]/.test(text)) return "text-thai";
+
+ if (/[\u0400-\u04FF]/.test(text)) return "text-cyrillic";
+
+ if (/[\u0250-\u02AF\u1D00-\u1D7F\u1D80-\u1DBF\u1DC0-\u1DFF]/.test(text))
+ return "text-ipa";
+
+ return "text-latin"; // default
+};
+
+export const ScripProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const getClass = useCallback((text: string) => {
+ return getScriptClass(text);
+ }, []);
+
+ return (
+ <ScriptContext.Provider value={{ getClass }}>
+ {children}
+ </ScriptContext.Provider>
+ );
+};
+
+export const useScript = () => useContext(ScriptContext);
+
+type TextElementProps = {
+ children?: React.ReactNode;
+ className?: string;
+};
+const createTextElement = (Component: ElementType) => {
+ return function TextElement({
+ children,
+ className,
+ ...rest
+ }: TextElementProps & React.ComponentPropsWithoutRef<ElementType>) {
+ const writingSystemClass =
+ typeof children === "string" ? getScriptClass(children) : "text-latin";
+
+ return (
+ <Component
+ className={`${writingSystemClass} ${className || ""}`}
+ {...rest}
+ >
+ {children}
+ </Component>
+ );
+ };
+};
+
+// Create all the text elements you need
+export const Span = createTextElement("span");
+export const P = createTextElement("p");
+export const H1 = createTextElement("h1");
+export const H2 = createTextElement("h2");
+export const H3 = createTextElement("h3");
+export const H4 = createTextElement("h4");
+export const H5 = createTextElement("h5");
+export const H6 = createTextElement("h6");
+export const Label = createTextElement("label");
+export const Small = createTextElement("small");
+
+interface Voice {
+ default: boolean;
+ lang: string;
+ localService: boolean;
+ name: string;
+ voiceURI: string;
+}
+
+export const useSpeechSynthesis = () => {
+ const [voices, setVoices] = useState<Voice[]>([]);
+ const [speaking, setSpeaking] = useState(false);
+ console.log({ voices }, "voices hook");
+
+ useEffect(() => {
+ // Function to get voices
+ const updateVoices = () => {
+ // Some browsers need a small delay for voices to be available
+ setTimeout(() => {
+ const availableVoices = window.speechSynthesis.getVoices();
+ if (availableVoices.length > 0) {
+ setVoices(availableVoices);
+ }
+ }, 100);
+ };
+
+ // Get initial voices
+ updateVoices();
+
+ window.speechSynthesis.addEventListener("voiceschanged", updateVoices);
+
+ // Cleanup
+ return () => {
+ window.speechSynthesis.removeEventListener("voiceschanged", updateVoices);
+ };
+ }, []);
+
+ const speak = (text: string, voiceName?: string) => {
+ const utterance = new SpeechSynthesisUtterance(text);
+
+ if (voiceName) {
+ const voice = voices.find((v) => v.name === voiceName);
+ if (voice) utterance.voice = voice;
+ }
+
+ utterance.onstart = () => setSpeaking(true);
+ utterance.onend = () => setSpeaking(false);
+ utterance.onerror = () => setSpeaking(false);
+
+ window.speechSynthesis.speak(utterance);
+ };
+
+ const stop = () => {
+ window.speechSynthesis.cancel();
+ setSpeaking(false);
+ };
+
+ return {
+ voices,
+ speaking,
+ speak,
+ stop,
+ };
+};
+
+// Example usage in a component:
+const SpeechComponent = () => {
+ const { voices, speaking, speak, stop } = useSpeechSynthesis();
+ const [selectedVoice, setSelectedVoice] = useState<string>("");
+
+ return (
+ <div>
+ <select
+ value={selectedVoice}
+ onChange={(e) => setSelectedVoice(e.target.value)}
+ >
+ <option value="">Default Voice</option>
+ {voices.map((voice) => (
+ <option key={voice.name} value={voice.name}>
+ {voice.name} ({voice.lang})
+ </option>
+ ))}
+ </select>
+
+ <button
+ onClick={() => speak("Hello, world!", selectedVoice)}
+ disabled={speaking}
+ >
+ {speaking ? "Speaking..." : "Speak"}
+ </button>
+
+ {speaking && <button onClick={stop}>Stop</button>}
+ </div>
+ );
+};
diff --git a/packages/prosody-ui/src/hooks/useModal.tsx b/packages/prosody-ui/src/hooks/useModal.tsx
new file mode 100644
index 0000000..7164bcb
--- /dev/null
+++ b/packages/prosody-ui/src/hooks/useModal.tsx
@@ -0,0 +1,53 @@
+import { franc } from "franc-all";
+import React, {
+ ReactElement,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+function useModal() {
+ const [achild, setChild] = useState<ReactNode>(null);
+ return <Modal close={() => setChild(null)}>{achild}</Modal>;
+}
+export default useModal;
+
+function Modal({
+ children,
+ height = "fit-content",
+ width = "80%",
+ close,
+}: {
+ children: ReactNode;
+ close: () => void;
+ width?: any;
+ height?: any;
+}) {
+ function onKey(event: any) {
+ if (event.key === "Escape") close();
+
+ useEffect(() => {
+ document.addEventListener("keyup", onKey);
+ return () => {
+ document.removeEventListener("keyup", onKey);
+ };
+ }, [children]);
+ }
+
+ function clickAway(e: React.MouseEvent) {
+ e.stopPropagation();
+ if (!modalRef.current || !modalRef.current.contains(e.target as any))
+ close();
+ }
+ const modalRef = useRef<HTMLDivElement>(null);
+ const style = { width, height };
+ return (
+ <div id="modal-bg" onClick={clickAway}>
+ <div id="modal-fg" style={style} ref={modalRef}>
+ {children}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/prosody-ui/src/hooks/useTTS.tsx b/packages/prosody-ui/src/hooks/useTTS.tsx
new file mode 100644
index 0000000..671d078
--- /dev/null
+++ b/packages/prosody-ui/src/hooks/useTTS.tsx
@@ -0,0 +1,3 @@
+function useTTS(text: string) {}
+
+export default useTTS;
diff --git a/packages/prosody-ui/src/latin/LatinText.tsx b/packages/prosody-ui/src/latin/LatinText.tsx
new file mode 100644
index 0000000..e5b13ff
--- /dev/null
+++ b/packages/prosody-ui/src/latin/LatinText.tsx
@@ -0,0 +1,77 @@
+import React, { useCallback, useEffect, useState } from "react";
+import type { AnalyzeRes } from "../logic/types";
+import { ColoredText } from "../components/Sentence";
+import Word from "../components/Word";
+import { iso6393To1 } from "../logic/iso6393to1";
+
+function segmentate(
+ text: string,
+ lang: string,
+ granularity: "sentence" | "word" | "grapheme",
+) {
+ // TODO proper error handling here
+ console.log("segmenting", lang);
+ const la = iso6393To1[lang];
+ const lng = la || lang;
+ const segmenter = new Intl.Segmenter(lng, { granularity });
+ const segments = Array.from(segmenter.segment(text));
+ console.log("seg", segments[0]);
+ return segments.reduce((acc: string[], s) => {
+ const trimmed = s.segment.trim();
+ if (trimmed) return [...acc, trimmed];
+ else return acc;
+ }, []);
+}
+
+export default function LatinText({
+ text,
+ lang,
+ openWord,
+}: {
+ text: string;
+ lang: string;
+ openWord?: (word: string) => void;
+}) {
+ useEffect(() => {
+ const sentences = segmentate(text, lang, "sentence");
+ if (sentences) setSentences(sentences);
+ }, [text]);
+ const [sentences, setSentences] = useState<string[]>([]);
+ console.log({ sentences });
+ return (
+ <>
+ {sentences.map((s, i) => (
+ <Sentence key={s + i} text={s} lang={lang} openWord={openWord} />
+ ))}
+ </>
+ );
+}
+
+function Sentence({
+ text,
+ lang,
+ openWord,
+}: {
+ text: string;
+ lang: string;
+
+ openWord?: (word: string) => void;
+}) {
+ useEffect(() => {
+ const w = segmentate(text, lang, "word");
+ if (w) setWords(w);
+ console.log({ words });
+ }, [text]);
+ const [words, setWords] = useState<string[]>([]);
+ console.log({ words });
+
+ // const [data, setData] = useState<Record<string, AnalyzeRes>>({});
+ const [word, setWord] = useState<AnalyzeRes>();
+
+ return (
+ <>
+ <ColoredText frags={words} fn={openWord} />;
+ {word && <Word data={word} lang={lang} />}
+ </>
+ );
+}
diff --git a/packages/prosody-ui/src/logic/iso6393to1.ts b/packages/prosody-ui/src/logic/iso6393to1.ts
new file mode 100644
index 0000000..4c4deed
--- /dev/null
+++ b/packages/prosody-ui/src/logic/iso6393to1.ts
@@ -0,0 +1,186 @@
+export const iso6393To1: Record<string, string> = {
+ aar: "aa",
+ abk: "ab",
+ afr: "af",
+ aka: "ak",
+ amh: "am",
+ ara: "ar",
+ arg: "an",
+ asm: "as",
+ ava: "av",
+ ave: "ae",
+ aym: "ay",
+ aze: "az",
+ bak: "ba",
+ bam: "bm",
+ bel: "be",
+ ben: "bn",
+ bis: "bi",
+ bod: "bo",
+ bos: "bs",
+ bre: "br",
+ bul: "bg",
+ cat: "ca",
+ ces: "cs",
+ cha: "ch",
+ che: "ce",
+ chu: "cu",
+ chv: "cv",
+ cor: "kw",
+ cos: "co",
+ cre: "cr",
+ cym: "cy",
+ dan: "da",
+ deu: "de",
+ div: "dv",
+ dzo: "dz",
+ ell: "el",
+ eng: "en",
+ epo: "eo",
+ est: "et",
+ eus: "eu",
+ ewe: "ee",
+ fao: "fo",
+ fas: "fa",
+ fij: "fj",
+ fin: "fi",
+ fra: "fr",
+ fry: "fy",
+ ful: "ff",
+ gla: "gd",
+ gle: "ga",
+ glg: "gl",
+ glv: "gv",
+ grn: "gn",
+ guj: "gu",
+ hat: "ht",
+ hau: "ha",
+ hbs: "sh",
+ heb: "he",
+ her: "hz",
+ hin: "hi",
+ hmo: "ho",
+ hrv: "hr",
+ hun: "hu",
+ hye: "hy",
+ ibo: "ig",
+ ido: "io",
+ iii: "ii",
+ iku: "iu",
+ ile: "ie",
+ ina: "ia",
+ ind: "id",
+ ipk: "ik",
+ isl: "is",
+ ita: "it",
+ jav: "jv",
+ jpn: "ja",
+ kal: "kl",
+ kan: "kn",
+ kas: "ks",
+ kat: "ka",
+ kau: "kr",
+ kaz: "kk",
+ khm: "km",
+ kik: "ki",
+ kin: "rw",
+ kir: "ky",
+ kom: "kv",
+ kon: "kg",
+ kor: "ko",
+ kua: "kj",
+ kur: "ku",
+ lao: "lo",
+ lat: "la",
+ lav: "lv",
+ lim: "li",
+ lin: "ln",
+ lit: "lt",
+ ltz: "lb",
+ lub: "lu",
+ lug: "lg",
+ mah: "mh",
+ mal: "ml",
+ mar: "mr",
+ mkd: "mk",
+ mlg: "mg",
+ mlt: "mt",
+ mon: "mn",
+ mri: "mi",
+ msa: "ms",
+ mya: "my",
+ nau: "na",
+ nav: "nv",
+ nbl: "nr",
+ nde: "nd",
+ ndo: "ng",
+ nep: "ne",
+ nld: "nl",
+ nno: "nn",
+ nob: "nb",
+ nor: "no",
+ nya: "ny",
+ oci: "oc",
+ oji: "oj",
+ ori: "or",
+ orm: "om",
+ oss: "os",
+ pan: "pa",
+ pli: "pi",
+ pol: "pl",
+ por: "pt",
+ pus: "ps",
+ que: "qu",
+ roh: "rm",
+ ron: "ro",
+ run: "rn",
+ rus: "ru",
+ sag: "sg",
+ san: "sa",
+ sin: "si",
+ slk: "sk",
+ slv: "sl",
+ sme: "se",
+ smo: "sm",
+ sna: "sn",
+ snd: "sd",
+ som: "so",
+ sot: "st",
+ spa: "es",
+ sqi: "sq",
+ srd: "sc",
+ srp: "sr",
+ ssw: "ss",
+ sun: "su",
+ swa: "sw",
+ swe: "sv",
+ tah: "ty",
+ tam: "ta",
+ tat: "tt",
+ tel: "te",
+ tgk: "tg",
+ tgl: "tl",
+ tha: "th",
+ tir: "ti",
+ ton: "to",
+ tsn: "tn",
+ tso: "ts",
+ tuk: "tk",
+ tur: "tr",
+ twi: "tw",
+ uig: "ug",
+ ukr: "uk",
+ urd: "ur",
+ uzb: "uz",
+ ven: "ve",
+ vie: "vi",
+ vol: "vo",
+ wln: "wa",
+ wol: "wo",
+ xho: "xh",
+ yid: "yi",
+ yor: "yo",
+ zha: "za",
+ zho: "zh",
+ zul: "zu",
+};
diff --git a/packages/prosody-ui/src/logic/stanza.ts b/packages/prosody-ui/src/logic/stanza.ts
new file mode 100644
index 0000000..9e59450
--- /dev/null
+++ b/packages/prosody-ui/src/logic/stanza.ts
@@ -0,0 +1,86 @@
+import type { AsyncRes, Result } from "sortug";
+
+const ENDPOINT = "http://localhost:8102";
+export async function segmenter(text: string, lang: string) {
+ try {
+ const body = JSON.stringify({ lang, string: text });
+ const opts = {
+ headers: { "Content-type": "application/json" },
+ method: "POST",
+ body,
+ };
+ const res = await fetch(ENDPOINT + "/segment", opts);
+ console.log("stanza", res);
+ const j = await res.json();
+ return { ok: j };
+ } catch (e) {
+ return { error: `${e}` };
+ }
+}
+export async function idLang(text: string) {
+ try {
+ const body = JSON.stringify({ string: text });
+ const opts = {
+ headers: { "Content-type": "application/json" },
+ method: "POST",
+ body,
+ };
+ const res = await fetch(ENDPOINT + "/detect-lang", opts);
+ const j = await res.json();
+ return { ok: j };
+ } catch (e) {
+ return { error: `${e}` };
+ }
+}
+
+export type Sentence = {
+ text: string;
+ sentiment: number;
+ constituency: string;
+ dependencies: Dependency[];
+ entities: Entity[];
+ tokens: Token[];
+ words: Word[];
+};
+export type Dependency = Array<[Word, string, Word]>;
+export type Word = {
+ id: number;
+ text: string;
+ lemma: string;
+ upos: string;
+ xpos: string;
+ feats: string;
+ head: number;
+ deprel: string;
+ start_char: number;
+ end_char: number;
+};
+export type Token = {
+ id: [number, number];
+ text: string;
+ misc: string;
+ words: Word[];
+ start_char: number;
+ end_char: number;
+ ner: string;
+};
+export type Entity = {
+ text: string;
+ misc: string;
+ start_char: number;
+ end_char: number;
+ type: string;
+};
+// "amod",
+// {
+// "id": 1,
+// "text": "Stony",
+// "lemma": "Stony",
+// "upos": "ADJ",
+// "xpos": "NNP",
+// "feats": "Degree=Pos",
+// "head": 3,
+// "deprel": "amod",
+// "start_char": 0,
+// "end_char": 5
+// }
diff --git a/packages/prosody-ui/src/logic/types.ts b/packages/prosody-ui/src/logic/types.ts
new file mode 100644
index 0000000..ac308cf
--- /dev/null
+++ b/packages/prosody-ui/src/logic/types.ts
@@ -0,0 +1,48 @@
+export type Cookie = {
+ domain: string;
+ path: string;
+ hostOnly: boolean;
+ httpOnly: boolean;
+ secure: boolean;
+ session: boolean;
+ sameSite: SameSite;
+ storeId: null;
+ name: string;
+ value: string;
+};
+export type CookiesMap = Record<string, CookieMap>;
+export type CookieMap = Record<string, Cookie>;
+export type KeyMap = Record<string, string>;
+type SameSite = null | "no_restriction"; // TODO
+
+export type APIRes = { API: { app: string; api_key: string } };
+export type CookieRes = { Cookie: { app: string; cookie: CookieMap } };
+export type CookiesRes = { cookies: CookiesMap; apiKeys: KeyMap };
+
+// words
+export type Meaning = {
+ pos: string; // part of speech;
+ meaning: string[];
+ etymology: string;
+ references?: any;
+};
+
+export type Prompts = {
+ translate: string;
+};
+export type AnalyzeRes = {
+ word: string;
+ syllables: string[];
+ ipa: string;
+ pos: POS;
+};
+type PosTuple = [string, POS];
+type POS = string;
+
+export type WordData = {
+ spelling: string;
+ lang: string;
+ ipa: string;
+ meanings: Meaning[];
+ references?: any;
+};
diff --git a/packages/prosody-ui/src/logic/utils.ts b/packages/prosody-ui/src/logic/utils.ts
new file mode 100644
index 0000000..737a6ec
--- /dev/null
+++ b/packages/prosody-ui/src/logic/utils.ts
@@ -0,0 +1,66 @@
+import type { Result } from "sortug";
+
+export function detectScript(text: string): Result<string> {
+ const scripts = {
+ Latin: /[\u0000-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F]/g,
+ Cyrillic: /[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F]/g,
+ Greek: /[\u0370-\u03FF\u1F00-\u1FFF]/g,
+ Hebrew: /[\u0590-\u05FF]/g,
+ Arabic: /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/g,
+ Devanagari: /[\u0900-\u097F]/g, // Hindi, Sanskrit, etc.
+ Bengali: /[\u0980-\u09FF]/g,
+ Thai: /[\u0E00-\u0E7F]/g,
+ Chinese:
+ /[\u4E00-\u9FFF\u3400-\u4DBF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F]/g,
+ Japanese: /[\u3040-\u309F\u30A0-\u30FF\uFF00-\uFFEF\u4E00-\u9FAF]/g, // Includes Hiragana, Katakana
+ Korean: /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/g, // Includes Hangul
+ Armenian: /[\u0530-\u058F]/g,
+ Georgian: /[\u10A0-\u10FF]/g,
+ Khmer: /[\u1780-\u17FF]/g, // Cambodian
+ Myanmar: /[\u1000-\u109F]/g, // Burmese
+ Tamil: /[\u0B80-\u0BFF]/g,
+ Telugu: /[\u0C00-\u0C7F]/g,
+ Amharic: /[\u1200-\u137F]/g, // Ethiopian
+ };
+ const counts: Record<string, number> = {};
+
+ for (const [scriptName, regex] of Object.entries(scripts)) {
+ // Create an array of matches and count its length
+ const matches = text.match(regex) || [];
+ counts[scriptName] = matches.length;
+ }
+
+ let maxCount = 0;
+ let dominantScript = "Unknown";
+
+ for (const [scriptName, count] of Object.entries(counts)) {
+ if (count > maxCount) {
+ maxCount = count;
+ dominantScript = scriptName;
+ }
+ }
+ if (dominantScript === "Unknown") return { error: "Not detected" };
+ else return { ok: dominantScript };
+}
+
+export function langFromScript(script: string): Result<string> {
+ if (script === "Thai") return { ok: "th" };
+ if (script === "Japanese") return { ok: "ja" };
+ if (script === "Chinese") return { ok: "zh" };
+ if (script === "Korean") return { ok: "ko" };
+ else return { error: "too generic" };
+}
+export function scriptFromLang(lang: string, text: string): string {
+ if (lang == "th") return "Thai";
+ if (lang == "tha") return "Thai";
+ if (lang == "en") return "Engl";
+ if (lang == "es") return "Span";
+ if (lang == "cn") return "Hant";
+ if (lang == "zh") return "Hant";
+ if (lang == "ja") return "Japn";
+ else {
+ const res = detectScript(text);
+ if ("ok" in res) return res.ok;
+ else return "";
+ }
+}
diff --git a/packages/prosody-ui/src/logic/wiki.ts b/packages/prosody-ui/src/logic/wiki.ts
new file mode 100644
index 0000000..1325c0f
--- /dev/null
+++ b/packages/prosody-ui/src/logic/wiki.ts
@@ -0,0 +1,138 @@
+import type { AsyncRes, Result } from "sortug";
+import type { Meaning } from "./types";
+
+export function buildWiktionaryURL(word: string) {
+ const params = new URLSearchParams();
+ params.append("action", "parse");
+ params.append("page", word);
+ params.append("format", "json");
+ params.append("prop", "templates|text");
+ params.append("formatversion", "2");
+
+ const p = params.toString();
+ const url = `https://en.wiktionary.org/w/api.php?${p}`;
+ return url;
+}
+
+// export async function fetchWordInWiki(url: string) {
+// const opts = { method: "GET", body: null, headers: {} };
+// try {
+// const res = await proxyCall(url, opts);
+// console.log(res.headers.get("content-type"));
+// const j = await res.json();
+// return { ok: j };
+// } catch (e) {
+// return { error: `${e}` };
+// }
+// }
+
+export type WikiRes = {
+ url: string;
+ meanings: Meaning[];
+ ipa: string[];
+};
+const poses = [
+ "noun",
+ "verb",
+ "adjective",
+ "adverb",
+ "conjunction",
+ "determiner",
+ "preposition",
+ "definitions",
+];
+
+export function parseWiktionary(html: string, url: string): Result<WikiRes> {
+ try {
+ const dp = new DOMParser();
+ const doc = dp.parseFromString(html, "text/html");
+ const ipas = doc.querySelectorAll(".IPA");
+ const headings = doc.querySelectorAll(".mw-heading");
+ const ms: Meaning[] = [];
+ const doneIdx: number[] = [];
+ let currentRound: Meaning = { pos: "", meaning: [], etymology: "" };
+ for (let [idx, h] of Array.from(headings).entries()) {
+ const headingType: string = (h.firstChild as any).innerText;
+ if (!headingType) continue;
+ const ht = headingType.toLowerCase();
+ if (ht.includes("etymology")) currentRound.etymology = fillEtym(h);
+ else if (poses.includes(ht)) {
+ currentRound.pos = ht;
+ currentRound = fillMeaning(h, currentRound);
+ }
+ if (currentRound.pos) {
+ ms.push({ ...currentRound });
+ currentRound = { pos: "", meaning: [], etymology: "" };
+ }
+ if (ht === "references") break; // make sure it's one single language lol
+ }
+ const ipaStrings = Array.from(ipas).map((el: any) => el.innerText);
+ return { ok: { meanings: ms, ipa: ipaStrings, url } };
+ } catch (e) {
+ return { error: `${e}` };
+ }
+}
+
+function fillMeaning(el: Element, m: Meaning) {
+ const sibling = el.nextElementSibling;
+ if (!sibling) return m;
+ if (sibling?.tagName.toLowerCase() === "ol") {
+ for (let li of Array.from(sibling.children)) {
+ if (li.tagName.toLowerCase() !== "li") continue;
+ if (li.className.includes("empty-elt")) continue;
+ m.meaning.push(li.innerHTML);
+ }
+ }
+ if (m.meaning.length === 0) return fillMeaning(sibling, m);
+ else return m;
+}
+
+function fillEtym(el: Element, acc: string = ""): string {
+ const sibling = el.nextElementSibling;
+ if (!sibling) return acc;
+ if (sibling?.tagName.toLowerCase() === "p") acc += `\n${sibling.innerHTML}`;
+ if (!acc) return fillEtym(sibling, acc);
+ else return acc;
+}
+
+export function parseWiktionaryo(html: string, url: string): Result<WikiRes> {
+ try {
+ const dp = new DOMParser();
+ const doc = dp.parseFromString(html, "text/html");
+ const ipas = doc.querySelectorAll(".IPA");
+ const ols = doc.querySelectorAll("ol");
+ const ms = Array.from(ols).map((el) => {
+ let pos = "";
+ let etymology = "";
+ let meaning: string[] = [];
+ let posr = findPos(el);
+ if ("ok" in posr) pos = posr.ok;
+ for (let li of Array.from(el.children)) {
+ if (li.tagName !== "LI") continue;
+ meaning.push((li as any).innerText);
+ }
+ return { pos, meaning, etymology };
+ });
+ console.log(ipas, "ipa strings");
+ console.log(ols, "lists in wiki");
+ const ipaStrings = Array.from(ipas).map((el: any) => el.innerText);
+ return { ok: { meanings: ms, ipa: ipaStrings, url } };
+ } catch (e) {
+ return { error: `${e}` };
+ }
+}
+
+function findPos(el: Element): Result<string> {
+ let pichai = el.previousElementSibling;
+ console.log(pichai, "previous");
+ if (!pichai) return { error: "no pichai" };
+ if (pichai.classList.contains("mw-heading")) {
+ const h4 = pichai.querySelector("h4");
+ const h3 = pichai.querySelector("h3");
+ if (!h4 && !h3) return findPos(pichai);
+ else {
+ const id = (h4?.innerText || h3?.innerText)!;
+ return { ok: id };
+ }
+ } else return findPos(pichai);
+}
diff --git a/packages/prosody-ui/src/sortug.css b/packages/prosody-ui/src/sortug.css
new file mode 100644
index 0000000..c6280c0
--- /dev/null
+++ b/packages/prosody-ui/src/sortug.css
@@ -0,0 +1,248 @@
+
+/* SORTUG CSS */
+/* variables */
+:root {
+ --bai: rgba(255, 255, 255, 1);
+ --baizi: rgba(230, 230, 230);
+ --hui: rgba(130, 130, 130, 1);
+ --hei: rgba(0, 0, 0, 1);
+ --hong: rgb(141, 15, 15, 1);
+ --huang: rgb(230, 180, 60, 1);
+ --lan: rgb(30, 60, 80, 1);
+}
+
+[data-theme="dark"] {
+ --bg: hei;
+ --fg: baizi;
+}
+
+[data-theme="light"] {
+ --bg: white;
+ --fg: black;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#root {
+ height: 100%;
+ min-height: 100%;
+ overscroll-behavior: none;
+ color: var(--fg);
+ -webkit-font-smoothing: antialiased;
+ margin: 0;
+}
+
+/* tailwindy classes */
+.card {
+ padding: 1rem;
+ max-width: max-content;
+}
+
+button,
+.button {
+ max-width: max-content;
+ padding: 0.5rem;
+ border: 1px solid var(--fg);
+}
+
+/* borders */
+.nb {
+ border: none;
+}
+
+/* widths */
+.hw {
+ width: 50%;
+}
+
+.qw {
+ width: 25%;
+}
+
+.tqw {
+ width: 75%;
+}
+
+/* flex */
+.row {
+ display: flex;
+ align-items: center;
+}
+
+.sy {
+ overflow-y: scroll;
+}
+
+.fsy {
+ overflow-y: scroll;
+ height: 100%;
+}
+
+.fxc {
+ display: flex;
+ justify-content: center;
+ align-items: baseline;
+}
+
+/* flex spread */
+.fs {
+ display: flex;
+ justify-content: space-between;
+}
+
+.fsc {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.g1 {
+ gap: 0.5rem;
+}
+
+.g2 {
+ gap: 1rem;
+}
+
+.address {
+ font-family: "Courier New", Courier, monospace;
+}
+
+.spread {
+ justify-content: space-between;
+}
+
+.even {
+ justify-content: space-evenly;
+}
+
+.flexc {
+ justify-content: center;
+}
+
+.cp {
+ cursor: pointer;
+}
+
+/* centering */
+.gc {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.agc {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.ac {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.xc {
+ position: fixed;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 20;
+}
+
+.tc {
+ text-align: center;
+}
+
+.bc {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.blocks {
+ & * {
+ display: block;
+ }
+}
+
+.bold {
+ font-weight: 700;
+}
+
+.weak {
+ opacity: 0.7;
+}
+
+.all-c {
+ & * {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.mb-1 {
+ margin-bottom: 1rem;
+}
+
+.error {
+ color: red;
+ text-align: center;
+}
+
+.tabs {
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+
+ & .tab {
+ cursor: pointer;
+ opacity: 0.5;
+ }
+
+ & .tab.active {
+ opacity: 1;
+ }
+}
+
+.disabled {
+ opacity: 0.5;
+}
+
+.smol {
+ font-size: 0.9rem;
+}
+
+/* The Modal (background) */
+#modal-bg {
+ position: fixed;
+ z-index: 1;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 998;
+}
+
+/* Modal Content */
+#modal-fg {
+ background-color: var(--bg);
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ padding: 20px;
+ z-index: 999;
+ max-height: 90vh;
+ min-height: 20vh;
+ max-width: 90vw;
+ overflow: auto;
+}
diff --git a/packages/prosody-ui/src/styles/styles.css b/packages/prosody-ui/src/styles/styles.css
new file mode 100644
index 0000000..69351f1
--- /dev/null
+++ b/packages/prosody-ui/src/styles/styles.css
@@ -0,0 +1,281 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+#root>.spinner {
+ width: 100px;
+ height: 100px;
+}
+
+#entry>.spinner {
+ width: 80px;
+ height: 80px;
+}
+
+
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+#cookies {
+ & .active {
+ background-color: var(--huang);
+ }
+
+ & input {
+ margin-left: 1rem;
+ width: 100%;
+ }
+
+ & textarea {
+ width: 100%;
+ height: 500px;
+ resize: none;
+ outline: none;
+ }
+}
+
+#entry {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ padding: 1rem;
+ /* prov */
+ border: 2px solid black;
+
+ & div[lang="th"] {
+
+ & .tw-text,
+ & .tw-hashtag {
+ font-size: 3rem;
+ }
+ }
+
+ & .text-wrapper {
+ display: block;
+ margin: 0.5rem 0;
+ /* overflow: hidden; */
+ }
+
+ & .word {
+ display: inline-block;
+ transition: transform 0.3s ease;
+ }
+
+ & .word:hover {
+ transform: scale(1.4);
+ background-color: white;
+ }
+
+ & #tw-media {
+ max-width: 100%;
+
+ & img,
+ & video {
+ max-width: 100%;
+ }
+ }
+}
+
+#inner {
+ height: 100%;
+ max-height: 100%;
+ overflow-y: auto;
+}
+
+#entry-icons {
+ position: absolute;
+ bottom: 5%;
+ right: 5%;
+ width: 50px;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ & .avatar {
+ border: 2px solid black;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+
+ & img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+}
+
+#word-modal {
+ position: relative;
+
+ & .font-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 32px;
+ height: 32px;
+ }
+
+ & .save-icon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 32px;
+ height: 32px;
+ }
+
+ & .original {
+ font-size: 4rem;
+ margin-bottom: 1rem;
+ }
+
+ & .syllable {}
+
+ & .IPA {
+ font-size: 1.6rem;
+ line-height: 1.6rem;
+
+ & img {
+ width: 50px;
+ margin-left: 1rem;
+ }
+ }
+
+ & .meanings {
+
+ & .spinner {
+ width: 80px;
+ height: 80px;
+ }
+
+ & .meaning {
+ margin: 1rem auto;
+ }
+
+ & .pos {
+ font-size: 1.2rem;
+ margin-bottom: 0.3rem;
+ text-align: left;
+ }
+
+ & ol {
+ word-wrap: normal;
+ margin: auto;
+ text-align: left;
+ }
+ }
+}
+
+img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.flex1 {
+ width: 100%;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.flex-center {
+ justify-content: center;
+}
+
+/* p { */
+/* position: absolute; */
+/* top: 50%; */
+/* left: 50%; */
+/* transform: translate(-50%, -50%); */
+/* color: white; */
+/* background-color: rgba(0, 0, 0, 0.5); */
+/* padding: 10px; */
+/* border-radius: 5px; */
+/* } */
+#modal-bg {
+ height: 100vh;
+ width: 100vw;
+ background-color: rgb(0, 0, 0, 0.9);
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 100;
+}
+
+#modal-fg {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ width: 80%;
+ z-index: 101;
+ transform: translate(-50%, -50%);
+ /* background-color: var(--background-color); */
+ background-color: lightgrey;
+ font-size: 1.2rem;
+ padding: 1rem;
+ max-height: 80%;
+ overflow-y: scroll;
+}
+
+
+.text-ipa {
+ font-size: 1.5rem;
+}
+
+.text-thai {
+ font-size: 1.5rem;
+}
+
+
+/* // new */
+.word.cp {
+ margin: 0 0.5ch;
+}
+
+.lang-text-container {
+ display: flex;
+ flex-wrap: wrap;
+} \ No newline at end of file
diff --git a/packages/prosody-ui/src/thai/ThaiText.tsx b/packages/prosody-ui/src/thai/ThaiText.tsx
new file mode 100644
index 0000000..fc1e1e6
--- /dev/null
+++ b/packages/prosody-ui/src/thai/ThaiText.tsx
@@ -0,0 +1,49 @@
+import React, { useCallback, useEffect, useState } from "react";
+import "../assets/fonts/Thai/style.css";
+import { segmentateThai } from "./logic/thainlp";
+import type { AnalyzeRes } from "../logic/types";
+import { ColoredText } from "../components/Sentence";
+import Word from "../components/Word";
+
+export default function ThaiText({
+ text,
+ openWord,
+}: {
+ text: string;
+ openWord: (s: string) => void;
+}) {
+ useEffect(() => {
+ pythonseg();
+ }, [text]);
+
+ const [data, setData] = useState<Record<string, AnalyzeRes>>({});
+ const [modal, setModal] = useState<any>();
+ const pythonseg = useCallback(async () => {
+ const s2 = await segmentateThai(text.trim());
+ if ("ok" in s2) {
+ const ob = s2.ok.reduce((acc, item) => {
+ acc[item.word] = item;
+ return acc;
+ }, {} as any);
+ setData(ob);
+ console.log(s2, "s2");
+ } else console.error(s2.error);
+ }, [text]);
+
+ // function openWord(e: React.MouseEvent<any>) {
+ // const s = e.currentTarget.innerText;
+ // const d = data[s];
+ // setModal(d);
+ // // setModal(<WordModal data={d} lang={lang} />);
+ // }
+ return (
+ <div className="thaitext">
+ <ColoredText lang="tha" frags={Object.keys(data)} fn={openWord} />
+ {modal && <Word data={modal} lang={"tha"} />}
+ </div>
+ );
+}
+
+function ThaiWord() {
+ return <div />;
+}
diff --git a/packages/prosody-ui/src/thai/logic/thainlp.ts b/packages/prosody-ui/src/thai/logic/thainlp.ts
new file mode 100644
index 0000000..031bf4c
--- /dev/null
+++ b/packages/prosody-ui/src/thai/logic/thainlp.ts
@@ -0,0 +1,90 @@
+import type { AsyncRes } from "sortug";
+import type { AnalyzeRes } from "../../logic/types";
+
+const ENDPOINT = "http://192.168.1.110:8001";
+async function call(path: string, body: any) {
+ try {
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body: JSON.stringify(body),
+ };
+ const r1 = await fetch(ENDPOINT + path, opts);
+ // const r2 = await fetch(`http://192.168.1.110:8000/analyze`, opts);
+ const jj = await r1.json();
+ return { ok: jj };
+ } catch (e) {
+ return { error: `${e}` };
+ }
+}
+export async function analyzeTHWord(word: string): AsyncRes<AnalyzeRes> {
+ return await call("/analyze", { word });
+}
+export async function segmentateThai(sentence: string): AsyncRes<AnalyzeRes[]> {
+ return await call("/segmentate", { word: sentence });
+}
+
+export const POSMAP: Record<string, string> = {
+ ADJ: "Adjective",
+ ADP: "Adposition",
+ ADV: "Adverb",
+ AUX: "Auxiliary",
+ CCONJ: "Coordinating conjunction",
+ DET: "Determiner",
+ INTJ: "Interjection",
+ NOUN: "Noun",
+ NUM: "Numeral",
+ PART: "Particle",
+ PRON: "Pronoun",
+ PROPN: "Proper noun",
+ PUNCT: "Punctuation",
+ SCONJ: "Subordinating conjunction",
+ VERB: "Verb",
+ NPRP: "Proper noun",
+ NCNM: "Cardinal number",
+ NONM: "Ordinal number",
+ NLBL: "Label noun",
+ NCMN: "Common noun",
+ NTTL: "Title noun",
+ PPRS: "Personal pronoun",
+ PDMN: "Demonstrative pronoun",
+ PNTR: "Interrogative pronoun",
+ PREL: "Relative pronoun",
+ VACT: "Active verb",
+ VSTA: "Stative verb",
+ VATT: "Attributive verb",
+ XVBM: "Pre-verb auxiliary, before negator “ไม่”",
+ XVAM: "Pre-verb auxiliary, after negator “ไม่”",
+ XVMM: "Pre-verb, before or after negator “ไม่”",
+ XVBB: "Pre-verb auxiliary, in imperative mood",
+ XVAE: "Post-verb auxiliary",
+ DDAN: "classifier in between",
+ DDAC: "in between",
+ DDBQ: "classifier or preceding quantitative expression",
+ DDAQ: "following quantitative expression",
+ DIAC: "classifier in between",
+ DIBQ: "classifier or preceding quantitative expression",
+ DIAQ: "following quantitative expression",
+ DCNM: "Determiner, cardinal number expression",
+ DONM: "Determiner, ordinal number expression",
+ ADVN: "Adverb with normal form",
+ ADVI: "Adverb with iterative form",
+ ADVP: "Adverb with prefixed form",
+ ADVS: "Sentential adverb",
+ CNIT: "Unit classifier",
+ CLTV: "Collective classifier",
+ CMTR: "Measurement classifier",
+ CFQC: "Frequency classifier",
+ CVBL: "Verbal classifier",
+ JCRG: "Coordinating conjunction",
+ JCMP: "Comparative conjunction",
+ JSBR: "Subordinating conjunction",
+ RPRE: "Preposition",
+ INT: "Interjection",
+ FIXN: "Nominal prefix",
+ FIXV: "Adverbial prefix",
+ EAFF: "Ending for affirmative sentence",
+ EITT: "Ending for interrogative sentence",
+ NEG: "Negator",
+ PUNC: "Punctuation",
+};
diff --git a/packages/prosody-ui/src/zoom/FullText.tsx b/packages/prosody-ui/src/zoom/FullText.tsx
new file mode 100644
index 0000000..ec85f09
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/FullText.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { motion, AnimatePresence } from "motion/react";
+import Paragraph from "./Paragraph";
+import { useZoom } from "./hooks/useZoom";
+import { containerVariants, buttonVariants } from "./animations";
+import { NLP } from "sortug-ai";
+
+interface TextFocusMorphProps {
+ text: string;
+ doc: NLP.Spacy.SpacyRes;
+}
+
+const FullText: React.FC<TextFocusMorphProps> = ({ text, doc }) => {
+ const { viewState, navigateBack, handleElementClick } = useZoom();
+ const { level } = viewState;
+
+ // Split text into paragraphs
+ const paragraphs = text
+ .split("\n\n")
+ .map((p) => p.trim())
+ .filter(Boolean);
+
+ return (
+ <div className="text-focus-morph">
+ {level !== "text" && (
+ <AnimatePresence>
+ <motion.button
+ className="back-button"
+ onClick={navigateBack}
+ variants={buttonVariants}
+ initial="initial"
+ animate="animate"
+ exit="exit"
+ >
+ ← Back
+ </motion.button>
+ </AnimatePresence>
+ )}
+
+ <motion.div
+ className="content-container"
+ variants={containerVariants}
+ initial="text"
+ animate={level}
+ >
+ {paragraphs.map((paragraph, idx) => (
+ <Paragraph
+ doc={doc}
+ key={paragraph + idx}
+ rawText={paragraph}
+ context={{ idx, parentText: text, segmented: paragraphs }}
+ idx={idx}
+ />
+ ))}
+ </motion.div>
+ </div>
+ );
+};
+
+export default FullText;
diff --git a/packages/prosody-ui/src/zoom/Paragraph.tsx b/packages/prosody-ui/src/zoom/Paragraph.tsx
new file mode 100644
index 0000000..c26f806
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/Paragraph.tsx
@@ -0,0 +1,60 @@
+import React, { memo, useCallback, useEffect, useState } from "react";
+import { motion } from "motion/react";
+import type { ViewProps, LoadingStatus } from "./logic/types";
+import { NLP } from "sortug-ai";
+import Sentence from "./Sentence";
+import { paragraphVariants, createHoverEffect } from "./animations";
+import { useZoom } from "./hooks/useZoom";
+
+function Paragraph({ rawText, context, idx, doc }: ViewProps) {
+ const { viewState, handleElementClick } = useZoom();
+ const { level, pIndex } = viewState;
+ const selected = pIndex === idx;
+ const isFocused = level === "paragraph" && selected;
+
+ // State for sentences
+ const [loading, setLoading] = useState<LoadingStatus>("pending");
+
+ return (
+ <>
+ <motion.div
+ key={idx + rawText}
+ className={`paragraph-wrapper ${selected ? "selected" : ""}`}
+ custom={selected}
+ variants={paragraphVariants}
+ initial="text"
+ animate={level}
+ onClick={(e) => handleElementClick(e, idx)}
+ whileHover={
+ level === "text"
+ ? createHoverEffect(level, "text", "255, 255, 200")
+ : {}
+ }
+ >
+ {loading === "loading" && <div className="spinner" />}
+ {level === "text" || !selected || doc.segs.length === 0 ? (
+ <p className="paragraph">{rawText}</p>
+ ) : (
+ <div className="sentences-container">
+ {doc.segs.map((sentence, sentIdx) => (
+ <Sentence
+ key={sentence.text + sentIdx}
+ idx={sentIdx}
+ rawText={sentence.text}
+ spacy={sentence}
+ context={{
+ idx: sentIdx,
+ parentText: rawText,
+ segmented: doc.segs.map((s) => s.text),
+ }}
+ doc={doc}
+ />
+ ))}
+ </div>
+ )}
+ </motion.div>
+ </>
+ );
+}
+
+export default memo(Paragraph);
diff --git a/packages/prosody-ui/src/zoom/Sentence.tsx b/packages/prosody-ui/src/zoom/Sentence.tsx
new file mode 100644
index 0000000..1d90346
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/Sentence.tsx
@@ -0,0 +1,46 @@
+import React, { memo } from "react";
+import { motion } from "motion/react";
+import type { ViewProps, LoadingStatus } from "./logic/types";
+import { NLP } from "sortug-ai";
+import SpacyClause from "./SpacyClause";
+import { sentenceVariants, createHoverEffect } from "./animations";
+import { useZoom } from "./hooks/useZoom";
+
+interface Props extends ViewProps {
+ spacy: NLP.Spacy.Sentence;
+ stanza?: NLP.Stanza.Sentence;
+}
+
+function Sentence({ spacy, stanza, context, idx }: Props) {
+ const { viewState, handleElementClick } = useZoom();
+ const { level, sIndex } = viewState;
+ const selected = sIndex === idx;
+ const isFocused = level === "sentence" && selected;
+
+ return (
+ <>
+ <motion.span
+ key={idx + spacy.text}
+ className={`sentence-wrapper ${selected ? "selected" : ""}`}
+ custom={selected}
+ variants={sentenceVariants}
+ initial="paragraph"
+ animate={level}
+ onClick={(e) => handleElementClick(e, idx)}
+ whileHover={
+ level === "paragraph"
+ ? createHoverEffect(level, "paragraph", "200, 220, 255")
+ : {}
+ }
+ >
+ {level === "paragraph" || !selected ? (
+ <span className="sentence">{spacy.text}</span>
+ ) : (
+ <SpacyClause sentence={spacy} />
+ )}
+ </motion.span>
+ </>
+ );
+}
+
+export default memo(Sentence);
diff --git a/packages/prosody-ui/src/zoom/SpacyClause.tsx b/packages/prosody-ui/src/zoom/SpacyClause.tsx
new file mode 100644
index 0000000..6b6f178
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/SpacyClause.tsx
@@ -0,0 +1,125 @@
+import React, { memo, useState } from "react";
+import { motion } from "motion/react";
+import "./spacy.css";
+import { NLP } from "sortug-ai";
+// import { clauseVariants, createHoverEffect } from "./animations";
+// import { useZoom } from "./hooks/useZoom";
+
+function Grammar({ sentence }: { sentence: NLP.Spacy.Sentence }) {
+ const [hoveredClause, setHoveredClause] = useState<number | null>(null);
+
+ // Ref to manage the timeout for debouncing mouse leave
+ return (
+ <div className="clause-container">
+ {sentence.words.map((w, idx) => {
+ const isRoot =
+ w.ancestors.length === 0 || w.dep.toLowerCase() === "root";
+ const isSubj = NLP.Spacy.isChild(w, sentence.subj.id);
+ const isPred = !isSubj && !isRoot;
+ const predClass = isPred ? "pred" : "";
+ const relClass = isRoot ? "root" : `rel-${w.dep}`;
+ const ownClass = isRoot
+ ? ""
+ : isSubj
+ ? "subj"
+ : w.children.length === 0
+ ? ""
+ : `clause-${w.id}`;
+ const clase = w.ancestors.reduce((acc, item) => {
+ if (item === sentence.subj.id || item === sentence.root.id)
+ return acc;
+ else return `${acc} clause-${item}`;
+ }, ``);
+ const className = `suword ${relClass} ${ownClass} ${clase} ${predClass}`;
+ const isHovering =
+ !isRoot &&
+ !!hoveredClause &&
+ (w.id === hoveredClause || w.ancestors.includes(hoveredClause));
+ function handleClick(w: NLP.Spacy.Word) {
+ console.log("show the whole clause and all that", w);
+ }
+ return (
+ <ClauseSpan
+ word={w}
+ key={w.id}
+ className={className}
+ hovering={isHovering}
+ setHovering={setHoveredClause}
+ onClick={handleClick}
+ />
+ );
+ })}
+ </div>
+ );
+}
+
+const spanVariants: any = {
+ initial: {
+ // Base style
+ backgroundColor: "rgba(0, 0, 0, 0)", // Transparent background initially
+ fontWeight: "normal",
+ scale: 1,
+ zIndex: 0, // Default stacking
+ position: "relative", // Needed for zIndex to work reliably
+ // Add other base styles if needed
+ },
+ hovered: {
+ // Style when this span's group is hovered
+ backgroundColor: "rgba(255, 255, 0, 0.5)", // Yellow highlight
+ scale: 1.05,
+ zIndex: 1, // Bring hovered spans slightly forward
+ boxShadow: "0px 2px 5px rgba(0,0,0,0.2)",
+ // Add other hover effects
+ },
+};
+
+// Define the transition
+const spanTransition = {
+ type: "spring",
+ stiffness: 500,
+ damping: 30,
+ // duration: 0.1 // Or use duration for non-spring types
+};
+
+function ClauseSpan({
+ word,
+ className,
+ hovering,
+ setHovering,
+ onClick,
+}: {
+ word: NLP.Spacy.Word;
+ className: string;
+ hovering: boolean;
+ setHovering: (n: number | null) => void;
+ onClick: (w: NLP.Spacy.Word) => void;
+}) {
+ function handleMouseOver() {
+ setHovering(word.id);
+ // if (word.children.length > 0) setHovering(word.id);
+ // else setHovering(word.head);
+ }
+ function handleMouseLeave() {
+ setHovering(null);
+ }
+ function handleClick(e: React.MouseEvent) {
+ e.stopPropagation();
+ onClick(word);
+ }
+ return (
+ <motion.span
+ className={className}
+ variants={spanVariants}
+ initial="initial"
+ animate={hovering ? "hovered" : "initial"}
+ transition={spanTransition}
+ onMouseOver={handleMouseOver}
+ onMouseLeave={handleMouseLeave}
+ onClick={handleClick}
+ >
+ {word.text}
+ </motion.span>
+ );
+}
+
+export default memo(Grammar);
diff --git a/packages/prosody-ui/src/zoom/animations.ts b/packages/prosody-ui/src/zoom/animations.ts
new file mode 100644
index 0000000..6135e7f
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/animations.ts
@@ -0,0 +1,199 @@
+import type { Variants } from "motion/react";
+
+// Base transition configurations for consistent animations
+const baseTransition = {
+ duration: 0.5,
+ ease: [0.43, 0.13, 0.23, 0.96], // Improved easing for smoother feel
+};
+
+export const fadeTransition = {
+ ...baseTransition,
+ duration: 0.3,
+};
+
+// Shared variants for different view levels
+export const containerVariants: Variants = {
+ text: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+ paragraph: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+ sentence: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+ clause: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+ word: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+ syllable: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+ phoneme: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.05,
+ delayChildren: 0.1,
+ },
+ },
+};
+
+// Function to create element variants based on selection state
+export const createElementVariants = (
+ currentLevel: string,
+ nextLevel: string,
+ prevLevel: string,
+ selectedOpacity = 1,
+ unselectedOpacity = 0.1,
+ selectedScale = 1.05,
+ unselectedScale = 0.95,
+ selectedBlur = "0px",
+ unselectedBlur = "2px",
+ bgColor = "rgba(255, 255, 255, 0)", // Use rgba with 0 opacity instead of transparent
+): Variants => {
+ return {
+ [prevLevel]: {
+ opacity: 1,
+ scale: 1,
+ filter: "blur(0px)",
+ backgroundColor: "rgba(255, 255, 255, 0)", // Use rgba with 0 opacity
+ transition: baseTransition,
+ },
+ [currentLevel]: (isSelected: boolean) => ({
+ opacity: isSelected ? selectedOpacity : unselectedOpacity,
+ scale: isSelected ? selectedScale : unselectedScale,
+ filter: isSelected ? `blur(${selectedBlur})` : `blur(${unselectedBlur})`,
+ backgroundColor: isSelected ? bgColor : "rgba(255, 255, 255, 0)", // Use rgba with 0 opacity
+ transition: baseTransition,
+ }),
+ [nextLevel]: (isSelected: boolean) => ({
+ opacity: isSelected ? selectedOpacity : unselectedOpacity / 2,
+ scale: isSelected ? selectedScale : unselectedScale * 0.95,
+ filter: isSelected
+ ? `blur(${selectedBlur})`
+ : `blur(${parseInt(unselectedBlur) + 1}px)`,
+ backgroundColor: isSelected ? bgColor : "rgba(255, 255, 255, 0)", // Use rgba with 0 opacity
+ transition: baseTransition,
+ }),
+ };
+};
+
+// Pre-configured variants for each level
+export const paragraphVariants = createElementVariants(
+ "paragraph",
+ "sentence",
+ "text",
+ 1,
+ 0.1,
+ 1.05,
+ 0.95,
+ "0px",
+ "2px",
+ "rgba(200, 220, 255, 0.1)",
+);
+
+export const sentenceVariants = createElementVariants(
+ "sentence",
+ "clause",
+ "paragraph",
+ 1,
+ 0.1,
+ 1.1,
+ 0.95,
+ "0px",
+ "2px",
+ "rgba(200, 220, 255, 0.2)",
+);
+
+export const clauseVariants = createElementVariants(
+ "clause",
+ "word",
+ "sentence",
+ 1,
+ 0.1,
+ 1.1,
+ 0.95,
+ "0px",
+ "2px",
+ "rgba(220, 200, 255, 0.2)",
+);
+
+export const wordVariants = createElementVariants(
+ "word",
+ "syllable",
+ "clause",
+ 1,
+ 0.1,
+ 1.15,
+ 0.9,
+ "0px",
+ "2px",
+ "rgba(255, 200, 200, 0.2)",
+);
+
+export const syllableVariants = createElementVariants(
+ "syllable",
+ "phoneme",
+ "word",
+ 1,
+ 0.1,
+ 1.2,
+ 0.9,
+ "0px",
+ "2px",
+ "rgba(200, 255, 200, 0.2)",
+);
+
+// Button animations
+export const buttonVariants: Variants = {
+ initial: { opacity: 0, x: -20 },
+ animate: { opacity: 1, x: 0, transition: fadeTransition },
+ exit: { opacity: 0, x: -20, transition: fadeTransition },
+};
+
+// Hover effects
+export const createHoverEffect = (
+ level: string,
+ currentLevel: string,
+ color: string,
+) => {
+ if (level === currentLevel) {
+ return {
+ scale: 1.02,
+ backgroundColor: `rgba(${color}, 0.3)`,
+ transition: { duration: 0.2 },
+ };
+ }
+ return {
+ // Return empty animation with same properties to avoid errors
+ scale: 1,
+ backgroundColor: "rgba(255, 255, 255, 0)",
+ transition: { duration: 0.2 },
+ };
+};
diff --git a/packages/prosody-ui/src/zoom/hooks/useZoom.tsx b/packages/prosody-ui/src/zoom/hooks/useZoom.tsx
new file mode 100644
index 0000000..733ca06
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/hooks/useZoom.tsx
@@ -0,0 +1,135 @@
+import React, {
+ createContext,
+ useState,
+ useContext,
+ type ReactNode,
+} from "react";
+import type { ViewLevel, ViewState } from "../logic/types";
+
+// Type definitions for the context
+interface ZoomContextType {
+ viewState: ViewState;
+ setLevel: (level: ViewLevel) => void;
+ setParagraphIndex: (idx: number | null) => void;
+ setSentenceIndex: (idx: number | null) => void;
+ setClauseIndex: (idx: number | null) => void;
+ setWordIndex: (idx: number | null) => void;
+ setSyllableIndex: (idx: number | null) => void;
+ setPhonemeIndex: (idx: number | null) => void;
+ navigateBack: () => void;
+ handleElementClick: (e: React.MouseEvent, idx: number) => void;
+}
+
+// Create the context with default empty values
+const ZoomContext = createContext<ZoomContextType>({
+ viewState: {
+ level: "text",
+ pIndex: null,
+ sIndex: null,
+ cIndex: null,
+ wIndex: null,
+ yIndex: null,
+ fIndex: null,
+ },
+ setLevel: () => {},
+ setParagraphIndex: () => {},
+ setSentenceIndex: () => {},
+ setClauseIndex: () => {},
+ setWordIndex: () => {},
+ setSyllableIndex: () => {},
+ setPhonemeIndex: () => {},
+ navigateBack: () => {},
+ handleElementClick: () => {},
+});
+
+// Provider component
+export const ZoomProvider: React.FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ const [viewState, setViewState] = useState<ViewState>({
+ level: "text",
+ pIndex: null,
+ sIndex: null,
+ cIndex: null,
+ wIndex: null,
+ yIndex: null,
+ fIndex: null,
+ });
+
+ // Helper functions to update individual parts of the state
+ const setLevel = (level: ViewLevel) =>
+ setViewState((prev) => ({ ...prev, level }));
+ const setParagraphIndex = (pIndex: number | null) =>
+ setViewState((prev) => ({ ...prev, pIndex }));
+ const setSentenceIndex = (sIndex: number | null) =>
+ setViewState((prev) => ({ ...prev, sIndex }));
+ const setClauseIndex = (cIndex: number | null) =>
+ setViewState((prev) => ({ ...prev, cIndex }));
+ const setWordIndex = (wIndex: number | null) =>
+ setViewState((prev) => ({ ...prev, wIndex }));
+ const setSyllableIndex = (yIndex: number | null) =>
+ setViewState((prev) => ({ ...prev, yIndex }));
+ const setPhonemeIndex = (fIndex: number | null) =>
+ setViewState((prev) => ({ ...prev, fIndex }));
+
+ // Handle navigation levels
+ const navigateBack = () => {
+ const { level } = viewState;
+
+ if (level === "paragraph") {
+ setViewState((prev) => ({ ...prev, level: "text", pIndex: null }));
+ } else if (level === "sentence") {
+ setViewState((prev) => ({ ...prev, level: "paragraph", sIndex: null }));
+ } else if (level === "clause") {
+ setViewState((prev) => ({ ...prev, level: "sentence", cIndex: null }));
+ } else if (level === "word") {
+ setViewState((prev) => ({ ...prev, level: "clause", wIndex: null }));
+ } else if (level === "syllable") {
+ setViewState((prev) => ({ ...prev, level: "word", yIndex: null }));
+ } else if (level === "phoneme") {
+ setViewState((prev) => ({ ...prev, level: "syllable", fIndex: null }));
+ }
+ };
+
+ // Handle clicks on elements to navigate forward
+ const handleElementClick = (e: React.MouseEvent, idx: number) => {
+ e.stopPropagation();
+ const { level } = viewState;
+
+ if (level === "text") {
+ setViewState((prev) => ({ ...prev, level: "paragraph", pIndex: idx }));
+ } else if (level === "paragraph") {
+ setViewState((prev) => ({ ...prev, level: "sentence", sIndex: idx }));
+ } else if (level === "sentence") {
+ setViewState((prev) => ({ ...prev, level: "clause", cIndex: idx }));
+ } else if (level === "clause") {
+ setViewState((prev) => ({ ...prev, level: "word", wIndex: idx }));
+ } else if (level === "word") {
+ setViewState((prev) => ({ ...prev, level: "syllable", yIndex: idx }));
+ } else if (level === "syllable") {
+ setViewState((prev) => ({ ...prev, level: "phoneme", fIndex: idx }));
+ }
+ };
+
+ return (
+ <ZoomContext.Provider
+ value={{
+ viewState,
+ setLevel,
+ setParagraphIndex,
+ setSentenceIndex,
+ setClauseIndex,
+ setWordIndex,
+ setSyllableIndex,
+ setPhonemeIndex,
+ navigateBack,
+ handleElementClick,
+ }}
+ >
+ {children}
+ </ZoomContext.Provider>
+ );
+};
+
+// Custom hook to use the zoom context
+export const useZoom = () => useContext(ZoomContext);
diff --git a/packages/prosody-ui/src/zoom/index.ts b/packages/prosody-ui/src/zoom/index.ts
new file mode 100644
index 0000000..baf5db1
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/index.ts
@@ -0,0 +1,8 @@
+export { ZoomProvider, useZoom } from "./hooks/useZoom";
+import Paragraph from "./Paragraph";
+import FullText from "./FullText";
+import Sentence from "./Paragraph";
+import SpacyClause from "./SpacyClause";
+import type * as Types from "./logic/types";
+
+export { Paragraph, FullText, Sentence, SpacyClause, Types };
diff --git a/packages/prosody-ui/src/zoom/logic/types.ts b/packages/prosody-ui/src/zoom/logic/types.ts
new file mode 100644
index 0000000..bea68ff
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/logic/types.ts
@@ -0,0 +1,53 @@
+import type { NLP } from "sortug-ai";
+
+export type ViewLevel =
+ | "text"
+ | "paragraph"
+ | "sentence"
+ | "clause"
+ | "word"
+ | "syllable"
+ | "phoneme";
+export interface ViewState {
+ level: ViewLevel;
+ pIndex: number | null;
+ sIndex: number | null;
+ cIndex: number | null;
+ wIndex: number | null;
+ yIndex: number | null;
+ fIndex: number | null;
+}
+
+export interface ViewProps {
+ idx: number;
+ rawText: string;
+ context: Context;
+ doc: NLP.Spacy.SpacyRes;
+}
+export type Context = {
+ parentText: string;
+ segmented: string[];
+ idx: number;
+};
+
+export type WordData = {
+ confidence: number;
+ frequency: number | null;
+ id: number;
+ ipa: Array<{ ipa: string; tags: string[] }>;
+ spelling: string;
+ type: ExpressionType;
+ syllables: number;
+ lang: string;
+ prosody: any;
+ senses: Sense[];
+};
+export type ExpressionType = "word" | "expression" | "syllable";
+export type Sense = {
+ etymology: string;
+ pos: string;
+ forms: Array<{ form: string; tags: string[] }>;
+ related: any;
+ senses: Array<{ glosses: string[]; links: Array<[string, string]> }>;
+};
+export type LoadingStatus = "pending" | "loading" | "success" | "error";
diff --git a/packages/prosody-ui/src/zoom/spacy.css b/packages/prosody-ui/src/zoom/spacy.css
new file mode 100644
index 0000000..0077119
--- /dev/null
+++ b/packages/prosody-ui/src/zoom/spacy.css
@@ -0,0 +1,39 @@
+.suword {
+ margin-left: 0.5ch;
+ margin-right: 0.5ch;
+}
+
+/* .suword.pred { */
+/* color: gold; */
+/* } */
+
+/* Clause level */
+.clause-container {
+ max-width: 600px;
+ white-space: normal !important;
+ hyphens: auto;
+
+ padding: 2px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ will-change: transform, opacity, filter, background-color;
+
+ span {
+ white-space: normal !important;
+ }
+}
+
+.clause-container.selected {
+ background-color: rgba(220, 200, 255, 0.2);
+ z-index: 3;
+}
+
+.suword.subj {
+ color: blue;
+ /* border-bottom: 2px solid blue; */
+}
+
+.suword.root {
+ color: darkred;
+} \ No newline at end of file
diff --git a/packages/prosody-ui/tsconfig.json b/packages/prosody-ui/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/packages/prosody-ui/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}