@unabridged/midwest 0.23.0 → 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/app/assets/javascript/midwest/markdown.ts +79 -0
- package/app/assets/javascript/midwest.js +319 -2
- 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/dist/css/midwest.css +1 -1
- 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/midwest.js +319 -2
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -5374,20 +5374,97 @@ function cloneTemplate(template) {
|
|
|
5374
5374
|
return el;
|
|
5375
5375
|
}
|
|
5376
5376
|
|
|
5377
|
+
const PLACEHOLDER = "\uE000";
|
|
5378
|
+
function renderMarkdown(src) {
|
|
5379
|
+
const blocks = [];
|
|
5380
|
+
const withoutFences = src.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code) => {
|
|
5381
|
+
const body = escapeHtml(code.replace(/\n$/, ""));
|
|
5382
|
+
blocks.push(`<pre class="command-palette-code"><code>${body}</code></pre>`);
|
|
5383
|
+
return `${PLACEHOLDER}${blocks.length - 1}${PLACEHOLDER}`;
|
|
5384
|
+
});
|
|
5385
|
+
const lines = escapeHtml(withoutFences).split("\n");
|
|
5386
|
+
const out = [];
|
|
5387
|
+
let listOpen = false;
|
|
5388
|
+
const closeList = () => {
|
|
5389
|
+
if (listOpen) {
|
|
5390
|
+
out.push("</ul>");
|
|
5391
|
+
listOpen = false;
|
|
5392
|
+
}
|
|
5393
|
+
};
|
|
5394
|
+
for (const line of lines) {
|
|
5395
|
+
const placeholder = line.match(new RegExp(`^${PLACEHOLDER}(\\d+)${PLACEHOLDER}$`));
|
|
5396
|
+
if (placeholder) {
|
|
5397
|
+
closeList();
|
|
5398
|
+
out.push(line);
|
|
5399
|
+
continue;
|
|
5400
|
+
}
|
|
5401
|
+
const item = line.match(/^\s*[-*]\s+(.*)$/);
|
|
5402
|
+
if (item) {
|
|
5403
|
+
if (!listOpen) {
|
|
5404
|
+
out.push("<ul>");
|
|
5405
|
+
listOpen = true;
|
|
5406
|
+
}
|
|
5407
|
+
out.push(`<li>${inline(item[1])}</li>`);
|
|
5408
|
+
continue;
|
|
5409
|
+
}
|
|
5410
|
+
closeList();
|
|
5411
|
+
if (line.trim() !== "")
|
|
5412
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
5413
|
+
}
|
|
5414
|
+
closeList();
|
|
5415
|
+
return out.join("\n").replace(new RegExp(`${PLACEHOLDER}(\\d+)${PLACEHOLDER}`, "g"), (_match, index) => blocks[Number(index)]);
|
|
5416
|
+
}
|
|
5417
|
+
function inline(text) {
|
|
5418
|
+
return text.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(
|
|
5419
|
+
/\[([^\]]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g,
|
|
5420
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
5421
|
+
);
|
|
5422
|
+
}
|
|
5423
|
+
function escapeHtml(text) {
|
|
5424
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5377
5427
|
class CommandPalette extends Controller {
|
|
5378
|
-
static targets = [
|
|
5428
|
+
static targets = [
|
|
5429
|
+
"input",
|
|
5430
|
+
"list",
|
|
5431
|
+
"empty",
|
|
5432
|
+
"body",
|
|
5433
|
+
"turboList",
|
|
5434
|
+
"itemTemplate",
|
|
5435
|
+
"groupTemplate",
|
|
5436
|
+
"modes",
|
|
5437
|
+
"chat",
|
|
5438
|
+
"transcript",
|
|
5439
|
+
"starters",
|
|
5440
|
+
"messageTemplate",
|
|
5441
|
+
"actionTemplate"
|
|
5442
|
+
];
|
|
5379
5443
|
static values = {
|
|
5380
5444
|
hotkey: { type: String, default: "k" },
|
|
5381
5445
|
items: { type: Array, default: [] },
|
|
5382
5446
|
src: { type: String, default: "" },
|
|
5383
|
-
turboFrameUrl: { type: String, default: "" }
|
|
5447
|
+
turboFrameUrl: { type: String, default: "" },
|
|
5448
|
+
chatEnabled: { type: Boolean, default: false },
|
|
5449
|
+
chatMode: { type: String, default: "proxy" },
|
|
5450
|
+
chatTrigger: { type: String, default: "toggle" },
|
|
5451
|
+
chatAgentic: { type: Boolean, default: true },
|
|
5452
|
+
chatPlaceholder: { type: String, default: "Ask AI\u2026" },
|
|
5453
|
+
chatEndpoint: { type: String, default: "" },
|
|
5454
|
+
chatUrl: { type: String, default: "" },
|
|
5455
|
+
chatModel: { type: String, default: "" }
|
|
5384
5456
|
};
|
|
5385
5457
|
highlightedIndex = -1;
|
|
5386
5458
|
fetchAbort = null;
|
|
5387
5459
|
searchTimer;
|
|
5388
5460
|
allItems = [];
|
|
5461
|
+
mode = "command";
|
|
5462
|
+
commandPlaceholder = "";
|
|
5463
|
+
messages = [];
|
|
5464
|
+
streamAbort = null;
|
|
5389
5465
|
connect() {
|
|
5390
5466
|
this.allItems = this.itemsValue;
|
|
5467
|
+
this.commandPlaceholder = this.inputTarget.placeholder;
|
|
5391
5468
|
document.addEventListener("keydown", this.handleGlobalKeydown);
|
|
5392
5469
|
this.element.addEventListener("click", this.handleBackdropClick);
|
|
5393
5470
|
if (!this.turboFrameUrlValue) {
|
|
@@ -5399,6 +5476,8 @@ class CommandPalette extends Controller {
|
|
|
5399
5476
|
this.element.removeEventListener("click", this.handleBackdropClick);
|
|
5400
5477
|
if (this.fetchAbort)
|
|
5401
5478
|
this.fetchAbort.abort();
|
|
5479
|
+
if (this.streamAbort)
|
|
5480
|
+
this.streamAbort.abort();
|
|
5402
5481
|
if (this.searchTimer)
|
|
5403
5482
|
clearTimeout(this.searchTimer);
|
|
5404
5483
|
}
|
|
@@ -5432,6 +5511,8 @@ class CommandPalette extends Controller {
|
|
|
5432
5511
|
this.element.showModal();
|
|
5433
5512
|
this.inputTarget.value = "";
|
|
5434
5513
|
this.highlightedIndex = -1;
|
|
5514
|
+
if (this.chatEnabledValue)
|
|
5515
|
+
this.resetChat();
|
|
5435
5516
|
if (this.turboFrameUrlValue) {
|
|
5436
5517
|
this.navigateFrame("");
|
|
5437
5518
|
} else {
|
|
@@ -5445,6 +5526,8 @@ class CommandPalette extends Controller {
|
|
|
5445
5526
|
this.element.close();
|
|
5446
5527
|
}
|
|
5447
5528
|
filter() {
|
|
5529
|
+
if (this.mode === "chat")
|
|
5530
|
+
return;
|
|
5448
5531
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
5449
5532
|
if (this.turboFrameUrlValue) {
|
|
5450
5533
|
this.debouncedNavigateFrame(query);
|
|
@@ -5465,6 +5548,21 @@ class CommandPalette extends Controller {
|
|
|
5465
5548
|
this.renderItems(filtered);
|
|
5466
5549
|
}
|
|
5467
5550
|
navigate(event) {
|
|
5551
|
+
if (this.mode === "chat") {
|
|
5552
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
5553
|
+
event.preventDefault();
|
|
5554
|
+
this.send();
|
|
5555
|
+
} else if (event.key === "Escape") {
|
|
5556
|
+
event.preventDefault();
|
|
5557
|
+
this.close();
|
|
5558
|
+
}
|
|
5559
|
+
return;
|
|
5560
|
+
}
|
|
5561
|
+
if (event.key === "Tab" && !event.shiftKey && this.chatEnabledValue && this.chatTriggerValue === "toggle" && this.inputTarget.value.trim() === "") {
|
|
5562
|
+
event.preventDefault();
|
|
5563
|
+
this.setMode("chat");
|
|
5564
|
+
return;
|
|
5565
|
+
}
|
|
5468
5566
|
const items = this.visibleItems;
|
|
5469
5567
|
switch (event.key) {
|
|
5470
5568
|
case "ArrowDown":
|
|
@@ -5499,6 +5597,225 @@ class CommandPalette extends Controller {
|
|
|
5499
5597
|
this.selectItem(items[index]);
|
|
5500
5598
|
}
|
|
5501
5599
|
}
|
|
5600
|
+
// ── Chat mode ───────────────────────────────────────────────────────
|
|
5601
|
+
switchMode(event) {
|
|
5602
|
+
const mode = event.currentTarget.dataset.mode === "chat" ? "chat" : "command";
|
|
5603
|
+
this.setMode(mode);
|
|
5604
|
+
if (mode === "command")
|
|
5605
|
+
this.filter();
|
|
5606
|
+
this.inputTarget.focus();
|
|
5607
|
+
}
|
|
5608
|
+
useStarter(event) {
|
|
5609
|
+
this.inputTarget.value = event.currentTarget.textContent?.trim() || "";
|
|
5610
|
+
this.send();
|
|
5611
|
+
}
|
|
5612
|
+
resetChat() {
|
|
5613
|
+
if (this.streamAbort)
|
|
5614
|
+
this.streamAbort.abort();
|
|
5615
|
+
this.messages = [];
|
|
5616
|
+
if (this.hasTranscriptTarget)
|
|
5617
|
+
this.transcriptTarget.innerHTML = "";
|
|
5618
|
+
if (this.hasStartersTarget)
|
|
5619
|
+
this.startersTarget.hidden = false;
|
|
5620
|
+
this.setBusy(false);
|
|
5621
|
+
this.setMode("command");
|
|
5622
|
+
}
|
|
5623
|
+
setMode(mode) {
|
|
5624
|
+
if (!this.chatEnabledValue)
|
|
5625
|
+
return;
|
|
5626
|
+
this.mode = mode;
|
|
5627
|
+
const chat = mode === "chat";
|
|
5628
|
+
this.element.classList.toggle("chat-active", chat);
|
|
5629
|
+
if (this.hasModesTarget) {
|
|
5630
|
+
this.modesTarget.querySelectorAll(".command-palette-mode").forEach((el) => {
|
|
5631
|
+
el.setAttribute("aria-pressed", String(el.dataset.mode === mode));
|
|
5632
|
+
});
|
|
5633
|
+
}
|
|
5634
|
+
if (this.hasChatTarget)
|
|
5635
|
+
this.chatTarget.hidden = !chat;
|
|
5636
|
+
if (chat) {
|
|
5637
|
+
this.emptyTarget.hidden = true;
|
|
5638
|
+
if (this.hasListTarget)
|
|
5639
|
+
this.listTarget.hidden = true;
|
|
5640
|
+
if (this.hasTurboListTarget)
|
|
5641
|
+
this.turboListTarget.hidden = true;
|
|
5642
|
+
this.inputTarget.placeholder = this.chatPlaceholderValue;
|
|
5643
|
+
} else {
|
|
5644
|
+
this.inputTarget.placeholder = this.commandPlaceholder;
|
|
5645
|
+
if (this.hasTurboListTarget)
|
|
5646
|
+
this.turboListTarget.hidden = false;
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
stop() {
|
|
5650
|
+
if (this.streamAbort)
|
|
5651
|
+
this.streamAbort.abort();
|
|
5652
|
+
}
|
|
5653
|
+
send() {
|
|
5654
|
+
if (this.streamAbort)
|
|
5655
|
+
return;
|
|
5656
|
+
const text = this.inputTarget.value.trim();
|
|
5657
|
+
if (!text)
|
|
5658
|
+
return;
|
|
5659
|
+
this.appendMessage("user", text);
|
|
5660
|
+
this.messages.push({ role: "user", content: text });
|
|
5661
|
+
this.inputTarget.value = "";
|
|
5662
|
+
if (this.hasStartersTarget)
|
|
5663
|
+
this.startersTarget.hidden = true;
|
|
5664
|
+
void this.streamReply();
|
|
5665
|
+
}
|
|
5666
|
+
async streamReply() {
|
|
5667
|
+
const message = this.appendMessage("assistant", "");
|
|
5668
|
+
const bubble = message.querySelector(".command-palette-message-bubble");
|
|
5669
|
+
this.setBusy(true);
|
|
5670
|
+
this.streamAbort = new AbortController();
|
|
5671
|
+
let reply = "";
|
|
5672
|
+
try {
|
|
5673
|
+
const response = await fetch(this.chatRequestUrl(), {
|
|
5674
|
+
method: "POST",
|
|
5675
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
5676
|
+
body: JSON.stringify(this.chatRequestBody()),
|
|
5677
|
+
signal: this.streamAbort.signal
|
|
5678
|
+
});
|
|
5679
|
+
if (!response.ok || !response.body)
|
|
5680
|
+
throw new Error(`Chat request failed (${response.status})`);
|
|
5681
|
+
const reader = response.body.getReader();
|
|
5682
|
+
const decoder = new TextDecoder();
|
|
5683
|
+
let buffer = "";
|
|
5684
|
+
for (; ; ) {
|
|
5685
|
+
const { value, done } = await reader.read();
|
|
5686
|
+
if (done)
|
|
5687
|
+
break;
|
|
5688
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5689
|
+
const events = buffer.split("\n\n");
|
|
5690
|
+
buffer = events.pop() || "";
|
|
5691
|
+
for (const event of events) {
|
|
5692
|
+
const data = this.parseSseData(event);
|
|
5693
|
+
if (!data)
|
|
5694
|
+
continue;
|
|
5695
|
+
const delta = data.choices?.[0]?.delta?.content;
|
|
5696
|
+
if (delta) {
|
|
5697
|
+
reply += delta;
|
|
5698
|
+
bubble.innerHTML = renderMarkdown(reply);
|
|
5699
|
+
this.scrollTranscript();
|
|
5700
|
+
} else if (data.midwest) {
|
|
5701
|
+
this.handleAgentEvent(data.midwest);
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
this.messages.push({ role: "assistant", content: reply });
|
|
5706
|
+
} catch (error) {
|
|
5707
|
+
if (error.name === "AbortError") {
|
|
5708
|
+
if (reply)
|
|
5709
|
+
this.messages.push({ role: "assistant", content: reply });
|
|
5710
|
+
} else {
|
|
5711
|
+
message.classList.add("is-error");
|
|
5712
|
+
bubble.textContent = "Something went wrong. Please try again.";
|
|
5713
|
+
}
|
|
5714
|
+
} finally {
|
|
5715
|
+
this.streamAbort = null;
|
|
5716
|
+
this.setBusy(false);
|
|
5717
|
+
this.scrollTranscript();
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
parseSseData(event) {
|
|
5721
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
|
|
5722
|
+
if (!data || data === "[DONE]")
|
|
5723
|
+
return null;
|
|
5724
|
+
try {
|
|
5725
|
+
return JSON.parse(data);
|
|
5726
|
+
} catch {
|
|
5727
|
+
return null;
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
// Agentic events: the server streams a tool "action row" and/or a client
|
|
5731
|
+
// directive (navigate / run a palette command). Directives reuse the same
|
|
5732
|
+
// execution path as manual selection.
|
|
5733
|
+
handleAgentEvent(event) {
|
|
5734
|
+
if (event.type === "tool") {
|
|
5735
|
+
this.appendActionRow(event);
|
|
5736
|
+
} else if (event.type === "directive" && event.directive) {
|
|
5737
|
+
this.performDirective(event.directive);
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
// Action rows live in the current assistant message's card footer.
|
|
5741
|
+
appendActionRow(event) {
|
|
5742
|
+
if (!this.hasActionTemplateTarget)
|
|
5743
|
+
return;
|
|
5744
|
+
const footer = this.currentActionFooter();
|
|
5745
|
+
if (!footer)
|
|
5746
|
+
return;
|
|
5747
|
+
const action = cloneTemplate(this.actionTemplateTarget);
|
|
5748
|
+
const label = action.querySelector(".command-palette-action-label");
|
|
5749
|
+
if (label)
|
|
5750
|
+
label.textContent = event.label || "Action";
|
|
5751
|
+
footer.hidden = false;
|
|
5752
|
+
footer.appendChild(action);
|
|
5753
|
+
this.scrollTranscript();
|
|
5754
|
+
}
|
|
5755
|
+
currentActionFooter() {
|
|
5756
|
+
const messages = this.transcriptTarget.querySelectorAll(
|
|
5757
|
+
".command-palette-message.role-assistant"
|
|
5758
|
+
);
|
|
5759
|
+
const last = messages[messages.length - 1];
|
|
5760
|
+
return last ? last.querySelector(".command-palette-action-footer") : null;
|
|
5761
|
+
}
|
|
5762
|
+
performDirective(directive) {
|
|
5763
|
+
if (directive.url) {
|
|
5764
|
+
this.visit(directive.url, directive.method);
|
|
5765
|
+
} else if (directive.action) {
|
|
5766
|
+
this.element.dispatchEvent(
|
|
5767
|
+
new CustomEvent("midwest-command-palette:action", {
|
|
5768
|
+
bubbles: true,
|
|
5769
|
+
detail: { action: directive.action }
|
|
5770
|
+
})
|
|
5771
|
+
);
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
chatRequestUrl() {
|
|
5775
|
+
return this.chatModeValue === "proxy" ? this.chatEndpointValue : this.chatUrlValue;
|
|
5776
|
+
}
|
|
5777
|
+
chatRequestBody() {
|
|
5778
|
+
const body = { messages: this.messages, stream: true };
|
|
5779
|
+
if (this.chatModeValue !== "proxy" && this.chatModelValue)
|
|
5780
|
+
body.model = this.chatModelValue;
|
|
5781
|
+
if (this.chatModeValue === "proxy" && this.chatAgenticValue) {
|
|
5782
|
+
body.commands = this.allItems.map((item) => ({
|
|
5783
|
+
label: item.label,
|
|
5784
|
+
description: item.description,
|
|
5785
|
+
url: item.url,
|
|
5786
|
+
action: item.action,
|
|
5787
|
+
method: item.method,
|
|
5788
|
+
icon: item.icon
|
|
5789
|
+
}));
|
|
5790
|
+
}
|
|
5791
|
+
return body;
|
|
5792
|
+
}
|
|
5793
|
+
setBusy(busy) {
|
|
5794
|
+
this.element.classList.toggle("chat-busy", busy);
|
|
5795
|
+
if (this.hasStopTarget)
|
|
5796
|
+
this.stopTarget.hidden = !busy;
|
|
5797
|
+
if (this.hasTranscriptTarget)
|
|
5798
|
+
this.transcriptTarget.setAttribute("aria-busy", String(busy));
|
|
5799
|
+
}
|
|
5800
|
+
scrollTranscript() {
|
|
5801
|
+
if (this.hasTranscriptTarget)
|
|
5802
|
+
this.transcriptTarget.scrollTop = this.transcriptTarget.scrollHeight;
|
|
5803
|
+
}
|
|
5804
|
+
appendMessage(role, text) {
|
|
5805
|
+
const li = cloneTemplate(this.messageTemplateTarget);
|
|
5806
|
+
li.classList.add(`role-${role}`);
|
|
5807
|
+
if (role === "user")
|
|
5808
|
+
li.classList.add("is-end");
|
|
5809
|
+
const roleEl = li.querySelector(".command-palette-message-role");
|
|
5810
|
+
if (roleEl)
|
|
5811
|
+
roleEl.textContent = role === "user" ? "You" : "Assistant";
|
|
5812
|
+
const bubble = li.querySelector(".command-palette-message-bubble");
|
|
5813
|
+
if (text)
|
|
5814
|
+
bubble.textContent = text;
|
|
5815
|
+
this.transcriptTarget.appendChild(li);
|
|
5816
|
+
this.scrollTranscript();
|
|
5817
|
+
return li;
|
|
5818
|
+
}
|
|
5502
5819
|
// ── Remote search ───────────────────────────────────────────────────
|
|
5503
5820
|
fetchRemote(query) {
|
|
5504
5821
|
if (this.fetchAbort)
|