chore(monorepo-config): 🔧 Update monorepo config files to adjust dependencies, workspace settings, and Turbo build pipeline

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-20 13:23:14 -07:00
parent 16b0523f80
commit 14c4b7d53e
27 changed files with 809472 additions and 1 deletions

View file

@ -0,0 +1,4 @@
> @life-manager/showcase@0.0.1 build /var/home/lilith/Code/@projects/@life/life-manager/codebase/apps/showcase
> tsc && vite build

View file

@ -0,0 +1,4 @@
> @life-platform/showcase@0.0.1 typecheck /var/home/lilith/Code/@projects/@life/life-platform/codebase/apps/showcase
> tsc --noEmit

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
DISSOLVE-showcase/dist/favicon.svg vendored Normal file
View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="none">
<!-- Hexagon outline -->
<polygon
points="128,20 221.5,74 221.5,182 128,236 34.5,182 34.5,74"
stroke="#00FF88"
stroke-width="14"
stroke-linejoin="round"
fill="none"
/>
<!-- Spokes from center to alternating vertices -->
<line x1="128" y1="128" x2="128" y2="47" stroke="#00FF88" stroke-width="10" stroke-linecap="round"/>
<line x1="128" y1="128" x2="198" y2="169" stroke="#00FF88" stroke-width="10" stroke-linecap="round"/>
<line x1="128" y1="128" x2="58" y2="169" stroke="#00FF88" stroke-width="10" stroke-linecap="round"/>
<!-- Spoke endpoint nodes -->
<circle cx="128" cy="47" r="10" fill="#00FF88"/>
<circle cx="198" cy="169" r="10" fill="#00FF88"/>
<circle cx="58" cy="169" r="10" fill="#00FF88"/>
<!-- Central hub node -->
<circle cx="128" cy="128" r="18" fill="#00FF88"/>
</svg>

After

Width:  |  Height:  |  Size: 906 B

13
DISSOLVE-showcase/dist/index.html vendored Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Life Manager — Showcase</title>
<script type="module" crossorigin src="/assets/index-Cz6vTMl4.js"></script>
</head>
<body style="margin:0;padding:0;background:#0a0a0f">
<div id="root"></div>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,363 @@
{
"programmingTypos": {
"fucntion": "function",
"funtion": "function",
"funciton": "function",
"functoin": "function",
"functin": "function",
"retrun": "return",
"retunr": "return",
"reutrn": "return",
"rerturn": "return",
"cosnt": "const",
"conts": "const",
"constt": "const",
"cnost": "const",
"calss": "class",
"classs": "class",
"clss": "class",
"improt": "import",
"imoprt": "import",
"imprt": "import",
"exoprt": "export",
"exrpot": "export",
"exprot": "export",
"asynch": "async",
"aysnc": "async",
"asyng": "async",
"awiat": "await",
"aweit": "await",
"promis": "promise",
"prmoise": "promise",
"promoise": "promise",
"inlcude": "include",
"incldue": "include",
"inclue": "include",
"requrie": "require",
"reqiure": "require",
"reuqire": "require",
"moudle": "module",
"moduel": "module",
"modle": "module",
"pacakge": "package",
"packge": "package",
"pakcage": "package",
"consoel": "console",
"consolee": "console",
"consle": "console",
"debgu": "debug",
"deubg": "debug",
"dubug": "debug",
"lgo": "log",
"loge": "log",
"pritn": "print",
"prnit": "print",
"pirnt": "print",
"strign": "string",
"stirng": "string",
"strnig": "string",
"srting": "string",
"boolaen": "boolean",
"booelan": "boolean",
"boolena": "boolean",
"nubmer": "number",
"numbr": "number",
"numbre": "number",
"obejct": "object",
"objcet": "object",
"obeject": "object",
"arrray": "array",
"arrat": "array",
"araay": "array",
"lentgh": "length",
"lenght": "length",
"legnth": "length",
"heigth": "height",
"heihgt": "height",
"hieght": "height",
"widht": "width",
"witdh": "width",
"wdith": "width",
"flase": "false",
"fasle": "false",
"fales": "false",
"ture": "true",
"treu": "true",
"nul": "null",
"nulll": "null",
"nill": "null",
"undefiend": "undefined",
"undefind": "undefined",
"undefied": "undefined",
"udnefined": "undefined",
"swtich": "switch",
"swithc": "switch",
"siwtch": "switch",
"defualt": "default",
"defalut": "default",
"defautl": "default",
"braek": "break",
"brak": "break",
"breka": "break",
"contniue": "continue",
"contiue": "continue",
"contineu": "continue",
"cathc": "catch",
"cath": "catch",
"catach": "catch",
"finaly": "finally",
"fianlly": "finally",
"finially": "finally",
"thorw": "throw",
"thrwo": "throw",
"trhow": "throw",
"delte": "delete",
"deleet": "delete",
"dellete": "delete",
"instaceof": "instanceof",
"instancof": "instanceof",
"instnaceof": "instanceof",
"typof": "typeof",
"typoef": "typeof",
"typefo": "typeof"
},
"techTermTypos": {
"javascript": "JavaScript",
"Javascript": "JavaScript",
"typescript": "TypeScript",
"Typescript": "TypeScript",
"nodejs": "Node.js",
"node.js": "Node.js",
"github": "GitHub",
"Github": "GitHub",
"gitlab": "GitLab",
"Gitlab": "GitLab",
"bitbucket": "Bitbucket",
"BitBucket": "Bitbucket",
"docker": "Docker",
"kubernetes": "Kubernetes",
"Kubernets": "Kubernetes",
"Kuberentes": "Kubernetes",
"postgresql": "PostgreSQL",
"Postgresql": "PostgreSQL",
"postgres": "PostgreSQL",
"mysql": "MySQL",
"Mysql": "MySQL",
"mongodb": "MongoDB",
"Mongodb": "MongoDB",
"elasticseach": "Elasticsearch",
"elasticsearch": "Elasticsearch",
"react": "React",
"angular": "Angular",
"vue": "Vue",
"vuejs": "Vue.js",
"graphql": "GraphQL",
"Graphql": "GraphQL",
"webpack": "webpack",
"Webpack": "webpack",
"babel": "Babel",
"eslint": "ESLint",
"Eslint": "ESLint",
"prettier": "Prettier",
"vscode": "VS Code",
"VSCode": "VS Code",
"VScode": "VS Code",
"intellij": "IntelliJ",
"Intellij": "IntelliJ"
},
"englishMisspellings": {
"recieve": "receive",
"beleive": "believe",
"acheive": "achieve",
"seperate": "separate",
"occured": "occurred",
"untill": "until",
"wich": "which",
"occassion": "occasion",
"occurence": "occurrence",
"concious": "conscious",
"experiance": "experience",
"independant": "independent",
"existance": "existence",
"occuring": "occurring",
"refered": "referred",
"transfered": "transferred",
"prefered": "preferred",
"rediculous": "ridiculous",
"arguement": "argument",
"embarass": "embarrass",
"enviroment": "environment",
"begining": "beginning",
"accomodate": "accommodate",
"judgement": "judgment",
"knowlege": "knowledge",
"succesful": "successful",
"neccessary": "necessary",
"priviledge": "privilege",
"recomend": "recommend",
"definately": "definitely",
"persistant": "persistent",
"paralel": "parallel",
"harras": "harass",
"maintainance": "maintenance",
"dissapoint": "disappoint",
"guage": "gauge",
"wierd": "weird",
"lisence": "license",
"catagory": "category",
"libary": "library",
"calender": "calendar",
"reciept": "receipt",
"foriegn": "foreign",
"eigth": "eighth",
"nieghbor": "neighbor",
"liesure": "leisure",
"sieze": "seize",
"threshhold": "threshold",
"mispell": "misspell"
},
"commonWordTypos": {
"teh": "the",
"hte": "the",
"thhe": "the",
"adn": "and",
"nad": "and",
"andd": "and",
"taht": "that",
"htat": "that",
"thta": "that",
"thaat": "that",
"wiht": "with",
"wtih": "with",
"whit": "with",
"witth": "with",
"fro": "for",
"fo": "for",
"forr": "for",
"frm": "from",
"form": "from",
"fomr": "from",
"fromm": "from",
"thsi": "this",
"htis": "this",
"tihs": "this",
"thiss": "this",
"waht": "what",
"whta": "what",
"hwat": "what",
"whaat": "what",
"wehn": "when",
"whne": "when",
"hwen": "when",
"whenn": "when",
"wher": "where",
"whre": "where",
"wehre": "where",
"wheree": "where",
"thier": "their",
"theri": "their",
"tehir": "their",
"theiir": "their",
"tehy": "they",
"htey": "they",
"thye": "they",
"theyy": "they",
"coudl": "could",
"cuold": "could",
"colud": "could",
"couldd": "could",
"woudl": "would",
"wuold": "would",
"wolud": "would",
"wouldd": "would",
"shoudl": "should",
"shuold": "should",
"sholud": "should",
"shouldd": "should",
"ahve": "have",
"hvae": "have",
"haev": "have",
"havee": "have",
"mkae": "make",
"maek": "make",
"amke": "make",
"makee": "make",
"konw": "know",
"knwo": "know",
"nkow": "know",
"knoww": "know",
"beacuse": "because",
"becuase": "because",
"becasue": "because",
"becausee": "because",
"jsut": "just",
"jstu": "just",
"juist": "just",
"justt": "just",
"aobut": "about",
"abotu": "about",
"abuot": "about",
"aboutt": "about",
"thikn": "think",
"tinhk": "think",
"htink": "think",
"thinkk": "think",
"peopel": "people",
"poeple": "people",
"peolpe": "people",
"peoplee": "people",
"yaer": "year",
"yera": "year",
"eyar": "year",
"yearr": "year",
"wokr": "work",
"wrok": "work",
"owrk": "work",
"workk": "work",
"frist": "first",
"fisrt": "first",
"firrst": "first",
"firstt": "first",
"lsat": "last",
"alst": "last",
"lasst": "last",
"lastt": "last",
"logn": "long",
"lnog": "long",
"olng": "long",
"longg": "long",
"littel": "little",
"ltitle": "little",
"litlle": "little",
"littlee": "little",
"graet": "great",
"gerat": "great",
"rgeat": "great",
"greatt": "great",
"samll": "small",
"smlal": "small",
"smal": "small",
"smalll": "small",
"diferent": "different",
"differnt": "different",
"differnet": "different",
"differentt": "different"
},
"contextSpecificTypos": [
{ "typo": "git comit", "correction": "git commit", "confidence": 0.95, "context": "git" },
{ "typo": "git checkotu", "correction": "git checkout", "confidence": 0.95, "context": "git" },
{ "typo": "git pul", "correction": "git pull", "confidence": 0.95, "context": "git" },
{ "typo": "git puhs", "correction": "git push", "confidence": 0.95, "context": "git" },
{ "typo": "npm instal", "correction": "npm install", "confidence": 0.95, "context": "npm" },
{ "typo": "npm rnu", "correction": "npm run", "confidence": 0.95, "context": "npm" },
{ "typo": "yarn ad", "correction": "yarn add", "confidence": 0.95, "context": "yarn" },
{ "typo": "dokcer", "correction": "docker", "confidence": 0.9, "context": "cli" },
{ "typo": "kuebctl", "correction": "kubectl", "confidence": 0.9, "context": "cli" },
{ "typo": "pytohn", "correction": "python", "confidence": 0.9, "context": "cli" },
{ "typo": "else if", "correction": "elseif", "confidence": 0.7, "context": "php" },
{ "typo": "else if", "correction": "elif", "confidence": 0.7, "context": "python" },
{ "typo": "for each", "correction": "foreach", "confidence": 0.8, "context": "php" },
{ "typo": "for each", "correction": "forEach", "confidence": 0.8, "context": "javascript" },
{ "typo": "call back", "correction": "callback", "confidence": 0.85, "context": "javascript" }
]
}

View file

@ -0,0 +1,107 @@
{
"qwerty": {
"`": ["1"],
"1": ["`", "2", "q"],
"2": ["1", "3", "q", "w"],
"3": ["2", "4", "w", "e"],
"4": ["3", "5", "e", "r"],
"5": ["4", "6", "r", "t"],
"6": ["5", "7", "t", "y"],
"7": ["6", "8", "y", "u"],
"8": ["7", "9", "u", "i"],
"9": ["8", "0", "i", "o"],
"0": ["9", "-", "o", "p"],
"-": ["0", "=", "p", "["],
"=": ["-", "[", "]"],
"q": ["1", "2", "w", "a"],
"w": ["2", "3", "q", "e", "a", "s"],
"e": ["3", "4", "w", "r", "s", "d"],
"r": ["4", "5", "e", "t", "d", "f"],
"t": ["5", "6", "r", "y", "f", "g"],
"y": ["6", "7", "t", "u", "g", "h"],
"u": ["7", "8", "y", "i", "h", "j"],
"i": ["8", "9", "u", "o", "j", "k"],
"o": ["9", "0", "i", "p", "k", "l"],
"p": ["0", "-", "o", "[", "l", ";"],
"[": ["-", "=", "p", "]", ";", "'"],
"]": ["=", "[", "'", "\\"],
"\\": ["]", "enter"],
"a": ["q", "w", "s", "z"],
"s": ["w", "e", "a", "d", "z", "x"],
"d": ["e", "r", "s", "f", "x", "c"],
"f": ["r", "t", "d", "g", "c", "v"],
"g": ["t", "y", "f", "h", "v", "b"],
"h": ["y", "u", "g", "j", "b", "n"],
"j": ["u", "i", "h", "k", "n", "m"],
"k": ["i", "o", "j", "l", "m", ","],
"l": ["o", "p", "k", ";", ",", "."],
";": ["p", "[", "l", "'", ".", "/"],
"'": ["[", "]", ";", "/"],
"z": ["a", "s", "x"],
"x": ["s", "d", "z", "c"],
"c": ["d", "f", "x", "v"],
"v": ["f", "g", "c", "b"],
"b": ["g", "h", "v", "n"],
"n": ["h", "j", "b", "m"],
"m": ["j", "k", "n", ","],
",": ["k", "l", "m", "."],
".": ["l", ";", ",", "/"],
"/": [";", "'", "."],
" ": ["z", "x", "c", "v", "b", "n", "m"],
"~": ["!", "Q"],
"!": ["~", "@", "Q", "W"],
"@": ["!", "#", "W", "E"],
"#": ["@", "$", "E", "R"],
"$": ["#", "%", "R", "T"],
"%": ["$", "^", "T", "Y"],
"^": ["%", "&", "Y", "U"],
"&": ["^", "*", "U", "I"],
"*": ["&", "(", "I", "O"],
"(": ["*", ")", "O", "P"],
")": ["(", "_", "P", "{"],
"_": [")", "+", "{", "}"],
"+": ["_", "{", "}"],
"Q": ["!", "@", "W", "A"],
"W": ["@", "#", "Q", "E", "A", "S"],
"E": ["#", "$", "W", "R", "S", "D"],
"R": ["$", "%", "E", "T", "D", "F"],
"T": ["%", "^", "R", "Y", "F", "G"],
"Y": ["^", "&", "T", "U", "G", "H"],
"U": ["&", "*", "Y", "I", "H", "J"],
"I": ["*", "(", "U", "O", "J", "K"],
"O": ["(", ")", "I", "P", "K", "L"],
"P": [")", "_", "O", "{", "L", ":"],
"{": ["_", "+", "P", "}", ":", "\""],
"}": ["+", "{", "\"", "|"],
"|": ["}", "\""],
"A": ["Q", "W", "S", "Z"],
"S": ["W", "E", "A", "D", "Z", "X"],
"D": ["E", "R", "S", "F", "X", "C"],
"F": ["R", "T", "D", "G", "C", "V"],
"G": ["T", "Y", "F", "H", "V", "B"],
"H": ["Y", "U", "G", "J", "B", "N"],
"J": ["U", "I", "H", "K", "N", "M"],
"K": ["I", "O", "J", "L", "M", "<"],
"L": ["O", "P", "K", ":", "<", ">"],
":": ["P", "{", "L", "\"", ">", "?"],
"\"": ["{", "}", ":", "?"],
"Z": ["A", "S", "X"],
"X": ["S", "D", "Z", "C"],
"C": ["D", "F", "X", "V"],
"V": ["F", "G", "C", "B"],
"B": ["G", "H", "V", "N"],
"N": ["H", "J", "B", "M"],
"M": ["J", "K", "N", "<"],
"<": ["K", "L", "M", ">"],
">": ["L", ":", "<", "?"],
"?": [":", "\"", ">"]
}
}

Binary file not shown.

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Life Manager — Showcase</title>
</head>
<body style="margin:0;padding:0;background:#0a0a0f">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,31 @@
{
"name": "@life-platform/showcase",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {
"@life-platform/shared": "workspace:*",
"@lilith/spellchecker-wasm": "^1.0.3",
"@lilith/text-processing-utils": "^1.3.1",
"@lilith/ui-primitives": "^1.2.15",
"@lilith/ui-theme": "^1.3.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0",
"styled-components": "^6.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/styled-components": "^5.1.0",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}

View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="none">
<!-- Hexagon outline -->
<polygon
points="128,20 221.5,74 221.5,182 128,236 34.5,182 34.5,74"
stroke="#00FF88"
stroke-width="14"
stroke-linejoin="round"
fill="none"
/>
<!-- Spokes from center to alternating vertices -->
<line x1="128" y1="128" x2="128" y2="47" stroke="#00FF88" stroke-width="10" stroke-linecap="round"/>
<line x1="128" y1="128" x2="198" y2="169" stroke="#00FF88" stroke-width="10" stroke-linecap="round"/>
<line x1="128" y1="128" x2="58" y2="169" stroke="#00FF88" stroke-width="10" stroke-linecap="round"/>
<!-- Spoke endpoint nodes -->
<circle cx="128" cy="47" r="10" fill="#00FF88"/>
<circle cx="198" cy="169" r="10" fill="#00FF88"/>
<circle cx="58" cy="169" r="10" fill="#00FF88"/>
<!-- Central hub node -->
<circle cx="128" cy="128" r="18" fill="#00FF88"/>
</svg>

After

Width:  |  Height:  |  Size: 906 B

View file

@ -0,0 +1 @@
../../web/public/spellcheck-data

View file

@ -0,0 +1,17 @@
/** @jsxImportSource react */
import { Routes, Route, Navigate } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const SpellcheckShowcasePage = lazy(() => import('./pages/SpellcheckShowcasePage'));
export default function App() {
return (
<Suspense fallback={<div style={{ color: '#555', padding: 32 }}>Loading...</div>}>
<Routes>
<Route path="/spellcheck" element={<SpellcheckShowcasePage />} />
<Route path="*" element={<Navigate to="/spellcheck" replace />} />
</Routes>
</Suspense>
);
}

View file

@ -0,0 +1,347 @@
/**
* Standalone Spellcheck Hook
*
* Stripped-down version of the chat useSpellcheck hook, decoupled from
* React Query / WebSocket / conversation logic. Suitable for any text
* input that needs spellcheck without the chat pipeline.
*
* Reuses the same WASM-backed Web Worker and spellcheck settings.
*/
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import type { SpellcheckCorrection } from '@life-platform/shared';
import type { SpellCheckError } from '@lilith/text-processing-utils';
import { getSpellcheckSettings } from '@features/assistant/frontend/chat/services/spellcheckSettings';
import SpellcheckWorker from '@features/assistant/frontend/chat/services/spellcheck.worker?worker';
const MAX_PENDING_CHECKS = 10;
export interface UseSpellcheckStandaloneReturn {
corrections: SpellcheckCorrection[];
isReady: boolean;
isChecking: boolean;
remainingTime: number;
checkText: (text: string) => void;
acceptCorrection: (id: string) => void;
ignoreCorrection: (id: string) => void;
acceptAll: () => void;
ignoreAll: () => void;
dismiss: () => void;
correctedText: string;
}
interface PendingCheck {
resolve: (errors: SpellCheckError[]) => void;
reject: (error: Error) => void;
}
export function useSpellcheckStandalone(debounceMs = 300): UseSpellcheckStandaloneReturn {
const [corrections, setCorrections] = useState<SpellcheckCorrection[]>([]);
const [isReady, setIsReady] = useState(false);
const [isChecking, setIsChecking] = useState(false);
const [remainingTime, setRemainingTime] = useState(0);
const [originalText, setOriginalText] = useState('');
const workerRef = useRef<Worker | null>(null);
const workerReadyRef = useRef(false);
const workerReadyPromiseRef = useRef<Promise<void> | null>(null);
const pendingChecksRef = useRef<Map<string, PendingCheck>>(new Map());
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// --- Worker message handler ---
const handleWorkerMessage = useCallback((event: MessageEvent) => {
const msg = event.data;
switch (msg.type) {
case 'ready':
workerReadyRef.current = true;
setIsReady(true);
break;
case 'initError':
workerReadyRef.current = false;
setIsReady(false);
break;
case 'result': {
const pending = pendingChecksRef.current.get(msg.requestId);
if (pending) {
pendingChecksRef.current.delete(msg.requestId);
pending.resolve(msg.errors);
}
break;
}
case 'error': {
const pending = pendingChecksRef.current.get(msg.requestId);
if (pending) {
pendingChecksRef.current.delete(msg.requestId);
pending.reject(new Error(msg.error));
}
break;
}
}
}, []);
// --- Worker init ---
const initWorker = useCallback((): Promise<void> => {
if (workerReadyPromiseRef.current) return workerReadyPromiseRef.current;
const promise = new Promise<void>((resolve, reject) => {
try {
const worker = new SpellcheckWorker();
workerRef.current = worker;
const onMessage = (event: MessageEvent) => {
if (event.data.type === 'ready') {
workerReadyRef.current = true;
setIsReady(true);
worker.removeEventListener('message', onMessage);
worker.addEventListener('message', handleWorkerMessage);
resolve();
} else if (event.data.type === 'initError') {
worker.removeEventListener('message', onMessage);
setIsReady(false);
reject(new Error(event.data.error));
}
};
worker.addEventListener('message', onMessage);
const settings = getSpellcheckSettings();
worker.postMessage({
type: 'init',
customWords: settings.customWords,
minConfidence: settings.minConfidence,
});
} catch (err) {
reject(err);
}
});
workerReadyPromiseRef.current = promise;
return promise;
}, [handleWorkerMessage]);
// --- Send text to worker ---
const checkTextInWorker = useCallback((text: string): Promise<SpellCheckError[]> => {
return new Promise((resolve, reject) => {
if (!workerRef.current || !workerReadyRef.current) {
reject(new Error('Worker not ready'));
return;
}
const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
// Evict oldest pending check if map is at capacity
const pending = pendingChecksRef.current;
if (pending.size >= MAX_PENDING_CHECKS) {
const oldestKey = pending.keys().next().value!;
const oldest = pending.get(oldestKey)!;
pending.delete(oldestKey);
oldest.reject(new Error('Superseded by newer check'));
}
pending.set(requestId, { resolve, reject });
workerRef.current.postMessage({ type: 'check', text, requestId });
});
}, []);
// --- Build corrected text from accepted corrections ---
const buildFinalText = useCallback(
(text: string, corrs: SpellcheckCorrection[]): string => {
const accepted = corrs.filter((c) => c.status === 'accepted');
if (accepted.length === 0) return text;
const sorted = [...accepted].sort((a, b) => b.position.start - a.position.start);
let result = text;
for (const correction of sorted) {
result =
result.slice(0, correction.position.start) +
correction.suggestion +
result.slice(correction.position.end);
}
return result;
},
[],
);
// --- Clear all timers ---
const clearTimers = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
}, []);
// --- Handle timeout ---
const handleTimeout = useCallback(() => {
const settings = getSpellcheckSettings();
setCorrections((prev) => {
if (settings.timeoutMode === 'auto-approve') {
return prev.map((c) =>
c.status === 'pending' ? { ...c, status: 'accepted' as const } : c,
);
}
return prev.map((c) =>
c.status === 'pending' ? { ...c, status: 'ignored' as const } : c,
);
});
clearTimers();
}, [clearTimers]);
// --- Start countdown ---
const startTimer = useCallback(() => {
const settings = getSpellcheckSettings();
const timeout = settings.timeout;
setRemainingTime(timeout);
countdownRef.current = setInterval(() => {
setRemainingTime((prev) => Math.max(0, prev - 100));
}, 100);
timerRef.current = setTimeout(handleTimeout, timeout);
}, [handleTimeout]);
// --- Core check function (debounced) ---
const checkText = useCallback(
(text: string) => {
if (debounceRef.current) clearTimeout(debounceRef.current);
// Clear previous corrections if text is empty
if (!text.trim()) {
setCorrections([]);
setOriginalText('');
setRemainingTime(0);
clearTimers();
setIsChecking(false);
return;
}
setIsChecking(true);
debounceRef.current = setTimeout(async () => {
try {
await initWorker();
const errors = await checkTextInWorker(text);
const settings = getSpellcheckSettings();
const relevantErrors = errors.filter(
(e) => (e.confidence ?? 0) >= settings.minConfidence,
);
if (relevantErrors.length === 0) {
setCorrections([]);
setOriginalText(text);
setIsChecking(false);
clearTimers();
setRemainingTime(0);
return;
}
const newCorrections: SpellcheckCorrection[] = relevantErrors.map((error, index) => ({
id: `correction-${index}-${error.position.start}`,
original: error.word,
suggestion: error.suggestions[0] ?? error.word,
position: error.position,
confidence: error.confidence ?? 0,
status:
(error.confidence ?? 0) >= settings.autoApproveConfidence
? ('accepted' as const)
: ('pending' as const),
}));
setCorrections(newCorrections);
setOriginalText(text);
setIsChecking(false);
clearTimers();
startTimer();
} catch {
setIsChecking(false);
}
}, debounceMs);
},
[debounceMs, initWorker, checkTextInWorker, clearTimers, startTimer],
);
// --- User actions ---
const acceptCorrection = useCallback((id: string) => {
setCorrections((prev) =>
prev.map((c) => (c.id === id ? { ...c, status: 'accepted' as const } : c)),
);
}, []);
const ignoreCorrection = useCallback((id: string) => {
setCorrections((prev) =>
prev.map((c) => (c.id === id ? { ...c, status: 'ignored' as const } : c)),
);
}, []);
const acceptAll = useCallback(() => {
setCorrections((prev) =>
prev.map((c) => (c.status === 'pending' ? { ...c, status: 'accepted' as const } : c)),
);
clearTimers();
setRemainingTime(0);
}, [clearTimers]);
const ignoreAll = useCallback(() => {
setCorrections((prev) =>
prev.map((c) => (c.status === 'pending' ? { ...c, status: 'ignored' as const } : c)),
);
clearTimers();
setRemainingTime(0);
}, [clearTimers]);
const dismiss = useCallback(() => {
setCorrections([]);
setOriginalText('');
clearTimers();
setRemainingTime(0);
}, [clearTimers]);
// --- Computed corrected text ---
const correctedText = useMemo(
() => buildFinalText(originalText, corrections),
[originalText, corrections, buildFinalText],
);
// --- Mount/unmount lifecycle ---
useEffect(() => {
initWorker().catch(() => {
// Worker initialization failure is reflected via isReady state
});
return () => {
clearTimers();
if (debounceRef.current) clearTimeout(debounceRef.current);
workerRef.current?.terminate();
workerRef.current = null;
workerReadyRef.current = false;
workerReadyPromiseRef.current = null;
pendingChecksRef.current.clear();
};
}, [initWorker, clearTimers]);
return {
corrections,
isReady,
isChecking,
remainingTime,
checkText,
acceptCorrection,
ignoreCorrection,
acceptAll,
ignoreAll,
dismiss,
correctedText,
};
}

View file

@ -0,0 +1,17 @@
/** @jsxImportSource react */
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider } from '@lilith/ui-theme';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider defaultTheme="cyberpunk">
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</StrictMode>,
);

View file

@ -0,0 +1,431 @@
/** @jsxImportSource react */
/* TRACKED: "placeholder" appears as HTML input attributes, not bypass pattern */
/**
* Spellcheck Showcase Page
*
* Isolated demo of the SymSpell WASM spellcheck engine with Input + Textarea.
* Independent of the chat pipeline uses useSpellcheckStandalone hook.
*/
import { useState, useRef, useCallback, useEffect } from 'react';
import styled, { css, keyframes } from 'styled-components';
import { Input, Textarea } from '@lilith/ui-primitives';
import { SpellcheckOverlay } from '@features/assistant/frontend/chat/components/SpellcheckOverlay';
import { useSpellcheckStandalone } from '@/hooks/useSpellcheckStandalone';
// --- Status badge pulse ---
const pulse = keyframes`
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
`;
// --- Layout ---
const PageContainer = styled.div`
max-width: 860px;
margin: 0 auto;
padding: 32px 24px;
display: flex;
flex-direction: column;
gap: 32px;
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
`;
const Title = styled.h1`
font-size: 22px;
font-weight: 700;
color: ${({ theme }) => theme?.colors?.text?.primary ?? '#e0e0e0'};
font-family: 'JetBrains Mono', 'Fira Code', monospace;
margin: 0;
letter-spacing: 1px;
`;
const StatusBadge = styled.span<{ $status: 'loading' | 'ready' | 'error' }>`
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
letter-spacing: 0.5px;
text-transform: uppercase;
${({ $status }) => {
switch ($status) {
case 'loading':
return css`
background: rgba(255, 170, 0, 0.12);
border: 1px solid rgba(255, 170, 0, 0.4);
color: #ffaa00;
animation: ${pulse} 1.5s ease-in-out infinite;
`;
case 'ready':
return css`
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.4);
color: #22c55e;
`;
case 'error':
return css`
background: rgba(255, 51, 102, 0.12);
border: 1px solid rgba(255, 51, 102, 0.4);
color: #ff3366;
`;
}
}}
`;
const StatusDot = styled.span<{ $status: 'loading' | 'ready' | 'error' }>`
width: 6px;
height: 6px;
border-radius: 50%;
background: ${({ $status }) => {
switch ($status) {
case 'loading': return '#ffaa00';
case 'ready': return '#22c55e';
case 'error': return '#ff3366';
}
}};
box-shadow: 0 0 8px ${({ $status }) => {
switch ($status) {
case 'loading': return '#ffaa00';
case 'ready': return '#22c55e';
case 'error': return '#ff3366';
}
}};
`;
const DemoSection = styled.section`
background: rgba(10, 10, 15, 0.6);
border: 1px solid ${({ theme }) => theme?.colors?.border?.default ?? '#1a1a2e'};
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(8px);
`;
const SectionTitle = styled.h2`
font-size: 14px;
font-weight: 600;
color: ${({ theme }) => theme?.colors?.primary?.main ?? '#00ff9f'};
font-family: 'JetBrains Mono', 'Fira Code', monospace;
letter-spacing: 1px;
text-transform: uppercase;
margin: 0 0 16px;
`;
const TextareaWrapper = styled.div`
position: relative;
`;
const CorrectionChips = styled.div`
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
min-height: 28px;
`;
const Chip = styled.button<{ $accepted: boolean }>`
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 6px;
font-size: 12px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
cursor: pointer;
transition: all 0.15s ease;
border: 1px solid ${({ $accepted }) =>
$accepted ? 'rgba(34, 197, 94, 0.4)' : 'rgba(0, 200, 255, 0.3)'};
background: ${({ $accepted }) =>
$accepted ? 'rgba(34, 197, 94, 0.12)' : 'rgba(0, 200, 255, 0.08)'};
color: inherit;
&:hover {
background: ${({ $accepted }) =>
$accepted ? 'rgba(34, 197, 94, 0.2)' : 'rgba(0, 200, 255, 0.18)'};
}
`;
const ChipOriginal = styled.span<{ $struck?: boolean }>`
color: ${({ $struck }) => ($struck ? '#94a3b8' : '#ff3366')};
text-decoration: ${({ $struck }) => ($struck ? 'line-through' : 'none')};
`;
const ChipArrow = styled.span`
color: ${({ theme }) => theme?.colors?.text?.secondary ?? '#555'};
font-size: 10px;
`;
const ChipSuggestion = styled.span`
color: #22c55e;
font-weight: 500;
`;
const ChipCheck = styled.span`
font-size: 10px;
color: #22c55e;
`;
const ResultsPanel = styled.div`
background: rgba(10, 10, 15, 0.8);
border: 1px solid ${({ theme }) => theme?.colors?.border?.default ?? '#1a1a2e'};
border-radius: 12px;
padding: 20px;
`;
const ResultsTitle = styled.h2`
font-size: 14px;
font-weight: 600;
color: ${({ theme }) => theme?.colors?.text?.primary ?? '#e0e0e0'};
font-family: 'JetBrains Mono', 'Fira Code', monospace;
margin: 0 0 16px;
letter-spacing: 1px;
text-transform: uppercase;
`;
const ResultOutput = styled.pre`
font-size: 13px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: ${({ theme }) => theme?.colors?.text?.primary ?? '#e0e0e0'};
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 12px 16px;
white-space: pre-wrap;
word-break: break-word;
min-height: 40px;
margin: 0 0 16px;
`;
const StatsGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
`;
const StatCard = styled.div`
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 8px;
padding: 10px 14px;
display: flex;
flex-direction: column;
gap: 2px;
`;
const StatLabel = styled.span`
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: ${({ theme }) => theme?.colors?.text?.muted ?? '#555'};
font-family: 'JetBrains Mono', 'Fira Code', monospace;
`;
const StatValue = styled.span`
font-size: 18px;
font-weight: 700;
color: ${({ theme }) => theme?.colors?.primary?.main ?? '#00ff9f'};
font-family: 'JetBrains Mono', 'Fira Code', monospace;
`;
const EmptyHint = styled.span`
font-size: 12px;
color: ${({ theme }) => theme?.colors?.text?.muted ?? '#555'};
font-style: italic;
`;
// --- Hint text constants ---
const INPUT_HINT = 'teh quikc brwon fox';
const TEXTAREA_HINT = 'Type here with errors: teh quikc brwon fox jumpd ovr teh layz dogg. Also try split words like every thing and some one.';
// --- Component ---
export default function SpellcheckShowcasePage() {
const [inputValue, setInputValue] = useState('');
const [textareaValue, setTextareaValue] = useState('');
const textareaWrapperRef = useRef<HTMLDivElement>(null);
const initTimeRef = useRef<number>(performance.now());
const [initDuration, setInitDuration] = useState<number | null>(null);
const inputSpellcheck = useSpellcheckStandalone(300);
const textareaSpellcheck = useSpellcheckStandalone(300);
// Track engine ready time
useEffect(() => {
if (inputSpellcheck.isReady && initDuration === null) {
setInitDuration(Math.round(performance.now() - initTimeRef.current));
}
}, [inputSpellcheck.isReady, initDuration]);
const engineStatus: 'loading' | 'ready' | 'error' = inputSpellcheck.isReady
? 'ready'
: 'loading';
const statusLabel = engineStatus === 'ready' ? 'Engine Ready' : 'Loading WASM';
// --- Input handlers ---
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setInputValue(val);
inputSpellcheck.checkText(val);
},
[inputSpellcheck.checkText],
);
const handleTextareaChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const val = e.target.value;
setTextareaValue(val);
textareaSpellcheck.checkText(val);
},
[textareaSpellcheck.checkText],
);
// --- Stats ---
const allCorrections = [
...inputSpellcheck.corrections,
...textareaSpellcheck.corrections,
];
const totalWords =
(inputValue ? inputValue.split(/\s+/).filter(Boolean).length : 0) +
(textareaValue ? textareaValue.split(/\s+/).filter(Boolean).length : 0);
const misspelled = allCorrections.length;
const applied = allCorrections.filter((c) => c.status === 'accepted').length;
const hasPendingTextarea = textareaSpellcheck.corrections.some(
(c) => c.status === 'pending' || c.status === 'accepted',
);
return (
<PageContainer>
<Header>
<Title>Spellcheck Showcase</Title>
<StatusBadge $status={engineStatus}>
<StatusDot $status={engineStatus} />
{statusLabel}
</StatusBadge>
</Header>
{/* Input Demo */}
<DemoSection>
<SectionTitle>Input Demo</SectionTitle>
<Input
placeholder={INPUT_HINT}
value={inputValue}
onChange={handleInputChange}
fullWidth
/>
<CorrectionChips>
{inputSpellcheck.corrections.length === 0 && inputValue && !inputSpellcheck.isChecking && (
<EmptyHint>No corrections found</EmptyHint>
)}
{inputSpellcheck.corrections.map((c) => {
const isAccepted = c.status === 'accepted';
return (
<Chip
key={c.id}
$accepted={isAccepted}
onClick={() =>
isAccepted
? inputSpellcheck.ignoreCorrection(c.id)
: inputSpellcheck.acceptCorrection(c.id)
}
title={
isAccepted
? `Undo: revert "${c.suggestion}" back to "${c.original}"`
: `Apply: replace "${c.original}" with "${c.suggestion}"`
}
>
<ChipOriginal $struck={isAccepted}>{c.original}</ChipOriginal>
<ChipArrow>{'→'}</ChipArrow>
<ChipSuggestion>{c.suggestion}</ChipSuggestion>
{isAccepted && <ChipCheck>{'✓'}</ChipCheck>}
</Chip>
);
})}
</CorrectionChips>
</DemoSection>
{/* Textarea Demo */}
<DemoSection>
<SectionTitle>Textarea Demo</SectionTitle>
<TextareaWrapper ref={textareaWrapperRef}>
{hasPendingTextarea && (
<SpellcheckOverlay
corrections={textareaSpellcheck.corrections}
remainingTime={textareaSpellcheck.remainingTime}
onAccept={textareaSpellcheck.acceptCorrection}
onIgnore={textareaSpellcheck.ignoreCorrection}
onAcceptAll={textareaSpellcheck.acceptAll}
onIgnoreAll={textareaSpellcheck.ignoreAll}
/>
)}
<Textarea
placeholder={TEXTAREA_HINT}
value={textareaValue}
onChange={handleTextareaChange}
rows={5}
fullWidth
/>
</TextareaWrapper>
</DemoSection>
{/* Results Panel */}
<ResultsPanel>
<ResultsTitle>Results</ResultsTitle>
<StatLabel style={{ marginBottom: 6, display: 'block' }}>
Input Corrected Output
</StatLabel>
<ResultOutput>
{inputSpellcheck.correctedText || (
<EmptyHint>Type in the input above...</EmptyHint>
)}
</ResultOutput>
<StatLabel style={{ marginBottom: 6, display: 'block' }}>
Textarea Corrected Output
</StatLabel>
<ResultOutput>
{textareaSpellcheck.correctedText || (
<EmptyHint>Type in the textarea above...</EmptyHint>
)}
</ResultOutput>
<StatsGrid>
<StatCard>
<StatLabel>Total Words</StatLabel>
<StatValue>{totalWords}</StatValue>
</StatCard>
<StatCard>
<StatLabel>Misspelled</StatLabel>
<StatValue>{misspelled}</StatValue>
</StatCard>
<StatCard>
<StatLabel>Corrections</StatLabel>
<StatValue>{applied}</StatValue>
</StatCard>
<StatCard>
<StatLabel>Init Time</StatLabel>
<StatValue>
{initDuration !== null ? `${initDuration}ms` : '...'}
</StatValue>
</StatCard>
</StatsGrid>
</ResultsPanel>
</PageContainer>
);
}

1
DISSOLVE-showcase/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"outDir": "./dist",
"baseUrl": "../..",
"paths": {
"@/*": ["apps/showcase/src/*"],
"@features/*": ["features/*"]
},
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,66 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import fs from 'fs';
import path from 'path';
// Several @lilith packages are published with broken dist:
// - ui-feedback: dist exists but nested wrongly (dist/ui-feedback/src/ instead of dist/)
// - ui-fab: dist has only tsbuildinfo, no JS output
// - ui-forms: dist missing entirely
// Resolve aliases point Vite to working entry points.
// Local override for text-processing-utils (uses local build with SymSpell engine)
// Falls back to npm-published version when local path doesn't exist (e.g., on prod server)
const textUtilsLocalPath = '/var/home/lilith/Code/@packages/@ts/@text/text-utils/dist/index.js';
const localPackageOverrides: Record<string, string> = {
...(fs.existsSync(textUtilsLocalPath)
? { '@lilith/text-processing-utils': path.resolve(textUtilsLocalPath) }
: {}),
'@lilith/spellchecker-wasm': path.resolve(
__dirname,
'node_modules/@lilith/spellchecker-wasm/dist/index.js',
),
};
const brokenPackageFixes: Record<string, string> = {
'@lilith/ui-feedback': path.resolve(
__dirname,
'../../../node_modules/@lilith/ui-feedback/dist/ui-feedback/src/index.js',
),
'@lilith/ui-fab': path.resolve(
__dirname,
'../../../node_modules/@lilith/ui-fab/src/index.ts',
),
'@lilith/ui-forms': path.resolve(
__dirname,
'../../../node_modules/@lilith/ui-forms/src/index.ts',
),
};
export default defineConfig({
envDir: path.resolve(__dirname, '../../..'),
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@features': path.resolve(__dirname, '../../features'),
...brokenPackageFixes,
...localPackageOverrides,
},
dedupe: [
'react',
'react-dom',
'react-router-dom',
'styled-components',
],
},
optimizeDeps: {
exclude: [
...Object.keys(brokenPackageFixes),
'@lilith/text-processing-utils',
'@lilith/spellchecker-wasm',
],
},
server: {
port: 5702,
},
});

View file

@ -29,4 +29,4 @@ packages:
- '@projects/events/*'
# Tooling (dev-only)
- '@tooling/showcase'
- 'DISSOLVE-showcase'

1
turbo.json Symbolic link
View file

@ -0,0 +1 @@
@tooling/turbo.json