diff --git a/client/main.tsx b/client/main.tsx
index 4399937..35bbf7f 100644
--- a/client/main.tsx
+++ b/client/main.tsx
@@ -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 = ;
- const inFlightMessages = new Map();
+ const inFlightMessages = new Map();
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({message.u});
} else if ("f" in message) {
- chatlog.append({message.f});
+ const response = new ChatResponse();
+ response.append(message.f);
+ response.finalize();
+ chatlog.append(response.element);
} else if ("s" in message) {
- const article = ;
- 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);
diff --git a/client/response.tsx b/client/response.tsx
new file mode 100644
index 0000000..6697fa9
--- /dev/null
+++ b/client/response.tsx
@@ -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 = ();
+
+ 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 === "") {
+ this.thinkingContext = (
+
+ {``}
+
+ ) as HTMLDetailsElement;
+ this.element.append(this.thinkingContext);
+ this.currentLine.remove();
+ } else if (this.thinkingContext && line === "") {
+ 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 = (
+
+
+
+ );
+ 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
+ }
+}
diff --git a/client/web/css/styles.css b/client/web/css/styles.css
index 220fb14..b5577d8 100644
--- a/client/web/css/styles.css
+++ b/client/web/css/styles.css
@@ -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;
}