@unabridged/midwest 0.22.1 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/app/assets/javascript/midwest/index.ts +8 -0
- package/app/assets/javascript/midwest/keyboard_navigation.ts +68 -0
- package/app/assets/javascript/midwest/markdown.ts +79 -0
- package/app/assets/javascript/midwest.js +795 -39
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest/utilities.css +4 -0
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +13 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js +42 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js.map +1 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/markdown.js +52 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/markdown.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js +270 -2
- package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +8 -37
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js +83 -0
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js +42 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +43 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +273 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -0
- package/dist/javascript/midwest.js +795 -39
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,6 +32,10 @@ import PasswordRequirements from '../../../components/midwest/password_requireme
|
|
|
32
32
|
import CodeBlock from '../../../components/midwest/code_block_component/code_block_component_controller.js';
|
|
33
33
|
import DatePicker from '../../../components/midwest/form/date_picker_component/date_picker_component_controller.js';
|
|
34
34
|
import TimePicker from '../../../components/midwest/form/time_picker_component/time_picker_component_controller.js';
|
|
35
|
+
import ViewportObserver from '../../../components/midwest/intersection_observer_component/intersection_observer_component_controller.js';
|
|
36
|
+
import FocusTrap from '../../../components/midwest/focus_trap_component/focus_trap_component_controller.js';
|
|
37
|
+
import Onboarding from '../../../components/midwest/onboarding_component/onboarding_component_controller.js';
|
|
38
|
+
import ThemeSwitch from '../../../components/midwest/form/switch_component/theme_switch_controller.js';
|
|
35
39
|
|
|
36
40
|
function registerMidwestControllers(application) {
|
|
37
41
|
application.register("midwest-card", Card);
|
|
@@ -68,6 +72,10 @@ function registerMidwestControllers(application) {
|
|
|
68
72
|
application.register("midwest-code-block", CodeBlock);
|
|
69
73
|
application.register("midwest-date-picker", DatePicker);
|
|
70
74
|
application.register("midwest-time-picker", TimePicker);
|
|
75
|
+
application.register("midwest-intersection-observer", ViewportObserver);
|
|
76
|
+
application.register("midwest-focus-trap", FocusTrap);
|
|
77
|
+
application.register("midwest-onboarding", Onboarding);
|
|
78
|
+
application.register("midwest-theme-switch", ThemeSwitch);
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../../../../app/assets/javascript/midwest/index.ts"],"sourcesContent":["import Card from '../../../components/midwest/card_component/card_component_controller'\nimport Banner from '../../../components/midwest/banner_component/banner_component_controller'\nimport Input from '../../../components/midwest/form/input_component/input_component_controller'\nimport File from '../../../components/midwest/form/file_component/file_component_controller'\nimport Dropdown from '../../../components/midwest/dropdown_component/dropdown_component_controller'\nimport Prose from '../../../components/midwest/prose_component/prose_component_controller'\nimport Dialog from '../../../components/midwest/dialog_component/dialog_component_controller'\nimport Tabs from '../../../components/midwest/tabs_component/tabs_component_controller'\nimport Tooltip from '../../../components/midwest/tooltip_component/tooltip_component_controller'\nimport Carousel from '../../../components/midwest/carousel_component/carousel_component_controller'\nimport FormLiveSummary from '../../../components/midwest/form/live_summary_component/live_summary_component_controller'\nimport Form from '../../../components/midwest/form/form_controller'\nimport CountdownTimer from '../../../components/midwest/countdown_timer_component/countdown_timer_component_controller'\nimport Badge from '../../../components/midwest/badge_component/badge_component_controller'\nimport Confirmation from '../../../components/midwest/confirmation_component/confirmation_component_controller'\nimport Chart from '../../../components/midwest/chart_component/chart_component_controller'\nimport Range from '../../../components/midwest/form/range_component/range_component_controller'\nimport Rating from '../../../components/midwest/rating_component/rating_component_controller'\nimport Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller'\nimport Image from '../../../components/midwest/image_component/image_component_controller'\nimport ColorPicker from '../../../components/midwest/form/color_picker_component/color_picker_component_controller'\nimport Notification from '../../../components/midwest/notification_component/notification_component_controller'\nimport HorizontalScroll from '../../../components/midwest/horizontal_scroll_component/horizontal_scroll_component_controller'\nimport Table from '../../../components/midwest/table_component/table_component_controller'\nimport LoadMore from '../../../components/midwest/table_component/load_more_controller'\nimport Pagination from '../../../components/midwest/pagination_component/pagination_component_controller'\nimport ShowIf from '../../../components/midwest/show_if_component/show_if_component_controller'\nimport TreeView from '../../../components/midwest/tree_view_component/tree_view_component_controller'\nimport CommandPalette from '../../../components/midwest/command_palette_component/command_palette_component_controller'\nimport Repeatable from '../../../components/midwest/form/repeatable_component/repeatable_component_controller'\nimport PasswordRequirements from '../../../components/midwest/password_requirements_component/password_requirements_component_controller'\nimport CodeBlock from '../../../components/midwest/code_block_component/code_block_component_controller'\nimport DatePicker from '../../../components/midwest/form/date_picker_component/date_picker_component_controller'\nimport TimePicker from '../../../components/midwest/form/time_picker_component/time_picker_component_controller'\n/* IMPORTS */\n\nexport { Card, Banner, CountdownTimer, Chart }\n\nexport function registerMidwestControllers (application: any) {\n application.register('midwest-card', Card)\n application.register('midwest-banner', Banner)\n application.register('midwest-countdown-timer', CountdownTimer)\n application.register('midwest-input', Input)\n application.register('midwest-file', File)\n application.register('midwest-dropdown', Dropdown)\n application.register('midwest-prose', Prose)\n application.register('midwest-dialog', Dialog)\n application.register('midwest-tabs', Tabs)\n application.register('midwest-tooltip', Tooltip)\n application.register('midwest-carousel', Carousel)\n application.register('midwest-form-live-summary', FormLiveSummary)\n application.register('midwest-form', Form)\n application.register('midwest-badge', Badge)\n application.register('midwest-confirmation', Confirmation)\n application.register('midwest-chart', Chart)\n application.register('midwest-range', Range)\n application.register('midwest-rating', Rating)\n application.register('midwest-autocomplete', Autocomplete)\n application.register('midwest-image', Image)\n application.register('midwest-color-picker', ColorPicker)\n application.register('midwest-notification', Notification)\n application.register('midwest-horizontal-scroll', HorizontalScroll)\n application.register('midwest-table', Table)\n application.register('midwest-load-more', LoadMore)\n application.register('midwest-pagination', Pagination)\n application.register('midwest-show-if', ShowIf)\n application.register('midwest-tree-view', TreeView)\n application.register('midwest-command-palette', CommandPalette)\n application.register('midwest-repeatable', Repeatable)\n application.register('midwest-password-requirements', PasswordRequirements)\n application.register('midwest-code-block', CodeBlock)\n application.register('midwest-date-picker', DatePicker)\n application.register('midwest-time-picker', TimePicker)\n /* EXPORTS */\n}\n"],"names":["Confirmation"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../../../../app/assets/javascript/midwest/index.ts"],"sourcesContent":["import Card from '../../../components/midwest/card_component/card_component_controller'\nimport Banner from '../../../components/midwest/banner_component/banner_component_controller'\nimport Input from '../../../components/midwest/form/input_component/input_component_controller'\nimport File from '../../../components/midwest/form/file_component/file_component_controller'\nimport Dropdown from '../../../components/midwest/dropdown_component/dropdown_component_controller'\nimport Prose from '../../../components/midwest/prose_component/prose_component_controller'\nimport Dialog from '../../../components/midwest/dialog_component/dialog_component_controller'\nimport Tabs from '../../../components/midwest/tabs_component/tabs_component_controller'\nimport Tooltip from '../../../components/midwest/tooltip_component/tooltip_component_controller'\nimport Carousel from '../../../components/midwest/carousel_component/carousel_component_controller'\nimport FormLiveSummary from '../../../components/midwest/form/live_summary_component/live_summary_component_controller'\nimport Form from '../../../components/midwest/form/form_controller'\nimport CountdownTimer from '../../../components/midwest/countdown_timer_component/countdown_timer_component_controller'\nimport Badge from '../../../components/midwest/badge_component/badge_component_controller'\nimport Confirmation from '../../../components/midwest/confirmation_component/confirmation_component_controller'\nimport Chart from '../../../components/midwest/chart_component/chart_component_controller'\nimport Range from '../../../components/midwest/form/range_component/range_component_controller'\nimport Rating from '../../../components/midwest/rating_component/rating_component_controller'\nimport Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller'\nimport Image from '../../../components/midwest/image_component/image_component_controller'\nimport ColorPicker from '../../../components/midwest/form/color_picker_component/color_picker_component_controller'\nimport Notification from '../../../components/midwest/notification_component/notification_component_controller'\nimport HorizontalScroll from '../../../components/midwest/horizontal_scroll_component/horizontal_scroll_component_controller'\nimport Table from '../../../components/midwest/table_component/table_component_controller'\nimport LoadMore from '../../../components/midwest/table_component/load_more_controller'\nimport Pagination from '../../../components/midwest/pagination_component/pagination_component_controller'\nimport ShowIf from '../../../components/midwest/show_if_component/show_if_component_controller'\nimport TreeView from '../../../components/midwest/tree_view_component/tree_view_component_controller'\nimport CommandPalette from '../../../components/midwest/command_palette_component/command_palette_component_controller'\nimport Repeatable from '../../../components/midwest/form/repeatable_component/repeatable_component_controller'\nimport PasswordRequirements from '../../../components/midwest/password_requirements_component/password_requirements_component_controller'\nimport CodeBlock from '../../../components/midwest/code_block_component/code_block_component_controller'\nimport DatePicker from '../../../components/midwest/form/date_picker_component/date_picker_component_controller'\nimport TimePicker from '../../../components/midwest/form/time_picker_component/time_picker_component_controller'\nimport IntersectionObserver from '../../../components/midwest/intersection_observer_component/intersection_observer_component_controller'\nimport FocusTrap from '../../../components/midwest/focus_trap_component/focus_trap_component_controller'\nimport Onboarding from '../../../components/midwest/onboarding_component/onboarding_component_controller'\nimport ThemeSwitch from '../../../components/midwest/form/switch_component/theme_switch_controller'\n/* IMPORTS */\n\nexport { Card, Banner, CountdownTimer, Chart }\n\nexport function registerMidwestControllers (application: any) {\n application.register('midwest-card', Card)\n application.register('midwest-banner', Banner)\n application.register('midwest-countdown-timer', CountdownTimer)\n application.register('midwest-input', Input)\n application.register('midwest-file', File)\n application.register('midwest-dropdown', Dropdown)\n application.register('midwest-prose', Prose)\n application.register('midwest-dialog', Dialog)\n application.register('midwest-tabs', Tabs)\n application.register('midwest-tooltip', Tooltip)\n application.register('midwest-carousel', Carousel)\n application.register('midwest-form-live-summary', FormLiveSummary)\n application.register('midwest-form', Form)\n application.register('midwest-badge', Badge)\n application.register('midwest-confirmation', Confirmation)\n application.register('midwest-chart', Chart)\n application.register('midwest-range', Range)\n application.register('midwest-rating', Rating)\n application.register('midwest-autocomplete', Autocomplete)\n application.register('midwest-image', Image)\n application.register('midwest-color-picker', ColorPicker)\n application.register('midwest-notification', Notification)\n application.register('midwest-horizontal-scroll', HorizontalScroll)\n application.register('midwest-table', Table)\n application.register('midwest-load-more', LoadMore)\n application.register('midwest-pagination', Pagination)\n application.register('midwest-show-if', ShowIf)\n application.register('midwest-tree-view', TreeView)\n application.register('midwest-command-palette', CommandPalette)\n application.register('midwest-repeatable', Repeatable)\n application.register('midwest-password-requirements', PasswordRequirements)\n application.register('midwest-code-block', CodeBlock)\n application.register('midwest-date-picker', DatePicker)\n application.register('midwest-time-picker', TimePicker)\n application.register('midwest-intersection-observer', IntersectionObserver)\n application.register('midwest-focus-trap', FocusTrap)\n application.register('midwest-onboarding', Onboarding)\n application.register('midwest-theme-switch', ThemeSwitch)\n /* EXPORTS */\n}\n"],"names":["Confirmation","IntersectionObserver"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Home", "End", "Escape", "Tab"];
|
|
2
|
+
function handleRovingFocusKeydown(event, items, options = {}) {
|
|
3
|
+
if (items.length === 0)
|
|
4
|
+
return false;
|
|
5
|
+
if (!NAVIGATION_KEYS.includes(event.key))
|
|
6
|
+
return false;
|
|
7
|
+
const { onEscape, onTab, loop = true } = options;
|
|
8
|
+
const current = items.findIndex((item) => item === document.activeElement);
|
|
9
|
+
switch (event.key) {
|
|
10
|
+
case "ArrowDown": {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
const next = current < items.length - 1 ? current + 1 : loop ? 0 : items.length - 1;
|
|
13
|
+
items[next].focus();
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
case "ArrowUp": {
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
const prev = current > 0 ? current - 1 : loop ? items.length - 1 : 0;
|
|
19
|
+
items[prev].focus();
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case "Home":
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
items[0].focus();
|
|
25
|
+
break;
|
|
26
|
+
case "End":
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
items[items.length - 1].focus();
|
|
29
|
+
break;
|
|
30
|
+
case "Escape":
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
onEscape?.();
|
|
33
|
+
break;
|
|
34
|
+
case "Tab":
|
|
35
|
+
onTab?.();
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { handleRovingFocusKeydown };
|
|
42
|
+
//# sourceMappingURL=keyboard_navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard_navigation.js","sources":["../../../../../../../app/assets/javascript/midwest/keyboard_navigation.ts"],"sourcesContent":["// Shared keyboard-navigation helpers for menu/list components.\n//\n// Implements the WAI-ARIA \"roving focus\" pattern: arrow keys move DOM focus\n// between a flat list of items, Home/End jump to the ends, and Escape/Tab hand\n// control back to the caller (typically to close a popover and restore focus).\n// Components own *which* elements are navigable and *what* closing means; this\n// module owns the key handling so the behavior stays consistent across the\n// library.\n\nconst NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape', 'Tab']\n\nexport interface RovingFocusOptions {\n // Called on Escape. Typically closes the menu and returns focus to the trigger.\n onEscape?: () => void\n // Called on Tab. The default Tab behavior is left intact so focus still leaves\n // the menu; use this to close the menu alongside it.\n onTab?: () => void\n // When true (default) arrow navigation wraps from the last item to the first\n // and vice versa. When false it stops at the ends.\n loop?: boolean\n}\n\n// Handles a keydown against a roving-focus list. Returns true when the event\n// was one this helper is responsible for, so callers can `stopPropagation()`\n// only for keys that were actually handled.\nexport function handleRovingFocusKeydown (\n event: KeyboardEvent,\n items: HTMLElement[],\n options: RovingFocusOptions = {}\n): boolean {\n if (items.length === 0) return false\n if (!NAVIGATION_KEYS.includes(event.key)) return false\n\n const { onEscape, onTab, loop = true } = options\n const current = items.findIndex(item => item === document.activeElement)\n\n switch (event.key) {\n case 'ArrowDown': {\n event.preventDefault()\n const next = current < items.length - 1 ? current + 1 : (loop ? 0 : items.length - 1)\n items[next].focus()\n break\n }\n case 'ArrowUp': {\n event.preventDefault()\n const prev = current > 0 ? current - 1 : (loop ? items.length - 1 : 0)\n items[prev].focus()\n break\n }\n case 'Home':\n event.preventDefault()\n items[0].focus()\n break\n case 'End':\n event.preventDefault()\n items[items.length - 1].focus()\n break\n case 'Escape':\n event.preventDefault()\n onEscape?.()\n break\n case 'Tab':\n onTab?.()\n break\n }\n\n return true\n}\n"],"names":[],"mappings":"AASA,MAAM,kBAAkB,CAAC,WAAA,EAAa,WAAW,MAAQ,EAAA,KAAA,EAAO,UAAU,KAAK,CAAA,CAAA;AAgBxE,SAAS,wBACd,CAAA,KAAA,EACA,KACA,EAAA,OAAA,GAA8B,EACrB,EAAA;AACT,EAAA,IAAI,MAAM,MAAW,KAAA,CAAA;AAAG,IAAO,OAAA,KAAA,CAAA;AAC/B,EAAA,IAAI,CAAC,eAAA,CAAgB,QAAS,CAAA,KAAA,CAAM,GAAG,CAAA;AAAG,IAAO,OAAA,KAAA,CAAA;AAEjD,EAAA,MAAM,EAAE,QAAA,EAAU,KAAO,EAAA,IAAA,GAAO,MAAS,GAAA,OAAA,CAAA;AACzC,EAAA,MAAM,UAAU,KAAM,CAAA,SAAA,CAAU,CAAQ,IAAA,KAAA,IAAA,KAAS,SAAS,aAAa,CAAA,CAAA;AAEvE,EAAA,QAAQ,MAAM,GAAK;AAAA,IACjB,KAAK,WAAa,EAAA;AAChB,MAAA,KAAA,CAAM,cAAe,EAAA,CAAA;AACrB,MAAM,MAAA,IAAA,GAAO,OAAU,GAAA,KAAA,CAAM,MAAS,GAAA,CAAA,GAAI,UAAU,CAAK,GAAA,IAAA,GAAO,CAAI,GAAA,KAAA,CAAM,MAAS,GAAA,CAAA,CAAA;AACnF,MAAM,KAAA,CAAA,IAAI,EAAE,KAAM,EAAA,CAAA;AAClB,MAAA,MAAA;AAAA,KACF;AAAA,IACA,KAAK,SAAW,EAAA;AACd,MAAA,KAAA,CAAM,cAAe,EAAA,CAAA;AACrB,MAAM,MAAA,IAAA,GAAO,UAAU,CAAI,GAAA,OAAA,GAAU,IAAK,IAAO,GAAA,KAAA,CAAM,SAAS,CAAI,GAAA,CAAA,CAAA;AACpE,MAAM,KAAA,CAAA,IAAI,EAAE,KAAM,EAAA,CAAA;AAClB,MAAA,MAAA;AAAA,KACF;AAAA,IACA,KAAK,MAAA;AACH,MAAA,KAAA,CAAM,cAAe,EAAA,CAAA;AACrB,MAAM,KAAA,CAAA,CAAC,EAAE,KAAM,EAAA,CAAA;AACf,MAAA,MAAA;AAAA,IACF,KAAK,KAAA;AACH,MAAA,KAAA,CAAM,cAAe,EAAA,CAAA;AACrB,MAAA,KAAA,CAAM,KAAM,CAAA,MAAA,GAAS,CAAC,CAAA,CAAE,KAAM,EAAA,CAAA;AAC9B,MAAA,MAAA;AAAA,IACF,KAAK,QAAA;AACH,MAAA,KAAA,CAAM,cAAe,EAAA,CAAA;AACrB,MAAW,QAAA,IAAA,CAAA;AACX,MAAA,MAAA;AAAA,IACF,KAAK,KAAA;AACH,MAAQ,KAAA,IAAA,CAAA;AACR,MAAA,MAAA;AAAA,GACJ;AAEA,EAAO,OAAA,IAAA,CAAA;AACT;;;;"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const PLACEHOLDER = "\uE000";
|
|
2
|
+
function renderMarkdown(src) {
|
|
3
|
+
const blocks = [];
|
|
4
|
+
const withoutFences = src.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code) => {
|
|
5
|
+
const body = escapeHtml(code.replace(/\n$/, ""));
|
|
6
|
+
blocks.push(`<pre class="command-palette-code"><code>${body}</code></pre>`);
|
|
7
|
+
return `${PLACEHOLDER}${blocks.length - 1}${PLACEHOLDER}`;
|
|
8
|
+
});
|
|
9
|
+
const lines = escapeHtml(withoutFences).split("\n");
|
|
10
|
+
const out = [];
|
|
11
|
+
let listOpen = false;
|
|
12
|
+
const closeList = () => {
|
|
13
|
+
if (listOpen) {
|
|
14
|
+
out.push("</ul>");
|
|
15
|
+
listOpen = false;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const placeholder = line.match(new RegExp(`^${PLACEHOLDER}(\\d+)${PLACEHOLDER}$`));
|
|
20
|
+
if (placeholder) {
|
|
21
|
+
closeList();
|
|
22
|
+
out.push(line);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const item = line.match(/^\s*[-*]\s+(.*)$/);
|
|
26
|
+
if (item) {
|
|
27
|
+
if (!listOpen) {
|
|
28
|
+
out.push("<ul>");
|
|
29
|
+
listOpen = true;
|
|
30
|
+
}
|
|
31
|
+
out.push(`<li>${inline(item[1])}</li>`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
closeList();
|
|
35
|
+
if (line.trim() !== "")
|
|
36
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
37
|
+
}
|
|
38
|
+
closeList();
|
|
39
|
+
return out.join("\n").replace(new RegExp(`${PLACEHOLDER}(\\d+)${PLACEHOLDER}`, "g"), (_match, index) => blocks[Number(index)]);
|
|
40
|
+
}
|
|
41
|
+
function inline(text) {
|
|
42
|
+
return text.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(
|
|
43
|
+
/\[([^\]]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g,
|
|
44
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function escapeHtml(text) {
|
|
48
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { renderMarkdown };
|
|
52
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.js","sources":["../../../../../../../app/assets/javascript/midwest/markdown.ts"],"sourcesContent":["// Tiny, dependency-free Markdown renderer for streamed chat responses.\n//\n// It is intentionally a small subset (paragraphs, lists, fenced/inline code,\n// bold, italic, and safe links) rather than a full CommonMark implementation —\n// the command palette streams tokens and cannot afford a heavy parser.\n//\n// Security: model output is untrusted. Text is HTML-escaped BEFORE any markup is\n// applied, and links are restricted to http(s)/mailto, so the result is safe to\n// assign to innerHTML.\n\nconst PLACEHOLDER = '\\uE000' // private-use char, unlikely in real text\n\nexport function renderMarkdown (src: string): string {\n const blocks: string[] = []\n\n // Pull fenced code blocks out first so their contents are never treated as markup.\n const withoutFences = src.replace(/```(\\w*)\\n?([\\s\\S]*?)```/g, (_match, _lang, code: string) => {\n const body = escapeHtml(code.replace(/\\n$/, ''))\n blocks.push(`<pre class=\"command-palette-code\"><code>${body}</code></pre>`)\n return `${PLACEHOLDER}${blocks.length - 1}${PLACEHOLDER}`\n })\n\n const lines = escapeHtml(withoutFences).split('\\n')\n const out: string[] = []\n let listOpen = false\n\n const closeList = () => {\n if (listOpen) {\n out.push('</ul>')\n listOpen = false\n }\n }\n\n for (const line of lines) {\n const placeholder = line.match(new RegExp(`^${PLACEHOLDER}(\\\\d+)${PLACEHOLDER}$`))\n if (placeholder) {\n closeList()\n out.push(line)\n continue\n }\n\n const item = line.match(/^\\s*[-*]\\s+(.*)$/)\n if (item) {\n if (!listOpen) {\n out.push('<ul>')\n listOpen = true\n }\n out.push(`<li>${inline(item[1])}</li>`)\n continue\n }\n\n closeList()\n if (line.trim() !== '') out.push(`<p>${inline(line)}</p>`)\n }\n\n closeList()\n\n return out\n .join('\\n')\n .replace(new RegExp(`${PLACEHOLDER}(\\\\d+)${PLACEHOLDER}`, 'g'), (_match, index: string) => blocks[Number(index)])\n}\n\nfunction inline (text: string): string {\n return text\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/\\*([^*]+)\\*/g, '<em>$1</em>')\n .replace(\n /\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+|mailto:[^\\s)]+)\\)/g,\n '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>'\n )\n}\n\nfunction escapeHtml (text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n"],"names":[],"mappings":"AAUA,MAAM,WAAc,GAAA,QAAA,CAAA;AAEb,SAAS,eAAgB,GAAqB,EAAA;AACnD,EAAA,MAAM,SAAmB,EAAC,CAAA;AAG1B,EAAA,MAAM,gBAAgB,GAAI,CAAA,OAAA,CAAQ,6BAA6B,CAAC,MAAA,EAAQ,OAAO,IAAiB,KAAA;AAC9F,IAAA,MAAM,OAAO,UAAW,CAAA,IAAA,CAAK,OAAQ,CAAA,KAAA,EAAO,EAAE,CAAC,CAAA,CAAA;AAC/C,IAAO,MAAA,CAAA,IAAA,CAAK,2CAA2C,IAAmB,CAAA,aAAA,CAAA,CAAA,CAAA;AAC1E,IAAA,OAAO,CAAG,EAAA,WAAA,CAAA,EAAc,MAAO,CAAA,MAAA,GAAS,CAAI,CAAA,EAAA,WAAA,CAAA,CAAA,CAAA;AAAA,GAC7C,CAAA,CAAA;AAED,EAAA,MAAM,KAAQ,GAAA,UAAA,CAAW,aAAa,CAAA,CAAE,MAAM,IAAI,CAAA,CAAA;AAClD,EAAA,MAAM,MAAgB,EAAC,CAAA;AACvB,EAAA,IAAI,QAAW,GAAA,KAAA,CAAA;AAEf,EAAA,MAAM,YAAY,MAAM;AACtB,IAAA,IAAI,QAAU,EAAA;AACZ,MAAA,GAAA,CAAI,KAAK,OAAO,CAAA,CAAA;AAChB,MAAW,QAAA,GAAA,KAAA,CAAA;AAAA,KACb;AAAA,GACF,CAAA;AAEA,EAAA,KAAA,MAAW,QAAQ,KAAO,EAAA;AACxB,IAAM,MAAA,WAAA,GAAc,KAAK,KAAM,CAAA,IAAI,OAAO,CAAI,CAAA,EAAA,WAAA,CAAA,MAAA,EAAoB,cAAc,CAAC,CAAA,CAAA;AACjF,IAAA,IAAI,WAAa,EAAA;AACf,MAAU,SAAA,EAAA,CAAA;AACV,MAAA,GAAA,CAAI,KAAK,IAAI,CAAA,CAAA;AACb,MAAA,SAAA;AAAA,KACF;AAEA,IAAM,MAAA,IAAA,GAAO,IAAK,CAAA,KAAA,CAAM,kBAAkB,CAAA,CAAA;AAC1C,IAAA,IAAI,IAAM,EAAA;AACR,MAAA,IAAI,CAAC,QAAU,EAAA;AACb,QAAA,GAAA,CAAI,KAAK,MAAM,CAAA,CAAA;AACf,QAAW,QAAA,GAAA,IAAA,CAAA;AAAA,OACb;AACA,MAAA,GAAA,CAAI,KAAK,CAAO,IAAA,EAAA,MAAA,CAAO,IAAK,CAAA,CAAC,CAAC,CAAQ,CAAA,KAAA,CAAA,CAAA,CAAA;AACtC,MAAA,SAAA;AAAA,KACF;AAEA,IAAU,SAAA,EAAA,CAAA;AACV,IAAI,IAAA,IAAA,CAAK,MAAW,KAAA,EAAA;AAAI,MAAA,GAAA,CAAI,IAAK,CAAA,CAAA,GAAA,EAAM,MAAO,CAAA,IAAI,CAAO,CAAA,IAAA,CAAA,CAAA,CAAA;AAAA,GAC3D;AAEA,EAAU,SAAA,EAAA,CAAA;AAEV,EAAA,OAAO,IACJ,IAAK,CAAA,IAAI,EACT,OAAQ,CAAA,IAAI,OAAO,CAAG,EAAA,WAAA,CAAA,MAAA,EAAoB,eAAe,GAAG,CAAA,EAAG,CAAC,MAAQ,EAAA,KAAA,KAAkB,OAAO,MAAO,CAAA,KAAK,CAAC,CAAC,CAAA,CAAA;AACpH,CAAA;AAEA,SAAS,OAAQ,IAAsB,EAAA;AACrC,EAAA,OAAO,IACJ,CAAA,OAAA,CAAQ,YAAc,EAAA,iBAAiB,CACvC,CAAA,OAAA,CAAQ,kBAAoB,EAAA,qBAAqB,CACjD,CAAA,OAAA,CAAQ,cAAgB,EAAA,aAAa,CACrC,CAAA,OAAA;AAAA,IACC,sDAAA;AAAA,IACA,+DAAA;AAAA,GACF,CAAA;AACJ,CAAA;AAEA,SAAS,WAAY,IAAsB,EAAA;AACzC,EAAO,OAAA,IAAA,CACJ,OAAQ,CAAA,IAAA,EAAM,OAAO,CAAA,CACrB,OAAQ,CAAA,IAAA,EAAM,MAAM,CAAA,CACpB,OAAQ,CAAA,IAAA,EAAM,MAAM,CAAA,CAAA;AACzB;;;;"}
|
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
import { cloneTemplate } from '../../../assets/javascript/midwest/template_helpers.js';
|
|
3
|
+
import { renderMarkdown } from '../../../assets/javascript/midwest/markdown.js';
|
|
3
4
|
|
|
4
5
|
class CommandPalette extends Controller {
|
|
5
|
-
static targets = [
|
|
6
|
+
static targets = [
|
|
7
|
+
"input",
|
|
8
|
+
"list",
|
|
9
|
+
"empty",
|
|
10
|
+
"body",
|
|
11
|
+
"turboList",
|
|
12
|
+
"itemTemplate",
|
|
13
|
+
"groupTemplate",
|
|
14
|
+
"modes",
|
|
15
|
+
"chat",
|
|
16
|
+
"transcript",
|
|
17
|
+
"starters",
|
|
18
|
+
"messageTemplate",
|
|
19
|
+
"actionTemplate"
|
|
20
|
+
];
|
|
6
21
|
static values = {
|
|
7
22
|
hotkey: { type: String, default: "k" },
|
|
8
23
|
items: { type: Array, default: [] },
|
|
9
24
|
src: { type: String, default: "" },
|
|
10
|
-
turboFrameUrl: { type: String, default: "" }
|
|
25
|
+
turboFrameUrl: { type: String, default: "" },
|
|
26
|
+
chatEnabled: { type: Boolean, default: false },
|
|
27
|
+
chatMode: { type: String, default: "proxy" },
|
|
28
|
+
chatTrigger: { type: String, default: "toggle" },
|
|
29
|
+
chatAgentic: { type: Boolean, default: true },
|
|
30
|
+
chatPlaceholder: { type: String, default: "Ask AI\u2026" },
|
|
31
|
+
chatEndpoint: { type: String, default: "" },
|
|
32
|
+
chatUrl: { type: String, default: "" },
|
|
33
|
+
chatModel: { type: String, default: "" }
|
|
11
34
|
};
|
|
12
35
|
highlightedIndex = -1;
|
|
13
36
|
fetchAbort = null;
|
|
14
37
|
searchTimer;
|
|
15
38
|
allItems = [];
|
|
39
|
+
mode = "command";
|
|
40
|
+
commandPlaceholder = "";
|
|
41
|
+
messages = [];
|
|
42
|
+
streamAbort = null;
|
|
16
43
|
connect() {
|
|
17
44
|
this.allItems = this.itemsValue;
|
|
45
|
+
this.commandPlaceholder = this.inputTarget.placeholder;
|
|
18
46
|
document.addEventListener("keydown", this.handleGlobalKeydown);
|
|
19
47
|
this.element.addEventListener("click", this.handleBackdropClick);
|
|
20
48
|
if (!this.turboFrameUrlValue) {
|
|
@@ -26,6 +54,8 @@ class CommandPalette extends Controller {
|
|
|
26
54
|
this.element.removeEventListener("click", this.handleBackdropClick);
|
|
27
55
|
if (this.fetchAbort)
|
|
28
56
|
this.fetchAbort.abort();
|
|
57
|
+
if (this.streamAbort)
|
|
58
|
+
this.streamAbort.abort();
|
|
29
59
|
if (this.searchTimer)
|
|
30
60
|
clearTimeout(this.searchTimer);
|
|
31
61
|
}
|
|
@@ -59,6 +89,8 @@ class CommandPalette extends Controller {
|
|
|
59
89
|
this.element.showModal();
|
|
60
90
|
this.inputTarget.value = "";
|
|
61
91
|
this.highlightedIndex = -1;
|
|
92
|
+
if (this.chatEnabledValue)
|
|
93
|
+
this.resetChat();
|
|
62
94
|
if (this.turboFrameUrlValue) {
|
|
63
95
|
this.navigateFrame("");
|
|
64
96
|
} else {
|
|
@@ -72,6 +104,8 @@ class CommandPalette extends Controller {
|
|
|
72
104
|
this.element.close();
|
|
73
105
|
}
|
|
74
106
|
filter() {
|
|
107
|
+
if (this.mode === "chat")
|
|
108
|
+
return;
|
|
75
109
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
76
110
|
if (this.turboFrameUrlValue) {
|
|
77
111
|
this.debouncedNavigateFrame(query);
|
|
@@ -92,6 +126,21 @@ class CommandPalette extends Controller {
|
|
|
92
126
|
this.renderItems(filtered);
|
|
93
127
|
}
|
|
94
128
|
navigate(event) {
|
|
129
|
+
if (this.mode === "chat") {
|
|
130
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
this.send();
|
|
133
|
+
} else if (event.key === "Escape") {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
this.close();
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (event.key === "Tab" && !event.shiftKey && this.chatEnabledValue && this.chatTriggerValue === "toggle" && this.inputTarget.value.trim() === "") {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
this.setMode("chat");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
95
144
|
const items = this.visibleItems;
|
|
96
145
|
switch (event.key) {
|
|
97
146
|
case "ArrowDown":
|
|
@@ -126,6 +175,225 @@ class CommandPalette extends Controller {
|
|
|
126
175
|
this.selectItem(items[index]);
|
|
127
176
|
}
|
|
128
177
|
}
|
|
178
|
+
// ── Chat mode ───────────────────────────────────────────────────────
|
|
179
|
+
switchMode(event) {
|
|
180
|
+
const mode = event.currentTarget.dataset.mode === "chat" ? "chat" : "command";
|
|
181
|
+
this.setMode(mode);
|
|
182
|
+
if (mode === "command")
|
|
183
|
+
this.filter();
|
|
184
|
+
this.inputTarget.focus();
|
|
185
|
+
}
|
|
186
|
+
useStarter(event) {
|
|
187
|
+
this.inputTarget.value = event.currentTarget.textContent?.trim() || "";
|
|
188
|
+
this.send();
|
|
189
|
+
}
|
|
190
|
+
resetChat() {
|
|
191
|
+
if (this.streamAbort)
|
|
192
|
+
this.streamAbort.abort();
|
|
193
|
+
this.messages = [];
|
|
194
|
+
if (this.hasTranscriptTarget)
|
|
195
|
+
this.transcriptTarget.innerHTML = "";
|
|
196
|
+
if (this.hasStartersTarget)
|
|
197
|
+
this.startersTarget.hidden = false;
|
|
198
|
+
this.setBusy(false);
|
|
199
|
+
this.setMode("command");
|
|
200
|
+
}
|
|
201
|
+
setMode(mode) {
|
|
202
|
+
if (!this.chatEnabledValue)
|
|
203
|
+
return;
|
|
204
|
+
this.mode = mode;
|
|
205
|
+
const chat = mode === "chat";
|
|
206
|
+
this.element.classList.toggle("chat-active", chat);
|
|
207
|
+
if (this.hasModesTarget) {
|
|
208
|
+
this.modesTarget.querySelectorAll(".command-palette-mode").forEach((el) => {
|
|
209
|
+
el.setAttribute("aria-pressed", String(el.dataset.mode === mode));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (this.hasChatTarget)
|
|
213
|
+
this.chatTarget.hidden = !chat;
|
|
214
|
+
if (chat) {
|
|
215
|
+
this.emptyTarget.hidden = true;
|
|
216
|
+
if (this.hasListTarget)
|
|
217
|
+
this.listTarget.hidden = true;
|
|
218
|
+
if (this.hasTurboListTarget)
|
|
219
|
+
this.turboListTarget.hidden = true;
|
|
220
|
+
this.inputTarget.placeholder = this.chatPlaceholderValue;
|
|
221
|
+
} else {
|
|
222
|
+
this.inputTarget.placeholder = this.commandPlaceholder;
|
|
223
|
+
if (this.hasTurboListTarget)
|
|
224
|
+
this.turboListTarget.hidden = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
stop() {
|
|
228
|
+
if (this.streamAbort)
|
|
229
|
+
this.streamAbort.abort();
|
|
230
|
+
}
|
|
231
|
+
send() {
|
|
232
|
+
if (this.streamAbort)
|
|
233
|
+
return;
|
|
234
|
+
const text = this.inputTarget.value.trim();
|
|
235
|
+
if (!text)
|
|
236
|
+
return;
|
|
237
|
+
this.appendMessage("user", text);
|
|
238
|
+
this.messages.push({ role: "user", content: text });
|
|
239
|
+
this.inputTarget.value = "";
|
|
240
|
+
if (this.hasStartersTarget)
|
|
241
|
+
this.startersTarget.hidden = true;
|
|
242
|
+
void this.streamReply();
|
|
243
|
+
}
|
|
244
|
+
async streamReply() {
|
|
245
|
+
const message = this.appendMessage("assistant", "");
|
|
246
|
+
const bubble = message.querySelector(".command-palette-message-bubble");
|
|
247
|
+
this.setBusy(true);
|
|
248
|
+
this.streamAbort = new AbortController();
|
|
249
|
+
let reply = "";
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(this.chatRequestUrl(), {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
254
|
+
body: JSON.stringify(this.chatRequestBody()),
|
|
255
|
+
signal: this.streamAbort.signal
|
|
256
|
+
});
|
|
257
|
+
if (!response.ok || !response.body)
|
|
258
|
+
throw new Error(`Chat request failed (${response.status})`);
|
|
259
|
+
const reader = response.body.getReader();
|
|
260
|
+
const decoder = new TextDecoder();
|
|
261
|
+
let buffer = "";
|
|
262
|
+
for (; ; ) {
|
|
263
|
+
const { value, done } = await reader.read();
|
|
264
|
+
if (done)
|
|
265
|
+
break;
|
|
266
|
+
buffer += decoder.decode(value, { stream: true });
|
|
267
|
+
const events = buffer.split("\n\n");
|
|
268
|
+
buffer = events.pop() || "";
|
|
269
|
+
for (const event of events) {
|
|
270
|
+
const data = this.parseSseData(event);
|
|
271
|
+
if (!data)
|
|
272
|
+
continue;
|
|
273
|
+
const delta = data.choices?.[0]?.delta?.content;
|
|
274
|
+
if (delta) {
|
|
275
|
+
reply += delta;
|
|
276
|
+
bubble.innerHTML = renderMarkdown(reply);
|
|
277
|
+
this.scrollTranscript();
|
|
278
|
+
} else if (data.midwest) {
|
|
279
|
+
this.handleAgentEvent(data.midwest);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.messages.push({ role: "assistant", content: reply });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error.name === "AbortError") {
|
|
286
|
+
if (reply)
|
|
287
|
+
this.messages.push({ role: "assistant", content: reply });
|
|
288
|
+
} else {
|
|
289
|
+
message.classList.add("is-error");
|
|
290
|
+
bubble.textContent = "Something went wrong. Please try again.";
|
|
291
|
+
}
|
|
292
|
+
} finally {
|
|
293
|
+
this.streamAbort = null;
|
|
294
|
+
this.setBusy(false);
|
|
295
|
+
this.scrollTranscript();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
parseSseData(event) {
|
|
299
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
|
|
300
|
+
if (!data || data === "[DONE]")
|
|
301
|
+
return null;
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(data);
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Agentic events: the server streams a tool "action row" and/or a client
|
|
309
|
+
// directive (navigate / run a palette command). Directives reuse the same
|
|
310
|
+
// execution path as manual selection.
|
|
311
|
+
handleAgentEvent(event) {
|
|
312
|
+
if (event.type === "tool") {
|
|
313
|
+
this.appendActionRow(event);
|
|
314
|
+
} else if (event.type === "directive" && event.directive) {
|
|
315
|
+
this.performDirective(event.directive);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Action rows live in the current assistant message's card footer.
|
|
319
|
+
appendActionRow(event) {
|
|
320
|
+
if (!this.hasActionTemplateTarget)
|
|
321
|
+
return;
|
|
322
|
+
const footer = this.currentActionFooter();
|
|
323
|
+
if (!footer)
|
|
324
|
+
return;
|
|
325
|
+
const action = cloneTemplate(this.actionTemplateTarget);
|
|
326
|
+
const label = action.querySelector(".command-palette-action-label");
|
|
327
|
+
if (label)
|
|
328
|
+
label.textContent = event.label || "Action";
|
|
329
|
+
footer.hidden = false;
|
|
330
|
+
footer.appendChild(action);
|
|
331
|
+
this.scrollTranscript();
|
|
332
|
+
}
|
|
333
|
+
currentActionFooter() {
|
|
334
|
+
const messages = this.transcriptTarget.querySelectorAll(
|
|
335
|
+
".command-palette-message.role-assistant"
|
|
336
|
+
);
|
|
337
|
+
const last = messages[messages.length - 1];
|
|
338
|
+
return last ? last.querySelector(".command-palette-action-footer") : null;
|
|
339
|
+
}
|
|
340
|
+
performDirective(directive) {
|
|
341
|
+
if (directive.url) {
|
|
342
|
+
this.visit(directive.url, directive.method);
|
|
343
|
+
} else if (directive.action) {
|
|
344
|
+
this.element.dispatchEvent(
|
|
345
|
+
new CustomEvent("midwest-command-palette:action", {
|
|
346
|
+
bubbles: true,
|
|
347
|
+
detail: { action: directive.action }
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
chatRequestUrl() {
|
|
353
|
+
return this.chatModeValue === "proxy" ? this.chatEndpointValue : this.chatUrlValue;
|
|
354
|
+
}
|
|
355
|
+
chatRequestBody() {
|
|
356
|
+
const body = { messages: this.messages, stream: true };
|
|
357
|
+
if (this.chatModeValue !== "proxy" && this.chatModelValue)
|
|
358
|
+
body.model = this.chatModelValue;
|
|
359
|
+
if (this.chatModeValue === "proxy" && this.chatAgenticValue) {
|
|
360
|
+
body.commands = this.allItems.map((item) => ({
|
|
361
|
+
label: item.label,
|
|
362
|
+
description: item.description,
|
|
363
|
+
url: item.url,
|
|
364
|
+
action: item.action,
|
|
365
|
+
method: item.method,
|
|
366
|
+
icon: item.icon
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
return body;
|
|
370
|
+
}
|
|
371
|
+
setBusy(busy) {
|
|
372
|
+
this.element.classList.toggle("chat-busy", busy);
|
|
373
|
+
if (this.hasStopTarget)
|
|
374
|
+
this.stopTarget.hidden = !busy;
|
|
375
|
+
if (this.hasTranscriptTarget)
|
|
376
|
+
this.transcriptTarget.setAttribute("aria-busy", String(busy));
|
|
377
|
+
}
|
|
378
|
+
scrollTranscript() {
|
|
379
|
+
if (this.hasTranscriptTarget)
|
|
380
|
+
this.transcriptTarget.scrollTop = this.transcriptTarget.scrollHeight;
|
|
381
|
+
}
|
|
382
|
+
appendMessage(role, text) {
|
|
383
|
+
const li = cloneTemplate(this.messageTemplateTarget);
|
|
384
|
+
li.classList.add(`role-${role}`);
|
|
385
|
+
if (role === "user")
|
|
386
|
+
li.classList.add("is-end");
|
|
387
|
+
const roleEl = li.querySelector(".command-palette-message-role");
|
|
388
|
+
if (roleEl)
|
|
389
|
+
roleEl.textContent = role === "user" ? "You" : "Assistant";
|
|
390
|
+
const bubble = li.querySelector(".command-palette-message-bubble");
|
|
391
|
+
if (text)
|
|
392
|
+
bubble.textContent = text;
|
|
393
|
+
this.transcriptTarget.appendChild(li);
|
|
394
|
+
this.scrollTranscript();
|
|
395
|
+
return li;
|
|
396
|
+
}
|
|
129
397
|
// ── Remote search ───────────────────────────────────────────────────
|
|
130
398
|
fetchRemote(query) {
|
|
131
399
|
if (this.fetchAbort)
|