summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bun.lock (renamed from packages/tweetdeck/bun.lock)144
-rw-r--r--packages/ai/bun.lock361
-rw-r--r--packages/ai/src/nlp/index.ts1
-rw-r--r--packages/ai/src/nlp/spacy.ts2
-rw-r--r--packages/ai/src/nlp/stanza.ts2
-rw-r--r--packages/db/bun.lock25
-rw-r--r--packages/db/index.ts1
-rw-r--r--packages/db/package.json4
-rw-r--r--packages/db/src/index.ts327
-rw-r--r--packages/db/src/schema.sql97
-rw-r--r--packages/db/src/server.ts66
-rw-r--r--packages/lang/CLAUDE.md106
-rw-r--r--packages/lang/README.md23
-rw-r--r--packages/lang/bun.lock64
-rw-r--r--packages/lang/src/index.ts3
-rw-r--r--packages/lang/test.ts24
-rw-r--r--packages/langlib/.gitignore (renamed from packages/lang/.gitignore)0
-rw-r--r--packages/langlib/README.md15
-rw-r--r--packages/langlib/index.ts6
-rw-r--r--packages/langlib/package.json (renamed from packages/lang/package.json)2
-rw-r--r--packages/langlib/package.worked11
-rw-r--r--packages/langlib/src/dbtypes.ts69
-rw-r--r--packages/langlib/src/iso/index.ts (renamed from packages/lang/src/iso/index.ts)0
-rw-r--r--packages/langlib/src/iso/iso15924.ts (renamed from packages/lang/src/iso/iso15924.ts)0
-rw-r--r--packages/langlib/src/iso/iso6393-to-1.js (renamed from packages/lang/src/iso/iso6393-to-1.js)0
-rw-r--r--packages/langlib/src/iso/iso6393-to-2b.js (renamed from packages/lang/src/iso/iso6393-to-2b.js)0
-rw-r--r--packages/langlib/src/iso/iso6393-to-2t.js (renamed from packages/lang/src/iso/iso6393-to-2t.js)0
-rw-r--r--packages/langlib/src/iso/iso6393.js (renamed from packages/lang/src/iso/iso6393.js)0
-rw-r--r--packages/langlib/src/lang/index.ts (renamed from packages/lang/src/lang/index.ts)0
-rw-r--r--packages/langlib/src/types.ts (renamed from packages/lang/src/types.ts)2
-rw-r--r--packages/langlib/src/unicode/index.ts (renamed from packages/lang/src/unicode/index.ts)45
-rw-r--r--packages/langlib/tsconfig.json (renamed from packages/lang/tsconfig.json)3
-rw-r--r--packages/prosody-ui/bun.lock318
-rw-r--r--packages/prosody-ui/index.ts3
-rw-r--r--packages/prosody-ui/package.json9
-rw-r--r--packages/prosody-ui/src/LangText.tsx80
-rw-r--r--packages/prosody-ui/src/Paragraph.tsx2
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan-2.ttf (renamed from packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf)bin5359608 -> 5359608 bytes
-rw-r--r--packages/prosody-ui/src/components/Colors.tsx198
-rw-r--r--packages/prosody-ui/src/components/Sentence.tsx173
-rw-r--r--packages/prosody-ui/src/components/word/FullWordData.tsx156
-rw-r--r--packages/prosody-ui/src/components/word/Phonetic.tsx92
-rw-r--r--packages/prosody-ui/src/components/word/Semantic.tsx184
-rw-r--r--packages/prosody-ui/src/fonts/FontChanger.tsx90
-rw-r--r--packages/prosody-ui/src/fonts/Jpan.tsx14
-rw-r--r--packages/prosody-ui/src/fonts/Latn.tsx14
-rw-r--r--packages/prosody-ui/src/fonts/Thai.tsx6
-rw-r--r--packages/prosody-ui/src/fonts/useLangFont.tsx2
-rw-r--r--packages/prosody-ui/src/latin/LatinText.tsx4
-rw-r--r--packages/prosody-ui/src/logic/stanza.ts2
-rw-r--r--packages/prosody-ui/src/logic/types.ts3
-rw-r--r--packages/prosody-ui/src/logic/utils.ts2
-rw-r--r--packages/prosody-ui/src/logic/wiki.ts2
-rw-r--r--packages/prosody-ui/src/styles/styles.css37
-rw-r--r--packages/prosody-ui/src/thai/ThaiText.tsx38
-rw-r--r--packages/prosody-ui/src/thai/logic/thainlp.ts10
-rw-r--r--packages/prosody-ui/src/themes/ThemeSwitcher.tsx130
-rw-r--r--packages/prosody-ui/src/themes/themes.ts321
-rw-r--r--packages/prosody-ui/src/zoom/FullText.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/Paragraph.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/Sentence.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/SpacyClause.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/logic/types.ts2
-rw-r--r--packages/prosody-ui/tsconfig.json5
-rw-r--r--packages/sortug/bun.lock25
-rw-r--r--packages/sortug/index.ts2
-rw-r--r--packages/sortug/src/styles.css (renamed from packages/prosody-ui/src/sortug.css)5
-rw-r--r--packages/sortug/src/utils.ts50
-rw-r--r--packages/tweetdeck/package.json8
-rw-r--r--packages/tweetdeck/src/App.tsx311
-rw-r--r--packages/tweetdeck/src/Test.tsx19
-rw-r--r--packages/tweetdeck/src/components/TweetCard.tsx2
-rw-r--r--packages/tweetdeck/src/index.ts3
-rw-r--r--packages/tweetdeck/src/pages/Deck.tsx310
74 files changed, 2379 insertions, 1664 deletions
diff --git a/packages/tweetdeck/bun.lock b/bun.lock
index a51402d..2a8f986 100644
--- a/packages/tweetdeck/bun.lock
+++ b/bun.lock
@@ -2,16 +2,104 @@
"lockfileVersion": 1,
"workspaces": {
"": {
+ "name": "@sortug/sorlang",
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ "packages/ai": {
+ "name": "@sortug/ai",
+ "version": "0.1.0",
+ "dependencies": {
+ "@anthropic-ai/sdk": "latest",
+ "@elevenlabs/elevenlabs-js": "^2.24.1",
+ "@google/genai": "latest",
+ "@sortug/langlib": "workspace:*",
+ "@sortug/lib": "workspace:*",
+ "groq-sdk": "latest",
+ "openai": "latest",
+ "playht": "latest",
+ "replicate": "latest",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ "@types/mime-types": "^3.0.1",
+ },
+ "peerDependencies": {
+ "typescript": "latest",
+ },
+ },
+ "packages/db": {
+ "name": "@sortug/sorlang-db",
+ "dependencies": {
+ "@sortug/langlib": "workspace:*",
+ "@sortug/lib": "workspace:*",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ "packages/langlib": {
+ "name": "@sortug/langlib",
+ "dependencies": {
+ "franc-all": "^7.2.0",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ "packages/prosody-ui": {
+ "name": "@sortug/prosody-ui",
+ "version": "0.1.0",
+ "dependencies": {
+ "@sortug/ai": "workspace:*",
+ "@sortug/langlib": "workspace:*",
+ "@sortug/lib": "workspace:*",
+ "@tabler/icons-react": "^3.35.0",
+ "franc-all": "^7.2.0",
+ "motion": "^12.11.3",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.2",
+ "@types/react": "^19.2.6",
+ },
+ "peerDependencies": {
+ "react": ">=19.0.0",
+ "typescript": "^5.0.0",
+ },
+ },
+ "packages/sortug": {
+ "name": "@sortug/lib",
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ "packages/tweetdeck": {
"name": "bun-react-tweetdeck",
+ "version": "0.1.0",
"dependencies": {
+ "@sortug/ai": "workspace:*",
+ "@sortug/lib": "workspace:*",
+ "@sortug/prosody-ui": "workspace:*",
+ "@sortug/sorlang-db": "workspace:*",
"bun_python": "^0.1.10",
"lucide-react": "latest",
"node-html-parser": "^7.0.1",
- "prosody-ui": "file:../../libs/prosody-ui",
"react": "^19",
"react-dom": "^19",
- "sortug": "file:../../libs/sortug",
- "sortug-ai": "file:../../libs/models",
+ "react-hot-toast": "^2.6.0",
},
"devDependencies": {
"@types/bun": "latest",
@@ -25,7 +113,7 @@
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
- "@elevenlabs/elevenlabs-js": ["@elevenlabs/elevenlabs-js@2.24.1", "", { "dependencies": { "command-exists": "^1.2.9", "node-fetch": "^2.7.0", "ws": "^8.18.3" } }, "sha512-i6bDExgK9lYne1vLhy85JJ3O8bNi5vPTfcgq8kT3HG4+3rgkUJtg5UP29Mn1KONc4ZOeYUomzxJ820uLkT9z6g=="],
+ "@elevenlabs/elevenlabs-js": ["@elevenlabs/elevenlabs-js@2.25.0", "", { "dependencies": { "command-exists": "^1.2.9", "node-fetch": "^2.7.0", "ws": "^8.18.3" } }, "sha512-ySSGvjk1YwyywD7g6YuRY4L/KFLr2tYraaIw6Jlj1w+H4mqePo+Jzq2rsBcI5i71epzrw/51qXuAFp9qobZbuQ=="],
"@google/genai": ["@google/genai@1.30.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.20.1" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w=="],
@@ -59,9 +147,23 @@
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
+ "@sortug/ai": ["@sortug/ai@workspace:packages/ai"],
+
+ "@sortug/langlib": ["@sortug/langlib@workspace:packages/langlib"],
+
+ "@sortug/lib": ["@sortug/lib@workspace:packages/sortug"],
+
+ "@sortug/prosody-ui": ["@sortug/prosody-ui@workspace:packages/prosody-ui"],
+
+ "@sortug/sorlang-db": ["@sortug/sorlang-db@workspace:packages/db"],
+
+ "@tabler/icons": ["@tabler/icons@3.35.0", "", {}, "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ=="],
+
+ "@tabler/icons-react": ["@tabler/icons-react@3.35.0", "", { "dependencies": { "@tabler/icons": "3.35.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g=="],
+
"@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/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
"@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="],
@@ -91,8 +193,6 @@
"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.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@@ -103,7 +203,9 @@
"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=="],
+ "bun-react-tweetdeck": ["bun-react-tweetdeck@workspace:packages/tweetdeck"],
+
+ "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"bun_python": ["bun_python@0.1.10", "", { "peerDependencies": { "typescript": "^5.7.3" } }, "sha512-6c5owYOI7lYI7lBbYX99L6SQ5dT4jsabsV8yKDX15zi3cRurl/nWO576L3KbSTpGUR8wqQ8TDGRS7Wqwg9gunQ=="],
@@ -207,6 +309,8 @@
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+ "goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
+
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
@@ -231,12 +335,6 @@
"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=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -301,8 +399,6 @@
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
- "prosody-ui": ["prosody-ui@file:../../libs/prosody-ui", { "dependencies": { "franc-all": "^7.2.0", "glotscript": "file:../glotscript", "motion": "^12.11.3", "sortug": "file:../sortug", "sortug-ai": "file:../models" }, "peerDependencies": { "react": ">=19.0.0", "react-dom": ">=19.0.0", "typescript": "^5.0.0" } }],
-
"protobufjs": ["protobufjs@7.5.4", "", { "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-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
@@ -311,6 +407,8 @@
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+ "react-hot-toast": ["react-hot-toast@2.6.0", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg=="],
+
"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=="],
@@ -331,10 +429,6 @@
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
- "sortug": ["@sortug/lib@file:../../libs/sortug", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" } }],
-
- "sortug-ai": ["@sortug/ai@file:../../libs/models", { "dependencies": { "@anthropic-ai/sdk": "latest", "@elevenlabs/elevenlabs-js": "^2.24.1", "@google/genai": "latest", "bcp-47": "^2.1.0", "franc-all": "^7.2.0", "groq-sdk": "latest", "iso-639-3": "file:../lang", "openai": "latest", "playht": "latest", "replicate": "latest", "sortug": "file:../:sortug" }, "devDependencies": { "@types/bun": "latest", "@types/mime-types": "^3.0.1" }, "peerDependencies": { "typescript": "latest" } }],
-
"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-width-cjs": ["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=="],
@@ -391,16 +485,6 @@
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
- "prosody-ui/glotscript": ["glotscript@file:../../libs/glotscript", {}],
-
- "prosody-ui/sortug": ["sortug@file:../../libs/sortug", {}],
-
- "prosody-ui/sortug-ai": ["sortug-ai@file:../../libs/models", {}],
-
- "sortug-ai/iso-639-3": ["iso-639-3@file:../../libs/lang", {}],
-
- "sortug-ai/sortug": ["sortug@file:../../libs/:sortug", {}],
-
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
diff --git a/packages/ai/bun.lock b/packages/ai/bun.lock
deleted file mode 100644
index 8cdbaaf..0000000
--- a/packages/ai/bun.lock
+++ /dev/null
@@ -1,361 +0,0 @@
-{
- "lockfileVersion": 1,
- "workspaces": {
- "": {
- "name": "@sortug/ai",
- "dependencies": {
- "@anthropic-ai/sdk": "latest",
- "@elevenlabs/elevenlabs-js": "^2.24.1",
- "@google/genai": "latest",
- "bcp-47": "^2.1.0",
- "franc-all": "^7.2.0",
- "groq-sdk": "latest",
- "iso-639-3": "file:../lang",
- "openai": "latest",
- "playht": "latest",
- "replicate": "latest",
- "sortug": "file:../sortug",
- },
- "devDependencies": {
- "@types/bun": "latest",
- "@types/mime-types": "^3.0.1",
- },
- "peerDependencies": {
- "typescript": "latest",
- },
- },
- },
- "packages": {
- "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.70.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-AGEhifuvE22VxfQ5ROxViTgM8NuVQzEvqcN8bttR4AP24ythmNE/cL/SrOz79xiv7/osrsmCyErjsistJi7Z8A=="],
-
- "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
-
- "@elevenlabs/elevenlabs-js": ["@elevenlabs/elevenlabs-js@2.24.1", "", { "dependencies": { "command-exists": "^1.2.9", "node-fetch": "^2.7.0", "ws": "^8.18.3" } }, "sha512-i6bDExgK9lYne1vLhy85JJ3O8bNi5vPTfcgq8kT3HG4+3rgkUJtg5UP29Mn1KONc4ZOeYUomzxJ820uLkT9z6g=="],
-
- "@google/genai": ["@google/genai@1.30.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.20.1" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w=="],
-
- "@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="],
-
- "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
-
- "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
-
- "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
-
- "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
-
- "@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/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="],
-
- "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
-
- "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
-
- "@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.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
-
- "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.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
-
- "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
- "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.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
-
- "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
-
- "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=="],
-
- "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="],
-
- "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="],
-
- "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
-
- "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
-
- "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
-
- "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
- "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
-
- "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=="],
-
- "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
-
- "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=="],
-
- "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
-
- "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.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
-
- "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
-
- "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
-
- "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=="],
-
- "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
-
- "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@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
-
- "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
-
- "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=="],
-
- "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
-
- "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
-
- "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
-
- "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
-
- "groq-sdk": ["groq-sdk@0.36.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-wvxl7i6QWxLcIfM00mQQybYk15OAXJG0NBBQuMDHrQ2vi68uz2RqFTBKUNfEOVz8Lwy4eAgQIPBEFW5P3cXybA=="],
-
- "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
-
- "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=="],
-
- "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
- "iso-639-3": ["@sortug/lang@file:../lang", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" } }],
-
- "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-
- "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
-
- "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
-
- "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=="],
-
- "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
- "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=="],
-
- "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
- "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
-
- "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@6.9.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg=="],
-
- "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
-
- "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
-
- "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
- "peek-readable": ["peek-readable@5.4.2", "", {}, "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg=="],
-
- "playht": ["playht@0.21.0", "", { "dependencies": { "@grpc/grpc-js": "^1.9.4", "axios": "^1.4.0", "cross-fetch": "^4.0.0", "deepmerge-ts": "^7.1.5", "file-type": "^18.5.0", "protobufjs": "^7.2.5", "tslib": "^2.1.0" } }, "sha512-63dWfsoGNOxfl91U3knrON4HcgtdPZ+e0Q3F8JX22T6dvX17i217lfw8cq1OzIBWVxpHms8ebhgUU/Gvs0/8Eg=="],
-
- "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
-
- "protobufjs": ["protobufjs@7.5.4", "", { "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-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
-
- "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
-
- "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.4.0", "", { "optionalDependencies": { "readable-stream": ">=4.0.0" } }, "sha512-1ufKejfUVz/azy+5TnzQP7U1+MHVWZ6psnQ06az8byUUnRhT+DZ/MvewzB1NQYBVMgNKR7xPDtTwlcP5nv/5+w=="],
-
- "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
-
- "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
-
- "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
-
- "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
-
- "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
-
- "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
- "sortug": ["@sortug/lib@file:../sortug", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" } }],
-
- "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-width-cjs": ["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=="],
-
- "strip-ansi-cjs": ["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=="],
-
- "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
-
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
- "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
-
- "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=="],
-
- "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
- "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=="],
-
- "wrap-ansi-cjs": ["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.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
-
- "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=="],
-
- "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
-
- "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
- "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
-
- "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
-
- "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
-
- "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
-
- "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
- }
-}
diff --git a/packages/ai/src/nlp/index.ts b/packages/ai/src/nlp/index.ts
index ebed586..0b71b69 100644
--- a/packages/ai/src/nlp/index.ts
+++ b/packages/ai/src/nlp/index.ts
@@ -1,6 +1,5 @@
import * as Spacy from "./spacy";
import * as Stanza from "./stanza";
-import * as ISO from "./iso";
import { ocr } from "./ocr";
import type * as Types from "./types";
export * from "./nlp";
diff --git a/packages/ai/src/nlp/spacy.ts b/packages/ai/src/nlp/spacy.ts
index 829c77e..cdaff9e 100644
--- a/packages/ai/src/nlp/spacy.ts
+++ b/packages/ai/src/nlp/spacy.ts
@@ -1,5 +1,5 @@
import type { AsyncRes, Result } from "@sortug/lib";
-import { detectLang } from "./iso";
+import { detectLang } from "@sortug/langlib";
const ENDPOINT = "http://localhost:8102";
export async function run(text: string, langg?: string): AsyncRes<SpacyRes> {
diff --git a/packages/ai/src/nlp/stanza.ts b/packages/ai/src/nlp/stanza.ts
index 90fa1fc..a2892bf 100644
--- a/packages/ai/src/nlp/stanza.ts
+++ b/packages/ai/src/nlp/stanza.ts
@@ -1,5 +1,5 @@
import type { AsyncRes, Result } from "@sortug/lib";
-import { detectLang } from "./iso";
+import { detectLang } from "@sortug/langlib";
const ENDPOINT = "http://localhost:8102";
export async function segmenter(
diff --git a/packages/db/bun.lock b/packages/db/bun.lock
deleted file mode 100644
index cedc049..0000000
--- a/packages/db/bun.lock
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "lockfileVersion": 1,
- "workspaces": {
- "": {
- "name": "@sortug/sorlang-db",
- "devDependencies": {
- "@types/bun": "latest",
- },
- "peerDependencies": {
- "typescript": "^5",
- },
- },
- },
- "packages": {
- "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
-
- "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
-
- "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
-
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
- "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
- }
-}
diff --git a/packages/db/index.ts b/packages/db/index.ts
index ceecd52..c5660a0 100644
--- a/packages/db/index.ts
+++ b/packages/db/index.ts
@@ -1,3 +1,4 @@
import DB from "./src";
export default DB;
+export { handler } from "./src/server";
diff --git a/packages/db/package.json b/packages/db/package.json
index a121b42..fc2c534 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -7,5 +7,9 @@
},
"peerDependencies": {
"typescript": "^5"
+ },
+ "dependencies": {
+ "@sortug/lib": "workspace:*",
+ "@sortug/langlib": "workspace:*"
}
}
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
index 6a89ee2..1425a43 100644
--- a/packages/db/src/index.ts
+++ b/packages/db/src/index.ts
@@ -1,39 +1,242 @@
-import { Database } from "bun:sqlite";
-import type { FullWordDataDB } from "./types";
+import { SQL } from "bun";
import { SRSQueries } from "./srs";
+// NEW API
+//
+// https://bun.com/docs/runtime/sql
+// import { sql } from "bun";
+
+// // Basic insert with direct values
+// const [user] = await sql`
+// INSERT INTO users (name, email)
+// VALUES (${name}, ${email})
+// RETURNING *
+// `;
+
+// // Using object helper for cleaner syntax
+// const userData = {
+// name: "Alice",
+// email: "alice@example.com",
+// };
+
+// const [newUser] = await sql`
+// INSERT INTO users ${sql(userData)}
+// RETURNING *
+// `;
+// // Expands to: INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')
+
+const DB_PATH = "/home/y/code/bun/sorlang/bulkdata/db.db";
+import type { FullWordDataDB } from "@sortug/langlib";
export class Queries {
- db: Database;
- srs: SRSQueries;
+ db: SQL;
+ ready = false;
+ // srs: SRSQueries;
constructor() {
- const db = new Database("/home/y/code/bun/sorlang/bulkdata/unified.db");
- db.exec("PRAGMA journal_mode = WAL"); // Enable Write-Ahead Logging for better performance
- db.exec("PRAGMA foreign_keys = ON");
- db.exec("PRAGMA cache_size = -8000"); // Increase cache size to 8MB
- db.exec("PRAGMA mmap_size = 30000000000");
- db.exec("PRAGMA temp_store = MEMORY"); // Store temp tables in memory
- db.exec("PRAGMA synchronous = NORMAL"); // Slightly less safe but faster
-
+ console.log("setting sorlang DB");
+ const db = new SQL({
+ adapter: "sqlite",
+ filename: DB_PATH,
+ create: false,
+ });
this.db = db;
- this.srs = new SRSQueries(db);
+ // this.srs = new SRSQueries(db);
+ }
+ async init() {
+ try {
+ await this.db`PRAGMA journal_mode = WAL`; // Enable Write-Ahead Logging for better performance
+ await this.db`PRAGMA foreign_keys = ON`;
+ await this.db`PRAGMA cache_size = -8000`; // Increase cache size to 8MB
+ await this.db`PRAGMA mmap_size = 30000000000`;
+ await this.db`PRAGMA temp_store = MEMORY`; // Store temp tables in memory
+ await this.db`PRAGMA synchronous = NORMAL`; // Slightly less safe but faster
+ this.ready = true;
+ } catch (e) {
+ console.error("error starting db", e);
+ }
+ }
+
+ async fetchExpressionById(id: number) {
+ const query = await this.db`
+ SELECT * FROM expressions WHERE id = ${id}
+ `;
+ return query;
+ }
+ // TODO word_phonetics is MANY TO ONE vs expressions, so it's possible for word_phonetics to lack entries for syllables
+ async fetchExpressionBySpelling(
+ spelling: string,
+ lang: string,
+ ): Promise<FullWordDataDB | null> {
+ const data = await this.db`
+SELECT
+ json_object(
+ 'id', e.id,
+ 'spelling', e.spelling,
+ 'frequency', e.frequency,
+ 'phonetic', (
+ SELECT json_object(
+ 'id', wp.id,
+ 'tone_sequence', wp.tone_sequence,
+ 'syl_seq', wp.syllable_sequence,
+ 'syllable_count', wp.syllable_count,
+ 'ipa', wp.ipa,
+ 'syllables', (
+ SELECT json_group_array(
+ json_object(
+ 'stressed', CASE WHEN sw.stressed = 1 THEN json('true') ELSE json('false') END,
+ 'long', CASE WHEN s.long = 1 THEN json('true') ELSE json('false') END,
+ 'spelling', s.text,
+ 'ipa', s.ipa,
+ 'onset', (
+ SELECT json_object('ipa', o.ipa, 'spelling', o.text)
+ FROM onsets o WHERE o.id = s.onset
+ ),
+ 'nucleus', (
+ SELECT json_object('ipa', n.ipa, 'spelling', n.text)
+ FROM nucleus n WHERE n.id = s.nucleus
+ ),
+ 'medial', (
+ SELECT json_object('ipa', m.ipa, 'spelling', m.text)
+ FROM medials m WHERE m.id = s.medial
+ ),
+ 'coda', (
+ SELECT json_object('ipa', c.ipa, 'spelling', c.text)
+ FROM codas c WHERE c.id = s.coda
+ ),
+ 'rhyme', (
+ SELECT json_object('ipa', r.ipa, 'spelling', r.text)
+ FROM rhymes r WHERE r.id = s.rhyme
+ ),
+ 'tone', (
+ SELECT json_object('letters', t.ipa, 'numbers', t.nums, 'name', t.name)
+ FROM tones t WHERE t.id = s.tone
+ )
+ )
+ )
+ FROM syllables_words sw
+ JOIN syllables s ON sw.syl_id = s.id
+ WHERE sw.word_id = wp.id
+ ORDER BY sw.idx ASC
+ )
+ )
+ FROM word_phonetics wp
+ WHERE wp.word_id = e.id
+ -- Select the most general pronunciation (sense_id IS NULL) or the first available
+ ORDER BY (wp.sense_id IS NULL) DESC, wp.id ASC
+ LIMIT 1
+ ),
+ 'senses', (
+ SELECT json_group_array(
+ json_object(
+ 'id', sn.id,
+ 'confidence', sn.confidence,
+ 'etymology', sn.etymology,
+ 'pos', sn.pos,
+ 'glosses', (
+ SELECT json_group_array(sub.gloss)
+ FROM subsenses sub
+ WHERE sub.sid = sn.id
+ ),
+ 'examples', (
+ SELECT json_group_array(
+ json_object(
+ 'example', ex.example,
+ 'ref', ex.ref
+ )
+ )
+ FROM examples ex
+ WHERE ex.sid = sn.id
+ ),
+ 'categories', (
+ SELECT json_group_array(wc.category)
+ FROM word_categories wc
+ WHERE wc.word_id = sn.id
+ ),
+ 'derivation', (
+ SELECT json_group_array(
+ json_object(
+ 'type', d.type,
+ 'text', d.text,
+ 'tags', json(d.tags)
+ )
+ )
+ FROM derivation d
+ WHERE d.sid = sn.id
+ )
+ )
+ )
+ FROM senses sn
+ WHERE sn.parent_id = e.id
+ )
+ ) AS full_word_data
+FROM expressions e
+WHERE e.spelling = ${spelling} AND e.lang = ${lang};
+ `;
+
+ if (data.length > 1) {
+ console.log({ spelling, lang, data });
+ throw new Error("more rows than 1 wtf");
+ }
+ if (data.length === 0) {
+ console.log({ spelling, lang, data });
+ return null;
+ }
+ const row = data[0];
+ const json = JSON.parse(row.full_word_data);
+ const phonetic = JSON.parse(json.phonetic);
+ const obj = { ...json, phonetic };
+ return obj;
}
- fetchExpressionById(id: number) {
- const query = this.db.query(
- `
- SELECT * FROM expressions WHERE id = ?
- `,
- );
- return query.get(id) as any;
+
+ // TODO combine with this old query to get both phonetic and semantic data
+ // Thing is the tables have changed, used to be that the senses table had JSONB columns for senses, forms and related but those are now proper tables now with one-to-many relation to the senses table
+
+ async fetchSenses(spelling: string, lang: string) {
+ const rows = await this.db`
+ WITH sense_data AS (
+ SELECT
+ s.*,
+ GROUP_CONCAT(DISTINCT ss.id || ':' || ss.gloss, '|') as subsenses_data,
+ GROUP_CONCAT(DISTINCT ex.id || ':' || ex.example || ':' || COALESCE(ex.ref, ''), '|') as examples_data,
+ GROUP_CONCAT(DISTINCT d.id || ':' || d.type || ':' || d.text, '|') as derivation_data,
+ GROUP_CONCAT(DISTINCT wc.category, '|') as categories_data
+ FROM senses s
+ LEFT JOIN subsenses ss ON ss.sid = s.id
+ LEFT JOIN examples ex ON ex.sid = s.id
+ LEFT JOIN derivation d ON d.sid = s.id
+ LEFT JOIN word_categories wc ON wc.word_id = s.id
+ GROUP BY s.id
+ )
+ SELECT e.*,
+ (SELECT
+ json_group_array(json_object(
+ 'id', sd.id,
+ 'pos', sd.pos,
+ 'etymology', sd.etymology,
+ 'confidence', sd.confidence,
+ 'subsenses_data', sd.subsenses_data,
+ 'examples_data', sd.examples_data,
+ 'derivation_data', sd.derivation_data,
+ 'categories_data', sd.categories_data
+ ))
+ FROM sense_data sd
+ WHERE sd.parent_id = e.id
+ ) as senses_array
+ FROM expressions e
+ WHERE e.spelling = ${spelling} AND e.lang = ${lang}
+ ORDER BY e.frequency DESC`;
+
+ return rows;
}
- fetchWordsByToneAndSyls1(tones: Array<string | null>) {
+
+ // Tones and syls
+ async fetchWordsByToneAndSyls1(tones: Array<string | null>) {
const toneString = tones
.reduce((acc: string, item) => {
if (!item) return `${acc},%`;
else return `${acc},${item}`;
}, "")
.slice(1);
- const query = this.db.query(
- `
+ const data = await this.db`
WITH word_tone_sequences AS (
SELECT
wp.ipa,
@@ -48,21 +251,19 @@ export class Queries {
)
SELECT *
FROM word_tone_sequences
- WHERE tone_sequence LIKE ?
- AND syllable_count = ?
- `,
- );
- return query.all(toneString, tones.length) as any[];
+ WHERE tone_sequence LIKE ${toneString}
+ AND syllable_count = ${tones.length}
+ `;
+ return data;
}
- fetchWordsByToneAndSylsO(tones: Array<string | null>) {
+ async fetchWordsByToneAndSylsO(tones: Array<string | null>) {
const toneString = tones
.reduce((acc: string, item) => {
if (!item) return `${acc},%`;
else return `${acc},${item}`;
}, "")
.slice(1);
- const query = this.db.query(
- `
+ const data = await this.db`
WITH word_tone_sequences AS (
SELECT
w.id as word_id,
@@ -87,25 +288,25 @@ export class Queries {
tone_sequence,
syllable_count
FROM word_tone_sequences
- WHERE tone_sequence LIKE ?
- AND syllable_count = ?
+ WHERE tone_sequence LIKE ${toneString}
+ AND syllable_count = ${tones.length}
ORDER BY frequency ASC NULLS LAST;
- `,
- );
+ `;
// TODO combine with this old query to get both phonetic and semantic data
// Thing is the tables have changed, used to be that the senses table had JSONB columns for senses, forms and related but those are now proper tables now with one-to-many relation to the senses table
- return query.all(toneString, tones.length) as any[];
+ return data;
}
- fetchWordsByToneAndSyls(tones: Array<string | null>): FullWordDataDB[] {
+ async fetchWordsByToneAndSyls(
+ tones: Array<string | null>,
+ ): Promise<FullWordDataDB[]> {
const toneString = tones
.reduce((acc: string, item) => {
if (!item) return `${acc},%`;
else return `${acc},${item}`;
}, "")
.slice(1);
- const query = this.db.query(
- `
+ const data = await this.db`
WITH word_tone_sequences AS (
SELECT
w.id as word_id,
@@ -161,54 +362,14 @@ export class Queries {
)
SELECT *
FROM word_tone_sequences
- WHERE tone_sequence LIKE ?
- AND syllable_count = ?
+ WHERE tone_sequence LIKE ${toneString}
+ AND syllable_count = ${tones.length}
ORDER BY frequency ASC NULLS LAST;
- `,
- );
+ `;
// TODO combine with this old query to get both phonetic and semantic data
// Thing is the tables have changed, used to be that the senses table had JSONB columns for senses, forms and related but those are now proper tables now with one-to-many relation to the senses table
- return query.all(toneString, tones.length) as any[];
- }
- // TODO combine with this old query to get both phonetic and semantic data
- // Thing is the tables have changed, used to be that the senses table had JSONB columns for senses, forms and related but those are now proper tables now with one-to-many relation to the senses table
-
- fetchSenses(spelling: string, lang: string) {
- const query = this.db.query(`
- WITH sense_data AS (
- SELECT
- s.*,
- GROUP_CONCAT(DISTINCT ss.id || ':' || ss.gloss, '|') as subsenses_data,
- GROUP_CONCAT(DISTINCT ex.id || ':' || ex.example || ':' || COALESCE(ex.ref, ''), '|') as examples_data,
- GROUP_CONCAT(DISTINCT d.id || ':' || d.type || ':' || d.text, '|') as derivation_data,
- GROUP_CONCAT(DISTINCT wc.category, '|') as categories_data
- FROM senses s
- LEFT JOIN subsenses ss ON ss.sid = s.id
- LEFT JOIN examples ex ON ex.sid = s.id
- LEFT JOIN derivation d ON d.sid = s.id
- LEFT JOIN word_categories wc ON wc.word_id = s.id
- GROUP BY s.id
- )
- SELECT e.*,
- (SELECT
- json_group_array(json_object(
- 'id', sd.id,
- 'pos', sd.pos,
- 'etymology', sd.etymology,
- 'confidence', sd.confidence,
- 'subsenses_data', sd.subsenses_data,
- 'examples_data', sd.examples_data,
- 'derivation_data', sd.derivation_data,
- 'categories_data', sd.categories_data
- ))
- FROM sense_data sd
- WHERE sd.parent_id = e.id
- ) as senses_array
- FROM expressions e
- WHERE e.spelling = ? AND e.lang = ?
- ORDER BY e.frequency DESC`);
- return query.all(spelling, lang);
+ return data;
}
}
export default Queries;
diff --git a/packages/db/src/schema.sql b/packages/db/src/schema.sql
index c4a7c76..85d0d77 100644
--- a/packages/db/src/schema.sql
+++ b/packages/db/src/schema.sql
@@ -1,82 +1,11 @@
-/**
- * UNIFIED DATABASE SCHEMA FOR THAI LANGUAGE LEARNING APPLICATION
- *
- * This file consolidates 3 redundant database schemas:
- * - schema.sql (main schema with courses and SRS)
- * - prosodyschema.sql (phonetic analysis with syllable breakdown)
- * - senseschema.sql (semantic analysis with subsenses and derivations)
- *
- * REDUNDANCY ANALYSIS:
- * ====================
- *
- ** MAJOR CONFLICTS RESOLVED:
- * 1. Languages table:
- * - schema.sql: uses 'code' as PRIMARY KEY
- * - prosodyschema.sql: uses 'iso6392' as PRIMARY KEY
- * - RESOLVED: Unified with both code systems supported
- *
- * 2. Senses table:
- * - schema.sql: includes ipa/prosody JSONB fields
- * - senseschema.sql: missing phonetic fields
- * - RESOLVED: Enhanced version with all fields
- *
- * 3. Word/Expression entities:
- * - schema.sql: uses 'expressions' table
- * - prosodyschema.sql: uses 'words' table
- * - RESOLVED: Standardized on 'expressions' terminology
- *
- * 4. Categories tables:
- * - EXACT DUPLICATES in schema.sql and senseschema.sql
- * - RESOLVED: Keep one instance, remove duplicate
- *
- * UNIQUE FEATURES PRESERVED:
- * ========================
- *
- * FROM schema.sql (Main Course Learning):
- * - User management: users, cookies
- * - Course structure: lessons, cards, cards_lessons, cards_expressions
- * - SRS tracking: user_progress, attempts
- * - Bookmarks and user features
- *
- * FROM prosodyschema.sql (Phonetic Analysis):
- * - Detailed syllable breakdown: tones, onsets, medials, nucleus, codas, rhymes
- * - Phonetic patterns: word_phonetics, syllables_words
- * - Rhyme analysis: word_rhymes, words_wrhymes
- *
- * FROM senseschema.sql (Semantic Analysis):
- * - Detailed definitions: subsenses
- * - Etymology tracking: derivation
- *
- * FOREIGN KEY RELATIONSHIPS:
- * =========================
- * - Updated all references from 'words.id' to 'expressions.id'
- * - Unified language references to use languages.code
- * - Maintained proper cascade relationships
- *
- * BENEFITS OF UNIFICATION:
- * =======================
- * 1. Single source of truth for database structure
- * 2. Eliminated conflicting table definitions
- * 3. Preserved all unique functionality
- * 4. Improved foreign key consistency
- * 5. Better support for comprehensive Thai language analysis
- */
--- Enable foreign key support and performance optimizations
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA cache_size = -2000;
PRAGMA mmap_size = 30000000000;
+PRAGMA busy_timeout = 5000;
+
-/**
- * UNIFIED LANGUAGES TABLE
- *
- * RESOLVES CONFLICT between:
- * - schema.sql: code (PRIMARY KEY), name, native_name
- * - prosodyschema.sql: iso6392 (PRIMARY KEY), english
- *
- * NOW SUPPORTS multiple language code systems for maximum compatibility
- */
CREATE TABLE IF NOT EXISTS languages (
code TEXT PRIMARY KEY, -- Primary language code (ISO 639-1 preferred)
name TEXT NOT NULL, -- English name
@@ -85,14 +14,7 @@ CREATE TABLE IF NOT EXISTS languages (
CONSTRAINT name_unique UNIQUE (name)
);
-/**
- * CORE CONTENT TABLES
- * Standardized on 'expressions' terminology from schema.sql
- * Enhanced with fields from both schemas
- */
--- Main expressions table (formerly 'words' in prosodyschema.sql)
--- UNIFIED from schema.sql expressions and prosodyschema.sql words
CREATE TABLE IF NOT EXISTS expressions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
spelling TEXT NOT NULL,
@@ -104,8 +26,6 @@ CREATE TABLE IF NOT EXISTS expressions (
CONSTRAINT spell_unique UNIQUE (spelling, lang)
);
--- Enhanced senses table with phonetic capabilities
--- MERGED from schema.sql senses (with ipa/prosody) and senseschema.sql senses
CREATE TABLE IF NOT EXISTS senses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER NOT NULL,
@@ -153,14 +73,6 @@ CREATE TABLE IF NOT EXISTS derivation (
FOREIGN KEY (sid) REFERENCES senses(id) ON DELETE CASCADE
);
-/**
- * PHONETIC ANALYSIS TABLES (FROM prosodyschema.sql)
- *
- * Comprehensive syllable breakdown system for Thai language analysis
- * These tables provide detailed phonetic analysis beyond basic JSONB fields
- */
-
-
-- Syllable component tables
CREATE TABLE IF NOT EXISTS tones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -252,10 +164,11 @@ CREATE TABLE IF NOT EXISTS word_phonetics (
syllable_sequence TEXT NOT NULL, -- Comma-separated syllables
stressed INTEGER, -- index of stressed syllable
tone_sequence TEXT, -- Comma-separated tones
- tag JSONB, -- Pattern/usage tag
+ tags TEXT, -- Usually dialect, array from wikisource, we join it with "/-/"
notes TEXT,
FOREIGN KEY (word_id) REFERENCES expressions(id),
- FOREIGN KEY (sense_id) REFERENCES senses(id)
+ FOREIGN KEY (sense_id) REFERENCES senses(id),
+ CONSTRAINT phonetics_unique UNIQUE (ipa, tags)
);
-- Rhyme analysis tables (UNIQUE to prosodyschema.sql)
diff --git a/packages/db/src/server.ts b/packages/db/src/server.ts
new file mode 100644
index 0000000..6479b7f
--- /dev/null
+++ b/packages/db/src/server.ts
@@ -0,0 +1,66 @@
+import DB from ".";
+
+const db = new DB();
+const ready = db.init();
+
+type Route = Partial<
+ Record<
+ Bun.Serve.HTTPMethod,
+ Bun.Serve.Handler<Bun.BunRequest, Bun.Server<undefined>, Response>
+ >
+>;
+export const handler: Route = {
+ async GET(req: Bun.BunRequest) {
+ await ready;
+ if (!db.ready) return Response.json({ error: "DB failed to initialize" });
+ // const db = new DB();
+ console.log("Handling HTTP Request on DB", req.url);
+
+ try {
+ const url = new URL(req.url);
+ const params = url.searchParams;
+ const word = params.get("word");
+ const lang = params.get("lang");
+ if (!word) return Response.json({ error: "word param is required" });
+ if (!lang) return Response.json({ error: "lang param is required" });
+ const row = await db.fetchExpressionBySpelling(word, lang);
+ if (!row) return Response.json({ error: "No data found" });
+ else return Response.json({ ok: row });
+ } catch (e) {
+ return Response.json({ error: `${e}` });
+ }
+ },
+ async POST(req: Bun.BunRequest) {
+ await ready;
+ if (!db.ready) return Response.json({ error: "DB failed to initialize" });
+ // const db = new DB();
+ console.log("Handling HTTP Request on DB", req.url);
+
+ try {
+ const reqBody = (await req.json()) as RequestBody;
+ if (!reqBody) return Response.json({ error: "No request body" });
+ const returnData =
+ "getWordFull" in reqBody
+ ? db.fetchExpressionBySpelling(
+ reqBody.getWordFull.spelling,
+ reqBody.getWordFull.lang,
+ )
+ : null;
+ const row = await returnData;
+ console.log({ row });
+ if (!returnData || !row) return Response.json({ error: "No data found" });
+ else return Response.json({ ok: row });
+ } catch (e) {
+ return Response.json({ error: `${e}` });
+ }
+ },
+};
+
+type BySpelling = { spelling: string; lang: string };
+type ByLanguage = string; // iso6393
+type RequestBody =
+ | { getWordFull: BySpelling }
+ | { getWordsenses: BySpelling }
+ | { getWordPhonetics: BySpelling }
+ | { getLanguages: null }
+ | { getExpressions: ByLanguage };
diff --git a/packages/lang/CLAUDE.md b/packages/lang/CLAUDE.md
deleted file mode 100644
index 1ee6890..0000000
--- a/packages/lang/CLAUDE.md
+++ /dev/null
@@ -1,106 +0,0 @@
-
-Default to using Bun instead of Node.js.
-
-- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
-- Use `bun test` instead of `jest` or `vitest`
-- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
-- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
-- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
-- Bun automatically loads .env, so don't use dotenv.
-
-## APIs
-
-- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
-- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
-- `Bun.redis` for Redis. Don't use `ioredis`.
-- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
-- `WebSocket` is built-in. Don't use `ws`.
-- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
-- Bun.$`ls` instead of execa.
-
-## Testing
-
-Use `bun test` to run tests.
-
-```ts#index.test.ts
-import { test, expect } from "bun:test";
-
-test("hello world", () => {
- expect(1).toBe(1);
-});
-```
-
-## Frontend
-
-Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
-
-Server:
-
-```ts#index.ts
-import index from "./index.html"
-
-Bun.serve({
- routes: {
- "/": index,
- "/api/users/:id": {
- GET: (req) => {
- return new Response(JSON.stringify({ id: req.params.id }));
- },
- },
- },
- // optional websocket support
- websocket: {
- open: (ws) => {
- ws.send("Hello, world!");
- },
- message: (ws, message) => {
- ws.send(message);
- },
- close: (ws) => {
- // handle close
- }
- },
- development: {
- hmr: true,
- console: true,
- }
-})
-```
-
-HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
-
-```html#index.html
-<html>
- <body>
- <h1>Hello, world!</h1>
- <script type="module" src="./frontend.tsx"></script>
- </body>
-</html>
-```
-
-With the following `frontend.tsx`:
-
-```tsx#frontend.tsx
-import React from "react";
-
-// import .css files directly and it works
-import './index.css';
-
-import { createRoot } from "react-dom/client";
-
-const root = createRoot(document.body);
-
-export default function Frontend() {
- return <h1>Hello, world!</h1>;
-}
-
-root.render(<Frontend />);
-```
-
-Then, run index.ts
-
-```sh
-bun --hot ./index.ts
-```
-
-For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
diff --git a/packages/lang/README.md b/packages/lang/README.md
deleted file mode 100644
index dc185a1..0000000
--- a/packages/lang/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Langlib
-
-Utils to deal with languages and stuff.
-
-## Langcodes
-
-- ISO-639-1 are **two** characters. e.g. "en", "fr", "zh".
-- ISO-639-2 are **three** characters. e.g. "eng", "fra", "jpn", "cnm".
-- ISO-639-3 are also **three** characters. It's more exhasustive than ISO-639-2, supposedly covers every single language.
-### BCP-47
-BCP-47 (_Best Current Practice_) is a wider tag that includes in one string language, script and region.
-e.g.
-- `en-US`
-- `en-Latn-US`
-- `zh-Hans-CN`
-Format is `language-script-region-variant-extension`
-
-Region codes are ISO 3166-1
-Script tags are ISO 15924
-Variant tags aren't standardized.
-
-It may include all or some of these.
-
diff --git a/packages/lang/bun.lock b/packages/lang/bun.lock
deleted file mode 100644
index cb5bacd..0000000
--- a/packages/lang/bun.lock
+++ /dev/null
@@ -1,64 +0,0 @@
-{
- "lockfileVersion": 1,
- "workspaces": {
- "": {
- "name": "lang",
- "dependencies": {
- "bcp-47": "^2.1.0",
- "countryjs": "^1.8.0",
- "franc-all": "^7.2.0",
- },
- "devDependencies": {
- "@types/bun": "latest",
- },
- "peerDependencies": {
- "typescript": "^5",
- },
- },
- },
- "packages": {
- "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
-
- "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
-
- "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="],
-
- "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
- "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=="],
-
- "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
-
- "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
-
- "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
-
- "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
-
- "countryjs": ["countryjs@1.8.0", "", { "dependencies": { "lodash": "^4.17.2", "minimatch": "^3.0.3", "require-all": "^2.2.0" } }, "sha512-dOgtXkpGirsknf/sV3qBGUGzN9Xlb6KlshtXwG4hYT8cPaPjuvsu/aKq88mQlXkLqRAbXwaHs8RlwJgF8FODAA=="],
-
- "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
-
- "franc-all": ["franc-all@7.2.0", "", { "dependencies": { "trigram-utils": "^2.0.0" } }, "sha512-ZR6ciLQTDBaOvBdkOd8+vqDzaLtmIXRa9GCzcAlaBpqNAKg9QrtClPmqiKac5/xZXfCZGMo1d8dIu1T0BLhHEg=="],
-
- "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=="],
-
- "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
-
- "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
-
- "n-gram": ["n-gram@2.0.2", "", {}, "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ=="],
-
- "require-all": ["require-all@2.2.0", "", {}, "sha512-YWj/WNCxs+KxppuN3j11Ztqzl8MI/oWj4ERwEwgJ5gsHzWi8OAK7FepPu8MLv/Rn8Pov6aPdpRkaoO2Tb6m+zQ=="],
-
- "trigram-utils": ["trigram-utils@2.0.1", "", { "dependencies": { "collapse-white-space": "^2.0.0", "n-gram": "^2.0.0" } }, "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ=="],
-
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
- "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
- }
-}
diff --git a/packages/lang/src/index.ts b/packages/lang/src/index.ts
deleted file mode 100644
index 5ac22b2..0000000
--- a/packages/lang/src/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from "./iso";
-
-export { getScriptPredictor, separateScript } from "./unicode";
diff --git a/packages/lang/test.ts b/packages/lang/test.ts
deleted file mode 100644
index d1763ca..0000000
--- a/packages/lang/test.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { franc, francAll } from "franc-all";
-import { getScriptPredictor } from "./src";
-
-const a = "My name is John.";
-const a2 = "My name is John";
-const b = "ของไทย";
-const b2 = "ของไทย";
-const c = "こんにちは";
-const c2 = "こんにちは";
-//
-// This isn't working must be using some browser APIs to do the work I guess
-const fa = franc("My name is John.");
-const fa2 = francAll("My name is John");
-const fb = franc("ของไทย");
-const fb2 = francAll("ของไทย");
-const fc = franc("こんにちは");
-const fc2 = franc("こんにちは");
-
-console.log({ fa, fa2, fb, fb2, fc, fc2 });
-
-const Predictor = getScriptPredictor();
-const ua = Predictor(a);
-
-console.dir({ ua }, { depth: null });
diff --git a/packages/lang/.gitignore b/packages/langlib/.gitignore
index a14702c..a14702c 100644
--- a/packages/lang/.gitignore
+++ b/packages/langlib/.gitignore
diff --git a/packages/langlib/README.md b/packages/langlib/README.md
new file mode 100644
index 0000000..fe083f8
--- /dev/null
+++ b/packages/langlib/README.md
@@ -0,0 +1,15 @@
+# sortug
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/packages/langlib/index.ts b/packages/langlib/index.ts
new file mode 100644
index 0000000..c000f81
--- /dev/null
+++ b/packages/langlib/index.ts
@@ -0,0 +1,6 @@
+export const wtf = "wtf";
+export * from "./src/unicode";
+export * from "./src/iso";
+export * from "./src/dbtypes";
+export * from "./src/types";
+export * from "./src/lang";
diff --git a/packages/lang/package.json b/packages/langlib/package.json
index 49d50e1..d934090 100644
--- a/packages/lang/package.json
+++ b/packages/langlib/package.json
@@ -1,6 +1,6 @@
{
"name": "@sortug/langlib",
- "module": "src/index.ts",
+ "module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
diff --git a/packages/langlib/package.worked b/packages/langlib/package.worked
new file mode 100644
index 0000000..4839506
--- /dev/null
+++ b/packages/langlib/package.worked
@@ -0,0 +1,11 @@
+{
+ "name": "@sortug/langlib",
+ "module": "index.ts",
+ "type": "module",
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ }
+}
diff --git a/packages/langlib/src/dbtypes.ts b/packages/langlib/src/dbtypes.ts
new file mode 100644
index 0000000..d86d32e
--- /dev/null
+++ b/packages/langlib/src/dbtypes.ts
@@ -0,0 +1,69 @@
+export type ToneQuery = Array<string | null>;
+export type MutationType = { change: string } | { keep: string };
+export type MutationOrder = MutationType[];
+
+export const thaiTones: Record<string, string> = {
+ "˧": "mid",
+ "˨˩": "low",
+ "˥˩": "falling",
+ "˦˥": "high",
+ "˩˩˦": "rising",
+};
+export const thaiToneNums: Record<string, number> = {
+ "˧": 33,
+ "˨˩": 21,
+ "˥˩": 41,
+ "˦˥": 45,
+ "˩˩˦": 214,
+};
+
+export type FullWordData = {
+ id: number;
+ spelling: string;
+ frequency: number;
+ phonetic: PhoneticData;
+ senses: Sense[];
+};
+
+export type PhoneticData = {
+ word_id: number;
+ tone_sequence: string; // e.g. "1-4-3"
+ syl_seq: string; // e.g. "pre-si-dent"
+ syllable_count: number;
+ syllables: Syllable[];
+ ipa: string;
+};
+export type Sense = {
+ confidence: number;
+ examples: Example[];
+ categories: string[];
+ etymology: string;
+ derivation: Derivation[];
+ pos: string;
+ glosses: string[];
+};
+export type Tone = {
+ letters: string;
+ numbers: number;
+ name: string;
+};
+
+export type Phoneme = {
+ ipa: string;
+ spelling: string;
+};
+export type Syllable = {
+ stressed: boolean;
+ long: boolean;
+ spelling: string;
+ ipa: string;
+ nucleus: Phoneme;
+ onset: Phoneme;
+ medial: Phoneme;
+ coda: Phoneme;
+ rhyme: Phoneme;
+ tone: Tone;
+};
+
+export type Example = { ref: string; text: string };
+export type Derivation = { type: string; text: string; tags: any };
diff --git a/packages/lang/src/iso/index.ts b/packages/langlib/src/iso/index.ts
index 60467e0..60467e0 100644
--- a/packages/lang/src/iso/index.ts
+++ b/packages/langlib/src/iso/index.ts
diff --git a/packages/lang/src/iso/iso15924.ts b/packages/langlib/src/iso/iso15924.ts
index dcaa42d..dcaa42d 100644
--- a/packages/lang/src/iso/iso15924.ts
+++ b/packages/langlib/src/iso/iso15924.ts
diff --git a/packages/lang/src/iso/iso6393-to-1.js b/packages/langlib/src/iso/iso6393-to-1.js
index ed818f9..ed818f9 100644
--- a/packages/lang/src/iso/iso6393-to-1.js
+++ b/packages/langlib/src/iso/iso6393-to-1.js
diff --git a/packages/lang/src/iso/iso6393-to-2b.js b/packages/langlib/src/iso/iso6393-to-2b.js
index c65f0a1..c65f0a1 100644
--- a/packages/lang/src/iso/iso6393-to-2b.js
+++ b/packages/langlib/src/iso/iso6393-to-2b.js
diff --git a/packages/lang/src/iso/iso6393-to-2t.js b/packages/langlib/src/iso/iso6393-to-2t.js
index ab84590..ab84590 100644
--- a/packages/lang/src/iso/iso6393-to-2t.js
+++ b/packages/langlib/src/iso/iso6393-to-2t.js
diff --git a/packages/lang/src/iso/iso6393.js b/packages/langlib/src/iso/iso6393.js
index 6e728de..6e728de 100644
--- a/packages/lang/src/iso/iso6393.js
+++ b/packages/langlib/src/iso/iso6393.js
diff --git a/packages/lang/src/lang/index.ts b/packages/langlib/src/lang/index.ts
index f566672..f566672 100644
--- a/packages/lang/src/lang/index.ts
+++ b/packages/langlib/src/lang/index.ts
diff --git a/packages/lang/src/types.ts b/packages/langlib/src/types.ts
index 57e46b4..b9b163e 100644
--- a/packages/lang/src/types.ts
+++ b/packages/langlib/src/types.ts
@@ -48,3 +48,5 @@ export type ISO_6393_3 = {
iso6392T?: string;
iso6391?: string;
};
+
+export * from "./dbtypes";
diff --git a/packages/lang/src/unicode/index.ts b/packages/langlib/src/unicode/index.ts
index bf8821d..71736d4 100644
--- a/packages/lang/src/unicode/index.ts
+++ b/packages/langlib/src/unicode/index.ts
@@ -1,25 +1,24 @@
// Author: Amir Hossein Kargaran (TypeScript port)
// Date: March, 2025
-import { ISO_15924_CODE } from "../iso";
+import type { ISO_15924_CODE } from "../iso";
// Description: This code detects/separates the script(s) (writing system(s)) of the given text.
// TypeScript port of the original Python implementation.
// Types
-type ScriptRange = [number, number];
-type ScriptRanges = Record<string, ScriptRange[]>;
-
-type ScoredScript = [ISO_15924_CODE | null, number, ScriptDetails];
-
interface ScriptDetails {
- details: Record<ISO_15924_CODE, number> | null;
+ details: Map<ISO_15924_CODE, number> | null;
tie: boolean | null;
interval: number | null;
}
+type ScriptRange = [number, number];
+type ScriptRanges = Partial<Record<ISO_15924_CODE, ScriptRange[]>>;
+
+type ScoredScript = [ISO_15924_CODE | null, number, ScriptDetails];
// The SCRIPT_RANGES object contains Unicode ranges for different scripts
-export const SCRIPT_RANGES: ScriptRanges = {
+const SCRIPT_RANGES: ScriptRanges = {
Latn: [
[65, 90],
[97, 122],
@@ -1226,15 +1225,16 @@ export function getScriptPredictor(
replaceDigits: boolean = true,
): (sent: string) => ScoredScript {
// Create a map of code points to script names
- const histMap: Map<number, Set<string>> = new Map();
+ const histMap: Map<number, Set<ISO_15924_CODE>> = new Map();
for (const [key, ranges] of Object.entries(SCRIPT_RANGES)) {
+ const k = key as ISO_15924_CODE;
for (const [start, end] of ranges) {
for (let ordinal = start; ordinal <= end; ordinal++) {
if (!histMap.has(ordinal)) {
histMap.set(ordinal, new Set());
}
- histMap.get(ordinal)!.add(key);
+ histMap.get(ordinal)!.add(k);
}
}
}
@@ -1266,7 +1266,7 @@ export function getScriptPredictor(
}
// Count characters by script
- const scriptCount: Map<string, number> = new Map();
+ const scriptCount: Map<ISO_15924_CODE, number> = new Map();
for (const char of filteredText) {
const ordinal = char.codePointAt(0)!;
@@ -1278,14 +1278,14 @@ export function getScriptPredictor(
}
// Convert to sorted object for details
- const sortedScores: Record<string, number> = {};
+ const scoreMap: Map<ISO_15924_CODE, number> = new Map();
for (const [script, count] of scriptCount.entries()) {
- sortedScores[script] = count / filteredText.length;
+ scoreMap.set(script, count / filteredText.length);
}
// Find the script with maximum score
let maxScore = 0;
- let maxScript: string | null = null;
+ let maxScript: ISO_15924_CODE | null = null;
for (const [script, count] of scriptCount.entries()) {
const score = count / filteredText.length;
@@ -1296,21 +1296,18 @@ export function getScriptPredictor(
}
// Sort scores for details
- const sortedEntries = Object.entries(sortedScores).sort(
+ const sortedEntries = Array.from(scoreMap.entries()).sort(
(a, b) => b[1] - a[1],
);
- const sortedDetails: Record<string, number> =
- Object.fromEntries(sortedEntries);
-
// Calculate interval and check for ties
if (sortedEntries.length > 1) {
- const secondScore = sortedEntries[1][1];
+ const secondScore = sortedEntries[1]![1];
const interval = maxScore - secondScore;
return [
maxScript,
maxScore,
{
- details: sortedDetails,
+ details: scoreMap,
tie: interval === 0,
interval: interval,
},
@@ -1321,7 +1318,7 @@ export function getScriptPredictor(
maxScript,
maxScore,
{
- details: sortedDetails,
+ details: scoreMap,
tie: false,
interval: 1,
},
@@ -1406,12 +1403,10 @@ export function testSeparateScript(): void {
`Error: '${key}' script not found in detected scripts.`,
);
- const detectedTokens = detected[key]
- .split(" ")
+ const detectedTokens = detected[key]!.split(" ")
.map((x) => x.trim())
.filter((x) => x.length > 0);
- const groundTruthTokens = groundTruth[key]
- .split(" ")
+ const groundTruthTokens = groundTruth[key]!.split(" ")
.map((x) => x.trim())
.filter((x) => x.length > 0);
diff --git a/packages/lang/tsconfig.json b/packages/langlib/tsconfig.json
index bfa0fea..9c62f74 100644
--- a/packages/lang/tsconfig.json
+++ b/packages/langlib/tsconfig.json
@@ -3,7 +3,7 @@
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
- "module": "Preserve",
+ "module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
@@ -19,7 +19,6 @@
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
- "noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
diff --git a/packages/prosody-ui/bun.lock b/packages/prosody-ui/bun.lock
deleted file mode 100644
index c276ddd..0000000
--- a/packages/prosody-ui/bun.lock
+++ /dev/null
@@ -1,318 +0,0 @@
-{
- "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
index f7339a2..2c00fbe 100644
--- a/packages/prosody-ui/index.ts
+++ b/packages/prosody-ui/index.ts
@@ -1,3 +1,6 @@
+import "./src/styles/styles.css";
+import "@sortug/lib/src/styles.css";
+
import LangText from "./src/LangText.tsx";
import FontChanger from "./src/fonts/FontChanger.tsx";
import Paragraph from "./src/Paragraph.tsx";
diff --git a/packages/prosody-ui/package.json b/packages/prosody-ui/package.json
index cf9ad48..0902fbd 100644
--- a/packages/prosody-ui/package.json
+++ b/packages/prosody-ui/package.json
@@ -8,11 +8,12 @@
"react": ">=19.0.0"
},
"dependencies": {
- "franc-all": "^7.2.0",
- "motion": "^12.11.3",
- "@sortug/lib": "workspace:*",
+ "@sortug/ai": "workspace:*",
"@sortug/langlib": "workspace:*",
- "@sortug/ai": "workspace:*"
+ "@sortug/lib": "workspace:*",
+ "@tabler/icons-react": "^3.35.0",
+ "franc-all": "^7.2.0",
+ "motion": "^12.11.3"
},
"private": true,
"devDependencies": {
diff --git a/packages/prosody-ui/src/LangText.tsx b/packages/prosody-ui/src/LangText.tsx
index 790c499..ab9d4f4 100644
--- a/packages/prosody-ui/src/LangText.tsx
+++ b/packages/prosody-ui/src/LangText.tsx
@@ -2,27 +2,31 @@ 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 type { AnalyzeRes, ColorTheme, 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";
+import FullWord from "./components/word/FullWordData";
+import type { AsyncRes, Result } from "@sortug/lib";
+import type { FullWordData } from "@sortug/langlib";
export default function LangText({
text,
lang,
theme,
- fetchWiki,
handleWord,
+ handleError,
}: {
text: string;
+ theme?: ColorTheme;
lang?: string;
- theme?: string;
- fetchWiki?: (url: string) => Promise<string>;
- handleWord?: (wd: Result<WordData>) => any;
+ handleWord?: (word: AnalyzeRes) => any;
+ handleError?: (error: string) => any;
}) {
+ const background: ColorTheme = theme ? theme : "light";
const [llang, setLang] = useState("");
const [script, setScript] = useState(scriptFromLang(lang || "", text));
+ const [modal, setWordModal] = useState<FullWordData | null>(null);
useEffect(() => {
if (!lang) {
const res = franc(text);
@@ -31,7 +35,27 @@ export default function LangText({
}, [text]);
console.log("langtext", { text, llang, script });
- async function openWord(word: string) {
+ async function openWord(word: AnalyzeRes) {
+ if (handleWord) handleWord(word);
+ else {
+ const body = JSON.stringify({
+ getWordFull: { spelling: word.word, lang: llang },
+ });
+ const opts = {
+ method: "POST",
+ body,
+ headers: { "Content-type": "application/json" },
+ };
+ const res = await fetch("/api/db", opts);
+ const j = (await res.json()) as Result<FullWordData>;
+ console.log({ j });
+ if ("error" in j) {
+ if (handleError) handleError(j.error);
+ else console.error("error opening word", j.error);
+ } else {
+ setWordModal(j.ok);
+ }
+ }
// console.log("looking up", word);
// const url = buildWiktionaryURL(word);
// const html = await fetchWiki(url);
@@ -56,12 +80,20 @@ export default function LangText({
return (
<div className="lang-text-container">
{script === "Thai" ? (
- <ThaiText text={text} openWord={openWord} />
+ <ThaiText text={text} theme={background} openWord={openWord} />
) : script === "Latin" ? (
<LatinText text={text} lang={llang} openWord={openWord} />
) : (
<Generic text={text} lang={llang} />
)}
+ {modal && (
+ <WordModal
+ word={modal}
+ lang={llang}
+ theme={background}
+ onClose={() => setWordModal(null)}
+ />
+ )}
</div>
);
}
@@ -76,3 +108,35 @@ function Generic({ text, lang }: { text: string; lang: string }) {
// {data && <ColoredText frags={Object.keys(data)} />}
return <div className="lang-text-div"></div>;
}
+
+function WordModal({
+ word,
+ lang,
+ theme,
+ onClose,
+}: {
+ word: FullWordData;
+ lang: string;
+ theme: ColorTheme;
+ onClose: () => void;
+}) {
+ return (
+ <div
+ id="modal-bg"
+ role="dialog"
+ aria-modal="true"
+ onMouseDown={(event) => {
+ if (event.target === event.currentTarget) {
+ onClose();
+ }
+ }}
+ >
+ <div
+ id="modal-fg"
+ style={{ backgroundColor: "white", border: "5px solid black" }}
+ >
+ <FullWord data={word} lang={lang} theme={"light"} />
+ </div>
+ </div>
+ );
+}
diff --git a/packages/prosody-ui/src/Paragraph.tsx b/packages/prosody-ui/src/Paragraph.tsx
index 72c43a7..b911fa0 100644
--- a/packages/prosody-ui/src/Paragraph.tsx
+++ b/packages/prosody-ui/src/Paragraph.tsx
@@ -6,7 +6,7 @@ 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 type { Result } from "@sortug/lib";
import * as Stanza from "./logic/stanza";
import { iso6393To1 } from "./logic/iso6393to1";
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
index b387fc5..b387fc5 100644
--- a/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf
+++ b/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan-2.ttf
Binary files differ
diff --git a/packages/prosody-ui/src/components/Colors.tsx b/packages/prosody-ui/src/components/Colors.tsx
new file mode 100644
index 0000000..d98838f
--- /dev/null
+++ b/packages/prosody-ui/src/components/Colors.tsx
@@ -0,0 +1,198 @@
+import React from "react";
+import { notRandomFromArray, randomFromArrayMany } from "@sortug/lib";
+import "./sentence.css";
+import type { AnalyzeRes, ColorTheme, LangToColor } from "../logic/types";
+import type { POS_CODE } from "../thai/logic/thainlp";
+
+export function assignColors(keys: string[], theme?: ColorTheme): string[] {
+ const background = theme ? theme : "light";
+ const colors = colorPalette[background];
+ const reduced = randomFromArrayMany(colors, keys.length, false);
+ const assigned: string[] = [];
+ for (const key of keys) {
+ const color = notRandomFromArray(key, reduced);
+ assigned.push(color);
+ }
+ return assigned;
+}
+
+export function ColoredText({
+ frags,
+ fn,
+ lang,
+ theme,
+}: {
+ frags: LangToColor<unknown>[];
+ fn?: (s: any) => void;
+ lang?: string;
+ theme: ColorTheme;
+}) {
+ const colors = colorPalette[theme];
+ console.log("coloredText", theme);
+
+ // function getStyle(frags: AnalyzeRes[], i: number) {
+ // const prev = frags[i - 1];
+ // const prevC = prev ? notRandomFromArray(prev.word, colors) : "lol";
+ // const color = notRandomFromArray(s, colors);
+ // const opacity = prev && prevC === color ? 0.8 : 1;
+ // const style = { color, opacity };
+ // return style;
+ // }
+
+ return (
+ <>
+ {frags.map((s, i) => {
+ // old code
+ const prev = frags[i - 1];
+ const prevC = prev ? notRandomFromArray(prev.colorBy, colors) : "lol";
+ const color = notRandomFromArray(s.colorBy, colors);
+ const style = !prev ? { color } : { color };
+ return (
+ <CTInner
+ lang={lang}
+ key={s.display + i}
+ s={s}
+ style={style}
+ fn={fn}
+ />
+ );
+ })}
+ </>
+ );
+}
+
+export function CTInner({
+ s,
+ style,
+ fn,
+ lang,
+}: {
+ s: LangToColor<unknown>;
+ style: any;
+ fn?: (s: any) => void;
+ lang?: string;
+}) {
+ function handleClick(e: React.MouseEvent<HTMLSpanElement>) {
+ if (fn) {
+ e.stopPropagation();
+ fn(s.data);
+ }
+ }
+ return (
+ <span lang={lang} onClick={handleClick} className="word cp" style={style}>
+ {s.display}
+ </span>
+ );
+}
+
+export const colorPalette: Record<ColorTheme, string[]> = {
+ light: [
+ // Black Standard high contrast
+ "#000000",
+ // Charcoal Softer than pure black
+ "#36454F",
+ // Slate Grey Cool, dark grey-green
+ "#2F4F4F",
+ // Navy Blue Classic professional blue
+ "#000080",
+ // Midnight Blue Very deep, rich blue
+ "#191970",
+ // Cobalt Vivid, highly legible blue
+ "#0047AB",
+ // Teal Distinct blue-green
+ "#008080",
+ // Forest Green Nature-inspired dark green
+ "#006400",
+ // Pine Green Cooler, bluish green
+ "#01796F",
+ // Olive Drab Dark brownish-green
+ "#4B5320",
+ // Bronze Metallic brown-orange
+ "#CD7F32",
+ // Saddle Brown Robust earthy tone
+ "#8B4513",
+ // Chocolate Warm, readable orange-brown
+ "#D2691E",
+ // Burnt Sienna Reddish-orange earth tone
+ "#E97451",
+ // Firebrick Muted dark red
+ "#B22222",
+ // Crimson Vivid, alarming red
+ "#DC143C",
+ // Maroon Deep, serious red
+ "#800000",
+ // Burgundy Purple-leaning red
+ "#800020",
+ // Deep Pink High contrast magenta-pink
+ "#C71585",
+ // Dark Violet Vivid purple
+ "#9400D3",
+ // Indigo Deep blue-purple
+ "#4B0082",
+ // Purple Standard distinct purple
+ "#800080",
+ // Rebecca Purple Web-standard bluish purple
+ "#663399",
+ // Dim Gray Neutral, medium-dark gray
+ "#696969",
+ ],
+ dark: [
+ // White Standard high contrast
+ "#FFFFFF",
+ // Silver Soft readable grey
+ "#C0C0C0",
+ // Cream Warm white, easier on eyes
+ "#FFFDD0",
+ // Cyan The standard terminal blue-green
+ "#00FFFF",
+ // Sky Blue Pleasant, airy blue
+ "#87CEEB",
+ // Powder Blue Very pale, soft blue
+ "#B0E0E6",
+ // Aquamarine Bright neon blue-green
+ "#7FFFD4",
+ // Mint Green Soft, pastel green
+ "#98FB98",
+ // Lime Classic high-vis terminal green
+ "#00FF00",
+ // Chartreuse Yellow-green neon
+ "#7FFF00",
+ // Gold Bright yellow-orange
+ "#FFD700",
+ // Yellow Standard high-vis yellow
+ "#FFFF00",
+ // Khaki Muted, sandy yellow
+ "#F0E68C",
+ // Wheat Soft beige/earth tone
+ "#F5DEB3",
+ // Orange Standard distinctive orange
+ "#FFA500",
+ // Coral Pinkish-orange
+ "#FF7F50",
+ // Salmon Soft reddish-pink
+ "#FA8072",
+ // Hot Pink Vivid, high-energy pink
+ "#FF69B4",
+ // Magenta Pure, digital pink-purple
+ "#FF00FF",
+ // Plum Muted, readable purple
+ "#DDA0DD",
+ // Violet Bright, distinct purple
+ "#EE82EE",
+ // Lavender Very light purple-blue
+ "#E6E6FA",
+ // Periwinkle Soft indigo-blue
+ "#CCCCFF",
+ // Thistle Desaturated light purple
+ "#D8BFD8",
+ ],
+};
+
+// export const colors = [
+// "#8c2c2c",
+// "#000000",
+// "#ffd400",
+// "#1513a0",
+// "#7e7e7e",
+// "1eb52d",
+// ];
diff --git a/packages/prosody-ui/src/components/Sentence.tsx b/packages/prosody-ui/src/components/Sentence.tsx
index 33144ac..1986ba8 100644
--- a/packages/prosody-ui/src/components/Sentence.tsx
+++ b/packages/prosody-ui/src/components/Sentence.tsx
@@ -1,26 +1,49 @@
import React from "react";
-import { notRandomFromArray } from "sortug";
+import { notRandomFromArray } from "@sortug/lib";
import "./sentence.css";
+import type { AnalyzeRes, ColorTheme, LangToColor } from "../logic/types";
+import type { POS_CODE } from "../thai/logic/thainlp";
export function ColoredText({
frags,
fn,
lang,
+ theme,
}: {
- frags: string[];
- fn?: (s: string) => void;
+ frags: LangToColor<unknown>[];
+ fn?: (s: any) => void;
lang?: string;
+ theme: ColorTheme;
}) {
+ const colors = colorPalette[theme];
+ console.log("coloredText", theme);
+
+ // function getStyle(frags: AnalyzeRes[], i: number) {
+ // const prev = frags[i - 1];
+ // const prevC = prev ? notRandomFromArray(prev.word, colors) : "lol";
+ // const color = notRandomFromArray(s, colors);
+ // const opacity = prev && prevC === color ? 0.8 : 1;
+ // const style = { color, opacity };
+ // return style;
+ // }
+
return (
<>
{frags.map((s, i) => {
+ // old code
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} />;
+ const prevC = prev ? notRandomFromArray(prev.colorBy, colors) : "lol";
+ const color = notRandomFromArray(s.colorBy, colors);
+ const style = !prev ? { color } : { color };
+ return (
+ <CTInner
+ lang={lang}
+ key={s.display + i}
+ s={s}
+ style={style}
+ fn={fn}
+ />
+ );
})}
</>
);
@@ -32,26 +55,132 @@ export function CTInner({
fn,
lang,
}: {
- s: string;
+ s: LangToColor<unknown>;
style: any;
- fn?: (s: string) => void;
+ fn?: (s: any) => void;
lang?: string;
}) {
function handleClick(e: React.MouseEvent<HTMLSpanElement>) {
- console.log(!!fn, "fn");
- if (fn) fn(e.currentTarget.innerText.trim());
+ if (fn) {
+ e.stopPropagation();
+ fn(s.data);
+ }
}
return (
<span lang={lang} onClick={handleClick} className="word cp" style={style}>
- {s}
+ {s.display}
</span>
);
}
-export const colors = [
- "#8c2c2c",
- "#000000",
- "#ffd400",
- "#1513a0",
- "#7e7e7e",
- "1eb52d",
-];
+
+export const colorPalette: Record<ColorTheme, string[]> = {
+ light: [
+ // Black Standard high contrast
+ "#000000",
+ // Charcoal Softer than pure black
+ "#36454F",
+ // Slate Grey Cool, dark grey-green
+ "#2F4F4F",
+ // Navy Blue Classic professional blue
+ "#000080",
+ // Midnight Blue Very deep, rich blue
+ "#191970",
+ // Cobalt Vivid, highly legible blue
+ "#0047AB",
+ // Teal Distinct blue-green
+ "#008080",
+ // Forest Green Nature-inspired dark green
+ "#006400",
+ // Pine Green Cooler, bluish green
+ "#01796F",
+ // Olive Drab Dark brownish-green
+ "#4B5320",
+ // Bronze Metallic brown-orange
+ "#CD7F32",
+ // Saddle Brown Robust earthy tone
+ "#8B4513",
+ // Chocolate Warm, readable orange-brown
+ "#D2691E",
+ // Burnt Sienna Reddish-orange earth tone
+ "#E97451",
+ // Firebrick Muted dark red
+ "#B22222",
+ // Crimson Vivid, alarming red
+ "#DC143C",
+ // Maroon Deep, serious red
+ "#800000",
+ // Burgundy Purple-leaning red
+ "#800020",
+ // Deep Pink High contrast magenta-pink
+ "#C71585",
+ // Dark Violet Vivid purple
+ "#9400D3",
+ // Indigo Deep blue-purple
+ "#4B0082",
+ // Purple Standard distinct purple
+ "#800080",
+ // Rebecca Purple Web-standard bluish purple
+ "#663399",
+ // Dim Gray Neutral, medium-dark gray
+ "#696969",
+ ],
+ dark: [
+ // White Standard high contrast
+ "#FFFFFF",
+ // Silver Soft readable grey
+ "#C0C0C0",
+ // Cream Warm white, easier on eyes
+ "#FFFDD0",
+ // Cyan The standard terminal blue-green
+ "#00FFFF",
+ // Sky Blue Pleasant, airy blue
+ "#87CEEB",
+ // Powder Blue Very pale, soft blue
+ "#B0E0E6",
+ // Aquamarine Bright neon blue-green
+ "#7FFFD4",
+ // Mint Green Soft, pastel green
+ "#98FB98",
+ // Lime Classic high-vis terminal green
+ "#00FF00",
+ // Chartreuse Yellow-green neon
+ "#7FFF00",
+ // Gold Bright yellow-orange
+ "#FFD700",
+ // Yellow Standard high-vis yellow
+ "#FFFF00",
+ // Khaki Muted, sandy yellow
+ "#F0E68C",
+ // Wheat Soft beige/earth tone
+ "#F5DEB3",
+ // Orange Standard distinctive orange
+ "#FFA500",
+ // Coral Pinkish-orange
+ "#FF7F50",
+ // Salmon Soft reddish-pink
+ "#FA8072",
+ // Hot Pink Vivid, high-energy pink
+ "#FF69B4",
+ // Magenta Pure, digital pink-purple
+ "#FF00FF",
+ // Plum Muted, readable purple
+ "#DDA0DD",
+ // Violet Bright, distinct purple
+ "#EE82EE",
+ // Lavender Very light purple-blue
+ "#E6E6FA",
+ // Periwinkle Soft indigo-blue
+ "#CCCCFF",
+ // Thistle Desaturated light purple
+ "#D8BFD8",
+ ],
+};
+
+// export const colors = [
+// "#8c2c2c",
+// "#000000",
+// "#ffd400",
+// "#1513a0",
+// "#7e7e7e",
+// "1eb52d",
+// ];
diff --git a/packages/prosody-ui/src/components/word/FullWordData.tsx b/packages/prosody-ui/src/components/word/FullWordData.tsx
new file mode 100644
index 0000000..9b1fc69
--- /dev/null
+++ b/packages/prosody-ui/src/components/word/FullWordData.tsx
@@ -0,0 +1,156 @@
+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, ColorTheme, Meaning } from "@/logic/types";
+import { ColoredText } from "../Sentence.tsx";
+import { P, Span, useSpeechSynthesis } from "@/hooks/useLang.tsx";
+import type { FullWordData } from "@sortug/langlib";
+import { cycleNext } from "@sortug/lib";
+import FontChanger from "@/fonts/FontChanger.tsx";
+import Phonetic from "./Phonetic.tsx";
+
+function Word({
+ data,
+ lang,
+ theme,
+}: {
+ data: FullWordData;
+ lang: string;
+ theme: ColorTheme;
+}) {
+ 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(false);
+ const [meanings, setM] = useState<Meaning[]>([]);
+ const [fontIdx, setFont] = useState(0);
+
+ const { voices, speaking, speak, stop } = useSpeechSynthesis();
+ function playAudio() {
+ console.log({ voices, speaking });
+ console.log("word", data);
+ speak(data.spelling);
+ }
+ console.log({ data });
+
+ async function saveW() {}
+
+ return (
+ <div id="word-modal" title={data.spelling}>
+ <FontChanger text={data.spelling}>
+ <img className="save-icon cp" onClick={saveW} src={bookmarkIcon} />
+ <div className="original">
+ <ColoredText
+ frags={data.phonetic.syllables.map((s) => ({
+ data: s,
+ display: s.spelling,
+ colorBy: s.tone.name,
+ }))}
+ theme={theme}
+ />
+ </div>
+ <Phonetic data={data} lang={lang} theme={theme} />
+ <div className="pronunciation IPA flex1 flex-center">
+ <P>{data.phonetic.ipa}</P>
+ <img onClick={playAudio} className="icon cp" src={speakerIcon} />
+ </div>
+ <div className="meanings">
+ {loading ? (
+ <img src={spinner} className="spinner bc" />
+ ) : (
+ data.senses.map((m) => (
+ <div key={JSON.stringify(m)} className="meaning">
+ <div className="pos">
+ <Span>{m.pos}</Span>
+ </div>
+ <ol>
+ {m.glosses.map((t, i) => (
+ <li key={t + i} className="translation">
+ <P>{t}</P>
+ </li>
+ ))}
+ </ol>
+ </div>
+ ))
+ )}
+ {error && <div className="error">{error}</div>}
+ </div>
+ </FontChanger>
+ </div>
+ );
+}
+
+export default Word;
+
+<Card className="absolute inset-0 backface-hidden rotate-y-180 flex flex-col overflow-hidden border-slate-200 dark:border-slate-800 shadow-lg bg-slate-50/50">
+ <div className="flex-1 overflow-hidden flex flex-col">
+ <Tabs defaultValue="meanings" className="flex-1 flex flex-col">
+ <div className="px-6 pt-6 pb-2 bg-white border-b">
+ <TabsList className="grid w-full grid-cols-3">
+ <TabsTrigger value="meanings">Meanings</TabsTrigger>
+ <TabsTrigger value="grammar">Grammar</TabsTrigger>
+ <TabsTrigger value="examples">Examples</TabsTrigger>
+ </TabsList>
+ </div>
+
+ <div className="flex-1 overflow-y-auto p-6">
+ <TabsContent value="meanings" className="mt-0 space-y-4">
+ <EnhancedWordMeanings word={word} />
+ </TabsContent>
+
+ <TabsContent value="grammar" className="mt-0 space-y-6">
+ <div className="space-y-4">
+ <div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
+ <h3 className="font-semibold text-blue-900 mb-2">
+ Tone Analysis
+ </h3>
+ <div className="flex flex-wrap gap-2">
+ {tones.map((tone, idx) => (
+ <Badge key={idx} variant="outline" className="bg-white">
+ Syl {idx + 1}:{" "}
+ <span
+ className={cn("ml-1 font-bold", getColorByTone(tone))}
+ >
+ {tone}
+ </span>
+ </Badge>
+ ))}
+ </div>
+ </div>
+
+ <div className="bg-slate-100 p-4 rounded-lg border border-slate-200">
+ <h3 className="font-semibold text-slate-900 mb-2">
+ Word Structure
+ </h3>
+ <p className="text-sm text-slate-600">
+ This word consists of {syls.length} syllable
+ {syls.length > 1 ? "s" : ""}. The tone pattern is essential for
+ conveying the correct meaning.
+ </p>
+ </div>
+ </div>
+ </TabsContent>
+
+ <TabsContent value="examples" className="mt-0 space-y-4">
+ <ExamplesTab
+ word={word}
+ moreExamples={word.senses?.flatMap((s) => s.examples || [])}
+ />
+ </TabsContent>
+ </div>
+ </Tabs>
+ </div>
+</Card>;
diff --git a/packages/prosody-ui/src/components/word/Phonetic.tsx b/packages/prosody-ui/src/components/word/Phonetic.tsx
new file mode 100644
index 0000000..db3d0cb
--- /dev/null
+++ b/packages/prosody-ui/src/components/word/Phonetic.tsx
@@ -0,0 +1,92 @@
+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 type { AnalyzeRes, ColorTheme, Meaning } from "@/logic/types";
+import { P, Span, useSpeechSynthesis } from "@/hooks/useLang.tsx";
+import type { FullWordData, Syllable, Tone } from "@sortug/langlib";
+import { cycleNext } from "@sortug/lib";
+import FontChanger from "../fonts/FontChanger.tsx";
+import { assignColors } from "../Colors.tsx";
+import { IconBadgeFilled, IconSpeakerphone } from "@tabler/icons-react";
+
+function Phonetic({
+ data,
+ lang,
+ theme,
+}: {
+ data: FullWordData;
+ lang: string;
+ theme: ColorTheme;
+}) {
+ 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 [loading, setLoading] = useState(false);
+
+ const { voices, speaking, speak, stop } = useSpeechSynthesis();
+ function playAudio() {
+ setLoading(true);
+ console.log({ voices, speaking });
+ console.log("word", data);
+ speak(data.spelling);
+ setLoading(false);
+ }
+ console.log({ data });
+
+ async function saveW() {}
+
+ return (
+ <div className="phonetic-data">
+ <div className="pronunciation IPA flex1 flex-center">
+ <P>{data.phonetic.ipa}</P>
+ {loading ? (
+ <img src={spinner} className="spinner bc" />
+ ) : (
+ <IconSpeakerphone onClick={playAudio} />
+ )}
+ </div>
+ <Syllables data={data} />
+ </div>
+ );
+}
+
+export default Phonetic;
+
+function Syllables({ data }: { data: FullWordData }) {
+ const syllables = data.phonetic.syllables;
+
+ console.log(data.phonetic.tone_sequence);
+ const isTonal = !!data.phonetic.tone_sequence;
+ const colorMap = isTonal
+ ? (s: Syllable) => s.tone.name
+ : (s: Syllable) => (s.stressed ? "stressed" : "neuter");
+ const colors = assignColors(syllables.map(colorMap));
+ return (
+ <div className="syllables">
+ {data.phonetic.syllables.map((syl) => (
+ <div className="syllable">
+ {syl.tone.letters && <Tone tone={syl.tone} />}
+ <span>{syl.spelling}</span>
+ </div>
+ ))}
+ </div>
+ );
+}
+function Tone({ tone }: { tone: Tone }) {
+ return (
+ <div className="tone">
+ <IconBadgeFilled>{tone.letters}</IconBadgeFilled>
+ </div>
+ );
+}
diff --git a/packages/prosody-ui/src/components/word/Semantic.tsx b/packages/prosody-ui/src/components/word/Semantic.tsx
new file mode 100644
index 0000000..059194c
--- /dev/null
+++ b/packages/prosody-ui/src/components/word/Semantic.tsx
@@ -0,0 +1,184 @@
+import { useEffect, useState } from "react";
+import type { Example, FullWordData } from "@sortug/langlib";
+import { IconBadgeFilled, IconSparkles } from "@tabler/icons-react";
+
+type Tab = "meanings" | "grammar" | "examples";
+function Semantic({ data }: { data: FullWordData }) {
+ return (
+ <div className="">
+ <div className="flex-col">
+ <div className="tab-container">
+ {data.senses.map((sense, i) => (
+ <div>
+ <div key={data.spelling + sense.etymology + i} className="">
+ {sense.pos && <div className="">{sense.pos}</div>}
+
+ <ul className="">
+ {sense.glosses.map((gloss, idx: number) => (
+ <li key={idx} className="text-gray-700">
+ {gloss}
+ </li>
+ ))}
+ </ul>
+
+ {sense.etymology && (
+ <div className="">
+ <strong>Etymology:</strong> {sense.etymology}
+ </div>
+ )}
+
+ {sense.categories.length > 0 && (
+ <div className="">
+ <strong>Categories:</strong> {sense.categories.join(", ")}
+ </div>
+ )}
+ {sense.derivation.length > 0 && (
+ <div className="">
+ <strong>Derived forms:</strong>
+ {sense.derivation.map((dr, i) => (
+ <div key={dr.text + i}>
+ {dr.type}: {dr.text} - {dr.tags}
+ </div>
+ ))}
+ </div>
+ )}
+ {sense.examples.length > 0 && (
+ <Examples data={data} examples={sense.examples} />
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export default Semantic;
+
+function ExamplesTab({
+ data,
+ examples,
+}: {
+ data: FullWordData;
+ examples: Example[];
+}) {
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [generatedExamples, setGeneratedExamples] = useState<any[]>([]);
+
+ const generateExamples = async () => {
+ setIsGenerating(true);
+
+ try {
+ // Get the primary meaning from the first sense
+ const primaryMeaning =
+ data.senses?.[0]?.glosses?.[0] || "unknown meaning";
+
+ const response = await fetch("/api/generate-examples", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ word: data.spelling,
+ meaning: primaryMeaning,
+ examples,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to generate examples");
+ }
+
+ const j = await response.json();
+ setGeneratedExamples(j.examples || []);
+ } catch (err) {
+ console.error("Error generating examples:", err);
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+ return (
+ <div className="">
+ <div className="">
+ <h4 className="">Usage Examples</h4>
+
+ {/* Generate More Button */}
+ <div className="">
+ <button
+ onClick={generateExamples}
+ disabled={isGenerating}
+ className=""
+ >
+ {isGenerating ? (
+ <>
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
+ Generating Examples...
+ </>
+ ) : (
+ <>
+ <IconSparkles size={16} />
+ Generate More Example Sentences
+ </>
+ )}
+ </button>
+ </div>
+
+ {/* Examples Display */}
+ <div className="">
+ {examples.map((example, idx) => (
+ <div
+ key={`original-${idx}`}
+ className="p-3 bg-white rounded border-l-4 border-blue-400"
+ >
+ <p className="text-sm text-gray-700 italic">
+ {example?.text || ""}
+ </p>
+ {example.ref && (
+ <p className="text-xs text-gray-500 mt-1">
+ Source: {example.ref}
+ </p>
+ )}
+ </div>
+ ))}
+
+ {generatedExamples.length > 0 && (
+ <>
+ <h5 className="text-sm font-medium text-gray-700 mb-2 mt-4">
+ AI-Generated Examples:
+ </h5>
+ {generatedExamples.map((example, idx) => (
+ <div
+ key={`generated-${idx}`}
+ className="p-3 bg-white rounded border-l-4 border-green-400"
+ >
+ <p className="text-sm text-gray-800 font-medium mb-1">
+ {example.thai}
+ </p>
+ <p className="text-sm text-gray-600 mb-1">
+ {example.english}
+ </p>
+ {example.context && (
+ <p className="text-xs text-gray-500 italic">
+ Context: {example.context}
+ </p>
+ )}
+ </div>
+ ))}
+ </>
+ )}
+
+ {/* No Examples */}
+ {!moreExamples?.length && !generatedExamples.length && (
+ <div className="p-3 bg-white rounded border-l-4 border-orange-400">
+ <p className="text-sm text-gray-600 italic">
+ No examples available for this word. Click "Generate More
+ Example Sentences" to get AI-generated examples.
+ </p>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/prosody-ui/src/fonts/FontChanger.tsx b/packages/prosody-ui/src/fonts/FontChanger.tsx
index 15c932e..9ae9c84 100644
--- a/packages/prosody-ui/src/fonts/FontChanger.tsx
+++ b/packages/prosody-ui/src/fonts/FontChanger.tsx
@@ -1,64 +1,68 @@
import React, { useEffect, useState, type ReactNode } from "react";
import fontIcon from "../assets/icons/font.svg";
-import { getScriptPredictor } from "glotscript";
+import { getScriptPredictor, type ISO_15924_CODE } from "@sortug/langlib";
import ThaiFontLoader from "./Thai";
+import HanFontLoader from "./Hani";
+import LatnFontLoader from "./Latn";
+import JpanFontLoader from "./Jpan";
-function FontChanger({ text }: { text: string }) {
- const [script, setScript] = useState<string | null>(null);
+function findFontCount(lang: ISO_15924_CODE): number {
+ if (lang === "Thai") return 7;
+ if (lang === "Jpan") return 6;
+ // TODO get more latin fonts
+ if (lang === "Latn") return 1;
+ if ((lang as any) === "IPA") return 6;
+ if (lang.startsWith("Han")) return 23;
+ return 0;
+}
+
+function FontChanger({
+ text,
+ script,
+ children,
+}: {
+ text: string;
+ script?: ISO_15924_CODE;
+ lang?: string;
+ children: ReactNode;
+}) {
+ const [script2, setScript] = useState<ISO_15924_CODE | null>(script || null);
useEffect(() => {
+ if (script) return;
const predictor = getScriptPredictor();
const res = predictor(text);
console.log("script predicted", res);
- setScript(res[0]);
+ const rescript: ISO_15924_CODE | null = res[0];
+ if (!rescript) {
+ console.error("script undetected", text);
+ return;
+ }
+ setScript(rescript);
+ setFontCount(findFontCount(rescript));
}, [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);
}
+ if (!script2)
+ return <div className="error">Couldn't detect script of {text}</div>;
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 className={`font-changer font-${script}-${fontIdx}`}>
+ <img className="font-icon cp" onClick={changeFont} src={fontIcon} />
+ {script2 === "Thai" ? (
+ <ThaiFontLoader>{children}</ThaiFontLoader>
+ ) : script2.startsWith("Han") ? (
+ <HanFontLoader>{children}</HanFontLoader>
+ ) : script2 === "Jpan" ? (
+ <JpanFontLoader>{children}</JpanFontLoader>
+ ) : script2 === "Latn" ? (
+ <LatnFontLoader>{children}</LatnFontLoader>
+ ) : 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/Jpan.tsx b/packages/prosody-ui/src/fonts/Jpan.tsx
new file mode 100644
index 0000000..f9cc602
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/Jpan.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/Latn.tsx b/packages/prosody-ui/src/fonts/Latn.tsx
new file mode 100644
index 0000000..f9cc602
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/Latn.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
index 0048316..62b886b 100644
--- a/packages/prosody-ui/src/fonts/Thai.tsx
+++ b/packages/prosody-ui/src/fonts/Thai.tsx
@@ -1,8 +1,8 @@
-import React, { useState, type ReactNode } from "react";
+import { type ReactNode } from "react";
import "../assets/fonts/Thai/style.css";
-function ThaiFontLoader({ text }: { text: string }) {
- return <div>{text}</div>;
+function ThaiFontLoader({ children }: { children: ReactNode }) {
+ return <>{children}</>;
}
export default ThaiFontLoader;
diff --git a/packages/prosody-ui/src/fonts/useLangFont.tsx b/packages/prosody-ui/src/fonts/useLangFont.tsx
index 36fa603..5467b18 100644
--- a/packages/prosody-ui/src/fonts/useLangFont.tsx
+++ b/packages/prosody-ui/src/fonts/useLangFont.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState, type ReactNode } from "react";
import fontIcon from "../assets/icons/font.svg";
-import { getScriptPredictor } from "glotscript";
+import { getScriptPredictor } from "@sortug/langlib";
function useLangFont({ text }: { text: string }) {
useEffect(() => {
diff --git a/packages/prosody-ui/src/latin/LatinText.tsx b/packages/prosody-ui/src/latin/LatinText.tsx
index e5b13ff..073baff 100644
--- a/packages/prosody-ui/src/latin/LatinText.tsx
+++ b/packages/prosody-ui/src/latin/LatinText.tsx
@@ -30,7 +30,7 @@ export default function LatinText({
}: {
text: string;
lang: string;
- openWord?: (word: string) => void;
+ openWord?: (word: AnalyzeRes) => void;
}) {
useEffect(() => {
const sentences = segmentate(text, lang, "sentence");
@@ -55,7 +55,7 @@ function Sentence({
text: string;
lang: string;
- openWord?: (word: string) => void;
+ openWord?: (word: AnalyzeRes) => void;
}) {
useEffect(() => {
const w = segmentate(text, lang, "word");
diff --git a/packages/prosody-ui/src/logic/stanza.ts b/packages/prosody-ui/src/logic/stanza.ts
index 9e59450..b74a064 100644
--- a/packages/prosody-ui/src/logic/stanza.ts
+++ b/packages/prosody-ui/src/logic/stanza.ts
@@ -1,4 +1,4 @@
-import type { AsyncRes, Result } from "sortug";
+import type { AsyncRes, Result } from "@sortug/lib";
const ENDPOINT = "http://localhost:8102";
export async function segmenter(text: string, lang: string) {
diff --git a/packages/prosody-ui/src/logic/types.ts b/packages/prosody-ui/src/logic/types.ts
index ac308cf..cdae30e 100644
--- a/packages/prosody-ui/src/logic/types.ts
+++ b/packages/prosody-ui/src/logic/types.ts
@@ -46,3 +46,6 @@ export type WordData = {
meanings: Meaning[];
references?: any;
};
+
+export type ColorTheme = "light" | "dark";
+export type LangToColor<T> = { display: string; colorBy: string; data: T };
diff --git a/packages/prosody-ui/src/logic/utils.ts b/packages/prosody-ui/src/logic/utils.ts
index 737a6ec..90b2e1e 100644
--- a/packages/prosody-ui/src/logic/utils.ts
+++ b/packages/prosody-ui/src/logic/utils.ts
@@ -1,4 +1,4 @@
-import type { Result } from "sortug";
+import type { Result } from "@sortug/lib";
export function detectScript(text: string): Result<string> {
const scripts = {
diff --git a/packages/prosody-ui/src/logic/wiki.ts b/packages/prosody-ui/src/logic/wiki.ts
index 1325c0f..d3c56ee 100644
--- a/packages/prosody-ui/src/logic/wiki.ts
+++ b/packages/prosody-ui/src/logic/wiki.ts
@@ -1,4 +1,4 @@
-import type { AsyncRes, Result } from "sortug";
+import type { AsyncRes, Result } from "@sortug/lib";
import type { Meaning } from "./types";
export function buildWiktionaryURL(word: string) {
diff --git a/packages/prosody-ui/src/styles/styles.css b/packages/prosody-ui/src/styles/styles.css
index 69351f1..e98d1f0 100644
--- a/packages/prosody-ui/src/styles/styles.css
+++ b/packages/prosody-ui/src/styles/styles.css
@@ -149,6 +149,7 @@
#word-modal {
position: relative;
+ height: 80vh;
& .font-icon {
position: absolute;
@@ -225,42 +226,6 @@ img {
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;
}
diff --git a/packages/prosody-ui/src/thai/ThaiText.tsx b/packages/prosody-ui/src/thai/ThaiText.tsx
index fc1e1e6..794804a 100644
--- a/packages/prosody-ui/src/thai/ThaiText.tsx
+++ b/packages/prosody-ui/src/thai/ThaiText.tsx
@@ -1,49 +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 type { AnalyzeRes, ColorTheme, LangToColor } from "../logic/types";
import { ColoredText } from "../components/Sentence";
import Word from "../components/Word";
export default function ThaiText({
text,
openWord,
+ theme,
}: {
text: string;
- openWord: (s: string) => void;
+ openWord: (s: AnalyzeRes) => void;
+ theme: ColorTheme;
}) {
useEffect(() => {
pythonseg();
}, [text]);
- const [data, setData] = useState<Record<string, AnalyzeRes>>({});
+ const [data, setData] = useState<Array<LangToColor<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);
+ const ob = s2.ok.reduce(
+ (acc, item) => {
+ acc[item.word] = item;
+ return acc;
+ },
+ {} as Record<string, AnalyzeRes>,
+ );
+ const d = Object.values(ob).map((w) => ({
+ data: w,
+ colorBy: w.pos,
+ display: w.word,
+ }));
+ setData(d);
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} />
+ <ColoredText lang="tha" theme={theme} frags={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
index 031bf4c..dc6ed23 100644
--- a/packages/prosody-ui/src/thai/logic/thainlp.ts
+++ b/packages/prosody-ui/src/thai/logic/thainlp.ts
@@ -1,4 +1,4 @@
-import type { AsyncRes } from "sortug";
+import type { AsyncRes } from "@sortug/lib";
import type { AnalyzeRes } from "../../logic/types";
const ENDPOINT = "http://192.168.1.110:8001";
@@ -24,7 +24,7 @@ export async function segmentateThai(sentence: string): AsyncRes<AnalyzeRes[]> {
return await call("/segmentate", { word: sentence });
}
-export const POSMAP: Record<string, string> = {
+export const POSMAP = {
ADJ: "Adjective",
ADP: "Adposition",
ADV: "Adverb",
@@ -87,4 +87,8 @@ export const POSMAP: Record<string, string> = {
EITT: "Ending for interrogative sentence",
NEG: "Negator",
PUNC: "Punctuation",
-};
+} as const;
+type POSTYPE = typeof POSMAP;
+
+export type POS_CODE = keyof POSTYPE;
+export type POS = POSTYPE[POS_CODE];
diff --git a/packages/prosody-ui/src/themes/ThemeSwitcher.tsx b/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
new file mode 100644
index 0000000..bae617f
--- /dev/null
+++ b/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
@@ -0,0 +1,130 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from "react";
+import { themes, type Theme, type ThemeName } from "./themes";
+
+interface ThemeContextType {
+ theme: Theme;
+ themeName: ThemeName;
+ setTheme: (name: ThemeName) => void;
+ availableThemes: ThemeName[];
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+interface ThemeProviderProps {
+ children: ReactNode;
+ defaultTheme?: ThemeName;
+}
+
+export const ThemeProvider: React.FC<ThemeProviderProps> = ({
+ children,
+ defaultTheme = "light",
+}) => {
+ const [themeName, setThemeName] = useState<ThemeName>(() => {
+ const savedTheme = localStorage.getItem("theme") as ThemeName;
+ if (savedTheme && themes[savedTheme]) {
+ return savedTheme;
+ }
+
+ if (
+ window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches
+ ) {
+ return "dark";
+ }
+
+ return defaultTheme;
+ });
+
+ const theme = themes[themeName];
+
+ useEffect(() => {
+ const root = document.documentElement;
+
+ root.setAttribute("data-theme", themeName);
+
+ // Set color variables
+ Object.entries(theme.colors).forEach(([key, value]) => {
+ const cssVarName = `--color-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set typography variables
+ Object.entries(theme.typography).forEach(([key, value]) => {
+ const cssVarName = `--${key
+ .replace(/([A-Z])/g, "-$1")
+ .toLowerCase()
+ .replace("font-", "font-")
+ .replace("size", "")
+ .replace("weight", "")}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set spacing variables
+ Object.entries(theme.spacing).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set radius variables
+ Object.entries(theme.radius).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set transition variables
+ Object.entries(theme.transitions).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Legacy variables for backward compatibility
+ root.style.setProperty("--text-color", theme.colors.text);
+ root.style.setProperty("--background-color", theme.colors.background);
+
+ localStorage.setItem("theme", themeName);
+ }, [themeName, theme]);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const handleChange = (e: MediaQueryListEvent) => {
+ const savedTheme = localStorage.getItem("theme");
+ if (!savedTheme) {
+ setThemeName(e.matches ? "dark" : "light");
+ }
+ };
+
+ mediaQuery.addEventListener("change", handleChange);
+ return () => mediaQuery.removeEventListener("change", handleChange);
+ }, []);
+
+ const setTheme = (name: ThemeName) => {
+ if (themes[name]) {
+ setThemeName(name);
+ }
+ };
+
+ const value: ThemeContextType = {
+ theme,
+ themeName,
+ setTheme,
+ availableThemes: Object.keys(themes) as ThemeName[],
+ };
+
+ return (
+ <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
+ );
+};
+
+export const useTheme = (): ThemeContextType => {
+ const context = useContext(ThemeContext);
+ if (context === undefined) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+ return context;
+};
diff --git a/packages/prosody-ui/src/themes/themes.ts b/packages/prosody-ui/src/themes/themes.ts
new file mode 100644
index 0000000..5637c97
--- /dev/null
+++ b/packages/prosody-ui/src/themes/themes.ts
@@ -0,0 +1,321 @@
+export type ThemeName =
+ | "light"
+ | "dark"
+ | "sepia"
+ | "noir"
+ | "ocean"
+ | "forest"
+ | "gruvbox";
+
+export interface ThemeColors {
+ primary: string;
+ primaryHover: string;
+ secondary: string;
+ accent: string;
+ accentHover: string;
+ background: string;
+ surface: string;
+ surfaceHover: string;
+ text: string;
+ textSecondary: string;
+ textMuted: string;
+ border: string;
+ borderLight: string;
+ success: string;
+ warning: string;
+ error: string;
+ info: string;
+ link: string;
+ linkHover: string;
+ shadow: string;
+ overlay: string;
+}
+
+export interface ThemeTypography {
+ fontSizeXs: string;
+ fontSizeSm: string;
+ fontSizeMd: string;
+ fontSizeLg: string;
+ fontSizeXl: string;
+ fontWeightNormal: string;
+ fontWeightMedium: string;
+ fontWeightSemibold: string;
+ fontWeightBold: string;
+}
+
+export interface ThemeSpacing {
+ spacingXs: string;
+ spacingSm: string;
+ spacingMd: string;
+ spacingLg: string;
+ spacingXl: string;
+}
+
+export interface ThemeRadius {
+ radiusSm: string;
+ radiusMd: string;
+ radiusLg: string;
+ radiusFull: string;
+}
+
+export interface ThemeTransitions {
+ transitionFast: string;
+ transitionNormal: string;
+ transitionSlow: string;
+}
+
+export interface Theme {
+ name: ThemeName;
+ colors: ThemeColors;
+ typography: ThemeTypography;
+ spacing: ThemeSpacing;
+ radius: ThemeRadius;
+ transitions: ThemeTransitions;
+}
+
+// Common theme properties
+const commonTypography: ThemeTypography = {
+ fontSizeXs: "0.75rem",
+ fontSizeSm: "0.875rem",
+ fontSizeMd: "1rem",
+ fontSizeLg: "1.125rem",
+ fontSizeXl: "1.25rem",
+ fontWeightNormal: "400",
+ fontWeightMedium: "500",
+ fontWeightSemibold: "600",
+ fontWeightBold: "700",
+};
+
+const commonSpacing: ThemeSpacing = {
+ spacingXs: "0.25rem",
+ spacingSm: "0.5rem",
+ spacingMd: "1rem",
+ spacingLg: "1.5rem",
+ spacingXl: "2rem",
+};
+
+const commonRadius: ThemeRadius = {
+ radiusSm: "0.25rem",
+ radiusMd: "0.5rem",
+ radiusLg: "0.75rem",
+ radiusFull: "9999px",
+};
+
+const commonTransitions: ThemeTransitions = {
+ transitionFast: "150ms ease",
+ transitionNormal: "250ms ease",
+ transitionSlow: "350ms ease",
+};
+
+export const themes: Record<ThemeName, Theme> = {
+ light: {
+ name: "light",
+ colors: {
+ primary: "#543fd7",
+ primaryHover: "#4532b8",
+ secondary: "#f39c12",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#ffffff",
+ surface: "#f8f9fa",
+ surfaceHover: "#e9ecef",
+ text: "#212529",
+ textSecondary: "#495057",
+ textMuted: "#6c757d",
+ border: "#dee2e6",
+ borderLight: "#e9ecef",
+ success: "#28a745",
+ warning: "#ffc107",
+ error: "#dc3545",
+ info: "#17a2b8",
+ link: "#543fd7",
+ linkHover: "#4532b8",
+ shadow: "rgba(0, 0, 0, 0.1)",
+ overlay: "rgba(0, 0, 0, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ dark: {
+ name: "dark",
+ colors: {
+ primary: "#7c6ef7",
+ primaryHover: "#9085f9",
+ secondary: "#f39c12",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#0d1117",
+ surface: "#161b22",
+ surfaceHover: "#21262d",
+ text: "#c9d1d9",
+ textSecondary: "#8b949e",
+ textMuted: "#6e7681",
+ border: "#30363d",
+ borderLight: "#21262d",
+ success: "#3fb950",
+ warning: "#d29922",
+ error: "#f85149",
+ info: "#58a6ff",
+ link: "#58a6ff",
+ linkHover: "#79b8ff",
+ shadow: "rgba(0, 0, 0, 0.3)",
+ overlay: "rgba(0, 0, 0, 0.7)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ sepia: {
+ name: "sepia",
+ colors: {
+ primary: "#8b4513",
+ primaryHover: "#6b3410",
+ secondary: "#d2691e",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#f4e8d0",
+ surface: "#ede0c8",
+ surfaceHover: "#e6d9c0",
+ text: "#3e2723",
+ textSecondary: "#5d4037",
+ textMuted: "#6d4c41",
+ border: "#d7ccc8",
+ borderLight: "#e0d5d0",
+ success: "#689f38",
+ warning: "#ff9800",
+ error: "#d32f2f",
+ info: "#0288d1",
+ link: "#8b4513",
+ linkHover: "#6b3410",
+ shadow: "rgba(62, 39, 35, 0.1)",
+ overlay: "rgba(62, 39, 35, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ noir: {
+ name: "noir",
+ colors: {
+ primary: "#ffffff",
+ primaryHover: "#e0e0e0",
+ secondary: "#808080",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#000000",
+ surface: "#0a0a0a",
+ surfaceHover: "#1a1a1a",
+ text: "#ffffff",
+ textSecondary: "#b0b0b0",
+ textMuted: "#808080",
+ border: "#333333",
+ borderLight: "#1a1a1a",
+ success: "#4caf50",
+ warning: "#ff9800",
+ error: "#f44336",
+ info: "#2196f3",
+ link: "#b0b0b0",
+ linkHover: "#ffffff",
+ shadow: "rgba(255, 255, 255, 0.1)",
+ overlay: "rgba(0, 0, 0, 0.9)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ ocean: {
+ name: "ocean",
+ colors: {
+ primary: "#006994",
+ primaryHover: "#005577",
+ secondary: "#00acc1",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#e1f5fe",
+ surface: "#b3e5fc",
+ surfaceHover: "#81d4fa",
+ text: "#01579b",
+ textSecondary: "#0277bd",
+ textMuted: "#4fc3f7",
+ border: "#81d4fa",
+ borderLight: "#b3e5fc",
+ success: "#00c853",
+ warning: "#ffab00",
+ error: "#d50000",
+ info: "#00b0ff",
+ link: "#0277bd",
+ linkHover: "#01579b",
+ shadow: "rgba(1, 87, 155, 0.1)",
+ overlay: "rgba(1, 87, 155, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ forest: {
+ name: "forest",
+ colors: {
+ primary: "#2e7d32",
+ primaryHover: "#1b5e20",
+ secondary: "#689f38",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#f1f8e9",
+ surface: "#dcedc8",
+ surfaceHover: "#c5e1a5",
+ text: "#1b5e20",
+ textSecondary: "#33691e",
+ textMuted: "#558b2f",
+ border: "#aed581",
+ borderLight: "#c5e1a5",
+ success: "#4caf50",
+ warning: "#ff9800",
+ error: "#f44336",
+ info: "#03a9f4",
+ link: "#388e3c",
+ linkHover: "#2e7d32",
+ shadow: "rgba(27, 94, 32, 0.1)",
+ overlay: "rgba(27, 94, 32, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ gruvbox: {
+ name: "gruvbox",
+ colors: {
+ primary: "#fe8019",
+ primaryHover: "#d65d0e",
+ secondary: "#fabd2f",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#282828",
+ surface: "#3c3836",
+ surfaceHover: "#504945",
+ text: "#ebdbb2",
+ textSecondary: "#d5c4a1",
+ textMuted: "#bdae93",
+ border: "#665c54",
+ borderLight: "#504945",
+ success: "#b8bb26",
+ warning: "#fabd2f",
+ error: "#fb4934",
+ info: "#83a598",
+ link: "#8ec07c",
+ linkHover: "#b8bb26",
+ shadow: "rgba(0, 0, 0, 0.3)",
+ overlay: "rgba(40, 40, 40, 0.8)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+};
diff --git a/packages/prosody-ui/src/zoom/FullText.tsx b/packages/prosody-ui/src/zoom/FullText.tsx
index ec85f09..9b7fe63 100644
--- a/packages/prosody-ui/src/zoom/FullText.tsx
+++ b/packages/prosody-ui/src/zoom/FullText.tsx
@@ -3,7 +3,7 @@ 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";
+import { NLP } from "@sortug/ai";
interface TextFocusMorphProps {
text: string;
diff --git a/packages/prosody-ui/src/zoom/Paragraph.tsx b/packages/prosody-ui/src/zoom/Paragraph.tsx
index c26f806..b149468 100644
--- a/packages/prosody-ui/src/zoom/Paragraph.tsx
+++ b/packages/prosody-ui/src/zoom/Paragraph.tsx
@@ -1,7 +1,7 @@
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 { NLP } from "@sortug/ai";
import Sentence from "./Sentence";
import { paragraphVariants, createHoverEffect } from "./animations";
import { useZoom } from "./hooks/useZoom";
diff --git a/packages/prosody-ui/src/zoom/Sentence.tsx b/packages/prosody-ui/src/zoom/Sentence.tsx
index 1d90346..fc75773 100644
--- a/packages/prosody-ui/src/zoom/Sentence.tsx
+++ b/packages/prosody-ui/src/zoom/Sentence.tsx
@@ -1,7 +1,7 @@
import React, { memo } from "react";
import { motion } from "motion/react";
import type { ViewProps, LoadingStatus } from "./logic/types";
-import { NLP } from "sortug-ai";
+import { NLP } from "@sortug/ai";
import SpacyClause from "./SpacyClause";
import { sentenceVariants, createHoverEffect } from "./animations";
import { useZoom } from "./hooks/useZoom";
diff --git a/packages/prosody-ui/src/zoom/SpacyClause.tsx b/packages/prosody-ui/src/zoom/SpacyClause.tsx
index 6b6f178..c08a291 100644
--- a/packages/prosody-ui/src/zoom/SpacyClause.tsx
+++ b/packages/prosody-ui/src/zoom/SpacyClause.tsx
@@ -1,7 +1,7 @@
import React, { memo, useState } from "react";
import { motion } from "motion/react";
import "./spacy.css";
-import { NLP } from "sortug-ai";
+import { NLP } from "@sortug/ai";
// import { clauseVariants, createHoverEffect } from "./animations";
// import { useZoom } from "./hooks/useZoom";
diff --git a/packages/prosody-ui/src/zoom/logic/types.ts b/packages/prosody-ui/src/zoom/logic/types.ts
index bea68ff..fd72601 100644
--- a/packages/prosody-ui/src/zoom/logic/types.ts
+++ b/packages/prosody-ui/src/zoom/logic/types.ts
@@ -1,4 +1,4 @@
-import type { NLP } from "sortug-ai";
+import type { NLP } from "@sortug/ai";
export type ViewLevel =
| "text"
diff --git a/packages/prosody-ui/tsconfig.json b/packages/prosody-ui/tsconfig.json
index 238655f..5a512ba 100644
--- a/packages/prosody-ui/tsconfig.json
+++ b/packages/prosody-ui/tsconfig.json
@@ -22,6 +22,9 @@
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
- "noPropertyAccessFromIndexSignature": false
+ "noPropertyAccessFromIndexSignature": false,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
}
}
diff --git a/packages/sortug/bun.lock b/packages/sortug/bun.lock
deleted file mode 100644
index d50feb5..0000000
--- a/packages/sortug/bun.lock
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "lockfileVersion": 1,
- "workspaces": {
- "": {
- "name": "sortug",
- "devDependencies": {
- "@types/bun": "latest",
- },
- "peerDependencies": {
- "typescript": "^5",
- },
- },
- },
- "packages": {
- "@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
-
- "@types/node": ["@types/node@24.0.1", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw=="],
-
- "bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
-
- "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
-
- "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
- }
-}
diff --git a/packages/sortug/index.ts b/packages/sortug/index.ts
index 039454f..0ab7ee4 100644
--- a/packages/sortug/index.ts
+++ b/packages/sortug/index.ts
@@ -1,4 +1,2 @@
export type * from "./src/types";
export * from "./src/utils";
-import styles from "./src/styles.module.css";
-export { styles };
diff --git a/packages/prosody-ui/src/sortug.css b/packages/sortug/src/styles.css
index c6280c0..375e620 100644
--- a/packages/prosody-ui/src/sortug.css
+++ b/packages/sortug/src/styles.css
@@ -1,4 +1,3 @@
-
/* SORTUG CSS */
/* variables */
:root {
@@ -234,7 +233,7 @@ button,
/* Modal Content */
#modal-fg {
- background-color: var(--bg);
+ background-color: var(--modal-bg);
position: fixed;
top: 50%;
left: 50%;
@@ -245,4 +244,4 @@ button,
min-height: 20vh;
max-width: 90vw;
overflow: auto;
-}
+} \ No newline at end of file
diff --git a/packages/sortug/src/utils.ts b/packages/sortug/src/utils.ts
index 687b8db..43fac8a 100644
--- a/packages/sortug/src/utils.ts
+++ b/packages/sortug/src/utils.ts
@@ -1,25 +1,45 @@
+export function cycleNext(current: number, max: number) {
+ if (current + 1 === max) return 0;
+ else return current + 1;
+}
+export function cyclePrev(current: number, max: number) {
+ if (current === 0) return max;
+ else return current - 1;
+}
+
export function randomFromArray<T>(a: Array<T>): T {
const l = a.length;
- const ind = Math.floor(Math.random() * l);
- if (ind === l) return a[ind - 1];
- else return a[ind];
+ if (l === 0) throw new Error("Empty array!");
+ const randomIdx = Math.floor(Math.random() * l);
+ const idx = randomIdx === l ? randomIdx - 1 : randomIdx;
+ const el = a[idx]!;
+ return el;
}
-export function randomFromArrayAcc<T>(a: Array<T>, s?: Set<T>): T {
- const st = s ? s : new Set(a);
- const l = a.length;
- const ind = Math.floor(Math.random() * l);
- const res = ind === l ? a[ind - 1] : a[ind];
- if (st.has(res)) return randomFromArrayAcc(a, st);
- else {
- st.add(res);
- // TODO have to return this too?
- return res;
- }
+export function randomFromArrayMany<T>(
+ a: Array<T>,
+ count: number,
+ canRepeat = false,
+): Array<T> {
+ if (canRepeat) return [...new Array(count)].map((s) => randomFromArray(a));
+ else return randomFromArrayNoRepeat(a, count);
+}
+export function randomFromArrayNoRepeat<T>(
+ a: Array<T>,
+ count: number,
+ acc?: Set<T>,
+): T[] {
+ const all = new Set(a);
+ const st = acc ? acc : new Set<T>();
+ if (all.size === st.size || st.size === count) return [...st];
+ const el = randomFromArray(a);
+ if (!st.has(el)) st.add(el);
+ return randomFromArrayNoRepeat(a, count, st);
}
+
export function notRandomFromArray<T>(data: string, a: Array<T>): T {
const l = a.length;
const ind = hashTextToNumber(data, l - 1);
- return a[ind];
+ return a[ind]!;
}
function hashTextToNumber(text: string, max: number): number {
diff --git a/packages/tweetdeck/package.json b/packages/tweetdeck/package.json
index bc9a71c..b7ce11a 100644
--- a/packages/tweetdeck/package.json
+++ b/packages/tweetdeck/package.json
@@ -9,14 +9,16 @@
"start": "NODE_ENV=production bun src/index.ts"
},
"dependencies": {
+ "@sortug/ai": "workspace:*",
+ "@sortug/lib": "workspace:*",
+ "@sortug/prosody-ui": "workspace:*",
+ "@sortug/sorlang-db": "workspace:*",
"bun_python": "^0.1.10",
"lucide-react": "latest",
"node-html-parser": "^7.0.1",
"react": "^19",
"react-dom": "^19",
- "@sortug/lib": "workspce:*",
- "@sortug/prosody-ui": "workspce:*",
- "@sortug/ai": "workspce:*"
+ "react-hot-toast": "^2.6.0"
},
"devDependencies": {
"@types/react": "^19",
diff --git a/packages/tweetdeck/src/App.tsx b/packages/tweetdeck/src/App.tsx
index 924ff9a..44b6405 100644
--- a/packages/tweetdeck/src/App.tsx
+++ b/packages/tweetdeck/src/App.tsx
@@ -1,310 +1,15 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
-import "./styles/normalize.css";
import "./styles/index.css";
-import { Sidebar, type NewAccountInput } from "./components/Sidebar";
-import { ColumnBoard } from "./components/ColumnBoard";
-import { AddColumnModal } from "./components/AddColumnModal";
-import { usePersistentState } from "./hooks/usePersistentState";
-import type {
- ColumnSnapshot,
- ColumnState,
- DeckAccount,
- DeckColumn,
- DeckListsCache,
- FullscreenState,
-} from "./types/app";
-import type { Tweet } from "./lib/fetching/types";
-import { generateId } from "./lib/utils/id";
-import { twitterClient } from "./lib/client/twitterClient";
-import { FullscreenColumn } from "./components/FullscreenColumn";
-
-const ACCOUNTS_KEY = "tweetdeck.accounts";
-const COLUMNS_KEY = "tweetdeck.columns";
+import "./styles/normalize.css";
+import { Toaster } from "react-hot-toast";
+import Deck from "./pages/Deck";
+import Test from "./Test";
export function App() {
- const [accounts, setAccounts] = usePersistentState<DeckAccount[]>(
- ACCOUNTS_KEY,
- [],
- );
- const [columns, setColumns] = usePersistentState<DeckColumn[]>(
- COLUMNS_KEY,
- [],
- );
- const [listsCache, setListsCache] = useState<DeckListsCache>({});
- const [activeAccountId, setActiveAccountId] = useState<string | undefined>(
- () => accounts[0]?.id,
- );
- const [isModalOpen, setModalOpen] = useState(false);
- const [toast, setToast] = useState<string | null>(null);
- const [fullscreen, setFullscreen] = useState<FullscreenState | null>(null);
- const [columnSnapshots, setColumnSnapshots] = useState<
- Record<string, ColumnSnapshot>
- >({});
-
- useEffect(() => {
- if (!activeAccountId) {
- const firstAccount = accounts[0];
- if (firstAccount) {
- setActiveAccountId(firstAccount.id);
- }
- }
- }, [accounts, activeAccountId]);
-
- useEffect(() => {
- const acs = accounts.filter((a) => !a.avatar || !a.username);
- console.log({ acs });
- const nacs = acs.map(async (acc) => {
- const our = await twitterClient.own({ cookie: acc.cookie });
- const nacc = {
- ...acc,
- handle: our.username,
- label: our.name,
- avatar: our.avatar,
- };
- return nacc;
- });
- Promise.all(nacs)
- .then((acs) => setAccounts(acs))
- .catch((err) => console.error(err));
- }, []);
-
- const handleAddAccount = useCallback(
- (payload: NewAccountInput) => {
- const label = `Session ${accounts.length + 1}`;
- const account: DeckAccount = {
- id: generateId(),
- label,
- accent: randomAccent(),
- cookie: payload.cookie.trim(),
- createdAt: Date.now(),
- };
- setAccounts((prev) => [...prev, account]);
- setActiveAccountId(account.id);
- setToast(`${account.label} is ready`);
- },
- [accounts.length, setAccounts],
- );
-
- const handleRemoveAccount = useCallback(
- (accountId: string) => {
- setAccounts((prev) => prev.filter((account) => account.id !== accountId));
- setColumns((prev) =>
- prev.filter((column) => column.accountId !== accountId),
- );
- setListsCache((prev) => {
- const next = { ...prev };
- delete next[accountId];
- return next;
- });
- if (activeAccountId === accountId) {
- setActiveAccountId(undefined);
- }
- },
- [activeAccountId, setAccounts, setColumns],
- );
-
- const handleAddColumn = useCallback(
- (column: Omit<DeckColumn, "id">) => {
- const nextColumn = { ...column, id: generateId() };
- setColumns((prev) => [...prev, nextColumn]);
- setToast(`${nextColumn.title} added to deck`);
- },
- [setColumns],
- );
-
- const handleRemoveColumn = useCallback(
- (id: string) => {
- setColumns((prev) => prev.filter((column) => column.id !== id));
- },
- [setColumns],
- );
-
- const fetchLists = useCallback(
- async (accountId: string) => {
- const account = accounts.find((acc) => acc.id === accountId);
- if (!account) throw new Error("Account not found");
- if (listsCache[accountId]) return listsCache[accountId];
- console.log({ listsCache });
- const lists = await twitterClient.lists({ cookie: account.cookie });
- console.log({ lists });
- setListsCache((prev) => ({ ...prev, [accountId]: lists }));
- return lists;
- },
- [accounts, listsCache],
- );
-
- const handleColumnStateChange = useCallback(
- (columnId: string, state: ColumnState) => {
- setColumns((prev) =>
- prev.map((column) =>
- column.id === columnId ? { ...column, state } : column,
- ),
- );
- },
- [setColumns],
- );
-
- const handleColumnSnapshot = useCallback(
- (columnId: string, snapshot: ColumnSnapshot) => {
- setColumnSnapshots((prev) => {
- const existing = prev[columnId];
- if (
- existing &&
- existing.tweets === snapshot.tweets &&
- existing.label === snapshot.label
- ) {
- return prev;
- }
- return {
- ...prev,
- [columnId]: { tweets: snapshot.tweets, label: snapshot.label },
- };
- });
- },
- [],
- );
-
- const openFullscreen = useCallback(
- (payload: FullscreenState) => {
- const snapshot = columnSnapshots[payload.column.id];
- setFullscreen({
- ...payload,
- tweets: snapshot?.tweets ?? payload.tweets,
- columnLabel: snapshot?.label ?? payload.columnLabel,
- });
- },
- [columnSnapshots],
- );
-
- useEffect(() => {
- if (!fullscreen) return;
- const snapshot = columnSnapshots[fullscreen.column.id];
- if (!snapshot) return;
- if (
- snapshot.tweets === fullscreen.tweets &&
- snapshot.label === fullscreen.columnLabel
- ) {
- return;
- }
- setFullscreen((prev) => {
- if (!prev) return prev;
- if (prev.column.id !== fullscreen.column.id) return prev;
- const tweets = snapshot.tweets;
- const index = Math.min(prev.index, Math.max(tweets.length - 1, 0));
- return {
- ...prev,
- tweets,
- columnTitle: snapshot.label,
- index,
- };
- });
- }, [columnSnapshots, fullscreen]);
-
- const content = useMemo(
- () => (
- <ColumnBoard
- columns={columns}
- accounts={accounts}
- onRemove={handleRemoveColumn}
- onStateChange={handleColumnStateChange}
- onSnapshot={handleColumnSnapshot}
- onEnterFullscreen={openFullscreen}
- />
- ),
- [
- accounts,
- columns,
- handleRemoveColumn,
- handleColumnStateChange,
- handleColumnSnapshot,
- openFullscreen,
- ],
- );
-
return (
- <div className="app-shell">
- <Sidebar
- accounts={accounts}
- activeAccountId={activeAccountId}
- onActivate={(id) => setActiveAccountId(id)}
- onAddAccount={handleAddAccount}
- onRemoveAccount={handleRemoveAccount}
- onAddColumn={() => setModalOpen(true)}
- />
-
- <main>{content}</main>
-
- <AddColumnModal
- accounts={accounts}
- activeAccountId={activeAccountId}
- isOpen={isModalOpen}
- onClose={() => setModalOpen(false)}
- onAdd={handleAddColumn}
- fetchLists={fetchLists}
- listsCache={listsCache}
- />
-
- {toast && (
- <div className="toast" onAnimationEnd={() => setToast(null)}>
- {toast}
- </div>
- )}
-
- {fullscreen && (
- <FullscreenColumn
- state={fullscreen}
- onExit={() => setFullscreen(null)}
- onNavigate={(step) => {
- setFullscreen((prev) => {
- if (!prev) return prev;
- if (!prev.tweets.length) return prev;
- const nextIndex = Math.min(
- prev.tweets.length - 1,
- Math.max(0, prev.index + step),
- );
- if (nextIndex === prev.index) return prev;
- return { ...prev, index: nextIndex };
- });
- }}
- hasPrevColumn={fullscreen.columnIndex > 0}
- hasNextColumn={fullscreen.columnIndex < columns.length - 1}
- onSwitchColumn={(direction) => {
- setFullscreen((prev) => {
- if (!prev) return prev;
- const nextIndex = prev.columnIndex + direction;
- if (nextIndex < 0) return prev;
- if (nextIndex >= columns.length) {
- setModalOpen(true);
- return prev;
- }
- const nextColumn = columns[nextIndex];
- if (!nextColumn) return prev;
- const snapshot = columnSnapshots[nextColumn.id];
- const account = accounts.find(
- (acc) => acc.id === nextColumn.accountId,
- );
- const tweets = snapshot?.tweets ?? [];
- return {
- column: nextColumn,
- columnIndex: nextIndex,
- columnLabel: snapshot?.label ?? nextColumn.title,
- accent: account?.accent ?? prev.accent,
- tweets,
- index: 0,
- };
- });
- }}
- onAddColumn={() => setModalOpen(true)}
- />
- )}
- </div>
+ <>
+ <Test />
+ <Toaster position="top-center" />
+ </>
);
}
-
-function randomAccent(): string {
- const palette = ["#7f5af0", "#2cb67d", "#f25f4c", "#f0a500", "#19a7ce"];
- const pick = palette[Math.floor(Math.random() * palette.length)];
- return pick ?? "#7f5af0";
-}
-
export default App;
diff --git a/packages/tweetdeck/src/Test.tsx b/packages/tweetdeck/src/Test.tsx
new file mode 100644
index 0000000..28de9b9
--- /dev/null
+++ b/packages/tweetdeck/src/Test.tsx
@@ -0,0 +1,19 @@
+import "./styles/normalize.css";
+import "./styles/index.css";
+import { LangText } from "@sortug/prosody-ui";
+import toast from "react-hot-toast";
+
+export function Test() {
+ const text = `อุตุฯ ฉบับ 16 เช็กจังหวัดภาคใต้เจอฝนตกหนักถึงหนักมาก`;
+ return (
+ <div className="app-shell">
+ <LangText
+ lang="th"
+ text={text}
+ theme="dark"
+ handleError={(e) => toast.error(e)}
+ />
+ </div>
+ );
+}
+export default Test;
diff --git a/packages/tweetdeck/src/components/TweetCard.tsx b/packages/tweetdeck/src/components/TweetCard.tsx
index 7cd2936..c9e6219 100644
--- a/packages/tweetdeck/src/components/TweetCard.tsx
+++ b/packages/tweetdeck/src/components/TweetCard.tsx
@@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import { Bookmark, Heart, Link2, MessageCircle, Repeat2 } from "lucide-react";
import type { Tweet } from "../lib/fetching/types";
import { timeAgo } from "../lib/utils/time";
-import { LangText } from "prosody-ui";
+import { LangText } from "@sortug/prosody-ui";
interface TweetCardProps {
tweet: Tweet;
diff --git a/packages/tweetdeck/src/index.ts b/packages/tweetdeck/src/index.ts
index ccc86e7..9daa973 100644
--- a/packages/tweetdeck/src/index.ts
+++ b/packages/tweetdeck/src/index.ts
@@ -1,3 +1,4 @@
+import { handler } from "@sortug/sorlang-db";
import { serve } from "bun";
import index from "./index.html";
import { TwitterApiService } from "./lib/fetching/twitter-api";
@@ -39,7 +40,7 @@ const server = serve({
routes: {
// Serve index.html for all unmatched routes.
"/*": index,
-
+ "/api/db": handler,
"/api/hello": {
async GET(req) {
return Response.json({
diff --git a/packages/tweetdeck/src/pages/Deck.tsx b/packages/tweetdeck/src/pages/Deck.tsx
new file mode 100644
index 0000000..c6fa41a
--- /dev/null
+++ b/packages/tweetdeck/src/pages/Deck.tsx
@@ -0,0 +1,310 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import "../styles/normalize.css";
+import "../styles/index.css";
+import { Sidebar, type NewAccountInput } from "../components/Sidebar";
+import { ColumnBoard } from "../components/ColumnBoard";
+import { AddColumnModal } from "../components/AddColumnModal";
+import { usePersistentState } from "../hooks/usePersistentState";
+import type {
+ ColumnSnapshot,
+ ColumnState,
+ DeckAccount,
+ DeckColumn,
+ DeckListsCache,
+ FullscreenState,
+} from "../types/app";
+import type { Tweet } from "../lib/fetching/types";
+import { generateId } from "../lib/utils/id";
+import { twitterClient } from "../lib/client/twitterClient";
+import { FullscreenColumn } from "../components/FullscreenColumn";
+
+const ACCOUNTS_KEY = "tweetdeck.accounts";
+const COLUMNS_KEY = "tweetdeck.columns";
+
+export function App() {
+ const [accounts, setAccounts] = usePersistentState<DeckAccount[]>(
+ ACCOUNTS_KEY,
+ [],
+ );
+ const [columns, setColumns] = usePersistentState<DeckColumn[]>(
+ COLUMNS_KEY,
+ [],
+ );
+ const [listsCache, setListsCache] = useState<DeckListsCache>({});
+ const [activeAccountId, setActiveAccountId] = useState<string | undefined>(
+ () => accounts[0]?.id,
+ );
+ const [isModalOpen, setModalOpen] = useState(false);
+ const [toast, setToast] = useState<string | null>(null);
+ const [fullscreen, setFullscreen] = useState<FullscreenState | null>(null);
+ const [columnSnapshots, setColumnSnapshots] = useState<
+ Record<string, ColumnSnapshot>
+ >({});
+
+ useEffect(() => {
+ if (!activeAccountId) {
+ const firstAccount = accounts[0];
+ if (firstAccount) {
+ setActiveAccountId(firstAccount.id);
+ }
+ }
+ }, [accounts, activeAccountId]);
+
+ useEffect(() => {
+ const acs = accounts.filter((a) => !a.avatar || !a.username);
+ console.log({ acs });
+ const nacs = acs.map(async (acc) => {
+ const our = await twitterClient.own({ cookie: acc.cookie });
+ const nacc = {
+ ...acc,
+ handle: our.username,
+ label: our.name,
+ avatar: our.avatar,
+ };
+ return nacc;
+ });
+ Promise.all(nacs)
+ .then((acs) => setAccounts(acs))
+ .catch((err) => console.error(err));
+ }, []);
+
+ const handleAddAccount = useCallback(
+ (payload: NewAccountInput) => {
+ const label = `Session ${accounts.length + 1}`;
+ const account: DeckAccount = {
+ id: generateId(),
+ label,
+ accent: randomAccent(),
+ cookie: payload.cookie.trim(),
+ createdAt: Date.now(),
+ };
+ setAccounts((prev) => [...prev, account]);
+ setActiveAccountId(account.id);
+ setToast(`${account.label} is ready`);
+ },
+ [accounts.length, setAccounts],
+ );
+
+ const handleRemoveAccount = useCallback(
+ (accountId: string) => {
+ setAccounts((prev) => prev.filter((account) => account.id !== accountId));
+ setColumns((prev) =>
+ prev.filter((column) => column.accountId !== accountId),
+ );
+ setListsCache((prev) => {
+ const next = { ...prev };
+ delete next[accountId];
+ return next;
+ });
+ if (activeAccountId === accountId) {
+ setActiveAccountId(undefined);
+ }
+ },
+ [activeAccountId, setAccounts, setColumns],
+ );
+
+ const handleAddColumn = useCallback(
+ (column: Omit<DeckColumn, "id">) => {
+ const nextColumn = { ...column, id: generateId() };
+ setColumns((prev) => [...prev, nextColumn]);
+ setToast(`${nextColumn.title} added to deck`);
+ },
+ [setColumns],
+ );
+
+ const handleRemoveColumn = useCallback(
+ (id: string) => {
+ setColumns((prev) => prev.filter((column) => column.id !== id));
+ },
+ [setColumns],
+ );
+
+ const fetchLists = useCallback(
+ async (accountId: string) => {
+ const account = accounts.find((acc) => acc.id === accountId);
+ if (!account) throw new Error("Account not found");
+ if (listsCache[accountId]) return listsCache[accountId];
+ console.log({ listsCache });
+ const lists = await twitterClient.lists({ cookie: account.cookie });
+ console.log({ lists });
+ setListsCache((prev) => ({ ...prev, [accountId]: lists }));
+ return lists;
+ },
+ [accounts, listsCache],
+ );
+
+ const handleColumnStateChange = useCallback(
+ (columnId: string, state: ColumnState) => {
+ setColumns((prev) =>
+ prev.map((column) =>
+ column.id === columnId ? { ...column, state } : column,
+ ),
+ );
+ },
+ [setColumns],
+ );
+
+ const handleColumnSnapshot = useCallback(
+ (columnId: string, snapshot: ColumnSnapshot) => {
+ setColumnSnapshots((prev) => {
+ const existing = prev[columnId];
+ if (
+ existing &&
+ existing.tweets === snapshot.tweets &&
+ existing.label === snapshot.label
+ ) {
+ return prev;
+ }
+ return {
+ ...prev,
+ [columnId]: { tweets: snapshot.tweets, label: snapshot.label },
+ };
+ });
+ },
+ [],
+ );
+
+ const openFullscreen = useCallback(
+ (payload: FullscreenState) => {
+ const snapshot = columnSnapshots[payload.column.id];
+ setFullscreen({
+ ...payload,
+ tweets: snapshot?.tweets ?? payload.tweets,
+ columnLabel: snapshot?.label ?? payload.columnLabel,
+ });
+ },
+ [columnSnapshots],
+ );
+
+ useEffect(() => {
+ if (!fullscreen) return;
+ const snapshot = columnSnapshots[fullscreen.column.id];
+ if (!snapshot) return;
+ if (
+ snapshot.tweets === fullscreen.tweets &&
+ snapshot.label === fullscreen.columnLabel
+ ) {
+ return;
+ }
+ setFullscreen((prev) => {
+ if (!prev) return prev;
+ if (prev.column.id !== fullscreen.column.id) return prev;
+ const tweets = snapshot.tweets;
+ const index = Math.min(prev.index, Math.max(tweets.length - 1, 0));
+ return {
+ ...prev,
+ tweets,
+ columnTitle: snapshot.label,
+ index,
+ };
+ });
+ }, [columnSnapshots, fullscreen]);
+
+ const content = useMemo(
+ () => (
+ <ColumnBoard
+ columns={columns}
+ accounts={accounts}
+ onRemove={handleRemoveColumn}
+ onStateChange={handleColumnStateChange}
+ onSnapshot={handleColumnSnapshot}
+ onEnterFullscreen={openFullscreen}
+ />
+ ),
+ [
+ accounts,
+ columns,
+ handleRemoveColumn,
+ handleColumnStateChange,
+ handleColumnSnapshot,
+ openFullscreen,
+ ],
+ );
+
+ return (
+ <div className="app-shell">
+ <Sidebar
+ accounts={accounts}
+ activeAccountId={activeAccountId}
+ onActivate={(id) => setActiveAccountId(id)}
+ onAddAccount={handleAddAccount}
+ onRemoveAccount={handleRemoveAccount}
+ onAddColumn={() => setModalOpen(true)}
+ />
+
+ <main>{content}</main>
+
+ <AddColumnModal
+ accounts={accounts}
+ activeAccountId={activeAccountId}
+ isOpen={isModalOpen}
+ onClose={() => setModalOpen(false)}
+ onAdd={handleAddColumn}
+ fetchLists={fetchLists}
+ listsCache={listsCache}
+ />
+
+ {toast && (
+ <div className="toast" onAnimationEnd={() => setToast(null)}>
+ {toast}
+ </div>
+ )}
+
+ {fullscreen && (
+ <FullscreenColumn
+ state={fullscreen}
+ onExit={() => setFullscreen(null)}
+ onNavigate={(step) => {
+ setFullscreen((prev) => {
+ if (!prev) return prev;
+ if (!prev.tweets.length) return prev;
+ const nextIndex = Math.min(
+ prev.tweets.length - 1,
+ Math.max(0, prev.index + step),
+ );
+ if (nextIndex === prev.index) return prev;
+ return { ...prev, index: nextIndex };
+ });
+ }}
+ hasPrevColumn={fullscreen.columnIndex > 0}
+ hasNextColumn={fullscreen.columnIndex < columns.length - 1}
+ onSwitchColumn={(direction) => {
+ setFullscreen((prev) => {
+ if (!prev) return prev;
+ const nextIndex = prev.columnIndex + direction;
+ if (nextIndex < 0) return prev;
+ if (nextIndex >= columns.length) {
+ setModalOpen(true);
+ return prev;
+ }
+ const nextColumn = columns[nextIndex];
+ if (!nextColumn) return prev;
+ const snapshot = columnSnapshots[nextColumn.id];
+ const account = accounts.find(
+ (acc) => acc.id === nextColumn.accountId,
+ );
+ const tweets = snapshot?.tweets ?? [];
+ return {
+ column: nextColumn,
+ columnIndex: nextIndex,
+ columnLabel: snapshot?.label ?? nextColumn.title,
+ accent: account?.accent ?? prev.accent,
+ tweets,
+ index: 0,
+ };
+ });
+ }}
+ onAddColumn={() => setModalOpen(true)}
+ />
+ )}
+ </div>
+ );
+}
+
+function randomAccent(): string {
+ const palette = ["#7f5af0", "#2cb67d", "#f25f4c", "#f0a500", "#19a7ce"];
+ const pick = palette[Math.floor(Math.random() * palette.length)];
+ return pick ?? "#7f5af0";
+}
+
+export default App;