Skip to content

Commit 7d8f412

Browse files
committed
Add copy to clipboard function to chat messages
1 parent 2a4430c commit 7d8f412

File tree

3 files changed

+88
-35
lines changed

3 files changed

+88
-35
lines changed

llamafile/server/www/chatbot.css

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,28 +96,75 @@ ul li:first-child {
9696
}
9797

9898
.message {
99-
margin-bottom: 1rem;
10099
padding: 0.75rem;
101100
border-radius: 8px;
102-
max-width: 80%;
103101
word-wrap: break-word;
102+
background: var(--message-background);
104103
}
105104

106-
.message.user {
107-
background: #e9ecef;
105+
.message-wrapper.user {
106+
--message-background: #e9ecef;
108107
margin-left: auto;
108+
max-width: 80%;
109109
}
110110

111-
.message.assistant {
112-
background: #f8f9fa;
111+
.message-wrapper.assistant {
113112
margin-right: auto;
113+
/* Not needed because of the message controls */
114+
margin-bottom: 0;
115+
max-width: 90%;
114116
}
115117

116-
.message.system {
117-
background: #f8e3fa;
118+
.message-wrapper.system {
119+
--message-background: #f8e3fa;
118120
margin-right: auto;
119121
}
120122

123+
.message-wrapper {
124+
position: relative;
125+
overflow: visible;
126+
margin-bottom: 1rem;
127+
}
128+
129+
.message-controls {
130+
margin-left: 0.5rem;
131+
margin-right: 0.5rem;
132+
display: inline-flex;
133+
gap: 0.25rem;
134+
}
135+
136+
.message-wrapper.user .message-controls {
137+
right: 0;
138+
}
139+
140+
.message-controls button, .copy-button {
141+
position: static;
142+
padding: 0.25rem;
143+
border-radius: 4px;
144+
border: 1px solid #dee2e6;
145+
background: white;
146+
transition: all 0.1s;
147+
cursor: pointer;
148+
box-sizing: content-box;
149+
width: 16px;
150+
height: 16px;
151+
font-size: 16px;
152+
line-height: 16px;
153+
text-align: center;
154+
}
155+
156+
.message-controls button:hover,
157+
.message-controls button:focus,
158+
.copy-button:hover,
159+
.copy-button:focus {
160+
border-color: #aaa;
161+
}
162+
163+
.message-controls button img, .copy-button svg {
164+
width: 16px;
165+
height: 16px;
166+
}
167+
121168
.message img {
122169
max-width: 100%;
123170
height: auto;
@@ -264,12 +311,15 @@ button.complete-button:disabled {
264311

265312
.message pre {
266313
margin: 1rem auto;
267-
background: #fefefe;
314+
background: #f8f9fa;
315+
border: 1px solid #dee2e6;
268316
padding: 0.5rem;
269317
border-radius: 4px;
270318
overflow-x: auto;
271319
position: relative;
272320
white-space: pre-wrap;
321+
min-height: 1.5rem;
322+
box-sizing: content-box;
273323
}
274324

275325
.message blockquote {
@@ -289,27 +339,14 @@ button.complete-button:disabled {
289339
margin-left: 1rem;
290340
}
291341

292-
.copy-button {
342+
pre .copy-button {
293343
position: absolute;
294344
top: 0.5rem;
295345
right: 0.5rem;
296-
padding: 0.25rem;
297-
background: #fff;
298-
border: 1px solid #dee2e6;
299-
border-radius: 4px;
300-
cursor: pointer;
301-
opacity: 0.8;
302-
transition: opacity 0.2s;
303346
z-index: 1;
304347
}
305348

306-
.copy-button:hover {
307-
opacity: 1;
308-
}
309-
310349
.copy-button svg {
311-
width: 16px;
312-
height: 16px;
313350
display: block;
314351
}
315352

llamafile/server/www/chatbot.js

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,22 @@ function generateId() {
6262
return Date.now().toString(36) + Math.random().toString(36).substring(2);
6363
}
6464

65-
function createMessageElement(content, role) {
65+
function wrapMessageElement(messageElement, role) {
66+
const wrapper = document.createElement("div");
67+
wrapper.appendChild(messageElement);
68+
if (role == "assistant") {
69+
const controlContainer = document.createElement("div");
70+
controlContainer.appendChild(createCopyButton(() => messageElement.textContent, () => messageElement.innerHTML));
71+
controlContainer.classList.add("message-controls");
72+
wrapper.appendChild(controlContainer);
73+
}
74+
wrapper.classList.add("message-wrapper", role);
75+
return wrapper;
76+
}
77+
78+
function createMessageElement(content) {
6679
const messageDiv = document.createElement("div");
67-
messageDiv.classList.add("message", role);
80+
messageDiv.classList.add("message");
6881
let hdom = new HighlightDom(messageDiv);
6982
const high = new RenderMarkdown(hdom);
7083
high.feed(content);
@@ -105,6 +118,7 @@ async function handleChatStream(response) {
105118
const decoder = new TextDecoder();
106119
let buffer = "";
107120
let currentMessageElement = null;
121+
let currentMessageWrapper = null;
108122
let messageAppended = false;
109123
let finishReason = null;
110124
let hdom = null;
@@ -144,8 +158,9 @@ async function handleChatStream(response) {
144158
}
145159

146160
if (content && !messageAppended) {
147-
currentMessageElement = createMessageElement("", "assistant");
148-
chatMessages.appendChild(currentMessageElement);
161+
currentMessageElement = createMessageElement("");
162+
currentMessageWrapper = wrapMessageElement(currentMessageElement, "assistant");
163+
chatMessages.appendChild(currentMessageWrapper);
149164
hdom = new HighlightDom(currentMessageElement);
150165
high = new RenderMarkdown(hdom);
151166
messageAppended = true;
@@ -220,8 +235,8 @@ async function sendMessage() {
220235
abortController = new AbortController();
221236

222237
// add user message to chat
223-
const userMessageElement = createMessageElement(message, "user");
224-
chatMessages.appendChild(userMessageElement);
238+
const userMessageElement = createMessageElement(message);
239+
chatMessages.appendChild(wrapMessageElement(userMessageElement, "user"));
225240
scrollToBottom();
226241

227242
// update chat history
@@ -253,16 +268,16 @@ async function sendMessage() {
253268
chatHistory.push({ role: "assistant", content: lastMessage });
254269
} else {
255270
console.error("sendMessage() failed due to server error", response);
256-
chatMessages.appendChild(createMessageElement(
257-
`Server replied with error code ${response.status} ${response.statusText}`,
271+
chatMessages.appendChild(wrapMessageElement(createMessageElement(
272+
`Server replied with error code ${response.status} ${response.statusText}`),
258273
"system"));
259274
cleanupAfterMessage();
260275
}
261276
} catch (error) {
262277
if (error.name !== "AbortError") {
263278
console.error("sendMessage() failed due to unexpected exception", error);
264-
chatMessages.appendChild(createMessageElement(
265-
"There was an error processing your request.",
279+
chatMessages.appendChild(wrapMessageElement(createMessageElement(
280+
"There was an error processing your request."),
266281
"system"));
267282
}
268283
cleanupAfterMessage();
@@ -449,8 +464,8 @@ function startChat(history) {
449464
for (let i = 0; i < chatHistory.length; i++) {
450465
if (flagz.no_display_prompt && chatHistory[i].role == "system")
451466
continue;
452-
chatMessages.appendChild(createMessageElement(chatHistory[i].content,
453-
chatHistory[i].role));
467+
chatMessages.appendChild(wrapMessageElement(createMessageElement(chatHistory[i].content),
468+
chatHistory[i].role));
454469
}
455470
scrollToBottom();
456471
}

llamafile/server/www/clipboard.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function createCopyButton(textProviderFunction, htmlProviderFunction) {
3737
const copyButton = document.createElement('button');
3838
copyButton.className = 'copy-button';
3939
copyButton.innerHTML = clipboardIcon;
40+
copyButton.title = "Copy to clipboard";
4041
copyButton.addEventListener('click', async function () {
4142
try {
4243
await copyTextToClipboard(textProviderFunction(), htmlProviderFunction ? htmlProviderFunction() : null);

0 commit comments

Comments
 (0)