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; }