From 55acdd3f12674ddbc1a9a083f9bea7a2f09b237d Mon Sep 17 00:00:00 2001 From: Rokojori Date: Fri, 10 Jul 2026 20:18:15 +0200 Subject: [PATCH] =?UTF-8?q?history:=20RojoChatPanel=20=E2=80=94=20streamin?= =?UTF-8?q?g=20AI=20chat,=20LangChain=20backend,=20tab=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 292 +++++++++++++++- package.json | 5 +- server/index.ts | 2 + server/rojos/RojosAgent.ts | 46 +++ server/rojos/RojosConfig.ts | 5 + server/routes/rojos.ts | 85 +++++ src/commands/Command.ts | 4 +- src/commands/file-tree-commands/AddFile.ts | 7 +- .../rojo-chat-panel/rojo-chat-panel.ts | 175 ++++++++++ src/components/tab-container/tab-container.ts | 1 + src/editor/Editor.ts | 3 +- src/locales/generated/Locales.ts | 9 + src/locales/generated/commands/Commands.ts | 8 + .../file-tree-commands/FileTreeCommands.ts | 6 + workspace/_assets_/nav-data.js | 1 + workspace/actions/index.html | 5 +- .../07-July/09-Thursday/RojoChatPanel.txt | 31 ++ .../2026/07-July/09-Thursday/index.html | 203 +++++++++++ .../llama-app-source/backend/Agent.ts | 90 +++++ .../llama-app-source/backend/main.ts | 111 ++++++ .../llama-app-source/frontend/DOMEditor.ts | 233 +++++++++++++ .../llama-app-source/frontend/LLAMAChat.ts | 223 ++++++++++++ .../07-July/09-Thursday/rojo-chat-panel.jpg | Bin 0 -> 117674 bytes .../07-July/09-Thursday/rojo-chat-panel.svg | 325 ++++++++++++++++++ .../2026/07-July/10-Friday/CodePanel.txt | 2 + .../07-July/10-Friday/Panel Writing Guide.txt | 5 + workspace/history/index.html | 5 + workspace/outline/index.html | 36 -- 28 files changed, 1869 insertions(+), 49 deletions(-) create mode 100644 server/rojos/RojosAgent.ts create mode 100644 server/rojos/RojosConfig.ts create mode 100644 server/routes/rojos.ts create mode 100644 src/components/rojo-chat-panel/rojo-chat-panel.ts create mode 100644 src/locales/generated/Locales.ts create mode 100644 src/locales/generated/commands/Commands.ts create mode 100644 src/locales/generated/commands/file-tree-commands/FileTreeCommands.ts create mode 100644 workspace/history/2026/07-July/09-Thursday/RojoChatPanel.txt create mode 100644 workspace/history/2026/07-July/09-Thursday/index.html create mode 100644 workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/Agent.ts create mode 100644 workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/main.ts create mode 100644 workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/DOMEditor.ts create mode 100644 workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/LLAMAChat.ts create mode 100644 workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.jpg create mode 100644 workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg create mode 100644 workspace/history/2026/07-July/10-Friday/CodePanel.txt create mode 100644 workspace/history/2026/07-July/10-Friday/Panel Writing Guide.txt diff --git a/package-lock.json b/package-lock.json index 43463b1..9b9fe63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index 91531ef..fc794d1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/index.ts b/server/index.ts index e7c9eb9..68d6f2c 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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 ) => { diff --git a/server/rojos/RojosAgent.ts b/server/rojos/RojosAgent.ts new file mode 100644 index 0000000..6bde589 --- /dev/null +++ b/server/rojos/RojosAgent.ts @@ -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(); + +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 } ); + } +} diff --git a/server/rojos/RojosConfig.ts b/server/rojos/RojosConfig.ts new file mode 100644 index 0000000..3e87815 --- /dev/null +++ b/server/rojos/RojosConfig.ts @@ -0,0 +1,5 @@ +export const RojosConfig = { + baseURL: "http://0.0.0.0:8900/v1", + model: "LLAMA", + apiKey: "not-needed", +}; diff --git a/server/routes/rojos.ts b/server/routes/rojos.ts new file mode 100644 index 0000000..e038356 --- /dev/null +++ b/server/routes/rojos.ts @@ -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; diff --git a/src/commands/Command.ts b/src/commands/Command.ts index 549a09b..5793275 100644 --- a/src/commands/Command.ts +++ b/src/commands/Command.ts @@ -20,12 +20,12 @@ export abstract class Command getLocalizedTitle():Promise { - return LocaleManager.$().get( this._titleLocalePath ); + return LocaleManager.$.get( this._titleLocalePath ); } getLocalizedInfo():Promise { - return LocaleManager.$().get( this._infoLocalePath ); + return LocaleManager.$.get( this._infoLocalePath ); } abstract execute( context: CommandContext ): Promise diff --git a/src/commands/file-tree-commands/AddFile.ts b/src/commands/file-tree-commands/AddFile.ts index bbe41ed..efc5be7 100644 --- a/src/commands/file-tree-commands/AddFile.ts +++ b/src/commands/file-tree-commands/AddFile.ts @@ -5,12 +5,7 @@ export class AddFileCommand extends Command { constructor(){ super( "", "" ); } - async _doAction( context: CommandContext ): Promise - { - - } - - async _undoAction( context: CommandContext ): Promise + async execute( context: CommandContext ): Promise { } diff --git a/src/components/rojo-chat-panel/rojo-chat-panel.ts b/src/components/rojo-chat-panel/rojo-chat-panel.ts new file mode 100644 index 0000000..7fae094 --- /dev/null +++ b/src/components/rojo-chat-panel/rojo-chat-panel.ts @@ -0,0 +1,175 @@ +import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js'; + +declare const markdownit: ( options?: Record ) => { 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 = ` +
+
🤖
+ Rojo + + +
+
+
+
+
+ + +
+
+ `; + + 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 + { + 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 ); diff --git a/src/components/tab-container/tab-container.ts b/src/components/tab-container/tab-container.ts index 864fe3d..a0c8f9c 100644 --- a/src/components/tab-container/tab-container.ts +++ b/src/components/tab-container/tab-container.ts @@ -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, () => { diff --git a/src/editor/Editor.ts b/src/editor/Editor.ts index 4bb74ca..eb9585a 100644 --- a/src/editor/Editor.ts +++ b/src/editor/Editor.ts @@ -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 = new Map(); activeDoc: string | null = null; - undoManager: UndoManager readonly onDocumentOpened: EventSlot = new EventSlot(); diff --git a/src/locales/generated/Locales.ts b/src/locales/generated/Locales.ts new file mode 100644 index 0000000..040afd9 --- /dev/null +++ b/src/locales/generated/Locales.ts @@ -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; +} diff --git a/src/locales/generated/commands/Commands.ts b/src/locales/generated/commands/Commands.ts new file mode 100644 index 0000000..899628e --- /dev/null +++ b/src/locales/generated/commands/Commands.ts @@ -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; +} diff --git a/src/locales/generated/commands/file-tree-commands/FileTreeCommands.ts b/src/locales/generated/commands/file-tree-commands/FileTreeCommands.ts new file mode 100644 index 0000000..c7c7b5d --- /dev/null +++ b/src/locales/generated/commands/file-tree-commands/FileTreeCommands.ts @@ -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'; +} diff --git a/workspace/_assets_/nav-data.js b/workspace/_assets_/nav-data.js index 3fcfbff..cd0bc97 100644 --- a/workspace/_assets_/nav-data.js +++ b/workspace/_assets_/nav-data.js @@ -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' }, diff --git a/workspace/actions/index.html b/workspace/actions/index.html index 83c0367..91aab49 100644 --- a/workspace/actions/index.html +++ b/workspace/actions/index.html @@ -12,7 +12,10 @@

Actions

-

Repeatable procedures for maintaining the project. Run these at the appropriate moment in a working session.

+

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. + +

diff --git a/workspace/history/2026/07-July/09-Thursday/RojoChatPanel.txt b/workspace/history/2026/07-July/09-Thursday/RojoChatPanel.txt new file mode 100644 index 0000000..e2be8eb --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/RojoChatPanel.txt @@ -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. + + + + + diff --git a/workspace/history/2026/07-July/09-Thursday/index.html b/workspace/history/2026/07-July/09-Thursday/index.html new file mode 100644 index 0000000..de51161 --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/index.html @@ -0,0 +1,203 @@ + + + + + + Session Summary — 9 July 2026 + + + + +
+ +
+

Thursday, 9 July 2026

+

Roject — Session Summary

+

+ RojoChatPanel — a streaming AI chat panel for the editor, with LangChain backend, + markdown rendering, and tab container integration. +

+
+ +
+

What we built

+ +
+

RojoChatPanel — Backend

+

+ A new /api/rojos/chat route streams AI responses from an + OpenAI-compatible model (e.g. a local LLAMA). The backend is split into + three files under server/rojos/: +

+
    +
  • RojosConfig.ts — hardcoded config wrapper (baseURL, + model, apiKey). Designed to grow as more Rojo settings + are added.
  • +
  • RojosAgent.ts — in-memory session store (Map of + conversation ID → message history). Uses ChatOpenAI.stream() from + @langchain/openai with messages converted to + HumanMessage / AIMessage via @langchain/core.
  • +
  • server/routes/rojos.ts — Express router, POST /chat, + requires auth. Iterates the agent stream with for await, + writes newline-delimited JSON chunks ({ type:"CHAT", text }, + { type:"THINKING" }, { type:"DONE" }) directly + via res.write(), then calls res.end(). + After streaming, calls updateAgentConversation so the assistant + reply is appended to the session history.
  • +
+

+ The route is mounted at /api/rojos in server/index.ts, + keeping the namespace open for future Rojo settings endpoints. +

+
+ server/rojos/RojosConfig.ts + server/rojos/RojosAgent.ts + server/routes/rojos.ts + @langchain/openai + @langchain/core + SSE / res.write +
+
+ +
+

RojoChatPanel — Frontend Component

+

+ A new rojo-chat-panel custom element following the same pattern + as html-editor-panel. The element has three zones: +

+
    +
  • Toolbar — robot emoji icon, conversation name label, two + placeholder buttons (actions ★, settings ☰).
  • +
  • Chat history — scrolling div; user messages + are right-aligned bubble, assistant messages are left-aligned and rendered + with markdown-it (live as chunks arrive).
  • +
  • Input area — contenteditable div (3-line height), + a placeholder + button, and a â–² send button. + Enter sends; Shift+Enter inserts a newline.
  • +
+

+ Each panel instance generates a fresh crypto.randomUUID() + conversation ID on connect. Raw text is extracted from the + contenteditable with an inlined recursive walker (same logic + as DOMEditor.extractRawText). + markdown-it is loaded as a UMD global from + /vendor/markdown-it.min.js (built file copied from + node_modules, no bundler required). +

+
+ src/components/rojo-chat-panel/rojo-chat-panel.ts + public/components/rojo-chat-panel/rojo-chat-panel.css + public/vendor/markdown-it.min.js + contenteditable + ReadableStream +
+
+ +
+

Tab Container Integration

+

+ Rojo Chat was added to the panelTypes array in + TabContainer.openMenu(), so it appears under + Add → Rojo Chat in every tab container's ⋮ menu. + Duplicate already worked for free — it calls the stored + factory, creating a second panel with its own UUID and empty history. +

+

+ RojoChatPanel also implements addContextMenuEntries + (the EditorPanel interface), displaying a read-only label with + the first 8 characters of the conversation ID. +

+
+ src/components/tab-container/tab-container.ts + addContextMenuEntries + panelTypes +
+
+ +
+ +
+

Key Decisions

+ +
+ Split LangChain packages, not the monolithic langchain +

+ The example code used the monolithic langchain package with + createAgent. We switched to @langchain/core + + @langchain/openai and call model.stream() directly. + With no tools in scope yet, this is equivalent and avoids a large dependency. + Migrating to createReactAgent from @langchain/langgraph + when tools are added will be a small, localised change in RojosAgent.ts. +

+
+ +
+ RojosConfig.ts as a hardcoded TypeScript wrapper +

+ A JSON config file in data/ 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. +

+
+ +
+ markdown-it UMD copied to public/vendor/ +

+ 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 + public/vendor/ keeps it self-hosted, available offline, and + version-locked with the rest of node_modules. +

+
+ +
+ In-memory conversation sessions, UUID per panel instance +

+ Persisting conversations to data/ 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 + connectedCallback; named/saved conversations are deferred. +

+
+ +
+ for await + res.write() instead of Readable.pipe() +

+ The initial implementation used Readable.from(async function*(){}) + piped to the response. This was simplified to a direct for await + loop calling res.write() then res.end(), which is + easier to read and avoids the intermediate Readable wrapper. +

+
+ +
+ +
+

Structural Changes

+
+

+ package.json — added @langchain/core, @langchain/openai, markdown-it
+ server/rojos/RojosConfig.ts — new: model/provider config wrapper
+ server/rojos/RojosAgent.ts — new: in-memory session store, LangChain stream
+ server/routes/rojos.ts — new: POST /chat streaming endpoint
+ server/index.ts — added import rojosRouter, mounted at /api/rojos
+ src/components/rojo-chat-panel/rojo-chat-panel.ts — new: custom element
+ public/components/rojo-chat-panel/rojo-chat-panel.css — new: panel styles
+ public/vendor/markdown-it.min.js — new: UMD build (copied from node_modules)
+ public/editor.html — added rojo-chat-panel CSS, vendor script, module script
+ src/components/tab-container/tab-container.ts — added Rojo Chat to panelTypes +

+
+
+ +
+ Roject — session log — 9 July 2026 +
+ +
+ + + + + diff --git a/workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/Agent.ts b/workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/Agent.ts new file mode 100644 index 0000000..bdca67b --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/Agent.ts @@ -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(); + +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>> +{ + 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; +} diff --git a/workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/main.ts b/workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/main.ts new file mode 100644 index 0000000..24284ad --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/llama-app-source/backend/main.ts @@ -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(); \ No newline at end of file diff --git a/workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/DOMEditor.ts b/workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/DOMEditor.ts new file mode 100644 index 0000000..62a5ce2 --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/DOMEditor.ts @@ -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}`; + + 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 ); + } +} \ No newline at end of file diff --git a/workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/LLAMAChat.ts b/workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/LLAMAChat.ts new file mode 100644 index 0000000..f90e566 --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/llama-app-source/frontend/LLAMAChat.ts @@ -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 ); + } + ) + } +} \ No newline at end of file diff --git a/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.jpg b/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8519cd85484ebd43b10688a7c2111c15215fa7fc GIT binary patch literal 117674 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 literal 0 HcmV?d00001 diff --git a/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg b/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg new file mode 100644 index 0000000..0c9277d --- /dev/null +++ b/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg @@ -0,0 +1,325 @@ + + + +Type message...HELLO! Can you please help me?I want to know something about the UI!Could you tell me what is wrong with this?Conversation NameSure!Main Concepts- Projects- Groups- Rojos- Editor- Panels diff --git a/workspace/history/2026/07-July/10-Friday/CodePanel.txt b/workspace/history/2026/07-July/10-Friday/CodePanel.txt new file mode 100644 index 0000000..7d734da --- /dev/null +++ b/workspace/history/2026/07-July/10-Friday/CodePanel.txt @@ -0,0 +1,2 @@ +[ Code Panel ] +A new panel window will \ No newline at end of file diff --git a/workspace/history/2026/07-July/10-Friday/Panel Writing Guide.txt b/workspace/history/2026/07-July/10-Friday/Panel Writing Guide.txt new file mode 100644 index 0000000..328351d --- /dev/null +++ b/workspace/history/2026/07-July/10-Friday/Panel Writing Guide.txt @@ -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 diff --git a/workspace/history/index.html b/workspace/history/index.html index 7358173..5050a34 100644 --- a/workspace/history/index.html +++ b/workspace/history/index.html @@ -19,6 +19,11 @@

2026 — July

+
+

Thursday, 9 July 2026

+

RojoChatPanel — streaming AI chat panel for the editor with LangChain backend (@langchain/core + @langchain/openai), markdown-it rendering, and tab container integration.

+
+

Monday, 6 July 2026

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).

diff --git a/workspace/outline/index.html b/workspace/outline/index.html index 4ccfa6f..5eae564 100644 --- a/workspace/outline/index.html +++ b/workspace/outline/index.html @@ -134,42 +134,6 @@
-
-

Locales

-

- All user-visible strings are stored as plain files under locales/en/ - and accessed in code via LocaleManager.$.get( Locales.X.Y.member_ext ). - Never hardcode a user-visible string directly in TypeScript. -
- Read more about the locale system -

-
- -
-

Guides

-

- Reference guides for working in specific areas of the project. - Read the relevant guide before starting a task in that area. -
- Browse all guides -

- -
- -
-

Actions

-

- Repeatable procedures for maintaining the project. -
- Browse all actions -

- -