@rohansguliani/mux-chat-ui 0.3.0
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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/controller.d.ts +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1237 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +2 -0
- package/dist/model.d.ts +33 -0
- package/dist/mount.d.ts +56 -0
- package/dist/styles.d.ts +2 -0
- package/dist/testing.d.ts +49 -0
- package/dist/testing.js +61 -0
- package/dist/testing.js.map +1 -0
- package/dist/view/Chat.d.ts +26 -0
- package/dist/view/parts.d.ts +19 -0
- package/dist/view/primitives.d.ts +10 -0
- package/package.json +35 -0
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare const chatSelectors: {
|
|
2
|
+
/** The event stream is connected. */
|
|
3
|
+
readonly connected: "[data-slot=\"mux-chat-status\"][data-tone=\"ok\"]";
|
|
4
|
+
/** The chat can take a message. */
|
|
5
|
+
readonly ready: "[data-component=\"prompt-composer-surface\"][aria-busy=\"false\"]";
|
|
6
|
+
readonly input: "[data-component=\"prompt-input\"]";
|
|
7
|
+
readonly submit: "[data-action=\"prompt-submit\"]";
|
|
8
|
+
/** The submit button while a reply streams (clicking stops it). */
|
|
9
|
+
readonly stop: "[data-action=\"prompt-submit\"][data-working]";
|
|
10
|
+
readonly model: "[data-action=\"prompt-model\"]";
|
|
11
|
+
readonly newChat: "[data-action=\"new-chat\"]";
|
|
12
|
+
readonly retry: "[data-action=\"retry\"]";
|
|
13
|
+
readonly userMessage: "[data-component=\"user-message\"]";
|
|
14
|
+
readonly reply: "[data-component=\"assistant-message\"] [data-component=\"text-part\"]";
|
|
15
|
+
/** Reply text while it is still streaming. */
|
|
16
|
+
readonly liveReply: "[data-component=\"assistant-message\"][data-live] [data-component=\"text-part\"]";
|
|
17
|
+
readonly tool: "[data-component=\"assistant-message\"] .tool-collapsible[data-status]";
|
|
18
|
+
readonly question: "[data-component=\"mux-question\"]";
|
|
19
|
+
readonly questionOption: "[data-slot=\"mux-question-option\"]";
|
|
20
|
+
readonly questionAnswer: "[data-component=\"mux-question\"] [data-slot=\"mux-button\"][data-variant=\"primary\"]";
|
|
21
|
+
readonly permission: "[data-component=\"mux-permission\"]";
|
|
22
|
+
readonly notice: "[data-slot=\"mux-chat-notice\"]";
|
|
23
|
+
};
|
|
24
|
+
export type ChatSnapshot = {
|
|
25
|
+
/** The open chat session, once there is one. */
|
|
26
|
+
sessionID: string | null;
|
|
27
|
+
ready: boolean;
|
|
28
|
+
connected: boolean;
|
|
29
|
+
/** A reply is streaming. */
|
|
30
|
+
busy: boolean;
|
|
31
|
+
userMessages: string[];
|
|
32
|
+
replies: string[];
|
|
33
|
+
tools: Array<{
|
|
34
|
+
status: string;
|
|
35
|
+
label: string;
|
|
36
|
+
detail: string;
|
|
37
|
+
}>;
|
|
38
|
+
question: {
|
|
39
|
+
text: string;
|
|
40
|
+
options: string[];
|
|
41
|
+
} | null;
|
|
42
|
+
permission: string | null;
|
|
43
|
+
notice: string;
|
|
44
|
+
errors: string[];
|
|
45
|
+
};
|
|
46
|
+
/** What the chat mounted in `host` shows right now. */
|
|
47
|
+
export declare function readChat(host: Element): ChatSnapshot;
|
|
48
|
+
/** True once the chat mounted in `host` has a reply and nothing is streaming. */
|
|
49
|
+
export declare function replyFinished(host: Element): boolean;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/testing.ts
|
|
2
|
+
var chatSelectors = {
|
|
3
|
+
/** The event stream is connected. */
|
|
4
|
+
connected: "[data-slot=\"mux-chat-status\"][data-tone=\"ok\"]",
|
|
5
|
+
/** The chat can take a message. */
|
|
6
|
+
ready: "[data-component=\"prompt-composer-surface\"][aria-busy=\"false\"]",
|
|
7
|
+
input: "[data-component=\"prompt-input\"]",
|
|
8
|
+
submit: "[data-action=\"prompt-submit\"]",
|
|
9
|
+
/** The submit button while a reply streams (clicking stops it). */
|
|
10
|
+
stop: "[data-action=\"prompt-submit\"][data-working]",
|
|
11
|
+
model: "[data-action=\"prompt-model\"]",
|
|
12
|
+
newChat: "[data-action=\"new-chat\"]",
|
|
13
|
+
retry: "[data-action=\"retry\"]",
|
|
14
|
+
userMessage: "[data-component=\"user-message\"]",
|
|
15
|
+
reply: "[data-component=\"assistant-message\"] [data-component=\"text-part\"]",
|
|
16
|
+
/** Reply text while it is still streaming. */
|
|
17
|
+
liveReply: "[data-component=\"assistant-message\"][data-live] [data-component=\"text-part\"]",
|
|
18
|
+
tool: "[data-component=\"assistant-message\"] .tool-collapsible[data-status]",
|
|
19
|
+
question: "[data-component=\"mux-question\"]",
|
|
20
|
+
questionOption: "[data-slot=\"mux-question-option\"]",
|
|
21
|
+
questionAnswer: "[data-component=\"mux-question\"] [data-slot=\"mux-button\"][data-variant=\"primary\"]",
|
|
22
|
+
permission: "[data-component=\"mux-permission\"]",
|
|
23
|
+
notice: "[data-slot=\"mux-chat-notice\"]"
|
|
24
|
+
};
|
|
25
|
+
/** What the chat mounted in `host` shows right now. */
|
|
26
|
+
function readChat(host) {
|
|
27
|
+
const root = host.shadowRoot ?? host;
|
|
28
|
+
const one = (selector) => root.querySelector(selector);
|
|
29
|
+
const all = (selector) => [...root.querySelectorAll(selector)];
|
|
30
|
+
const text = (element) => element?.textContent ?? "";
|
|
31
|
+
const question = one("[data-component=\"mux-question\"]");
|
|
32
|
+
return {
|
|
33
|
+
sessionID: one("[data-component=\"mux-chat\"]")?.getAttribute("data-session-id") || null,
|
|
34
|
+
ready: !!one("[data-component=\"prompt-composer-surface\"][aria-busy=\"false\"]"),
|
|
35
|
+
connected: !!one("[data-slot=\"mux-chat-status\"][data-tone=\"ok\"]"),
|
|
36
|
+
busy: !!one("[data-component=\"assistant-message\"][data-live]") || !!one("[data-action=\"prompt-submit\"][data-working]"),
|
|
37
|
+
userMessages: all("[data-component=\"user-message\"] [data-slot=\"user-message-text\"]").map(text),
|
|
38
|
+
replies: all("[data-component=\"assistant-message\"] [data-component=\"text-part\"]").map(text),
|
|
39
|
+
tools: all("[data-component=\"assistant-message\"] .tool-collapsible[data-status]").map((row) => ({
|
|
40
|
+
status: row.getAttribute("data-status") ?? "",
|
|
41
|
+
label: row.querySelector("[data-slot=\"basic-tool-tool-title\"] [data-component=\"text-shimmer\"]")?.getAttribute("aria-label") ?? "",
|
|
42
|
+
detail: text(row.querySelector("[data-slot=\"basic-tool-tool-subtitle\"]"))
|
|
43
|
+
})),
|
|
44
|
+
question: question ? {
|
|
45
|
+
text: text(question.querySelector("[data-slot=\"mux-question-text\"]")),
|
|
46
|
+
options: [...question.querySelectorAll("[data-slot=\"mux-question-option-label\"]")].map(text)
|
|
47
|
+
} : null,
|
|
48
|
+
permission: text(one("[data-component=\"mux-permission\"] [data-slot=\"mux-question-text\"]")) || null,
|
|
49
|
+
notice: text(one("[data-slot=\"mux-chat-notice\"]")),
|
|
50
|
+
errors: all("[data-slot=\"mux-chat-error\"]").map(text)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** True once the chat mounted in `host` has a reply and nothing is streaming. */
|
|
54
|
+
function replyFinished(host) {
|
|
55
|
+
const root = host.shadowRoot ?? host;
|
|
56
|
+
return !root.querySelector("[data-component=\"assistant-message\"][data-live]") && !root.querySelector("[data-action=\"prompt-submit\"][data-working]") && !!root.querySelector("[data-component=\"assistant-message\"] [data-component=\"text-part\"]");
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { chatSelectors, readChat, replyFinished };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["// Stable hooks into a mounted chat for app tests (Playwright, Puppeteer, DOM\n// tests), so apps never depend on the chat's internal markup. Selectors pierce\n// the shadow root in Playwright. `readChat` and `replyFinished` run in the\n// page (pass them to `locator.evaluate` / `page.waitForFunction` with the\n// element the chat was mounted in); they are self-contained for that reason.\n\nexport const chatSelectors = {\n /** The event stream is connected. */\n connected: '[data-slot=\"mux-chat-status\"][data-tone=\"ok\"]',\n /** The chat can take a message. */\n ready: '[data-component=\"prompt-composer-surface\"][aria-busy=\"false\"]',\n input: '[data-component=\"prompt-input\"]',\n submit: '[data-action=\"prompt-submit\"]',\n /** The submit button while a reply streams (clicking stops it). */\n stop: '[data-action=\"prompt-submit\"][data-working]',\n model: '[data-action=\"prompt-model\"]',\n newChat: '[data-action=\"new-chat\"]',\n retry: '[data-action=\"retry\"]',\n userMessage: '[data-component=\"user-message\"]',\n reply: '[data-component=\"assistant-message\"] [data-component=\"text-part\"]',\n /** Reply text while it is still streaming. */\n liveReply: '[data-component=\"assistant-message\"][data-live] [data-component=\"text-part\"]',\n tool: '[data-component=\"assistant-message\"] .tool-collapsible[data-status]',\n question: '[data-component=\"mux-question\"]',\n questionOption: '[data-slot=\"mux-question-option\"]',\n questionAnswer: '[data-component=\"mux-question\"] [data-slot=\"mux-button\"][data-variant=\"primary\"]',\n permission: '[data-component=\"mux-permission\"]',\n notice: '[data-slot=\"mux-chat-notice\"]',\n} as const\n\nexport type ChatSnapshot = {\n /** The open chat session, once there is one. */\n sessionID: string | null\n ready: boolean\n connected: boolean\n /** A reply is streaming. */\n busy: boolean\n userMessages: string[]\n replies: string[]\n tools: Array<{ status: string; label: string; detail: string }>\n question: { text: string; options: string[] } | null\n permission: string | null\n notice: string\n errors: string[]\n}\n\n/** What the chat mounted in `host` shows right now. */\nexport function readChat(host: Element): ChatSnapshot {\n const root: ParentNode = (host as HTMLElement).shadowRoot ?? host\n const one = (selector: string) => root.querySelector(selector)\n const all = (selector: string) => [...root.querySelectorAll(selector)]\n const text = (element: Element | null) => element?.textContent ?? \"\"\n const question = one('[data-component=\"mux-question\"]')\n return {\n sessionID: one('[data-component=\"mux-chat\"]')?.getAttribute(\"data-session-id\") || null,\n ready: !!one('[data-component=\"prompt-composer-surface\"][aria-busy=\"false\"]'),\n connected: !!one('[data-slot=\"mux-chat-status\"][data-tone=\"ok\"]'),\n busy: !!one('[data-component=\"assistant-message\"][data-live]') || !!one('[data-action=\"prompt-submit\"][data-working]'),\n userMessages: all('[data-component=\"user-message\"] [data-slot=\"user-message-text\"]').map(text),\n replies: all('[data-component=\"assistant-message\"] [data-component=\"text-part\"]').map(text),\n tools: all('[data-component=\"assistant-message\"] .tool-collapsible[data-status]').map((row) => ({\n status: row.getAttribute(\"data-status\") ?? \"\",\n label: row.querySelector('[data-slot=\"basic-tool-tool-title\"] [data-component=\"text-shimmer\"]')?.getAttribute(\"aria-label\") ?? \"\",\n detail: text(row.querySelector('[data-slot=\"basic-tool-tool-subtitle\"]')),\n })),\n question: question\n ? { text: text(question.querySelector('[data-slot=\"mux-question-text\"]')), options: [...question.querySelectorAll('[data-slot=\"mux-question-option-label\"]')].map(text) }\n : null,\n permission: text(one('[data-component=\"mux-permission\"] [data-slot=\"mux-question-text\"]')) || null,\n notice: text(one('[data-slot=\"mux-chat-notice\"]')),\n errors: all('[data-slot=\"mux-chat-error\"]').map(text),\n }\n}\n\n/** True once the chat mounted in `host` has a reply and nothing is streaming. */\nexport function replyFinished(host: Element): boolean {\n const root: ParentNode = (host as HTMLElement).shadowRoot ?? host\n return !root.querySelector('[data-component=\"assistant-message\"][data-live]') &&\n !root.querySelector('[data-action=\"prompt-submit\"][data-working]') &&\n !!root.querySelector('[data-component=\"assistant-message\"] [data-component=\"text-part\"]')\n}\n"],"mappings":";AAMA,IAAa,gBAAgB;;CAE3B,WAAW;;CAEX,OAAO;CACP,OAAO;CACP,QAAQ;;CAER,MAAM;CACN,OAAO;CACP,SAAS;CACT,OAAO;CACP,aAAa;CACb,OAAO;;CAEP,WAAW;CACX,MAAM;CACN,UAAU;CACV,gBAAgB;CAChB,gBAAgB;CAChB,YAAY;CACZ,QAAQ;AACV;;AAmBA,SAAgB,SAAS,MAA6B;CACpD,MAAM,OAAoB,KAAqB,cAAc;CAC7D,MAAM,OAAO,aAAqB,KAAK,cAAc,QAAQ;CAC7D,MAAM,OAAO,aAAqB,CAAC,GAAG,KAAK,iBAAiB,QAAQ,CAAC;CACrE,MAAM,QAAQ,YAA4B,SAAS,eAAe;CAClE,MAAM,WAAW,IAAI,mCAAiC;CACtD,OAAO;EACL,WAAW,IAAI,+BAA6B,CAAC,EAAE,aAAa,iBAAiB,KAAK;EAClF,OAAO,CAAC,CAAC,IAAI,mEAA+D;EAC5E,WAAW,CAAC,CAAC,IAAI,mDAA+C;EAChE,MAAM,CAAC,CAAC,IAAI,mDAAiD,KAAK,CAAC,CAAC,IAAI,+CAA6C;EACrH,cAAc,IAAI,qEAAiE,CAAC,CAAC,IAAI,IAAI;EAC7F,SAAS,IAAI,uEAAmE,CAAC,CAAC,IAAI,IAAI;EAC1F,OAAO,IAAI,uEAAqE,CAAC,CAAC,KAAK,SAAS;GAC9F,QAAQ,IAAI,aAAa,aAAa,KAAK;GAC3C,OAAO,IAAI,cAAc,yEAAqE,CAAC,EAAE,aAAa,YAAY,KAAK;GAC/H,QAAQ,KAAK,IAAI,cAAc,0CAAwC,CAAC;EAC1E,EAAE;EACF,UAAU,WACN;GAAE,MAAM,KAAK,SAAS,cAAc,mCAAiC,CAAC;GAAG,SAAS,CAAC,GAAG,SAAS,iBAAiB,2CAAyC,CAAC,CAAC,CAAC,IAAI,IAAI;EAAE,IACtK;EACJ,YAAY,KAAK,IAAI,uEAAmE,CAAC,KAAK;EAC9F,QAAQ,KAAK,IAAI,iCAA+B,CAAC;EACjD,QAAQ,IAAI,gCAA8B,CAAC,CAAC,IAAI,IAAI;CACtD;AACF;;AAGA,SAAgB,cAAc,MAAwB;CACpD,MAAM,OAAoB,KAAqB,cAAc;CAC7D,OAAO,CAAC,KAAK,cAAc,mDAAiD,KAC1E,CAAC,KAAK,cAAc,+CAA6C,KACjE,CAAC,CAAC,KAAK,cAAc,uEAAmE;AAC5F"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ToolResult } from "../controller.js";
|
|
2
|
+
export type ResolvedOptions = {
|
|
3
|
+
url: string;
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
placeholder: string;
|
|
7
|
+
suggestions: Array<{
|
|
8
|
+
label: string;
|
|
9
|
+
prompt: string;
|
|
10
|
+
}>;
|
|
11
|
+
toolLabels: Record<string, string>;
|
|
12
|
+
agent: string;
|
|
13
|
+
sessionTitle: string;
|
|
14
|
+
storageKey: string;
|
|
15
|
+
preferredModel?: string;
|
|
16
|
+
onToolResult?: (result: ToolResult) => void;
|
|
17
|
+
};
|
|
18
|
+
export type ChatAPI = {
|
|
19
|
+
send(text: string): Promise<boolean>;
|
|
20
|
+
focus(): void;
|
|
21
|
+
newChat(): Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
export declare function Chat(props: {
|
|
24
|
+
options: ResolvedOptions;
|
|
25
|
+
api: (api: ChatAPI) => void;
|
|
26
|
+
}): import("solid-js").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Part, TextPart } from "@rohansguliani/mux-chat-core";
|
|
2
|
+
import { type JSX } from "solid-js";
|
|
3
|
+
/** One collapsible activity row (Mux BasicTool). */
|
|
4
|
+
export declare function ActivityRow(props: {
|
|
5
|
+
title: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
active: boolean;
|
|
8
|
+
name?: string;
|
|
9
|
+
status?: string;
|
|
10
|
+
children?: JSX.Element;
|
|
11
|
+
}): JSX.Element;
|
|
12
|
+
export declare function TextPartView(props: {
|
|
13
|
+
part: TextPart;
|
|
14
|
+
}): JSX.Element;
|
|
15
|
+
export declare function PartView(props: {
|
|
16
|
+
part: Part;
|
|
17
|
+
live: boolean;
|
|
18
|
+
labels: Record<string, string>;
|
|
19
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Mux's text shimmer (ui/components/text-shimmer): an animated sweep while `active`. */
|
|
2
|
+
export declare function TextShimmer(props: {
|
|
3
|
+
text: string;
|
|
4
|
+
active: boolean;
|
|
5
|
+
offset?: number;
|
|
6
|
+
}): import("solid-js").JSX.Element;
|
|
7
|
+
export declare const ChevronDown: () => import("solid-js").JSX.Element;
|
|
8
|
+
export declare const ArrowUp: () => import("solid-js").JSX.Element;
|
|
9
|
+
export declare const Stop: () => import("solid-js").JSX.Element;
|
|
10
|
+
export declare const Plus: () => import("solid-js").JSX.Element;
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rohansguliani/mux-chat-ui",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Mux's chat as a drop-in sidebar: streaming transcript, tool rows, question cards, model picker, and composer, rendered in a shadow root with Mux's own styles.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./testing": {
|
|
14
|
+
"types": "./dist/testing.d.ts",
|
|
15
|
+
"import": "./dist/testing.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": ["dist"],
|
|
20
|
+
"publishConfig": { "access": "public" },
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "rm -rf dist && vite build && tsc -p tsconfig.build.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest run --root .",
|
|
25
|
+
"sync-styles": "node scripts/sync-mux-styles.mjs"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@rohansguliani/mux-chat-core": "^0.3.0",
|
|
29
|
+
"dompurify": "^3.4.1",
|
|
30
|
+
"marked": "17.0.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"solid-js": "^1.9.0"
|
|
34
|
+
}
|
|
35
|
+
}
|