Merge pull request 'feat: LEM Chat TypeScript web components' (#18) from feat/lem-chat-components into main
This commit is contained in:
commit
bb00bd4add
12 changed files with 1545 additions and 0 deletions
1
lem-chat/.gitignore
vendored
Normal file
1
lem-chat/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
34
lem-chat/index.html
Normal file
34
lem-chat/index.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LEM Chat</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { height: 100%; background: #111; }
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
lem-chat {
|
||||
width: 720px;
|
||||
height: 85vh;
|
||||
max-height: 800px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
lem-chat { width: 100%; height: 100%; max-height: none; border-radius: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<lem-chat
|
||||
endpoint="http://localhost:8090"
|
||||
model="local"
|
||||
max-tokens="2048"
|
||||
></lem-chat>
|
||||
<script type="module" src="dist/lem-chat.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
515
lem-chat/package-lock.json
generated
Normal file
515
lem-chat/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
{
|
||||
"name": "lem-chat",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lem-chat",
|
||||
"version": "0.1.0",
|
||||
"license": "EUPL-1.2",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lem-chat/package.json
Normal file
16
lem-chat/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "lem-chat",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "EUPL-1.2",
|
||||
"scripts": {
|
||||
"build": "esbuild src/lem-chat.ts --bundle --format=esm --outfile=dist/lem-chat.js",
|
||||
"watch": "esbuild src/lem-chat.ts --bundle --format=esm --outfile=dist/lem-chat.js --watch",
|
||||
"dev": "esbuild src/lem-chat.ts --bundle --format=esm --outfile=dist/lem-chat.js --watch --servedir=.",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
195
lem-chat/src/lem-chat.ts
Normal file
195
lem-chat/src/lem-chat.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { chatStyles } from './styles';
|
||||
import type { ChatMessage, ChatCompletionChunk, LemSendDetail } from './types';
|
||||
import { LemMessages } from './lem-messages';
|
||||
import { LemInput } from './lem-input';
|
||||
import './lem-message';
|
||||
|
||||
export class LemChat extends HTMLElement {
|
||||
private shadow!: ShadowRoot;
|
||||
private messages!: LemMessages;
|
||||
private input!: LemInput;
|
||||
private statusEl!: HTMLDivElement;
|
||||
private history: ChatMessage[] = [];
|
||||
private abortController: AbortController | null = null;
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ['endpoint', 'model', 'system-prompt', 'max-tokens', 'temperature'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = chatStyles;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'header';
|
||||
|
||||
this.statusEl = document.createElement('div');
|
||||
this.statusEl.className = 'header-status';
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'header-icon';
|
||||
icon.textContent = 'L';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'header-title';
|
||||
title.textContent = 'LEM';
|
||||
|
||||
const modelLabel = document.createElement('div');
|
||||
modelLabel.className = 'header-model';
|
||||
modelLabel.textContent = this.getAttribute('model') || 'local';
|
||||
|
||||
header.appendChild(this.statusEl);
|
||||
header.appendChild(icon);
|
||||
header.appendChild(title);
|
||||
header.appendChild(modelLabel);
|
||||
|
||||
this.messages = document.createElement('lem-messages') as LemMessages;
|
||||
this.input = document.createElement('lem-input') as LemInput;
|
||||
|
||||
this.shadow.appendChild(style);
|
||||
this.shadow.appendChild(header);
|
||||
this.shadow.appendChild(this.messages);
|
||||
this.shadow.appendChild(this.input);
|
||||
|
||||
this.addEventListener('lem-send', ((e: Event) => {
|
||||
const detail = (e as CustomEvent<LemSendDetail>).detail;
|
||||
this.handleSend(detail.text);
|
||||
}) as EventListener);
|
||||
|
||||
const systemPrompt = this.getAttribute('system-prompt');
|
||||
if (systemPrompt) {
|
||||
this.history.push({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
|
||||
this.checkConnection();
|
||||
requestAnimationFrame(() => this.input.focus());
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
get endpoint(): string {
|
||||
const attr = this.getAttribute('endpoint');
|
||||
if (!attr) return window.location.origin;
|
||||
return attr;
|
||||
}
|
||||
|
||||
get model(): string {
|
||||
return this.getAttribute('model') || '';
|
||||
}
|
||||
|
||||
get maxTokens(): number {
|
||||
const val = this.getAttribute('max-tokens');
|
||||
return val ? parseInt(val, 10) : 2048;
|
||||
}
|
||||
|
||||
get temperature(): number {
|
||||
const val = this.getAttribute('temperature');
|
||||
return val ? parseFloat(val) : 0.7;
|
||||
}
|
||||
|
||||
private async checkConnection(): Promise<void> {
|
||||
try {
|
||||
const resp = await fetch(`${this.endpoint}/v1/models`, {
|
||||
signal: AbortSignal.timeout(3000),
|
||||
});
|
||||
this.statusEl.classList.toggle('disconnected', !resp.ok);
|
||||
} catch {
|
||||
this.statusEl.classList.add('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSend(text: string): Promise<void> {
|
||||
this.messages.addMessage('user', text);
|
||||
this.history.push({ role: 'user', content: text });
|
||||
|
||||
const assistantMsg = this.messages.addMessage('assistant');
|
||||
assistantMsg.streaming = true;
|
||||
this.input.disabled = true;
|
||||
|
||||
this.abortController?.abort();
|
||||
this.abortController = new AbortController();
|
||||
|
||||
let fullResponse = '';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.endpoint}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: this.abortController.signal,
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
messages: this.history,
|
||||
max_tokens: this.maxTokens,
|
||||
temperature: this.temperature,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error('No response body');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const chunk: ChatCompletionChunk = JSON.parse(data);
|
||||
const delta = chunk.choices?.[0]?.delta;
|
||||
if (delta?.content) {
|
||||
fullResponse += delta.content;
|
||||
assistantMsg.appendToken(delta.content);
|
||||
this.messages.scrollToBottom();
|
||||
}
|
||||
} catch {
|
||||
// skip malformed chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
// user-initiated abort — ignore
|
||||
} else {
|
||||
const errorText =
|
||||
err instanceof Error ? err.message : 'Connection failed';
|
||||
if (!fullResponse) {
|
||||
assistantMsg.text = `\u26A0\uFE0F ${errorText}`;
|
||||
}
|
||||
this.statusEl.classList.add('disconnected');
|
||||
}
|
||||
} finally {
|
||||
assistantMsg.streaming = false;
|
||||
this.input.disabled = false;
|
||||
this.input.focus();
|
||||
this.abortController = null;
|
||||
if (fullResponse) {
|
||||
this.history.push({ role: 'assistant', content: fullResponse });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('lem-chat', LemChat);
|
||||
110
lem-chat/src/lem-input.ts
Normal file
110
lem-chat/src/lem-input.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { inputStyles } from './styles';
|
||||
import type { LemSendDetail } from './types';
|
||||
|
||||
export class LemInput extends HTMLElement {
|
||||
private shadow!: ShadowRoot;
|
||||
private textarea!: HTMLTextAreaElement;
|
||||
private sendBtn!: HTMLButtonElement;
|
||||
private _disabled = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = inputStyles;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'input-wrapper';
|
||||
|
||||
this.textarea = document.createElement('textarea');
|
||||
this.textarea.rows = 1;
|
||||
this.textarea.placeholder = 'Message LEM...';
|
||||
|
||||
this.sendBtn = document.createElement('button');
|
||||
this.sendBtn.className = 'send-btn';
|
||||
this.sendBtn.type = 'button';
|
||||
this.sendBtn.disabled = true;
|
||||
this.sendBtn.appendChild(this.createSendIcon());
|
||||
|
||||
wrapper.appendChild(this.textarea);
|
||||
wrapper.appendChild(this.sendBtn);
|
||||
this.shadow.appendChild(style);
|
||||
this.shadow.appendChild(wrapper);
|
||||
|
||||
this.textarea.addEventListener('input', () => {
|
||||
this.textarea.style.height = 'auto';
|
||||
this.textarea.style.height =
|
||||
Math.min(this.textarea.scrollHeight, 120) + 'px';
|
||||
this.sendBtn.disabled =
|
||||
this._disabled || this.textarea.value.trim() === '';
|
||||
});
|
||||
|
||||
this.textarea.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
|
||||
this.sendBtn.addEventListener('click', () => this.submit());
|
||||
}
|
||||
|
||||
private createSendIcon(): SVGSVGElement {
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(ns, 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 24 24');
|
||||
svg.setAttribute('fill', 'none');
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
svg.setAttribute('stroke-width', '2');
|
||||
svg.setAttribute('stroke-linecap', 'round');
|
||||
svg.setAttribute('stroke-linejoin', 'round');
|
||||
svg.setAttribute('width', '16');
|
||||
svg.setAttribute('height', '16');
|
||||
const line = document.createElementNS(ns, 'line');
|
||||
line.setAttribute('x1', '22');
|
||||
line.setAttribute('y1', '2');
|
||||
line.setAttribute('x2', '11');
|
||||
line.setAttribute('y2', '13');
|
||||
const polygon = document.createElementNS(ns, 'polygon');
|
||||
polygon.setAttribute('points', '22 2 15 22 11 13 2 9 22 2');
|
||||
svg.appendChild(line);
|
||||
svg.appendChild(polygon);
|
||||
return svg;
|
||||
}
|
||||
|
||||
private submit(): void {
|
||||
const text = this.textarea.value.trim();
|
||||
if (!text || this._disabled) return;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<LemSendDetail>('lem-send', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { text },
|
||||
})
|
||||
);
|
||||
this.textarea.value = '';
|
||||
this.textarea.style.height = 'auto';
|
||||
this.sendBtn.disabled = true;
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value: boolean) {
|
||||
this._disabled = value;
|
||||
this.textarea.disabled = value;
|
||||
this.sendBtn.disabled = value || this.textarea.value.trim() === '';
|
||||
this.textarea.placeholder = value ? 'LEM is thinking...' : 'Message LEM...';
|
||||
}
|
||||
|
||||
override focus(): void {
|
||||
this.textarea?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('lem-input', LemInput);
|
||||
154
lem-chat/src/lem-message.ts
Normal file
154
lem-chat/src/lem-message.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { messageStyles } from './styles';
|
||||
import { renderMarkdown } from './markdown';
|
||||
|
||||
interface ThinkSplit {
|
||||
think: string | null;
|
||||
response: string;
|
||||
}
|
||||
|
||||
export class LemMessage extends HTMLElement {
|
||||
private shadow!: ShadowRoot;
|
||||
private thinkPanel!: HTMLDivElement;
|
||||
private thinkContent!: HTMLDivElement;
|
||||
private thinkLabel!: HTMLDivElement;
|
||||
private contentEl!: HTMLDivElement;
|
||||
private cursorEl: HTMLSpanElement | null = null;
|
||||
private _text = '';
|
||||
private _streaming = false;
|
||||
private _thinkCollapsed = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
const role = this.getAttribute('role') || 'user';
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = messageStyles;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble';
|
||||
|
||||
const roleEl = document.createElement('div');
|
||||
roleEl.className = 'role';
|
||||
roleEl.textContent = role === 'assistant' ? 'LEM' : 'You';
|
||||
|
||||
this.thinkPanel = document.createElement('div');
|
||||
this.thinkPanel.className = 'think-panel';
|
||||
this.thinkPanel.style.display = 'none';
|
||||
|
||||
this.thinkLabel = document.createElement('div');
|
||||
this.thinkLabel.className = 'think-label';
|
||||
this.thinkLabel.textContent = '\u25BC reasoning';
|
||||
this.thinkLabel.addEventListener('click', () => {
|
||||
this._thinkCollapsed = !this._thinkCollapsed;
|
||||
this.thinkPanel.classList.toggle('collapsed', this._thinkCollapsed);
|
||||
this.thinkLabel.textContent = this._thinkCollapsed
|
||||
? '\u25B6 reasoning'
|
||||
: '\u25BC reasoning';
|
||||
});
|
||||
|
||||
this.thinkContent = document.createElement('div');
|
||||
this.thinkContent.className = 'think-content';
|
||||
this.thinkPanel.appendChild(this.thinkLabel);
|
||||
this.thinkPanel.appendChild(this.thinkContent);
|
||||
|
||||
this.contentEl = document.createElement('div');
|
||||
this.contentEl.className = 'content';
|
||||
|
||||
bubble.appendChild(roleEl);
|
||||
if (role === 'assistant') {
|
||||
bubble.appendChild(this.thinkPanel);
|
||||
}
|
||||
bubble.appendChild(this.contentEl);
|
||||
|
||||
this.shadow.appendChild(style);
|
||||
this.shadow.appendChild(bubble);
|
||||
|
||||
if (this._text) {
|
||||
this.updateContent();
|
||||
}
|
||||
}
|
||||
|
||||
get text(): string {
|
||||
return this._text;
|
||||
}
|
||||
|
||||
set text(value: string) {
|
||||
this._text = value;
|
||||
this.updateContent();
|
||||
}
|
||||
|
||||
get streaming(): boolean {
|
||||
return this._streaming;
|
||||
}
|
||||
|
||||
set streaming(value: boolean) {
|
||||
this._streaming = value;
|
||||
this.updateContent();
|
||||
}
|
||||
|
||||
appendToken(token: string): void {
|
||||
this._text += token;
|
||||
this.updateContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits text into think/response portions and renders each.
|
||||
*
|
||||
* Safety: renderMarkdown() escapes all HTML entities (& < > ") before any
|
||||
* inline formatting is applied. The source is the local MLX model output,
|
||||
* not arbitrary user HTML. Shadow DOM provides additional isolation.
|
||||
*/
|
||||
private updateContent(): void {
|
||||
if (!this.contentEl) return;
|
||||
const { think, response } = this.splitThink(this._text);
|
||||
|
||||
if (think !== null && this.thinkPanel) {
|
||||
this.thinkPanel.style.display = '';
|
||||
this.thinkContent.textContent = think;
|
||||
}
|
||||
|
||||
// renderMarkdown() escapes all HTML before formatting — safe for innerHTML
|
||||
// within Shadow DOM isolation, sourced from local MLX model only
|
||||
const responseHtml = renderMarkdown(response);
|
||||
this.contentEl.innerHTML = responseHtml;
|
||||
|
||||
if (this._streaming) {
|
||||
if (!this.cursorEl) {
|
||||
this.cursorEl = document.createElement('span');
|
||||
this.cursorEl.className = 'cursor';
|
||||
}
|
||||
if (think !== null && !this._text.includes('</think>')) {
|
||||
this.thinkContent.appendChild(this.cursorEl);
|
||||
} else {
|
||||
const lastChild = this.contentEl.lastElementChild || this.contentEl;
|
||||
lastChild.appendChild(this.cursorEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private splitThink(text: string): ThinkSplit {
|
||||
const thinkStart = text.indexOf('<think>');
|
||||
if (thinkStart === -1) {
|
||||
return { think: null, response: text };
|
||||
}
|
||||
const afterOpen = thinkStart + '<think>'.length;
|
||||
const thinkEnd = text.indexOf('</think>', afterOpen);
|
||||
if (thinkEnd === -1) {
|
||||
return {
|
||||
think: text.slice(afterOpen).trim(),
|
||||
response: text.slice(0, thinkStart).trim(),
|
||||
};
|
||||
}
|
||||
const thinkText = text.slice(afterOpen, thinkEnd).trim();
|
||||
const beforeThink = text.slice(0, thinkStart).trim();
|
||||
const afterThink = text.slice(thinkEnd + '</think>'.length).trim();
|
||||
const response = [beforeThink, afterThink].filter(Boolean).join('\n');
|
||||
return { think: thinkText, response };
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('lem-message', LemMessage);
|
||||
70
lem-chat/src/lem-messages.ts
Normal file
70
lem-chat/src/lem-messages.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { messagesStyles } from './styles';
|
||||
import type { LemMessage } from './lem-message';
|
||||
|
||||
export class LemMessages extends HTMLElement {
|
||||
private shadow!: ShadowRoot;
|
||||
private container!: HTMLDivElement;
|
||||
private emptyEl!: HTMLDivElement;
|
||||
private shouldAutoScroll = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = messagesStyles;
|
||||
|
||||
this.container = document.createElement('div');
|
||||
|
||||
this.emptyEl = document.createElement('div');
|
||||
this.emptyEl.className = 'empty';
|
||||
const emptyIcon = document.createElement('div');
|
||||
emptyIcon.className = 'empty-icon';
|
||||
emptyIcon.textContent = '\u2728';
|
||||
const emptyText = document.createElement('div');
|
||||
emptyText.className = 'empty-text';
|
||||
emptyText.textContent = 'Start a conversation';
|
||||
this.emptyEl.appendChild(emptyIcon);
|
||||
this.emptyEl.appendChild(emptyText);
|
||||
|
||||
this.shadow.appendChild(style);
|
||||
this.shadow.appendChild(this.emptyEl);
|
||||
this.shadow.appendChild(this.container);
|
||||
|
||||
this.addEventListener('scroll', () => {
|
||||
const threshold = 60;
|
||||
this.shouldAutoScroll =
|
||||
this.scrollHeight - this.scrollTop - this.clientHeight < threshold;
|
||||
});
|
||||
}
|
||||
|
||||
addMessage(role: string, text?: string): LemMessage {
|
||||
this.emptyEl.style.display = 'none';
|
||||
const msg = document.createElement('lem-message') as LemMessage;
|
||||
msg.setAttribute('role', role);
|
||||
this.container.appendChild(msg);
|
||||
if (text) {
|
||||
msg.text = text;
|
||||
}
|
||||
this.scrollToBottom();
|
||||
return msg;
|
||||
}
|
||||
|
||||
scrollToBottom(): void {
|
||||
if (this.shouldAutoScroll) {
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.container.replaceChildren();
|
||||
this.emptyEl.style.display = '';
|
||||
this.shouldAutoScroll = true;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('lem-messages', LemMessages);
|
||||
80
lem-chat/src/markdown.ts
Normal file
80
lem-chat/src/markdown.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function parseInline(text: string): string {
|
||||
let result = escapeHtml(text);
|
||||
result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
result = result.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
result = result.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
||||
result = result.replace(/(?<!\w)\*([^*]+)\*(?!\w)/g, '<em>$1</em>');
|
||||
result = result.replace(/(?<!\w)_([^_]+)_(?!\w)/g, '<em>$1</em>');
|
||||
return result;
|
||||
}
|
||||
|
||||
function wrapParagraph(lines: string[]): string {
|
||||
const joined = lines.join('<br>');
|
||||
if (joined.startsWith('<pre')) return joined;
|
||||
return `<p>${joined}</p>`;
|
||||
}
|
||||
|
||||
export function renderMarkdown(text: string): string {
|
||||
const lines = text.split('\n');
|
||||
const output: string[] = [];
|
||||
let inCodeBlock = false;
|
||||
let codeLines: string[] = [];
|
||||
let codeLang = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trimStart().startsWith('```')) {
|
||||
if (!inCodeBlock) {
|
||||
inCodeBlock = true;
|
||||
codeLang = line.trimStart().slice(3).trim();
|
||||
codeLines = [];
|
||||
} else {
|
||||
const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : '';
|
||||
output.push(`<pre${langAttr}><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`);
|
||||
inCodeBlock = false;
|
||||
codeLines = [];
|
||||
codeLang = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) {
|
||||
codeLines.push(line);
|
||||
continue;
|
||||
}
|
||||
if (line.trim() === '') {
|
||||
output.push('');
|
||||
continue;
|
||||
}
|
||||
output.push(parseInline(line));
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : '';
|
||||
output.push(`<pre${langAttr}><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`);
|
||||
}
|
||||
|
||||
const paragraphs: string[] = [];
|
||||
let current: string[] = [];
|
||||
for (const line of output) {
|
||||
if (line === '') {
|
||||
if (current.length > 0) {
|
||||
paragraphs.push(wrapParagraph(current));
|
||||
current = [];
|
||||
}
|
||||
} else {
|
||||
current.push(line);
|
||||
}
|
||||
}
|
||||
if (current.length > 0) {
|
||||
paragraphs.push(wrapParagraph(current));
|
||||
}
|
||||
|
||||
return paragraphs.join('');
|
||||
}
|
||||
325
lem-chat/src/styles.ts
Normal file
325
lem-chat/src/styles.ts
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
export const chatStyles = `
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--lem-bg, #1a1a1e);
|
||||
color: var(--lem-text, #e0e0e0);
|
||||
font-family: var(--lem-font, system-ui, -apple-system, sans-serif);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 18px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: var(--lem-accent, #5865f2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--lem-text, #e0e0e0);
|
||||
}
|
||||
|
||||
.header-model {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-left: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #43b581;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-status.disconnected {
|
||||
background: #f04747;
|
||||
}
|
||||
`;
|
||||
|
||||
export const messagesStyles = `
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px 0;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
:host::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
:host::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:host::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 36px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const messageStyles = `
|
||||
:host {
|
||||
display: block;
|
||||
padding: 6px 18px;
|
||||
}
|
||||
|
||||
:host([role="user"]) .bubble {
|
||||
background: var(--lem-msg-user, #2a2a3e);
|
||||
margin-left: 40px;
|
||||
border-radius: 12px 12px 4px 12px;
|
||||
}
|
||||
|
||||
:host([role="assistant"]) .bubble {
|
||||
background: var(--lem-msg-assistant, #1e1e2a);
|
||||
margin-right: 40px;
|
||||
border-radius: 12px 12px 12px 4px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 10px 14px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.role {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
:host([role="assistant"]) .role {
|
||||
color: var(--lem-accent, #5865f2);
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--lem-text, #e0e0e0);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content strong {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.content em {
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.content code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
color: #e8a0bf;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
margin: 8px 0;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
color: #c9d1d9;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.think-panel {
|
||||
margin: 6px 0 8px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(88, 101, 242, 0.06);
|
||||
border-left: 2px solid rgba(88, 101, 242, 0.3);
|
||||
border-radius: 0 6px 6px 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
line-height: 1.5;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.think-panel::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.think-panel::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.think-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(88, 101, 242, 0.5);
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.think-label:hover {
|
||||
color: rgba(88, 101, 242, 0.7);
|
||||
}
|
||||
|
||||
.think-panel.collapsed .think-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 16px;
|
||||
background: var(--lem-accent, #5865f2);
|
||||
border-radius: 1px;
|
||||
animation: blink 0.8s step-end infinite;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
`;
|
||||
|
||||
export const inputStyles = `
|
||||
:host {
|
||||
display: block;
|
||||
padding: 12px 16px 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: var(--lem-accent, #5865f2);
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--lem-text, #e0e0e0);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
max-height: 120px;
|
||||
min-height: 22px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: var(--lem-accent, #5865f2);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.send-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.send-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
`;
|
||||
31
lem-chat/src/types.ts
Normal file
31
lem-chat/src/types.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChatCompletionRequest {
|
||||
model: string;
|
||||
messages: ChatMessage[];
|
||||
max_tokens: number;
|
||||
temperature: number;
|
||||
stream: boolean;
|
||||
}
|
||||
|
||||
export interface ChatCompletionChunk {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: Array<{
|
||||
delta: {
|
||||
role?: string;
|
||||
content?: string;
|
||||
};
|
||||
index: number;
|
||||
finish_reason: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LemSendDetail {
|
||||
text: string;
|
||||
}
|
||||
14
lem-chat/tsconfig.json
Normal file
14
lem-chat/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"declaration": false,
|
||||
"isolatedModules": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue