|
1 | 1 | import { api, downloadAuthenticatedFile, openAuthenticatedFile, uploadFile, connectEvents, getToken, setToken, clearToken, workspacePhotoSrc, type User, type Channel, type Message, type Bot, type Computer, type Provider, type Workspace, type ModelPolicy, type AgentProgress, type AgentQuestions, type ThreadUsage, type RoutingModel } from "./api.ts"; |
2 | 2 | import { h, clear, add, md, color, initials, timeLabel, dayLabel, sameDay, beep, icon, helmMark, type ChannelLink } from "./dom.ts"; |
3 | | -import { openSettings, finishOpenRouterOAuth } from "./settings.ts"; |
| 3 | +import { openSettings, finishOpenRouterOAuth, refreshOpenSkillsSettings } from "./settings.ts"; |
4 | 4 | import { pushRoutingActivity } from "./routing.ts"; |
5 | 5 | import { openOnboarding } from "./onboarding.ts"; |
6 | 6 | import { defaultTerminalComputer, openTerminals, refitChannelTerminals, getTerminalChrome } from "./term.ts"; |
@@ -488,6 +488,13 @@ function onEvent(e: any): void { |
488 | 488 | if (S.view === "settings") renderChannelView(); |
489 | 489 | } else if (e.type === "providers_changed") { |
490 | 490 | void reloadProviders().then(() => { if (S.view === "settings") renderChannelView(); }); |
| 491 | + } else if (e.type === "skills_changed") { |
| 492 | + void loadWorkspace().then(() => { |
| 493 | + renderSidebar(); |
| 494 | + renderHeader(); |
| 495 | + if (S.view === "settings") renderChannelView(); |
| 496 | + refreshOpenSkillsSettings(); |
| 497 | + }); |
491 | 498 | } else if (e.type === "bot_update" && e.bot) { |
492 | 499 | const i = S.bots.findIndex((b) => b.id === e.bot.id); |
493 | 500 | if (i >= 0) S.bots[i] = e.bot; else S.bots = [...S.bots, e.bot]; |
@@ -777,13 +784,111 @@ function sidebar(drawer = false): HTMLElement { |
777 | 784 | archived.length ? h("div", {}, h("div", { class: "eyebrow px-2 pb-1 text-sidebar-muted" }, "Archived"), h("div", { class: "space-y-px opacity-65" }, ...archived.map(chan))) : null, |
778 | 785 | collab.length ? h("div", {}, h("div", { class: "eyebrow px-2 pb-1 text-sidebar-muted" }, "Human space"), h("div", { class: "space-y-px" }, ...collab.map(chan))) : null, |
779 | 786 | h("div", {}, sbSection("Direct messages", () => newDM()), h("div", { class: "space-y-px" }, ...dms.map(chan), dms.length === 0 && h("p", { class: "px-2 py-1 text-[13px] text-sidebar-muted" }, "No conversations yet")))), |
| 787 | + h("button", { |
| 788 | + class: "mx-2 mb-1 flex min-h-10 items-center gap-2 rounded-md px-2 py-1.5 text-xs text-sidebar-muted hover:bg-sidebar-hover hover:text-white", |
| 789 | + type: "button", |
| 790 | + dataset: { feedbackAction: "" }, |
| 791 | + onclick: () => { closeMobileMenu(); openFeedback(); }, |
| 792 | + }, icon("chat", 14), "Feedback"), |
780 | 793 | h("div", { class: "flex items-center gap-1 border-t border-white/10 p-1.5" }, |
781 | 794 | h("button", { class: "flex min-w-0 flex-1 items-center gap-2 rounded-md px-1.5 py-1 text-left hover:bg-sidebar-hover", title: "Open profile", onclick: (event: MouseEvent) => { closeMobileMenu(); openProfile(event.currentTarget as HTMLElement); } }, |
782 | 795 | avatar(S.me.display, "user", 8, S.me.avatar), |
783 | 796 | h("div", { class: "min-w-0 flex-1" }, h("div", { class: "truncate text-sm font-semibold text-white" }, S.me.display), h("div", { class: "flex items-center gap-1.5 truncate font-mono text-[10.5px] text-sidebar-muted" }, h("span", { class: "h-1.5 w-1.5 rounded-full bg-ok" }), "@" + S.me.username + (S.me.is_admin ? " · admin" : "")))), |
784 | 797 | h("button", { class: "grid h-10 w-10 shrink-0 place-items-center rounded-md text-sidebar-muted hover:bg-sidebar-hover hover:text-white", title: S.me.is_admin ? "Settings" : "Provider settings", "aria-label": "Open settings", onclick: () => { closeMobileMenu(); openSettings(S.me.is_admin ? "agents" : "providers"); } }, icon("gear")))); |
785 | 798 | } |
786 | 799 |
|
| 800 | +function openFeedback(): void { |
| 801 | + document.getElementById("feedback-modal")?.remove(); |
| 802 | + const overlay = h("div", { |
| 803 | + id: "feedback-modal", |
| 804 | + class: "modal-overlay fixed inset-0 z-[90] grid place-items-center bg-black/70 p-3", |
| 805 | + role: "dialog", |
| 806 | + "aria-modal": "true", |
| 807 | + "aria-label": "Send feedback", |
| 808 | + }); |
| 809 | + const comment = h("textarea", { |
| 810 | + class: "field min-h-36 resize-y", |
| 811 | + maxlength: 10000, |
| 812 | + placeholder: "What happened? What did you expect?", |
| 813 | + dataset: { feedbackComment: "" }, |
| 814 | + }) as HTMLTextAreaElement; |
| 815 | + const picker = h("input", { |
| 816 | + type: "file", |
| 817 | + multiple: true, |
| 818 | + class: "hidden", |
| 819 | + accept: "image/*,application/pdf,text/plain,application/json", |
| 820 | + dataset: { feedbackFiles: "" }, |
| 821 | + }) as HTMLInputElement; |
| 822 | + const fileList = h("div", { class: "space-y-1 text-xs text-muted" }); |
| 823 | + const diagnostics = h("input", { |
| 824 | + type: "checkbox", |
| 825 | + checked: false, |
| 826 | + class: "accent-accent", |
| 827 | + dataset: { feedbackDiagnostics: "" }, |
| 828 | + }) as HTMLInputElement; |
| 829 | + const status = h("p", { class: "min-h-5 text-sm text-muted", dataset: { feedbackStatus: "" } }); |
| 830 | + let files: File[] = []; |
| 831 | + const showFiles = (): void => { |
| 832 | + clear(fileList); |
| 833 | + fileList.append(...files.map((file) => h("div", { class: "truncate" }, `${file.name} · ${Math.ceil(file.size / 1024)} KB`))); |
| 834 | + }; |
| 835 | + picker.onchange = () => { |
| 836 | + const selected = [...(picker.files || [])].slice(0, 3); |
| 837 | + if (selected.some((file) => file.size > 5 * 1024 * 1024) || selected.reduce((sum, file) => sum + file.size, 0) > 10 * 1024 * 1024) { |
| 838 | + status.textContent = "Attachments are limited to 5 MB each and 10 MB total."; |
| 839 | + return; |
| 840 | + } |
| 841 | + files = selected; |
| 842 | + status.textContent = ""; |
| 843 | + showFiles(); |
| 844 | + }; |
| 845 | + const close = (): void => overlay.remove(); |
| 846 | + const submit = h("button", { |
| 847 | + class: "btn-primary text-sm", |
| 848 | + type: "button", |
| 849 | + dataset: { feedbackSubmit: "" }, |
| 850 | + onclick: async () => { |
| 851 | + submit.disabled = true; |
| 852 | + status.textContent = "Saving feedback…"; |
| 853 | + try { |
| 854 | + const uploads = []; |
| 855 | + for (const file of files) uploads.push(await uploadFile(file)); |
| 856 | + const result = await api<{ feedback: { id: string; state: string } }>("/api/feedback", { |
| 857 | + body: { comment: comment.value, send_diagnostics: diagnostics.checked, uploads }, |
| 858 | + }); |
| 859 | + status.textContent = `Saved as ${result.feedback.id}. Delivery will retry automatically if this host is offline.`; |
| 860 | + setTimeout(close, 1400); |
| 861 | + } catch (error) { |
| 862 | + status.textContent = (error as Error).message; |
| 863 | + submit.disabled = false; |
| 864 | + } |
| 865 | + }, |
| 866 | + }, "Send feedback") as HTMLButtonElement; |
| 867 | + const card = h("section", { class: "card w-full max-w-xl space-y-4 p-5 shadow-2xl" }, |
| 868 | + h("div", { class: "flex items-start justify-between gap-3" }, |
| 869 | + h("div", {}, |
| 870 | + h("h2", { class: "font-display text-xl text-fg" }, "Send feedback"), |
| 871 | + h("p", { class: "mt-1 text-sm leading-6 text-muted" }, "Tell us what feels broken or what would make 1Helm better.")), |
| 872 | + h("button", { class: "grid h-8 w-8 place-items-center rounded text-muted hover:bg-hover", "aria-label": "Close feedback", onclick: close }, icon("x"))), |
| 873 | + comment, |
| 874 | + h("div", {}, |
| 875 | + h("button", { class: "btn-subtle text-sm", type: "button", onclick: () => picker.click() }, "Add attachments"), |
| 876 | + picker, |
| 877 | + fileList), |
| 878 | + h("label", { class: "flex items-start gap-2 rounded-lg border border-line bg-panel p-3 text-sm text-fg" }, |
| 879 | + diagnostics, |
| 880 | + h("span", {}, "Include privacy-bounded diagnostics", |
| 881 | + h("span", { class: "mt-1 block text-xs leading-5 text-muted" }, "Optional. Includes app version, platform, runtime health, failed capability names/statuses, and connector health. Never includes chats, prompts, account content, terminal output, tokens, keys, or OAuth data."))), |
| 882 | + status, |
| 883 | + h("div", { class: "flex justify-end gap-2" }, |
| 884 | + h("button", { class: "btn-subtle text-sm", onclick: close }, "Cancel"), |
| 885 | + submit)); |
| 886 | + overlay.onclick = (event) => { if (event.target === overlay) close(); }; |
| 887 | + overlay.append(card); |
| 888 | + document.body.append(overlay); |
| 889 | + comment.focus(); |
| 890 | +} |
| 891 | + |
787 | 892 | function openProfile(anchor: HTMLElement): void { |
788 | 893 | document.getElementById("profile-popover")?.remove(); |
789 | 894 | const display = h("input", { class: "field", value: S.me.display, maxlength: 100 }) as HTMLInputElement; |
|
0 commit comments