parse code blocks and chain-of-thought out of chat responses
This commit is contained in:
parent
710a6de7bc
commit
a6f9824835
3 changed files with 173 additions and 11 deletions
|
@ -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
93
client/response.tsx
Normal 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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue