204 lines
9.7 KiB
HTML
204 lines
9.7 KiB
HTML
|
|
<!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>
|