diff --git a/src-svelte/package.json b/src-svelte/package.json
index ac433b6e..a4862963 100644
--- a/src-svelte/package.json
+++ b/src-svelte/package.json
@@ -62,9 +62,12 @@
"vitest": "^1.2.2"
},
"dependencies": {
+ "@fontsource/inconsolata": "^5.0.16",
"@fontsource/jetbrains-mono": "^5.0.11",
"@neodrag/svelte": "file:../forks/neodrag/packages/svelte",
"autosize": "^6.0.1",
- "nanoid": "^3.3.6"
+ "nanoid": "^3.3.6",
+ "svelte-highlight": "^7.6.0",
+ "svelte-markdown": "^0.4.1"
}
}
diff --git a/src-svelte/screenshots/baseline/screens/chat/conversation/bottom-scroll-indicator.png b/src-svelte/screenshots/baseline/screens/chat/conversation/bottom-scroll-indicator.png
index e1a7f07c..4ddb381f 100644
Binary files a/src-svelte/screenshots/baseline/screens/chat/conversation/bottom-scroll-indicator.png and b/src-svelte/screenshots/baseline/screens/chat/conversation/bottom-scroll-indicator.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/conversation/full-message-width.png b/src-svelte/screenshots/baseline/screens/chat/conversation/full-message-width.png
index 58b55db1..0afcf941 100644
Binary files a/src-svelte/screenshots/baseline/screens/chat/conversation/full-message-width.png and b/src-svelte/screenshots/baseline/screens/chat/conversation/full-message-width.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat-variant-1.png b/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat-variant-1.png
new file mode 100644
index 00000000..95d0e77d
Binary files /dev/null and b/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat-variant-1.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat.png b/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat.png
index 00b18aa8..22b8a03d 100644
Binary files a/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat.png and b/src-svelte/screenshots/baseline/screens/chat/conversation/multiline-chat.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/conversation/not-empty.png b/src-svelte/screenshots/baseline/screens/chat/conversation/not-empty.png
index fd230f0f..47b101ae 100644
Binary files a/src-svelte/screenshots/baseline/screens/chat/conversation/not-empty.png and b/src-svelte/screenshots/baseline/screens/chat/conversation/not-empty.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/message/ai-multiline.png b/src-svelte/screenshots/baseline/screens/chat/message/ai-multiline.png
index 9ce48bb5..6e04ec23 100644
Binary files a/src-svelte/screenshots/baseline/screens/chat/message/ai-multiline.png and b/src-svelte/screenshots/baseline/screens/chat/message/ai-multiline.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/message/code-variant-1.png b/src-svelte/screenshots/baseline/screens/chat/message/code-variant-1.png
new file mode 100644
index 00000000..52969983
Binary files /dev/null and b/src-svelte/screenshots/baseline/screens/chat/message/code-variant-1.png differ
diff --git a/src-svelte/screenshots/baseline/screens/chat/message/code.png b/src-svelte/screenshots/baseline/screens/chat/message/code.png
new file mode 100644
index 00000000..2eb08a1e
Binary files /dev/null and b/src-svelte/screenshots/baseline/screens/chat/message/code.png differ
diff --git a/src-svelte/src/routes/chat/Chat.stories.ts b/src-svelte/src/routes/chat/Chat.stories.ts
index ef4f6591..ce40b318 100644
--- a/src-svelte/src/routes/chat/Chat.stories.ts
+++ b/src-svelte/src/routes/chat/Chat.stories.ts
@@ -77,32 +77,18 @@ const conversation: ChatMessage[] = [
{
role: "Human",
text:
- "Okay, we need to fill this chat up to produce a scrollbar for " +
- 'Storybook. Say short phrases like "Yup" to fill this chat up quickly.',
- },
- {
- role: "AI",
- text: "Yup",
- },
- {
- role: "Human",
- text: "Nay",
- },
- {
- role: "AI",
- text: "Yay",
- },
- {
- role: "Human",
- text: "Say...",
+ "This is some Python code:\n\n" +
+ "```python\n" +
+ "def hello_world():\n" +
+ " print('Hello, world!')\n" +
+ "```\n\n" +
+ "Convert it to Rust",
},
{
role: "AI",
text:
- "AIs don't actually talk like this, you know? " +
- "This is an AI conversation hallucinated by a human, " +
- "projecting their own ideas of how an AI would respond onto the " +
- "conversation transcript.",
+ "Here's how the Python code you provided would look in Rust:\n\n" +
+ '```rust\nfn main() {\n println!("Hello, world!");\n}\n```',
},
];
@@ -144,7 +130,6 @@ BottomScrollIndicator.parameters = {
export const FullMessageWidth: StoryObj = Template.bind({}) as any;
FullMessageWidth.args = {
conversation,
- showMostRecentMessage: false,
};
FullMessageWidth.parameters = {
viewport: {
diff --git a/src-svelte/src/routes/chat/Chat.svelte b/src-svelte/src/routes/chat/Chat.svelte
index 4569ed56..dca492e4 100644
--- a/src-svelte/src/routes/chat/Chat.svelte
+++ b/src-svelte/src/routes/chat/Chat.svelte
@@ -26,8 +26,9 @@
let bottomShadow: HTMLDivElement;
onMount(() => {
- resizeConversationView();
- window.addEventListener("resize", resizeConversationView);
+ resizeConversationView(true);
+ const resizeCallback = () => resizeConversationView(false);
+ window.addEventListener("resize", resizeCallback);
let topScrollObserver = new IntersectionObserver(
intersectionCallback(topShadow),
@@ -39,7 +40,7 @@
bottomScrollObserver.observe(bottomIndicator);
return () => {
- window.removeEventListener("resize", resizeConversationView);
+ window.removeEventListener("resize", resizeCallback);
topScrollObserver.disconnect();
bottomScrollObserver.disconnect();
};
@@ -62,19 +63,21 @@
}
}
- function resizeConversationView() {
+ function resizeConversationView(initialMount = false) {
if (conversationView) {
conversationView.style.maxHeight = "8rem";
requestAnimationFrame(() => {
if (conversationView && conversationContainer) {
conversationView.style.maxHeight = `${conversationContainer.clientHeight}px`;
- if (showMostRecentMessage) {
- showChatBottom();
- }
const conversationDimensions =
conversationView.getBoundingClientRect();
conversationWidthPx.set(conversationDimensions.width);
+ if (initialMount && showMostRecentMessage) {
+ showChatBottom();
+ // scroll to bottom again in case resized elements produce greater scroll
+ setTimeout(showChatBottom, 120);
+ }
}
});
}
diff --git a/src-svelte/src/routes/chat/CodeRender.svelte b/src-svelte/src/routes/chat/CodeRender.svelte
new file mode 100644
index 00000000..59d21ac3
--- /dev/null
+++ b/src-svelte/src/routes/chat/CodeRender.svelte
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
diff --git a/src-svelte/src/routes/chat/Message.stories.ts b/src-svelte/src/routes/chat/Message.stories.ts
index 36a23dca..5c47ee5a 100644
--- a/src-svelte/src/routes/chat/Message.stories.ts
+++ b/src-svelte/src/routes/chat/Message.stories.ts
@@ -55,3 +55,22 @@ AIMultiline.parameters = {
defaultViewport: "tablet",
},
};
+
+export const Code: StoryObj = Template.bind({}) as any;
+Code.args = {
+ message: {
+ role: "Human",
+ text:
+ "This is some Python code:\n\n" +
+ "```python\n" +
+ "def hello_world():\n" +
+ " print('Hello, world!')\n" +
+ "```\n\n" +
+ "What do you think?",
+ },
+};
+Code.parameters = {
+ viewport: {
+ defaultViewport: "tablet",
+ },
+};
diff --git a/src-svelte/src/routes/chat/Message.svelte b/src-svelte/src/routes/chat/Message.svelte
index d238bb42..9b9e0aa9 100644
--- a/src-svelte/src/routes/chat/Message.svelte
+++ b/src-svelte/src/routes/chat/Message.svelte
@@ -1,9 +1,32 @@
- {message.text}
+
+
+
+
+
diff --git a/src-svelte/src/routes/chat/MessageUI.svelte b/src-svelte/src/routes/chat/MessageUI.svelte
index b92b8461..33a6e127 100644
--- a/src-svelte/src/routes/chat/MessageUI.svelte
+++ b/src-svelte/src/routes/chat/MessageUI.svelte
@@ -10,42 +10,77 @@
let initialResizeTimeoutId: ReturnType | undefined;
let finalResizeTimeoutId: ReturnType | undefined;
+ const remPx = 18;
+ // arrow size, left padding, and right padding
+ const messagePaddingPx = (0.5 + 0.75 + 0.75) * remPx;
// chat window size at which the message bubble should be full width
const MIN_FULL_WIDTH_PX = 400;
const MAX_WIDTH_PX = 600;
function maxMessageWidth(chatWidthPx: number) {
- if (chatWidthPx <= MIN_FULL_WIDTH_PX) {
- return chatWidthPx;
+ const availableWidthPx = chatWidthPx - messagePaddingPx;
+ if (availableWidthPx <= MIN_FULL_WIDTH_PX) {
+ return availableWidthPx;
}
- const fractionalWidth = Math.max(0.8 * chatWidthPx, MIN_FULL_WIDTH_PX);
+ const fractionalWidth = Math.max(0.8 * availableWidthPx, MIN_FULL_WIDTH_PX);
return Math.min(fractionalWidth, MAX_WIDTH_PX);
}
+ function resetChildren(textElement: HTMLDivElement) {
+ const pElements = textElement.querySelectorAll("p");
+ pElements.forEach((pElement) => {
+ pElement.style.width = "";
+ });
+
+ const codeElements = textElement.querySelectorAll(".code");
+ codeElements.forEach((codeElement) => {
+ codeElement.style.width = "";
+ });
+ }
+
+ function resizeChildren(textElement: HTMLDivElement, maxWidth: number) {
+ const pElements = textElement.querySelectorAll("p");
+ pElements.forEach((pElement) => {
+ const range = document.createRange();
+ range.selectNodeContents(pElement);
+ const textRect = range.getBoundingClientRect();
+ const actualTextWidth = textRect.width;
+
+ pElement.style.width = `${actualTextWidth}px`;
+ });
+
+ const codeElements = textElement.querySelectorAll(".code");
+ codeElements.forEach((codeElement) => {
+ let existingWidth = codeElement.getBoundingClientRect().width;
+ if (existingWidth > maxWidth) {
+ codeElement.style.width = `${maxWidth}px`;
+ }
+ });
+ }
+
function resizeBubble(chatWidthPx: number) {
if (chatWidthPx > 0 && textElement) {
try {
- textElement.style.width = "";
+ const markdownElement =
+ textElement.querySelector(".markdown");
+ if (!markdownElement) {
+ return;
+ }
+
+ resetChildren(markdownElement);
const maxWidth = maxMessageWidth(chatWidthPx);
- const currentWidth = textElement.getBoundingClientRect().width;
- const newWidth = Math.min(currentWidth, maxWidth);
- textElement.style.width = `${newWidth}px`;
+ const currentWidth = markdownElement.getBoundingClientRect().width;
+ const newWidth = Math.ceil(Math.min(currentWidth, maxWidth));
+ markdownElement.style.width = `${newWidth}px`;
if (finalResizeTimeoutId) {
clearTimeout(finalResizeTimeoutId);
}
finalResizeTimeoutId = setTimeout(() => {
- if (textElement) {
- const range = document.createRange();
- range.selectNodeContents(textElement);
- const textRect = range.getBoundingClientRect();
- const actualTextWidth = textRect.width;
-
- const finalWidth = Math.min(actualTextWidth, newWidth);
- textElement.style.width = `${finalWidth}px`;
- }
+ resizeChildren(markdownElement, maxWidth);
+ markdownElement.style.width = "";
}, 10);
} catch (err) {
console.warn("Cannot resize chat message bubble: ", err);
@@ -76,6 +111,7 @@
.message {
--message-color: gray;
--arrow-size: 0.5rem;
+ --internal-spacing: 0.75rem;
position: relative;
}
@@ -83,15 +119,15 @@
margin: 0.5rem var(--arrow-size);
border-radius: var(--corner-roundness);
width: fit-content;
- padding: 0.75rem;
+ padding: var(--internal-spacing);
box-sizing: border-box;
background-color: var(--message-color);
- white-space: pre-line;
text-align: left;
}
.text {
box-sizing: content-box;
+ width: fit-content;
max-width: 600px;
}
diff --git a/src-svelte/src/routes/storybook.test.ts b/src-svelte/src/routes/storybook.test.ts
index 58e89fd1..f2c58a45 100644
--- a/src-svelte/src/routes/storybook.test.ts
+++ b/src-svelte/src/routes/storybook.test.ts
@@ -111,7 +111,7 @@ const components: ComponentTestConfig[] = [
},
{
path: ["screens", "chat", "message"],
- variants: ["human", "ai", "ai-multiline"],
+ variants: ["human", "ai", "ai-multiline", "code"],
},
{
path: ["screens", "chat", "conversation"],
diff --git a/src-svelte/src/routes/styles.css b/src-svelte/src/routes/styles.css
index 34a9f92d..f563cc03 100644
--- a/src-svelte/src/routes/styles.css
+++ b/src-svelte/src/routes/styles.css
@@ -1,4 +1,5 @@
@import "@fontsource/jetbrains-mono";
+@import "@fontsource/inconsolata";
@font-face {
font-family: 'Nasalization';
diff --git a/yarn.lock b/yarn.lock
index 3f7e59f8..3b8175f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1386,6 +1386,11 @@
resolved "https://registry.yarnpkg.com/@fontsource/fira-mono/-/fira-mono-4.5.10.tgz#443be4b2b4fc6e685b88431fcfdaf8d5f5639bbf"
integrity sha512-bxUnRP8xptGRo8YXeY073DSpfK74XpSb0ZyRNpHV9WvLnJ7TwPOjZll8hTMin7zLC6iOp59pDZ8EQDj1gzgAQQ==
+"@fontsource/inconsolata@^5.0.16":
+ version "5.0.16"
+ resolved "https://registry.yarnpkg.com/@fontsource/inconsolata/-/inconsolata-5.0.16.tgz#8237065e6bb0366f66c49cebeea7d1d2d1fa504e"
+ integrity sha512-xcKlutUGBGGwjlxHyWA8b/J+ayGaPkAEYQQfs0NchvnbKz4X8RoIVSoEWg27//4lFoR817mc1Z3yCMXvRajmBg==
+
"@fontsource/jetbrains-mono@^5.0.11":
version "5.0.18"
resolved "https://registry.yarnpkg.com/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.18.tgz#ac2d610201bb3bc0577ed907a94aed37af8aa537"
@@ -3096,6 +3101,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8"
integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==
+"@types/marked@^5.0.1":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@types/marked/-/marked-5.0.2.tgz#ca6b0cd7a5c8799c8cd0963df0b3e1a9021dcdfa"
+ integrity sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==
+
"@types/mdast@^3.0.0":
version "3.0.15"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
@@ -6709,6 +6719,11 @@ he@1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+highlight.js@11.9.0:
+ version "11.9.0"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
+ integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
+
hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@@ -7856,6 +7871,11 @@ markdown-to-jsx@^7.1.8:
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.0.tgz#4606c5c549a6f6cb87604c35f5ee4f42959ffb6b"
integrity sha512-zilc+MIkVVXPyTb4iIUTIz9yyqfcWjszGXnwF9K/aiBWcHXFcmdEMTkG01/oQhwSCH7SY1BnG6+ev5BzWmbPrg==
+marked@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-5.1.2.tgz#62b5ccfc75adf72ca3b64b2879b551d89e77677f"
+ integrity sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==
+
mdast-util-definitions@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2"
@@ -10542,11 +10562,26 @@ svelte-eslint-parser@^0.32.2:
postcss "^8.4.25"
postcss-scss "^4.0.6"
+svelte-highlight@^7.6.0:
+ version "7.6.0"
+ resolved "https://registry.yarnpkg.com/svelte-highlight/-/svelte-highlight-7.6.0.tgz#b811f72c3530fab12f8babeca2363b3fac53f725"
+ integrity sha512-J9X1d07iMIKZMAqNAhlkjLX/FS+7R2lPrqVul7i+EleVZIOYvBhtx7ES62bc661a70nKNOS05yr9JAvyQPPOIA==
+ dependencies:
+ highlight.js "11.9.0"
+
svelte-hmr@^0.15.3:
version "0.15.3"
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.15.3.tgz#df54ccde9be3f091bf5f18fc4ef7b8eb6405fbe6"
integrity sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==
+svelte-markdown@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/svelte-markdown/-/svelte-markdown-0.4.1.tgz#ddf13cfd6e0f29a02a82854b48766b527ec90f8d"
+ integrity sha512-pOlLY6EruKJaWI9my/2bKX8PdTeP5CM0s4VMmwmC2prlOkjAf+AOmTM4wW/l19Y6WZ87YmP8+ZCJCCwBChWjYw==
+ dependencies:
+ "@types/marked" "^5.0.1"
+ marked "^5.1.2"
+
svelte-preprocess@^5.0.0, svelte-preprocess@^5.0.4, svelte-preprocess@^5.1.0:
version "5.1.3"
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz#7682239fe53f724c845b53026816fdfe15d028f9"