parse code blocks and chain-of-thought out of chat responses

This commit is contained in:
Charlotte Som 2025-02-26 13:35:53 +00:00
parent 710a6de7bc
commit a6f9824835
3 changed files with 173 additions and 11 deletions

View file

@ -1,4 +1,5 @@
import { Signal } from "@char/aftercare";
import { ChatResponse } from "./response.tsx";
const main = document.querySelector("main")!;
@ -11,7 +12,7 @@ async function nav() {
button.addEventListener("click", e => {
e.preventDefault();
main.append(conversationUI(conversation.id, conversation.name));
main.append(conversationUI(conversation.id));
nav.remove();
});
@ -24,7 +25,7 @@ async function nav() {
_tap={b =>
b.addEventListener("click", e => {
e.preventDefault();
main.append(conversationUI("new", "New conversation"));
main.append(conversationUI("new"));
nav.remove();
})
}
@ -72,7 +73,7 @@ function conversationUI(id: string) {
});
const chatlog = <section className="chatlog" />;
const inFlightMessages = new Map<string, Element>();
const inFlightMessages = new Map<string, ChatResponse>();
socket.addEventListener("message", event => {
if (typeof event.data !== "string") return;
@ -87,15 +88,20 @@ function conversationUI(id: string) {
} else if ("u" in message) {
chatlog.append(<article className="user">{message.u}</article>);
} else if ("f" in message) {
chatlog.append(<article className="assistant">{message.f}</article>);
const response = new ChatResponse();
response.append(message.f);
response.finalize();
chatlog.append(response.element);
} else if ("s" in message) {
const article = <article className="assistant" />;
inFlightMessages.set(message.s, article);
chatlog.append(article);
const response = new ChatResponse();
inFlightMessages.set(message.s, response);
chatlog.append(response.element);
} else if ("r" in message && "c" in message) {
const article = inFlightMessages.get(message.r)!;
article.append(message.c);
} else if ("d" in message) {
const response = inFlightMessages.get(message.d);
if (response) response.finalize();
inFlightMessages.delete(message.d);
} else if ("n" in message) {
name.set(message.n);

93
client/response.tsx Normal file
View file

@ -0,0 +1,93 @@
import hljs from "npm:highlight.js/lib/core";
import javascript from "npm:highlight.js/lib/languages/javascript";
import python from "npm:highlight.js/lib/languages/python";
import rust from "npm:highlight.js/lib/languages/rust";
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("rust", rust);
hljs.registerLanguage("python", python);
export class ChatResponse {
element = (<article className="assistant" />);
currentLine: Text | undefined;
codeBlockContext: Element | undefined;
thinkingContext: HTMLDetailsElement | undefined;
currentContext(): Element {
if (this.codeBlockContext) return this.codeBlockContext;
if (this.thinkingContext) return this.thinkingContext;
return this.element;
}
finalizeLine() {
if (!this.currentLine?.textContent) return;
const line = this.currentLine.textContent;
if (!this.thinkingContext && line === "<think>") {
this.thinkingContext = (
<details open>
<summary>{`<think />`}</summary>
</details>
) as HTMLDetailsElement;
this.element.append(this.thinkingContext);
this.currentLine.remove();
} else if (this.thinkingContext && line === "</think>") {
this.currentLine.remove();
this.thinkingContext.open = false;
this.thinkingContext = undefined;
}
if (line.startsWith("```")) {
const remainder = line.substring(3).trimStart();
this.currentLine.remove();
if (!this.codeBlockContext) {
const pre = (
<pre>
<code className={`language-${remainder}`} />
</pre>
);
this.currentContext().append(pre);
this.codeBlockContext = pre.querySelector("code")!;
} else {
hljs.highlightElement(this.codeBlockContext as HTMLElement);
// TODO: highlight.js the guy
this.codeBlockContext = undefined;
this.currentContext().append(remainder);
}
}
}
append(text: string) {
const tokens = text
.split(/(\s+)/)
.flatMap(s => s.split(/(\n)/))
.filter(s => s);
for (const token of tokens) {
if (token === "\n") {
if (!this.currentLine) continue;
this.finalizeLine();
this.currentLine.appendData(token);
this.currentLine = undefined;
continue;
}
if (this.currentLine === undefined) {
this.currentLine = document.createTextNode(token);
this.currentContext().append(this.currentLine);
} else {
this.currentLine.appendData(token);
}
}
}
finalize() {
this.finalizeLine();
// TODO: check if we have a dangling think section or code block and lift it back out
}
}

View file

@ -1,16 +1,19 @@
:root {
--color-bg: 0 0 0;
--color-bg-x: 16 16 16;
--color-fg: 255 255 255;
--color-off-accent: var(--color-fg);
--alpha-text: 1;
line-height: 1.25;
}
::selection {
background-color: rgb(var(--color-accent) / 0.5);
}
input {
--color-bg: 16 16 16;
input[type="text"] {
background-color: rgb(var(--color-bg-x) / 1);
}
button {
@ -29,8 +32,7 @@ form input {
font-size: 16px;
}
section,
article {
:is(section, article, header) {
margin: 0;
}
@ -83,4 +85,65 @@ main,
padding-bottom: 0;
color: rgb(var(--color-text) / 0.5);
}
summary {
margin-bottom: 0;
color: rgb(var(--color-accent) / 1);
font-weight: bold;
}
}
pre > code {
font-size: 0.8em;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-title,
.hljs-section,
.hljs-doctag,
.hljs-name,
.hljs-strong {
font-weight: bold;
color: hsl(293, 63%, 76%);
}
.hljs-comment {
color: hsl(248, 7%, 56%);
}
.hljs-title,
.hljs-section,
.hljs-built_in,
.hljs-literal,
.hljs-type,
.hljs-addition,
.hljs-tag,
.hljs-quote,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: hsl(263, 68%, 72%);
}
.hljs-string,
.hljs-meta,
.hljs-subst,
.hljs-symbol,
.hljs-regexp,
.hljs-attribute,
.hljs-deletion,
.hljs-variable,
.hljs-template-variable,
.hljs-link,
.hljs-bullet {
color: hsl(330, 100%, 74%);
}
.hljs-number {
color: hsl(202, 87%, 76%);
}
.hljs-emphasis {
font-style: italic;
}