Skip to content

Commit 8f28184

Browse files
committed
feat: renderer for selection context
1 parent 8fe8444 commit 8f28184

File tree

7 files changed

+424
-135
lines changed

7 files changed

+424
-135
lines changed

_build/js/src/ui/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ export const arrowRight = `<svg xmlns="http://www.w3.org/2000/svg" width="24" he
3333
export const bot = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>`;
3434

3535
export const sparkles = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"></path><path d="M20 3v4"></path><path d="M22 5h-4"></path><path d="M4 17v2"></path><path d="M5 18H3"></path></svg>`;
36+
37+
export const textSelect = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-select"><path d="M14 21h1"></path><path d="M14 3h1"></path><path d="M19 3a2 2 0 0 1 2 2"></path><path d="M21 14v1"></path><path d="M21 19a2 2 0 0 1-2 2"></path><path d="M21 9v1"></path><path d="M3 14v1"></path><path d="M3 9v1"></path><path d="M5 21a2 2 0 0 1-2-2"></path><path d="M5 3a2 2 0 0 0-2 2"></path><path d="M7 12h10"></path><path d="M7 16h6"></path><path d="M7 8h8"></path><path d="M9 21h1"></path><path d="M9 3h1"></path></svg>`;

_build/js/src/ui/localChat/messageHandlers.ts

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,52 @@ import { globalState } from '../../globalState';
88
import { lng } from '../../lng';
99
import { confirmDialog } from '../cofirmDialog';
1010
import { icon } from '../dom/icon';
11-
import { copy, edit, plus, triangleError } from '../icons';
11+
import { copy, edit, plus, textSelect, triangleError } from '../icons';
1212
import { createElement, nlToBr } from '../utils';
1313

1414
import type { LocalChatConfig } from './types';
1515
import type {
1616
AssistantMessage,
1717
Message,
1818
UpdatableHTMLElement,
19+
UserAttachment,
1920
UserMessage,
21+
UserMessageContext,
2022
} from '../../chatHistory';
2123

24+
const contextRenderers: Record<string, undefined | ((context: UserMessageContext) => HTMLElement)> =
25+
{
26+
selection: (ctx) => {
27+
const tooltipEl = createElement('span', 'tooltip', ctx.value, { tabIndex: -1 });
28+
const btn = createElement('div', 'context', [icon(24, textSelect), tooltipEl], {
29+
tabIndex: 0,
30+
});
31+
32+
btn.addEventListener('keydown', (e) => {
33+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
34+
e.preventDefault();
35+
const scrollAmount = 30;
36+
tooltipEl.scrollTop += e.key === 'ArrowDown' ? scrollAmount : -scrollAmount;
37+
}
38+
});
39+
40+
return btn;
41+
},
42+
};
43+
44+
const attachmentRenderers: Record<
45+
string,
46+
undefined | ((attachment: UserAttachment) => HTMLElement)
47+
> = {
48+
image: (attachment) => {
49+
return createElement(
50+
'div',
51+
'attachment imagePreview',
52+
createElement('img', undefined, undefined, { src: attachment.value }),
53+
);
54+
},
55+
};
56+
2257
export const addUserMessage = (msg: UserMessage) => {
2358
const messageWrapper: UpdatableHTMLElement<UserMessage> = createElement(
2459
'div',
@@ -29,33 +64,41 @@ export const addUserMessage = (msg: UserMessage) => {
2964
const messageElement = createElement('div', 'message user');
3065

3166
const textContent = msg.content;
32-
const imagesContent = [];
33-
34-
for (const attachment of msg.attachments ?? []) {
35-
if (attachment.__type === 'image') {
36-
imagesContent.push(attachment.value);
37-
}
38-
}
3967

4068
const textDiv = createElement('div');
4169
textDiv.innerHTML = nlToBr(textContent);
4270
messageElement.appendChild(textDiv);
4371

44-
if (imagesContent.length > 0) {
45-
const imageRow = createElement('div', 'imageRow');
72+
const attachmentsWrapper = createElement('div', 'attachmentsWrapper');
73+
const contextsWrapper = createElement('div', 'contextsWrapper');
4674

47-
for (const imgContent of imagesContent) {
48-
const imageWrapper = createElement('div');
75+
for (const attachment of msg.attachments ?? []) {
76+
const renderer = attachmentRenderers[attachment.__type];
77+
if (!renderer) {
78+
continue;
79+
}
4980

50-
const img = createElement('img');
51-
img.src = imgContent;
81+
attachmentsWrapper.appendChild(renderer(attachment));
82+
if (!attachmentsWrapper.classList.contains('visible')) {
83+
attachmentsWrapper.classList.add('visible');
84+
}
85+
}
5286

53-
imageWrapper.appendChild(img);
54-
imageRow.appendChild(imageWrapper);
87+
for (const context of msg.contexts ?? []) {
88+
const renderer = context.renderer && contextRenderers[context.renderer];
89+
if (!renderer) {
90+
continue;
91+
}
92+
93+
contextsWrapper.appendChild(renderer(context));
94+
if (!contextsWrapper.classList.contains('visible')) {
95+
contextsWrapper.classList.add('visible');
5596
}
56-
messageElement.appendChild(imageRow);
5797
}
5898

99+
const inputAddons = createElement('div', 'inputAddons', [attachmentsWrapper, contextsWrapper]);
100+
messageElement.appendChild(inputAddons);
101+
59102
const actionsContainer = createElement('div', 'actions');
60103
actionsContainer.appendChild(
61104
createActionButton({
@@ -168,7 +211,7 @@ export const addAssistantMessage = (msg: AssistantMessage, config: LocalChatConf
168211
}
169212
}
170213

171-
return ''; // use external default escaping
214+
return '';
172215
},
173216
});
174217

_build/js/src/ui/localChat/modalInput.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,12 @@ export const buildModalInput = (config: LocalChatConfig) => {
9393

9494
inputWrapper.append(textarea, loading, sendBtn, stopBtn);
9595

96-
inputSection.append(buildModalInputAttachments(), inputWrapper);
97-
inputSection.append(buildModalInputContexts(), inputWrapper);
96+
const inputAddons = createElement('div', 'inputAddons', [
97+
buildModalInputAttachments(),
98+
buildModalInputContexts(),
99+
]);
100+
101+
inputSection.append(inputAddons, inputWrapper);
98102

99103
const modeButtons: Button[] = [];
100104

@@ -272,6 +276,7 @@ export const buildModalInput = (config: LocalChatConfig) => {
272276

273277
if (imageFile) {
274278
await handleImageUpload(imageFile);
279+
textarea.focus();
275280
return;
276281
}
277282

@@ -281,6 +286,7 @@ export const buildModalInput = (config: LocalChatConfig) => {
281286

282287
if (isRemote) {
283288
await handleImageUpload(remoteImageUrl, true);
289+
textarea.focus();
284290
return;
285291
}
286292

@@ -296,10 +302,12 @@ export const buildModalInput = (config: LocalChatConfig) => {
296302
} catch {
297303
addErrorMessage(lng('modai.error.failed_to_fetch_image'));
298304
}
305+
textarea.focus();
299306
return;
300307
}
301308

302309
addErrorMessage(lng('modai.error.only_image_files_are_allowed'));
310+
textarea.focus();
303311
});
304312

305313
textarea.addEventListener('paste', async (e) => {

_build/js/src/ui/localChat/modalInputAttachments.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { globalState } from '../../globalState';
2+
import { button } from '../dom/button';
23
import { applyStyles, createElement } from '../utils';
34

5+
import type { Button } from '../dom/button';
6+
47
export type AttachmentsWrapper = HTMLDivElement & {
58
visible: boolean;
69
show: () => void;
@@ -12,7 +15,7 @@ export type AttachmentsWrapper = HTMLDivElement & {
1215
attachments: Attachment[];
1316
};
1417

15-
export type Attachment = HTMLDivElement & {
18+
export type Attachment = Button & {
1619
__type: 'image';
1720
value: string;
1821
};
@@ -80,19 +83,19 @@ const addImageAttachment = (src: string) => {
8083
globalState.modal.attachments.removeAttachments();
8184
}
8285

83-
const attachment = createElement('div', 'imagePreview') as Attachment;
86+
const attachment = button(
87+
[
88+
createElement('img', undefined, '', { src }),
89+
createElement('div', 'trigger', '×', { tabIndex: -1 }),
90+
],
91+
() => {
92+
globalState.modal.attachments.removeAttachment(attachment);
93+
},
94+
'attachment imagePreview',
95+
) as Attachment;
96+
8497
attachment.__type = 'image';
8598
attachment.value = src;
8699

87-
const img = createElement('img', undefined, '', { src });
88-
const removeBtn = createElement('button', undefined, '×');
89-
90-
removeBtn.addEventListener('click', (e: MouseEvent) => {
91-
e.stopPropagation();
92-
globalState.modal.attachments.removeAttachment(attachment);
93-
});
94-
95-
attachment.append(img, removeBtn);
96-
97100
globalState.modal.attachments.addAttachment(attachment);
98101
};

_build/js/src/ui/localChat/modalInputContext.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { globalState } from '../../globalState';
2+
import { button } from '../dom/button';
3+
import { icon } from '../dom/icon';
4+
import { textSelect } from '../icons';
25
import { applyStyles, createElement } from '../utils';
36

47
import type { UserMessageContext } from '../../chatHistory';
@@ -18,9 +21,29 @@ export type Context = UserMessageContext & {
1821
el?: HTMLElement;
1922
};
2023

21-
const contextRenderers: Record<string, undefined | (() => HTMLElement)> = {
22-
selection: undefined,
23-
};
24+
const contextRenderers: Record<string, undefined | ((context: UserMessageContext) => HTMLElement)> =
25+
{
26+
selection: (ctx) => {
27+
const tooltipEl = createElement('span', 'tooltip', ctx.value, { tabIndex: -1 });
28+
const btn = button(
29+
[icon(24, textSelect), tooltipEl, createElement('div', 'trigger', '×', { tabIndex: -1 })],
30+
() => {
31+
globalState.modal.context.removeContext(ctx);
32+
},
33+
'context',
34+
);
35+
36+
btn.addEventListener('keydown', (e) => {
37+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
38+
e.preventDefault();
39+
const scrollAmount = 30;
40+
tooltipEl.scrollTop += e.key === 'ArrowDown' ? scrollAmount : -scrollAmount;
41+
}
42+
});
43+
44+
return btn;
45+
},
46+
};
2447

2548
export const buildModalInputContexts = () => {
2649
const contextWrapper = createElement('div', 'contextsWrapper') as ContextWrapper;
@@ -58,12 +81,12 @@ export const buildModalInputContexts = () => {
5881
};
5982

6083
contextWrapper.addContext = (context) => {
61-
const index = contextWrapper.contexts.push(context);
84+
const index = contextWrapper.contexts.push(context) - 1;
6285

6386
const renderer = contextRenderers[context?.renderer || ''];
6487
if (renderer) {
6588
contextWrapper.show();
66-
const el = renderer();
89+
const el = renderer(context);
6790
contextWrapper.contexts[index].el = el;
6891
contextWrapper.appendChild(el);
6992
}

0 commit comments

Comments
 (0)