history: RojoChatPanel — streaming AI chat, LangChain backend, tab integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7b0c01560c
commit
55acdd3f12
|
|
@ -8,9 +8,12 @@
|
|||
"name": "roject",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.2.2",
|
||||
"@langchain/openai": "^1.5.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3"
|
||||
"express-session": "^1.17.3",
|
||||
"markdown-it": "^14.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
|
|
@ -21,6 +24,12 @@
|
|||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
|
|
@ -62,6 +71,48 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.2.tgz",
|
||||
"integrity": "sha512-KfjEOT6sCg0vvItagfEtGpmrGoLMGfma4Affb5BGEqPmS2YR3AxW54pABSkhQlzCehTB+0BnLquAe1lGF4J9zQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": ">=0.5.0 <1.0.0",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.5.4.tgz",
|
||||
"integrity": "sha512-KeWzWukSDmuR40umDHcJG9OqZ4PxRhsZdSlMS+hVKYawvu/fcB+zHs1QEu8e+5Y1SR3EN9yYB+51lo1f1j6MdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"openai": "^6.41.0",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
|
|
@ -272,12 +323,38 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
|
|
@ -456,6 +533,18 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
|
|
@ -501,6 +590,12 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
|
|
@ -735,6 +830,67 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.8.0.tgz",
|
||||
"integrity": "sha512-edm/zZ1jMQRt0GzavmJ0YPIsqKkXetffIpmR29li8P0SoZ0MI/xKk/tIAkPnxHCybmzJesIQf5CT7eLQCqv3kA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-queue": "6.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*",
|
||||
"ws": ">=7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
|
||||
"integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/markdown-it"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
|
|
@ -742,6 +898,33 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.3.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
|
||||
"integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/markdown-it"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.5.0",
|
||||
"linkify-it": "^5.0.2",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -751,6 +934,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
|
|
@ -817,6 +1006,15 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
|
|
@ -859,6 +1057,74 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.45.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz",
|
||||
"integrity": "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
|
||||
"@smithy/hash-node": ">=4.3.0 <5",
|
||||
"@smithy/signature-v4": ">=5.4.0 <6",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-provider-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/hash-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@smithy/signature-v4": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
|
@ -887,6 +1153,15 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
|
|
@ -1175,6 +1450,12 @@
|
|||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
|
|
@ -1237,6 +1518,15 @@
|
|||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@
|
|||
"build": "tsc --build tsconfig.client.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.2.2",
|
||||
"@langchain/openai": "^1.5.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3"
|
||||
"express-session": "^1.17.3",
|
||||
"markdown-it": "^14.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import projectsRouter from './routes/projects';
|
|||
import filesRouter from './routes/files';
|
||||
import localesRouter from './routes/locales';
|
||||
import layoutRouter from './routes/layout';
|
||||
import rojosRouter from './routes/rojos';
|
||||
import { generateLocales } from './localeGenerator';
|
||||
|
||||
generateLocales();
|
||||
|
|
@ -33,6 +34,7 @@ app.use( '/api/projects', projectsRouter );
|
|||
app.use( '/api/files', filesRouter );
|
||||
app.use( '/api/locales', localesRouter );
|
||||
app.use( '/api/layout', layoutRouter );
|
||||
app.use( '/api/rojos', rojosRouter );
|
||||
|
||||
app.get( '/', ( req, res ) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import { RojosConfig } from "./RojosConfig";
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
model: RojosConfig.model,
|
||||
apiKey: RojosConfig.apiKey,
|
||||
configuration: {
|
||||
baseURL: RojosConfig.baseURL,
|
||||
},
|
||||
});
|
||||
|
||||
type MessageData = { role: "user" | "assistant"; content: string };
|
||||
type SessionData = { id: string; messages: MessageData[] };
|
||||
|
||||
const sessions = new Map<string, SessionData>();
|
||||
|
||||
function toBaseMessages( messages: MessageData[] ): BaseMessage[]
|
||||
{
|
||||
return messages.map( m =>
|
||||
m.role === "user" ? new HumanMessage( m.content ) : new AIMessage( m.content )
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAgentStream( id: string, userMessage: string )
|
||||
{
|
||||
if ( ! sessions.has( id ) )
|
||||
{
|
||||
sessions.set( id, { id, messages: [] } );
|
||||
}
|
||||
|
||||
const session = sessions.get( id )!;
|
||||
session.messages.push( { role: "user", content: userMessage } );
|
||||
|
||||
return model.stream( toBaseMessages( session.messages ) );
|
||||
}
|
||||
|
||||
export function updateAgentConversation( id: string, assistantMessage: string )
|
||||
{
|
||||
const session = sessions.get( id );
|
||||
|
||||
if ( session )
|
||||
{
|
||||
session.messages.push( { role: "assistant", content: assistantMessage } );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export const RojosConfig = {
|
||||
baseURL: "http://0.0.0.0:8900/v1",
|
||||
model: "LLAMA",
|
||||
apiKey: "not-needed",
|
||||
};
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth";
|
||||
import { getAgentStream, updateAgentConversation } from "../rojos/RojosAgent";
|
||||
import { RJLog } from "../../src/library-ts/node/log/RJLog";
|
||||
|
||||
const router = Router();
|
||||
router.use( requireAuth );
|
||||
|
||||
router.post( "/chat", async ( req, res ) =>
|
||||
{
|
||||
const { id, message } = req.body as { id: string; message: string };
|
||||
|
||||
// RJLog.log( "Chat", { id, message } );
|
||||
|
||||
if ( !id || !message )
|
||||
{
|
||||
res.status( 400 ).json( { error: "id and message are required" } );
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const agentStream = await getAgentStream( id, message );
|
||||
|
||||
res.setHeader( "Content-Type", "text/event-stream" );
|
||||
res.setHeader( "Cache-Control", "no-cache" );
|
||||
res.setHeader( "Connection", "keep-alive" );
|
||||
res.flushHeaders();
|
||||
|
||||
const collected: string[] = [];
|
||||
|
||||
for await ( const chunk of agentStream )
|
||||
{
|
||||
const content = chunk.content;
|
||||
let text = "";
|
||||
|
||||
// RJLog.log( "Chunk", chunk );
|
||||
|
||||
if ( typeof content === "string" )
|
||||
{
|
||||
text = content;
|
||||
}
|
||||
else if ( Array.isArray( content ) )
|
||||
{
|
||||
for ( const c of content )
|
||||
{
|
||||
if ( typeof c === "string" )
|
||||
{
|
||||
text += c;
|
||||
}
|
||||
else if ( c && typeof c === "object" && "text" in c )
|
||||
{
|
||||
text += ( c as any ).text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( text.length > 0 )
|
||||
{
|
||||
collected.push( text );
|
||||
res.write( JSON.stringify( { type: "CHAT", text } ) + "\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
res.write( JSON.stringify( { type: "THINKING" } ) + "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
updateAgentConversation( id, collected.join( "" ) );
|
||||
|
||||
res.write( JSON.stringify( { type: "DONE" } ) + "\n" );
|
||||
res.end();
|
||||
}
|
||||
catch ( err )
|
||||
{
|
||||
console.error( err );
|
||||
|
||||
if ( !res.headersSent )
|
||||
{
|
||||
res.status( 500 ).json( { error: "Failed to get agent response" } );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
export default router;
|
||||
|
|
@ -20,12 +20,12 @@ export abstract class Command
|
|||
|
||||
getLocalizedTitle():Promise<string>
|
||||
{
|
||||
return LocaleManager.$().get( this._titleLocalePath );
|
||||
return LocaleManager.$.get( this._titleLocalePath );
|
||||
}
|
||||
|
||||
getLocalizedInfo():Promise<string>
|
||||
{
|
||||
return LocaleManager.$().get( this._infoLocalePath );
|
||||
return LocaleManager.$.get( this._infoLocalePath );
|
||||
}
|
||||
|
||||
abstract execute( context: CommandContext ): Promise<void>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,7 @@ export class AddFileCommand extends Command
|
|||
{
|
||||
constructor(){ super( "", "" ); }
|
||||
|
||||
async _doAction( context: CommandContext ): Promise<void>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
async _undoAction( context: CommandContext ): Promise<void>
|
||||
async execute( context: CommandContext ): Promise<void>
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
||||
|
||||
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
||||
|
||||
function extractRawText( node: Node ): string
|
||||
{
|
||||
if ( node.nodeType === Node.TEXT_NODE ) return node.nodeValue ?? '';
|
||||
if ( node.nodeName === 'BR' ) return '\n';
|
||||
if ( node.nodeType !== Node.ELEMENT_NODE ) return '';
|
||||
let text = '';
|
||||
node.childNodes.forEach( child => text += extractRawText( child ) );
|
||||
return text;
|
||||
}
|
||||
|
||||
class RojoChatPanel extends HTMLElement
|
||||
{
|
||||
_initialized = false;
|
||||
_conversationId: string = '';
|
||||
_sending = false;
|
||||
_md: { render: ( s: string ) => string } | null = null;
|
||||
|
||||
connectedCallback(): void
|
||||
{
|
||||
if ( this._initialized ) return;
|
||||
this._initialized = true;
|
||||
|
||||
this._conversationId = crypto.randomUUID();
|
||||
this._md = markdownit( { html: false, linkify: true, breaks: true } );
|
||||
|
||||
this.className = 'rojo-chat-panel';
|
||||
this.innerHTML = `
|
||||
<div class="rcp-toolbar">
|
||||
<div class="rcp-toolbar-icon">🤖</div>
|
||||
<span class="rcp-toolbar-name">Rojo</span>
|
||||
<button class="rcp-toolbar-btn" title="Actions">★</button>
|
||||
<button class="rcp-toolbar-btn" title="Settings">☰</button>
|
||||
</div>
|
||||
<div class="rcp-history"></div>
|
||||
<div class="rcp-input-area">
|
||||
<div class="rcp-input-text" contenteditable="true"></div>
|
||||
<div class="rcp-input-buttons">
|
||||
<button class="rcp-input-btn rcp-plus-btn" title="Attach">+</button>
|
||||
<button class="rcp-input-btn rcp-send-btn" title="Send">▲</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
||||
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
||||
|
||||
sendBtn.addEventListener( 'click', () => this._send() );
|
||||
|
||||
inputText.addEventListener( 'keydown', ( e: KeyboardEvent ) =>
|
||||
{
|
||||
if ( e.key === 'Enter' && !e.shiftKey )
|
||||
{
|
||||
e.preventDefault();
|
||||
this._send();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
async _send(): Promise<void>
|
||||
{
|
||||
if ( this._sending ) return;
|
||||
|
||||
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
||||
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
||||
const history = this.querySelector( '.rcp-history' ) as HTMLElement;
|
||||
|
||||
const text = extractRawText( inputText ).replaceAll( ' ', ' ' ).trim();
|
||||
if ( !text ) return;
|
||||
|
||||
inputText.innerHTML = '';
|
||||
this._sending = true;
|
||||
sendBtn.disabled = true;
|
||||
|
||||
const userBubble = document.createElement( 'div' );
|
||||
userBubble.className = 'rcp-user-bubble';
|
||||
userBubble.textContent = text;
|
||||
history.appendChild( userBubble );
|
||||
history.scrollTop = history.scrollHeight;
|
||||
|
||||
const assistantBubble = document.createElement( 'div' );
|
||||
assistantBubble.className = 'rcp-assistant-bubble';
|
||||
assistantBubble.textContent = '…';
|
||||
history.appendChild( assistantBubble );
|
||||
history.scrollTop = history.scrollHeight;
|
||||
|
||||
try
|
||||
{
|
||||
const response = await fetch( '/api/rojos/chat',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( { id: this._conversationId, message: text } ),
|
||||
}
|
||||
);
|
||||
|
||||
if ( !response.body ) throw new Error( 'No response body' );
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let markdown = '';
|
||||
|
||||
while ( true )
|
||||
{
|
||||
const { done, value } = await reader.read();
|
||||
if ( done ) break;
|
||||
|
||||
const raw = decoder.decode( value, { stream: true } );
|
||||
|
||||
console.log( "Received raw:", raw );
|
||||
|
||||
for ( const line of raw.split( '\n' ) )
|
||||
{
|
||||
const trimmed = line.trim();
|
||||
if ( !trimmed ) continue;
|
||||
|
||||
try
|
||||
{
|
||||
const msg = JSON.parse( trimmed ) as { type: string; text?: string };
|
||||
|
||||
if ( msg.type === 'CHAT' && msg.text )
|
||||
{
|
||||
markdown += msg.text;
|
||||
assistantBubble.innerHTML = this._md!.render( markdown );
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// partial or non-JSON chunk — skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const remaining = decoder.decode();
|
||||
if ( remaining.trim() )
|
||||
{
|
||||
try
|
||||
{
|
||||
const msg = JSON.parse( remaining.trim() ) as { type: string; text?: string };
|
||||
if ( msg.type === 'CHAT' && msg.text )
|
||||
{
|
||||
markdown += msg.text;
|
||||
assistantBubble.innerHTML = this._md!.render( markdown );
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
if ( !markdown )
|
||||
{
|
||||
assistantBubble.textContent = '(no response)';
|
||||
}
|
||||
}
|
||||
catch ( err )
|
||||
{
|
||||
assistantBubble.textContent = '(error: could not reach Rojo)';
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
this._sending = false;
|
||||
sendBtn.disabled = false;
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}
|
||||
|
||||
addContextMenuEntries( dir: ContextMenuDirectory ): void
|
||||
{
|
||||
dir.add( new ContextMenuReadOnlyEntry( dir, `Rojo Chat — ${this._conversationId.slice( 0, 8 )}` ) );
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define( 'rojo-chat-panel', RojoChatPanel );
|
||||
|
|
@ -76,6 +76,7 @@ class TabContainer extends HTMLElement {
|
|||
const panelTypes = [
|
||||
{ label: 'HTML Editor', panelType: 'html-editor', tag: 'html-editor-panel' },
|
||||
{ label: 'File Tree', panelType: 'file-tree', tag: 'file-tree-panel' },
|
||||
{ label: 'Rojo Chat', panelType: 'rojo-chat', tag: 'rojo-chat-panel' },
|
||||
];
|
||||
for (const pt of panelTypes) {
|
||||
addDir.add(new ContextMenuEntry(addDir, pt.label, () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EventSlot } from '../library-ts/browser/events/EventSlot.js';
|
||||
import UndoManager from '../library-ts/browser/undo/UndoManager.js';
|
||||
|
||||
|
||||
export interface DocumentOpenedEvent
|
||||
{
|
||||
|
|
@ -30,7 +30,6 @@ export class Editor
|
|||
projectName: string = '';
|
||||
openDocs: Map<string, { content: string; dirty: boolean }> = new Map();
|
||||
activeDoc: string | null = null;
|
||||
undoManager: UndoManager
|
||||
|
||||
|
||||
readonly onDocumentOpened: EventSlot<DocumentOpenedEvent> = new EventSlot();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
// Auto-generated by localeGenerator — do not edit manually.
|
||||
|
||||
import { Commands } from './commands/Commands.js';
|
||||
|
||||
export class Locales
|
||||
{
|
||||
static readonly greeting_txt = 'greeting.txt';
|
||||
static readonly Commands = Commands;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Auto-generated by localeGenerator — do not edit manually.
|
||||
|
||||
import { FileTreeCommands } from './file-tree-commands/FileTreeCommands.js';
|
||||
|
||||
export class Commands
|
||||
{
|
||||
static readonly FileTreeCommands = FileTreeCommands;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Auto-generated by localeGenerator — do not edit manually.
|
||||
|
||||
export class FileTreeCommands
|
||||
{
|
||||
static readonly addFile_txt = 'commands/file-tree-commands/add-file.txt';
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ var NAV_DATA = {
|
|||
title: 'History',
|
||||
path: 'history/index.html',
|
||||
children: [
|
||||
{ title: 'Thursday, 9 July 2026', path: 'history/2026/07-July/09-Thursday/index.html' },
|
||||
{ title: 'Monday, 6 July 2026', path: 'history/2026/07-July/06-Monday/index.html' },
|
||||
{ title: 'Sunday, 5 July 2026', path: 'history/2026/07-July/05-Sunday/index.html' },
|
||||
{ title: 'Saturday, 4 July 2026', path: 'history/2026/07-July/04-Saturday/index.html' },
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@
|
|||
|
||||
<header>
|
||||
<h1>Actions</h1>
|
||||
<p class="subtitle">Repeatable procedures for maintaining the project. Run these at the appropriate moment in a working session.</p>
|
||||
<p class="subtitle">Repeatable procedures for maintaining the project. These actions are for agents and are requested by the users
|
||||
to run at the appropriate moment in a working session.
|
||||
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
[ RojoChatPanel Feature]
|
||||
This new EditorPanel is a chat session, where the user can have an conversion with an agent/assistant.
|
||||
|
||||
It has a chat-history, where all the posts of the user are right-aligned, and all posts of the assistant are left-algined.
|
||||
The assistant writes markdown, which is processed by "markdown-it".
|
||||
When the user writes something into the chat box, the raw text of the element is extracted and put to the history.
|
||||
After that the responses of the agent is fetched/streamed from the server.
|
||||
|
||||
|
||||
[ RojoChatPanel UI ]
|
||||
There's a mock to demonstrate the UI Element placings here: "workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.jpg"
|
||||
The UI has three components, a toolbar like the others which has an icon for the Rojo assistant, a name for the conversation
|
||||
and two buttons on the right (actions and settings). Make them all placeholder in the first iteration.
|
||||
The second one is the chat history having the alternate aligned posts from user and agent.
|
||||
The third one the chat box, which has an area to input text (should be 3 lines high) and 2 buttons on the right, one with a plus
|
||||
which is placeholder now, and another with an arrow up for posting.
|
||||
|
||||
|
||||
[ RojoChatPanel Implementation ]
|
||||
|
||||
I already have some example code in "workspace/history/2026/07-July/09-Thursday/llama-app-source". Please try to exract
|
||||
what you need to built it yourself. The backend can be almost the same, just put into the server so that it works.
|
||||
Maybe the route could be "rojos-chat". There will be other settings for "rojos", so keep that open.
|
||||
|
||||
The frontend uses my library which you don't use in the project yet, so you can change it to fit the style of the RojoChatPanel the match the
|
||||
other panels in the Roject project.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Session Summary — 9 July 2026</title>
|
||||
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||||
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<header>
|
||||
<p class="date">Thursday, 9 July 2026</p>
|
||||
<h1>Roject — Session Summary</h1>
|
||||
<p class="subtitle">
|
||||
RojoChatPanel — a streaming AI chat panel for the editor, with LangChain backend,
|
||||
markdown rendering, and tab container integration.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>What we built</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>RojoChatPanel — Backend</h3>
|
||||
<p>
|
||||
A new <code>/api/rojos/chat</code> route streams AI responses from an
|
||||
OpenAI-compatible model (e.g. a local LLAMA). The backend is split into
|
||||
three files under <code>server/rojos/</code>:
|
||||
</p>
|
||||
<ul style="margin-top:0.75rem">
|
||||
<li><strong>RojosConfig.ts</strong> — hardcoded config wrapper (<code>baseURL</code>,
|
||||
<code>model</code>, <code>apiKey</code>). Designed to grow as more Rojo settings
|
||||
are added.</li>
|
||||
<li><strong>RojosAgent.ts</strong> — in-memory session store (<code>Map</code> of
|
||||
conversation ID → message history). Uses <code>ChatOpenAI.stream()</code> from
|
||||
<code>@langchain/openai</code> with messages converted to
|
||||
<code>HumanMessage</code> / <code>AIMessage</code> via <code>@langchain/core</code>.</li>
|
||||
<li><strong>server/routes/rojos.ts</strong> — Express router, <code>POST /chat</code>,
|
||||
requires auth. Iterates the agent stream with <code>for await</code>,
|
||||
writes newline-delimited JSON chunks (<code>{ type:"CHAT", text }</code>,
|
||||
<code>{ type:"THINKING" }</code>, <code>{ type:"DONE" }</code>) directly
|
||||
via <code>res.write()</code>, then calls <code>res.end()</code>.
|
||||
After streaming, calls <code>updateAgentConversation</code> so the assistant
|
||||
reply is appended to the session history.</li>
|
||||
</ul>
|
||||
<p style="margin-top:0.75rem">
|
||||
The route is mounted at <code>/api/rojos</code> in <code>server/index.ts</code>,
|
||||
keeping the namespace open for future Rojo settings endpoints.
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag">server/rojos/RojosConfig.ts</span>
|
||||
<span class="tag">server/rojos/RojosAgent.ts</span>
|
||||
<span class="tag">server/routes/rojos.ts</span>
|
||||
<span class="tag">@langchain/openai</span>
|
||||
<span class="tag">@langchain/core</span>
|
||||
<span class="tag">SSE / res.write</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>RojoChatPanel — Frontend Component</h3>
|
||||
<p>
|
||||
A new <code>rojo-chat-panel</code> custom element following the same pattern
|
||||
as <code>html-editor-panel</code>. The element has three zones:
|
||||
</p>
|
||||
<ul style="margin-top:0.75rem">
|
||||
<li><strong>Toolbar</strong> — robot emoji icon, conversation name label, two
|
||||
placeholder buttons (actions ★, settings ☰).</li>
|
||||
<li><strong>Chat history</strong> — scrolling <code>div</code>; user messages
|
||||
are right-aligned bubble, assistant messages are left-aligned and rendered
|
||||
with <code>markdown-it</code> (live as chunks arrive).</li>
|
||||
<li><strong>Input area</strong> — <code>contenteditable</code> div (3-line height),
|
||||
a placeholder <em>+</em> button, and a <em>▲</em> send button.
|
||||
Enter sends; Shift+Enter inserts a newline.</li>
|
||||
</ul>
|
||||
<p style="margin-top:0.75rem">
|
||||
Each panel instance generates a fresh <code>crypto.randomUUID()</code>
|
||||
conversation ID on connect. Raw text is extracted from the
|
||||
<code>contenteditable</code> with an inlined recursive walker (same logic
|
||||
as <code>DOMEditor.extractRawText</code>).
|
||||
<code>markdown-it</code> is loaded as a UMD global from
|
||||
<code>/vendor/markdown-it.min.js</code> (built file copied from
|
||||
<code>node_modules</code>, no bundler required).
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag">src/components/rojo-chat-panel/rojo-chat-panel.ts</span>
|
||||
<span class="tag">public/components/rojo-chat-panel/rojo-chat-panel.css</span>
|
||||
<span class="tag">public/vendor/markdown-it.min.js</span>
|
||||
<span class="tag">contenteditable</span>
|
||||
<span class="tag">ReadableStream</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Tab Container Integration</h3>
|
||||
<p>
|
||||
<em>Rojo Chat</em> was added to the <code>panelTypes</code> array in
|
||||
<code>TabContainer.openMenu()</code>, so it appears under
|
||||
<strong>Add → Rojo Chat</strong> in every tab container's <code>⋮</code> menu.
|
||||
<strong>Duplicate</strong> already worked for free — it calls the stored
|
||||
factory, creating a second panel with its own UUID and empty history.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
<code>RojoChatPanel</code> also implements <code>addContextMenuEntries</code>
|
||||
(the <code>EditorPanel</code> interface), displaying a read-only label with
|
||||
the first 8 characters of the conversation ID.
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag">src/components/tab-container/tab-container.ts</span>
|
||||
<span class="tag">addContextMenuEntries</span>
|
||||
<span class="tag">panelTypes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Key Decisions</h2>
|
||||
|
||||
<div class="decision">
|
||||
<strong>Split LangChain packages, not the monolithic <code>langchain</code></strong>
|
||||
<p>
|
||||
The example code used the monolithic <code>langchain</code> package with
|
||||
<code>createAgent</code>. We switched to <code>@langchain/core</code> +
|
||||
<code>@langchain/openai</code> and call <code>model.stream()</code> directly.
|
||||
With no tools in scope yet, this is equivalent and avoids a large dependency.
|
||||
Migrating to <code>createReactAgent</code> from <code>@langchain/langgraph</code>
|
||||
when tools are added will be a small, localised change in <code>RojosAgent.ts</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="decision">
|
||||
<strong><code>RojosConfig.ts</code> as a hardcoded TypeScript wrapper</strong>
|
||||
<p>
|
||||
A JSON config file in <code>data/</code> would allow runtime editing but
|
||||
adds complexity (read-on-start, hot-reload). Since the model endpoint is not
|
||||
expected to change frequently, a TS constant is simpler and caught by the
|
||||
type checker. The file is the single place to change when adding providers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="decision">
|
||||
<strong><code>markdown-it</code> UMD copied to <code>public/vendor/</code></strong>
|
||||
<p>
|
||||
The project has no bundler and all frontend modules are bare ES modules.
|
||||
A CDN script would require a network call. Copying the UMD build once into
|
||||
<code>public/vendor/</code> keeps it self-hosted, available offline, and
|
||||
version-locked with the rest of <code>node_modules</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="decision">
|
||||
<strong>In-memory conversation sessions, UUID per panel instance</strong>
|
||||
<p>
|
||||
Persisting conversations to <code>data/</code> adds file I/O and a
|
||||
naming/indexing scheme. For a first iteration, in-memory is sufficient and
|
||||
keeps the surface area small. Each panel gets its own UUID on
|
||||
<code>connectedCallback</code>; named/saved conversations are deferred.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="decision">
|
||||
<strong><code>for await</code> + <code>res.write()</code> instead of <code>Readable.pipe()</code></strong>
|
||||
<p>
|
||||
The initial implementation used <code>Readable.from(async function*(){})</code>
|
||||
piped to the response. This was simplified to a direct <code>for await</code>
|
||||
loop calling <code>res.write()</code> then <code>res.end()</code>, which is
|
||||
easier to read and avoids the intermediate <code>Readable</code> wrapper.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Structural Changes</h2>
|
||||
<div class="card">
|
||||
<p>
|
||||
<code>package.json</code> — added <code>@langchain/core</code>, <code>@langchain/openai</code>, <code>markdown-it</code><br>
|
||||
<code>server/rojos/RojosConfig.ts</code> — new: model/provider config wrapper<br>
|
||||
<code>server/rojos/RojosAgent.ts</code> — new: in-memory session store, LangChain stream<br>
|
||||
<code>server/routes/rojos.ts</code> — new: POST /chat streaming endpoint<br>
|
||||
<code>server/index.ts</code> — added <code>import rojosRouter</code>, mounted at <code>/api/rojos</code><br>
|
||||
<code>src/components/rojo-chat-panel/rojo-chat-panel.ts</code> — new: custom element<br>
|
||||
<code>public/components/rojo-chat-panel/rojo-chat-panel.css</code> — new: panel styles<br>
|
||||
<code>public/vendor/markdown-it.min.js</code> — new: UMD build (copied from node_modules)<br>
|
||||
<code>public/editor.html</code> — added rojo-chat-panel CSS, vendor script, module script<br>
|
||||
<code>src/components/tab-container/tab-container.ts</code> — added Rojo Chat to panelTypes
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Roject — session log — 9 July 2026
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<script>var NAV_ROOT = '../../../../';</script>
|
||||
<script src="../../../../_assets_/nav-data.js"></script>
|
||||
<script src="../../../../_assets_/nav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { ContentBlock, createAgent } from "langchain";
|
||||
import { RJLog } from "../library-ts/node/log/RJLog";
|
||||
import { IterableReadableStream } from "@langchain/core/utils/stream";
|
||||
|
||||
const model = new ChatOpenAI(
|
||||
{
|
||||
model: "LLAMA",
|
||||
apiKey: "not-needed",
|
||||
configuration:
|
||||
{
|
||||
baseURL: "http://0.0.0.0:8900/v1",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const agent = createAgent({
|
||||
model,
|
||||
tools: [],
|
||||
});
|
||||
|
||||
type MessageData =
|
||||
{
|
||||
role:"user"|"assistant";
|
||||
content:string
|
||||
}
|
||||
|
||||
type SessionData =
|
||||
{
|
||||
id:string;
|
||||
messages:MessageData[];
|
||||
}
|
||||
|
||||
const sessions = new Map<string,SessionData>();
|
||||
|
||||
export async function updateAgentConversation( id:string, agentMessage: string )
|
||||
{
|
||||
let sessionData = sessions.get( id );
|
||||
|
||||
let userMessageData:MessageData =
|
||||
{
|
||||
role: "assistant",
|
||||
content: agentMessage
|
||||
};
|
||||
|
||||
sessionData.messages.push( userMessageData );
|
||||
}
|
||||
|
||||
export async function getAgentStream( id:string, userMessage: string ):Promise< IterableReadableStream<Uint8Array<ArrayBufferLike>>>
|
||||
{
|
||||
let sessionData:SessionData = null;
|
||||
|
||||
if ( ! sessions.has( id ) )
|
||||
{
|
||||
let sd = { id, messages:[] };
|
||||
sessions.set( id, sd );
|
||||
}
|
||||
|
||||
sessionData = sessions.get( id );
|
||||
|
||||
let userMessageData:MessageData =
|
||||
{
|
||||
role: "user",
|
||||
content: userMessage
|
||||
};
|
||||
|
||||
sessionData.messages.push( userMessageData );
|
||||
|
||||
RJLog.log( "Asking:", userMessage );
|
||||
|
||||
let stream = await agent.stream(
|
||||
{
|
||||
messages: sessionData.messages
|
||||
// messages:
|
||||
// [
|
||||
// {
|
||||
// role:"user",
|
||||
// content:userMessage
|
||||
// }
|
||||
// ]
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
streamMode: "messages"
|
||||
}
|
||||
);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import fastify from 'fastify';
|
||||
import { getAgentStream, updateAgentConversation } from './Agent';
|
||||
import { RJLog } from "../library-ts/node/log/RJLog";
|
||||
import cors from "@fastify/cors";
|
||||
import { Readable } from 'stream';
|
||||
import { AIMessageChunk } from 'langchain';
|
||||
import { sleep } from "../library-ts/browser/tools/sleep";
|
||||
|
||||
async function main()
|
||||
{
|
||||
const server = fastify();
|
||||
|
||||
await server.register( cors, {
|
||||
origin: "http://localhost:1712"
|
||||
});
|
||||
|
||||
server.post( "/chat",
|
||||
async ( request, reply ) =>
|
||||
{
|
||||
RJLog.log( "on chat:", request.body );
|
||||
|
||||
try
|
||||
{
|
||||
let reb = request.body as { message:string, id:string };
|
||||
let message = reb.message;
|
||||
let id = reb.id;
|
||||
|
||||
|
||||
RJLog.log( "Parsing Message", reb, message );
|
||||
// const response = await getAgentResponse( message );
|
||||
// reply.send( { response } );
|
||||
|
||||
let agentStream = await getAgentStream( id, message );
|
||||
reply
|
||||
.header("Content-Type", "text/event-stream")
|
||||
.header("Cache-Control", "no-cache")
|
||||
.header("Connection", "keep-alive");
|
||||
|
||||
let agentMessages:string[] = [];
|
||||
|
||||
const readable = Readable.from(
|
||||
(
|
||||
async function* ()
|
||||
{
|
||||
for await ( const chunk of agentStream )
|
||||
{
|
||||
let data:[AIMessageChunk,any] = chunk as any;
|
||||
RJLog.log( chunk );
|
||||
|
||||
let content = data[ 0 ].content;
|
||||
|
||||
if ( content.length > 0 )
|
||||
{
|
||||
let text:string = "";
|
||||
|
||||
if ( typeof content === "string" )
|
||||
{
|
||||
text += content;
|
||||
}
|
||||
else if ( Array.isArray( content ) )
|
||||
{
|
||||
content.forEach(
|
||||
( c )=>
|
||||
{
|
||||
text += c.text;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
agentMessages.push( text );
|
||||
yield JSON.stringify( { type:"CHAT", text } );
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
yield JSON.stringify( { type:"THINKING" } );
|
||||
}
|
||||
}
|
||||
|
||||
updateAgentConversation( id, agentMessages.join( "" ) );
|
||||
|
||||
await sleep( 100 );
|
||||
|
||||
yield JSON.stringify( { type:"DONE" } );
|
||||
}
|
||||
)()
|
||||
);
|
||||
|
||||
return reply.send( readable );
|
||||
}
|
||||
catch( err )
|
||||
{
|
||||
console.error(err);
|
||||
reply.status(500).send( { error: 'Failed to get agent response' } );
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
server.listen(
|
||||
{ port: 9100 },
|
||||
() =>
|
||||
{
|
||||
RJLog.log( "Agent server listening at http://localhost:9100" );
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
import { HTMLNodeTreeWalker } from "../graphs/HTMLNodeTreeWalker";
|
||||
import { TreeWalker } from "../graphs/TreeWalker";
|
||||
|
||||
export class DOMEditor
|
||||
{
|
||||
static nodeListToArray( list:NodeList )
|
||||
{
|
||||
return Array.prototype.slice.call( list );
|
||||
}
|
||||
|
||||
static prefixData( source:string )
|
||||
{
|
||||
let singleReplaceRegex = /\[\s*-/g;
|
||||
source = source.replace(singleReplaceRegex, "[data-");
|
||||
let autoDataExtensionRegex = /\[(?!data-|([a-zA-Z0-9]+s*(\=|\~|\||\^|\$|\*|\])))/g;
|
||||
return source.replace( autoDataExtensionRegex, "[data-" );
|
||||
}
|
||||
|
||||
static copyAttribute( destination:Element, source:Element, sourceAttribute:string)
|
||||
{
|
||||
sourceAttribute = DOMEditor.prefixData( sourceAttribute );
|
||||
let value = source.getAttribute (sourceAttribute );
|
||||
destination.setAttribute( sourceAttribute, value );
|
||||
}
|
||||
|
||||
static copyAllAttributesThroughRawHTML( source:Element, destination:Element )
|
||||
{
|
||||
let attributes:[string,string][] = [];
|
||||
|
||||
for ( let i = 0; i < destination.attributes.length; i++ )
|
||||
{
|
||||
let attribute = destination.attributes[ i ];
|
||||
let attributeName = attribute.nodeName;
|
||||
let attributeValue = attribute.nodeValue;
|
||||
|
||||
attributes.push( [ attributeName, attributeValue ] );
|
||||
}
|
||||
|
||||
for ( let i = 0; i < source.attributes.length; i++ )
|
||||
{
|
||||
let attribute = source.attributes[ i ];
|
||||
let attributeName = attribute.nodeName;
|
||||
let attributeValue = attribute.nodeValue;
|
||||
|
||||
let index = attributes.findIndex( a => a[ 0 ] === attributeName );
|
||||
|
||||
if ( index === -1 )
|
||||
{
|
||||
attributes.push( [ attributeName, attributeValue ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
attributes[ index ][ 1 ] = attributeValue;
|
||||
}
|
||||
}
|
||||
|
||||
let innerHTML = destination.innerHTML;
|
||||
let attributesString = "";
|
||||
|
||||
for ( let i = 0; i < attributes.length; i++ )
|
||||
{
|
||||
if ( i !== 0 ){ attributesString += " "; }
|
||||
|
||||
let attribute = attributes[ i ];
|
||||
attributesString += `${attribute[ 0 ]}="${attribute[ 1 ]}"`;
|
||||
}
|
||||
|
||||
let tag = destination.nodeName.toLowerCase();
|
||||
|
||||
let html = `<${tag} ${attributesString}>${innerHTML}</${tag}>`;
|
||||
|
||||
console.log( "Copied html:", html );
|
||||
|
||||
destination.outerHTML = html;
|
||||
|
||||
return destination;
|
||||
|
||||
}
|
||||
|
||||
static copyAllAttributes( source:Element, destination:Element )
|
||||
{
|
||||
for ( let i = 0; i < source.attributes.length; i++ )
|
||||
{
|
||||
let attribute = source.attributes[ i ];
|
||||
let attributeName = attribute.nodeName;
|
||||
let attributeValue = attribute.nodeValue;
|
||||
destination.setAttribute( attributeName, attributeValue );
|
||||
}
|
||||
}
|
||||
|
||||
static cloneChildren( element:Element ):Node[]
|
||||
{
|
||||
let clones:Node[] = [];
|
||||
|
||||
for ( let i = 0; i < element.childNodes.length; i++ )
|
||||
{
|
||||
|
||||
clones.push( element.childNodes[ i ].cloneNode( true ) );
|
||||
}
|
||||
|
||||
|
||||
return clones;
|
||||
}
|
||||
|
||||
static reverseChildren( element:Element )
|
||||
{
|
||||
Array.from( element.childNodes ).reverse().forEach( node => {
|
||||
element.appendChild( node );
|
||||
});
|
||||
}
|
||||
|
||||
static getChildrenWidth( parent:Element, callback:( e:Element )=>boolean )
|
||||
{
|
||||
HTMLNodeTreeWalker.$.forAll
|
||||
}
|
||||
|
||||
static swapNodes( parent:Element, node1:Node, node2:Node )
|
||||
{
|
||||
if ( node1 === node2 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let next1 = node1.nextSibling;
|
||||
let next2 = node2.nextSibling;
|
||||
|
||||
if ( next1 === node2 )
|
||||
{
|
||||
parent.insertBefore( node2, node1 );
|
||||
}
|
||||
else if ( next2 === node1 )
|
||||
{
|
||||
parent.insertBefore( node1, node2 );
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.insertBefore( node1, next2 );
|
||||
parent.insertBefore( node2, next1 );
|
||||
}
|
||||
}
|
||||
|
||||
static _cssCreationRegex:RegExp;
|
||||
|
||||
static get cssCreationRegex()
|
||||
{
|
||||
if ( DOMEditor._cssCreationRegex)
|
||||
{
|
||||
return DOMEditor._cssCreationRegex;
|
||||
}
|
||||
|
||||
let cssCreationRules = [
|
||||
//"Attribute",
|
||||
/\[\s*(?:\w|-)+\s*(?:(?:\~|\||\^|\$|\*)?=)\s*(?:(?:"(?:\\"|.)*?")|(?:'(?:\\'|.)*?')|(?:(?:\w|-)+))\s*\]/,
|
||||
//"ID",
|
||||
/#(?:\w|-)+/,
|
||||
//"Class",
|
||||
/\.(?:\w|-)+/,
|
||||
//"Empty Attribute",
|
||||
/\[\s*(?:\w|-)+\s*]/,
|
||||
//"Element",
|
||||
/(?:\w|-)+/
|
||||
];
|
||||
|
||||
let regexString = "";
|
||||
|
||||
for (let i = 0; i < cssCreationRules.length; i++)
|
||||
{
|
||||
if (i !== 0)
|
||||
{
|
||||
regexString += "|";
|
||||
}
|
||||
|
||||
regexString += "(" + cssCreationRules[i].source + ")";
|
||||
}
|
||||
|
||||
DOMEditor._cssCreationRegex = new RegExp(regexString, "g");
|
||||
|
||||
return DOMEditor._cssCreationRegex;
|
||||
}
|
||||
|
||||
static extractRawText( node:Node )
|
||||
{
|
||||
if ( node.nodeType === Node.TEXT_NODE )
|
||||
{
|
||||
return node.nodeValue;
|
||||
}
|
||||
|
||||
if ( node.nodeType !== Node.ELEMENT_NODE )
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if ( node.nodeType === Node.ELEMENT_NODE && node.nodeName.toUpperCase() == "BR" )
|
||||
{
|
||||
return "\n";
|
||||
}
|
||||
|
||||
let element = node as Element;
|
||||
|
||||
let text = "";
|
||||
|
||||
element.childNodes.forEach( child =>
|
||||
{
|
||||
text += DOMEditor.extractRawText( child );
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static stringToDocument( html:string ):Document
|
||||
{
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString( html, "text/html" );
|
||||
return doc;
|
||||
}
|
||||
|
||||
static remove( node:Node )
|
||||
{
|
||||
if ( ! node )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! node.parentNode )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
node.parentNode.removeChild( node );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import { nextFrame } from "../library-ts/browser/animation/nextFrame";
|
||||
import { DOMEditor } from "../library-ts/browser/dom/DOMEditor";
|
||||
import { ElementAttribute } from "../library-ts/browser/dom/ElementAttribute";
|
||||
import { ElementType } from "../library-ts/browser/dom/ElementType";
|
||||
import { OnClick } from "../library-ts/browser/dom/EventListeners";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
export type StreamMessage = { type:string, content?:string};
|
||||
|
||||
export class LLAMAChat
|
||||
{
|
||||
static readonly type = new ElementType( "llama-chat" );
|
||||
static readonly inputBox = new ElementType( "input-box" );
|
||||
static readonly inputText = new ElementType( "input-text" );
|
||||
|
||||
static readonly chatBlock = new ElementType( "chat-block" );
|
||||
static readonly userBlock = new ElementType( "user-block" );
|
||||
static readonly llmBlock = new ElementType( "llm-block" );
|
||||
|
||||
static conversionID:string = null;
|
||||
|
||||
static applyOnDocument()
|
||||
{
|
||||
this.type.forEachInDoc(
|
||||
( e )=>
|
||||
{
|
||||
LLAMAChat.applyOnElement( e );
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static setConversionID()
|
||||
{
|
||||
const HASH_PREFIX = "#conversation-id=";
|
||||
|
||||
|
||||
const match = window.location.hash.match(
|
||||
/^#conversation-id=([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i
|
||||
);
|
||||
|
||||
if ( match )
|
||||
{
|
||||
LLAMAChat.conversionID = match[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
LLAMAChat.conversionID = crypto.randomUUID();
|
||||
window.location.hash = `${HASH_PREFIX}${LLAMAChat.conversionID}`;
|
||||
}
|
||||
|
||||
window.location.hash = `${HASH_PREFIX}${LLAMAChat.conversionID}`;
|
||||
|
||||
console.log( LLAMAChat.conversionID );
|
||||
}
|
||||
|
||||
static applyOnElement( element:Element )
|
||||
{
|
||||
LLAMAChat.setConversionID();
|
||||
|
||||
console.log( "LLMCHAT", element );
|
||||
let chatBlock = LLAMAChat.chatBlock.query( element );
|
||||
|
||||
let button = ElementType.button.query( LLAMAChat.inputBox.query( element ) );
|
||||
let inputText = LLAMAChat.inputText.query( element );
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
breaks: true
|
||||
});
|
||||
|
||||
OnClick.add( button,
|
||||
async ( ev )=>
|
||||
{
|
||||
let textValue = DOMEditor.extractRawText( inputText );
|
||||
textValue = textValue.replaceAll( "\u00a0", " ");
|
||||
let innerHTML = inputText.innerHTML;
|
||||
inputText.innerHTML = "";
|
||||
|
||||
let userBlock = LLAMAChat.userBlock.create();
|
||||
userBlock.innerHTML = innerHTML;
|
||||
|
||||
chatBlock.appendChild( userBlock );
|
||||
|
||||
ElementAttribute.contentEditable.removeFrom( inputText );
|
||||
|
||||
let postURL = "http://localhost:9100/chat";
|
||||
|
||||
let message = JSON.stringify( { message: textValue, id:LLAMAChat.conversionID } );
|
||||
|
||||
let postResponse = await fetch( postURL,
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "post", body:message
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! postResponse.body )
|
||||
{
|
||||
throw new Error("No response body");
|
||||
}
|
||||
|
||||
let llmBlock = LLAMAChat.llmBlock.create( "" );
|
||||
|
||||
chatBlock.appendChild( llmBlock );
|
||||
|
||||
|
||||
|
||||
|
||||
const reader = postResponse.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let markdown = "";
|
||||
let maxThinking = 10;
|
||||
let thinkingStart = new Date().getTime();
|
||||
let thinkingFPS = 2;
|
||||
|
||||
let processUpdate = ( m: StreamMessage ) =>
|
||||
{
|
||||
if ( m == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( m.type === "THINKING" )
|
||||
{
|
||||
let now = new Date().getTime();
|
||||
let elapsed = ( now - thinkingStart ) / 1000;
|
||||
let thinkingCounter = ( elapsed / thinkingFPS ) % maxThinking;
|
||||
|
||||
|
||||
let thinking = "";
|
||||
|
||||
for ( let i = 0; i < thinkingCounter; i++ )
|
||||
{
|
||||
thinking += ".";
|
||||
}
|
||||
|
||||
llmBlock.innerHTML = thinking;
|
||||
// llmBlock.appendChild( document.createTextNode( markdown ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( m.type === "CHAT" )
|
||||
{
|
||||
markdown += m.content;
|
||||
|
||||
console.log( m.content );
|
||||
}
|
||||
|
||||
llmBlock.innerHTML = "";
|
||||
llmBlock.innerHTML = md.render( markdown );
|
||||
|
||||
let codeElements = ElementType.code.queryAll( llmBlock );
|
||||
codeElements.forEach(
|
||||
( ce )=>
|
||||
{
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
while ( true )
|
||||
{
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if ( done )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
let updateValue = decoder.decode( value, { stream: true } );
|
||||
|
||||
let updateMessage:StreamMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
updateMessage = JSON.parse( updateValue );
|
||||
}
|
||||
catch( e )
|
||||
{
|
||||
console.log( "Not json:", updateValue );
|
||||
|
||||
let endingMessage:StreamMessage = { type: "DONE" };
|
||||
let endingMessageString = JSON.stringify( endingMessage );
|
||||
|
||||
if ( updateValue.endsWith( endingMessageString ) )
|
||||
{
|
||||
updateMessage = endingMessage;
|
||||
}
|
||||
}
|
||||
|
||||
await nextFrame();
|
||||
|
||||
processUpdate( updateMessage );
|
||||
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
let lastUpdateMessage:StreamMessage = JSON.parse( decoder.decode() );
|
||||
|
||||
await nextFrame();
|
||||
|
||||
markdown += decoder.decode(); // flush any buffered bytes
|
||||
|
||||
processUpdate( lastUpdateMessage );
|
||||
|
||||
console.log( markdown );
|
||||
}
|
||||
catch( e )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
ElementAttribute.contentEditable.to( inputText );
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
|
|
@ -0,0 +1,325 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="1080"
|
||||
height="2160"
|
||||
viewBox="0 0 1080 2160"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||
sodipodi:docname="rojo-chat-panel.svg"
|
||||
xml:space="preserve"
|
||||
inkscape:export-filename="rojo-chat-panel.jpg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#333333"
|
||||
bordercolor="#404040"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#333333"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.41781005"
|
||||
inkscape:cx="-276.44141"
|
||||
inkscape:cy="1626.3371"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs2"><rect
|
||||
x="180.70413"
|
||||
y="26.327753"
|
||||
width="573.227"
|
||||
height="98.130717"
|
||||
id="rect16" /><rect
|
||||
x="100.52415"
|
||||
y="1801.0577"
|
||||
width="879.58631"
|
||||
height="260.8841"
|
||||
id="rect5" /><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath7940"><rect
|
||||
style="fill:#333333;fill-opacity:1;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="rect7942"
|
||||
width="1440"
|
||||
height="810"
|
||||
x="0"
|
||||
y="0" /></clipPath></defs><g
|
||||
inkscape:label="Content"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="fill:#ffffff;fill-opacity:1"><rect
|
||||
style="fill:#000000;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect1"
|
||||
width="1080"
|
||||
height="1698.0284"
|
||||
x="0"
|
||||
y="0"
|
||||
ry="3.8862312" /><rect
|
||||
style="fill:#4a4a4a;fill-opacity:1;stroke-width:1.20124;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect2"
|
||||
width="1080"
|
||||
height="154.36333"
|
||||
x="0"
|
||||
y="0"
|
||||
ry="5.6077323" /><rect
|
||||
style="fill:#6f2525;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect3"
|
||||
width="1080"
|
||||
height="461.97156"
|
||||
x="0"
|
||||
y="1698.0284"
|
||||
ry="3.8862312" /><rect
|
||||
style="fill:#2d181c;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect4"
|
||||
width="971.27222"
|
||||
height="336.20959"
|
||||
x="56.034939"
|
||||
y="1759.1575"
|
||||
ry="27.743992" /><text
|
||||
xml:space="preserve"
|
||||
id="text5"
|
||||
style="font-size:40px;line-height:1.4;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, ';writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);display:inline;fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
transform="translate(9.5737285,4.7868643)"><tspan
|
||||
x="100.52344"
|
||||
y="1842.3586"
|
||||
id="tspan1">Type message...</tspan></text><g
|
||||
id="g19"><rect
|
||||
style="fill:#2c2c2c;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect6"
|
||||
width="828.63788"
|
||||
height="246.21414"
|
||||
x="191.48904"
|
||||
y="249.33499"
|
||||
ry="20.376312" /><text
|
||||
xml:space="preserve"
|
||||
style="font-size:40px;line-height:1.4;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, ';writing-mode:lr-tb;direction:ltr;fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
x="226.37257"
|
||||
y="331.7128"
|
||||
id="text6"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan6"
|
||||
x="226.37257"
|
||||
y="331.7128">HELLO! Can you please help me?</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="387.7128"
|
||||
id="tspan7">I want to know something about the UI!</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="443.7128"
|
||||
id="tspan8">Could you tell me what is wrong with this?</tspan></text></g><rect
|
||||
style="fill:#007bea;fill-opacity:1;stroke-width:0.987022;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect8"
|
||||
width="118.87884"
|
||||
height="108.66821"
|
||||
x="20.308945"
|
||||
y="19.46274"
|
||||
ry="20.698675" /><rect
|
||||
style="fill:#007bea;fill-opacity:1;stroke-width:0.987022;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect9"
|
||||
width="118.87884"
|
||||
height="108.66821"
|
||||
x="801.68555"
|
||||
y="19.46274"
|
||||
ry="20.698675" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect10"
|
||||
width="13.584198"
|
||||
height="1450.1163"
|
||||
x="1047.6836"
|
||||
y="190.1792"
|
||||
ry="7.6411281" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path11"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="5"
|
||||
sodipodi:cx="822.17078"
|
||||
sodipodi:cy="82.247093"
|
||||
sodipodi:r1="38.291626"
|
||||
sodipodi:r2="18.95949"
|
||||
sodipodi:arg1="-1.5707963"
|
||||
sodipodi:arg2="-0.9424778"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 822.17078,43.955467 11.1441,22.953077 25.2734,3.505787 -18.38596,17.691567 4.47571,25.119772 -22.50725,-12.01909 -22.50726,12.01909 4.47571,-25.119772 -18.38595,-17.691569 25.27339,-3.505785 z"
|
||||
inkscape:transform-center-y="-3.6565248"
|
||||
transform="translate(38.215913,-7.1802964)" /><rect
|
||||
style="fill:#007bea;fill-opacity:1;stroke-width:0.987022;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect11"
|
||||
width="118.87884"
|
||||
height="108.66821"
|
||||
x="938.77094"
|
||||
y="19.46274"
|
||||
ry="20.698675" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect12"
|
||||
width="90.051559"
|
||||
height="16.20928"
|
||||
x="953.34583"
|
||||
y="34.835823"
|
||||
ry="8.10464" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect13"
|
||||
width="90.051559"
|
||||
height="16.20928"
|
||||
x="953.34583"
|
||||
y="77.618423"
|
||||
ry="8.10464" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect14"
|
||||
width="90.051559"
|
||||
height="16.20928"
|
||||
x="953.34583"
|
||||
y="98.860138"
|
||||
ry="8.10464" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect15"
|
||||
width="90.051559"
|
||||
height="16.20928"
|
||||
x="953.34583"
|
||||
y="56.975071"
|
||||
ry="8.10464" /><text
|
||||
xml:space="preserve"
|
||||
id="text15"
|
||||
style="font-size:40px;line-height:1.4;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, ';writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect16);display:inline;fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
transform="matrix(1.2619008,0,0,1.2619008,-50.359278,5.1996215)"><tspan
|
||||
x="180.70508"
|
||||
y="67.628125"
|
||||
id="tspan2">Conversation Name</tspan></text><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect17"
|
||||
width="42.238461"
|
||||
height="31.201273"
|
||||
x="25.894936"
|
||||
y="49.242828"
|
||||
ry="5.6077323" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect18"
|
||||
width="54.336914"
|
||||
height="33.748318"
|
||||
x="79.170578"
|
||||
y="47.120293"
|
||||
ry="5.6077323" /><circle
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path18"
|
||||
cx="43.095547"
|
||||
cy="68.170013"
|
||||
r="10.718805" /><circle
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="circle18"
|
||||
cx="94.07946"
|
||||
cy="68.170013"
|
||||
r="10.718805" /><path
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke-width:1.7312;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 33.430521,85.74693 84.146719,2.572171 -0.73491,20.209909 -38.582553,-2.20472 -2.57217,-17.270284 z"
|
||||
id="path19" /><g
|
||||
id="g21"
|
||||
transform="translate(-157.39432,324.94312)"><rect
|
||||
style="fill:#2c2c2c;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect19"
|
||||
width="982.74652"
|
||||
height="587.48138"
|
||||
x="191.48904"
|
||||
y="249.33499"
|
||||
ry="20.376312" /><text
|
||||
xml:space="preserve"
|
||||
style="font-size:40px;line-height:1.4;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, ';writing-mode:lr-tb;direction:ltr;fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
x="226.37257"
|
||||
y="331.7128"
|
||||
id="text21"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan19"
|
||||
x="226.37257"
|
||||
y="331.7128">Sure!</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="387.7128"
|
||||
id="tspan21" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="443.7128"
|
||||
id="tspan22"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2 Bold'">Main Concepts</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="499.7128"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, '"
|
||||
id="tspan23">- Projects</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="555.71277"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, '"
|
||||
id="tspan24">- Groups</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="611.71277"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, '"
|
||||
id="tspan25">- Rojos</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="667.71277"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, '"
|
||||
id="tspan26">- Editor</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="226.37257"
|
||||
y="723.71277"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Exo 2';-inkscape-font-specification:'Exo 2, '"
|
||||
id="tspan27">- Panels</tspan></text><rect
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke-width:1.55888;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect27"
|
||||
width="113.92631"
|
||||
height="95.943802"
|
||||
x="1047.1316"
|
||||
y="1632.9744"
|
||||
ry="47.971901" /><rect
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke-width:1.55888;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect28"
|
||||
width="113.92631"
|
||||
height="95.943802"
|
||||
x="1047.1316"
|
||||
y="1482.7865"
|
||||
ry="47.971901" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path28"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="1074.7195"
|
||||
sodipodi:cy="1710.8225"
|
||||
sodipodi:r1="55.290321"
|
||||
sodipodi:r2="27.376123"
|
||||
sodipodi:arg1="-0.72393721"
|
||||
sodipodi:arg2="0.32326034"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 1116.1432,1674.2015 -15.4655,45.3173 -14.9554,45.4882 -31.5131,-36.0522 -31.9163,-35.6958 46.9787,-9.2651 z"
|
||||
inkscape:transform-center-x="2.5850365"
|
||||
inkscape:transform-center-y="-11.66389"
|
||||
transform="matrix(0.39596491,-0.43947252,0.43947252,0.39596491,-75.206204,1482.3098)" /></g><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect29"
|
||||
width="15.909109"
|
||||
height="63.936607"
|
||||
x="936.53625"
|
||||
y="1822.3434"
|
||||
ry="7.9545546" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="rect30"
|
||||
width="15.909109"
|
||||
height="63.936607"
|
||||
x="1846.3572"
|
||||
y="-976.45911"
|
||||
ry="7.9545546"
|
||||
transform="rotate(90)" /></g></svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,2 @@
|
|||
[ Code Panel ]
|
||||
A new panel window will
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
[ Editor Panel Writing Guide ]
|
||||
- Every panel should be added to TabContainer.openMenu ( in "src/components/tab-container/tab-container.ts" )
|
||||
- It should also be possible to duplicate the panel.
|
||||
- Panels need to have a toolbar
|
||||
- Panels should work on desktop, mobile and tablets for landscape/potrait
|
||||
|
|
@ -19,6 +19,11 @@
|
|||
<section>
|
||||
<h2>2026 — July</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/09-Thursday/index.html">Thursday, 9 July 2026</a></h3>
|
||||
<p>RojoChatPanel — streaming AI chat panel for the editor with LangChain backend (@langchain/core + @langchain/openai), markdown-it rendering, and tab container integration.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3><a href="2026/07-July/06-Monday/index.html">Monday, 6 July 2026</a></h3>
|
||||
<p>File tree rename/delete (right-click context menu), locale generator rewrite (per-directory output, new naming conventions), locale documentation, doc/ renamed to workspace/, fixed navigation header system, Guides/Actions workspace restructure, Update History action rewrite, Rojo chibi bone animator prototype (SVG pivot system, no graphics library).</p>
|
||||
|
|
|
|||
|
|
@ -134,42 +134,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Locales</h3>
|
||||
<p>
|
||||
All user-visible strings are stored as plain files under <code>locales/en/</code>
|
||||
and accessed in code via <code>LocaleManager.$.get( Locales.X.Y.member_ext )</code>.
|
||||
Never hardcode a user-visible string directly in TypeScript.
|
||||
<br>
|
||||
<a href="locales/index.html">Read more about the locale system</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Guides</h3>
|
||||
<p>
|
||||
Reference guides for working in specific areas of the project.
|
||||
Read the relevant guide before starting a task in that area.
|
||||
<br>
|
||||
<a href="../guides/index.html">Browse all guides</a>
|
||||
</p>
|
||||
<ul style="margin-top:0.75rem">
|
||||
<li><strong>Writing code</strong> → <a href="../guides/writing-typescript-code/index.html">Writing TypeScript Code</a></li>
|
||||
<li><strong>Adding user-visible strings</strong> → <a href="../guides/locales/index.html">Locales</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Actions</h3>
|
||||
<p>
|
||||
Repeatable procedures for maintaining the project.
|
||||
<br>
|
||||
<a href="../actions/index.html">Browse all actions</a>
|
||||
</p>
|
||||
<ul style="margin-top:0.75rem">
|
||||
<li><strong>End of session</strong> → <a href="../actions/update-history/index.html">Update History</a>, <a href="../actions/update-outline/index.html">Update Outline</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
|
|
|
|||
Loading…
Reference in New Issue