@abstractframework/panel-chat 0.1.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/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @abstractuic/panel-chat
2
+
3
+ Chat UI primitives for AbstractFramework-style UIs: thread rendering, message cards, a composer, and lightweight Markdown/JSON rendering.
4
+
5
+ ## Peer dependencies
6
+
7
+ Declared in `panel-chat/package.json`:
8
+
9
+ - `react@^18`, `react-dom@^18`
10
+ - `@abstractuic/ui-kit` (icons used by `ChatMessageCard`)
11
+
12
+ ## Install
13
+
14
+ - Workspace: add a dependency on `@abstractuic/panel-chat`
15
+ - npm (once published): `npm i @abstractuic/panel-chat`
16
+
17
+ ## Exported API
18
+
19
+ See `panel-chat/src/index.ts` for the authoritative export list. Common entry points:
20
+
21
+ - Components: `ChatThread`, `ChatComposer`, `ChatMessageCard`, `ChatMessageContent`
22
+ - Renderers: `Markdown`, `JsonViewer`
23
+ - Types: `PanelChatMessage`, `ChatMessage`, `ChatAttachment`, `ChatStat`
24
+ - Utils: `chatToMarkdown`, `copyText`, `downloadTextFile`, `tryParseJson`
25
+
26
+ ## Usage (typical)
27
+
28
+ ```tsx
29
+ import React, { useState } from "react";
30
+ import { ChatThread, ChatComposer, type ChatMessage } from "@abstractuic/panel-chat";
31
+
32
+ export function ChatView() {
33
+ const [value, setValue] = useState("");
34
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
35
+
36
+ return (
37
+ <>
38
+ <ChatThread messages={messages} autoScroll />
39
+ <ChatComposer value={value} onChange={setValue} onSubmit={() => setMessages((m) => [...m, { role: "user", content: value }])} />
40
+ </>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## Rendering rules
46
+
47
+ `ChatMessageContent` auto-detects JSON (via `tryParseJson` in `panel-chat/src/utils.ts`) and renders:
48
+
49
+ - JSON ⇒ `JsonViewer`
50
+ - otherwise ⇒ `Markdown` (or your `renderMarkdown` override)
51
+
52
+ Markdown is intentionally minimal and implemented in `panel-chat/src/markdown.tsx` (headings 1–3, code fences, lists, tables, blockquotes, hr, emphasis, and optional highlighting).
53
+
54
+ ## Styling & theming
55
+
56
+ - Components import `panel-chat/src/panel_chat.css`.
57
+ - For consistent tokens (colors/typography), import `@abstractuic/ui-kit/src/theme.css` in your app.
58
+
59
+ ## Related docs
60
+
61
+ - Getting started: [`docs/getting-started.md`](../docs/getting-started.md)
62
+ - Architecture: [`docs/architecture.md`](../docs/architecture.md)
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import "./panel_chat.css";
3
+ export type ChatComposerProps = {
4
+ value: string;
5
+ onChange: (value: string) => void;
6
+ onSubmit: () => void;
7
+ placeholder?: string;
8
+ disabled?: boolean;
9
+ busy?: boolean;
10
+ rows?: number;
11
+ autoFocus?: boolean;
12
+ className?: string;
13
+ textareaClassName?: string;
14
+ sendButtonClassName?: string;
15
+ sendLabel?: string;
16
+ busyLabel?: string;
17
+ actions?: React.ReactNode;
18
+ };
19
+ export declare const ChatComposer: React.ForwardRefExoticComponent<ChatComposerProps & React.RefAttributes<HTMLTextAreaElement>>;
20
+ //# sourceMappingURL=chat_composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat_composer.d.ts","sourceRoot":"","sources":["../src/chat_composer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,kBAAkB,CAAC;AAE1B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,YAAY,+FA6CvB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import "./panel_chat.css";
4
+ export const ChatComposer = React.forwardRef(function ChatComposer(props, ref) {
5
+ const rows = typeof props.rows === "number" && Number.isFinite(props.rows) ? Math.max(1, Math.trunc(props.rows)) : 3;
6
+ const disabled = Boolean(props.disabled);
7
+ const busy = Boolean(props.busy);
8
+ const can_submit = !disabled && !busy && Boolean(String(props.value || "").trim());
9
+ return (_jsxs("div", { className: ["pc-composer", props.className].filter(Boolean).join(" "), children: [_jsx("textarea", { ref: ref, className: ["pc-composer__textarea", props.textareaClassName].filter(Boolean).join(" "), value: props.value, onChange: (e) => props.onChange(e.target.value), rows: rows, spellCheck: false, autoCorrect: "off", autoCapitalize: "off", autoComplete: "off", placeholder: props.placeholder || "", disabled: disabled, autoFocus: Boolean(props.autoFocus), onKeyDown: (e) => {
10
+ if (e.key === "Enter" && !e.shiftKey) {
11
+ e.preventDefault();
12
+ props.onSubmit();
13
+ }
14
+ } }), _jsxs("div", { className: "pc-composer__row", children: [_jsx("div", { className: "pc-composer__actions", children: props.actions }), _jsx("button", { type: "button", className: props.sendButtonClassName || "pc-btn", disabled: !can_submit, onClick: () => props.onSubmit(), children: busy ? props.busyLabel || "Thinking…" : props.sendLabel || "Send" })] })] }));
15
+ });
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import "./panel_chat.css";
3
+ export type ChatMessageLevel = "info" | "warn" | "error";
4
+ export type ChatMessage = {
5
+ id?: string;
6
+ role: string;
7
+ content: string;
8
+ ts?: string;
9
+ title?: string;
10
+ level?: ChatMessageLevel;
11
+ kind?: string;
12
+ };
13
+ export type ChatAttachment = {
14
+ id?: string;
15
+ label: string;
16
+ target?: string;
17
+ title?: string;
18
+ disabled?: boolean;
19
+ onClick?: () => void;
20
+ };
21
+ export type ChatStat = {
22
+ id?: string;
23
+ label: string;
24
+ title?: string;
25
+ icon?: React.ReactNode;
26
+ onClick?: () => void;
27
+ };
28
+ export type ChatMessageCardProps = {
29
+ message: ChatMessage;
30
+ className?: string;
31
+ renderMarkdown?: (markdown: string) => React.ReactElement;
32
+ attachments?: ChatAttachment[];
33
+ stats?: ChatStat[];
34
+ onSpeakToggle?: (message: ChatMessage) => void;
35
+ getSpeakState?: (message: ChatMessage) => "idle" | "loading" | "playing" | "paused";
36
+ showCopy?: boolean;
37
+ jsonCollapseAfterDepth?: number;
38
+ };
39
+ export declare function ChatMessageCard(props: ChatMessageCardProps): React.ReactElement;
40
+ export default ChatMessageCard;
41
+ //# sourceMappingURL=chat_message_card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat_message_card.d.ts","sourceRoot":"","sources":["../src/chat_message_card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,kBAAkB,CAAC;AAO1B,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEzD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAyBF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,KAAK,CAAC,YAAY,CAAC;IAC1D,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC/C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,KAAK,CAAC,YAAY,CAqH/E;AAED,eAAe,eAAe,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import "./panel_chat.css";
4
+ import { Icon } from "@abstractframework/ui-kit";
5
+ import { ChatMessageContent } from "./message_content";
6
+ import { copyText } from "./utils";
7
+ function _role_ui(m) {
8
+ const role = String(m.role || "").trim().toLowerCase() || "system";
9
+ const kind = String(m.kind || "").trim().toLowerCase();
10
+ const lvl = String(m.level || "").trim().toLowerCase();
11
+ if (role === "system" && kind === "report_bug") {
12
+ return { label: String(m.title || "").trim() || "Bug report", icon: "warning", variant: "report_bug" };
13
+ }
14
+ if (role === "system" && kind === "report_feature") {
15
+ return { label: String(m.title || "").trim() || "Feature request", icon: "check", variant: "report_feature" };
16
+ }
17
+ if (role === "user")
18
+ return { label: "You", icon: "user", variant: "user" };
19
+ if (role === "assistant")
20
+ return { label: "Agent", icon: "bot", variant: "assistant" };
21
+ const sys_label = String(m.title || "").trim() || (lvl === "error" ? "Error" : lvl === "warn" ? "Warning" : "System");
22
+ const sys_icon = lvl === "error" ? "error" : lvl === "warn" ? "warning" : "info";
23
+ const sys_variant = lvl === "error" ? "error" : lvl === "warn" ? "warn" : "status";
24
+ return { label: sys_label, icon: sys_icon, variant: sys_variant };
25
+ }
26
+ export function ChatMessageCard(props) {
27
+ const m = props.message;
28
+ const role_ui = useMemo(() => _role_ui(m), [m.kind, m.level, m.role, m.title]);
29
+ const ts = String(m.ts || "").trim();
30
+ const time_label = ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
31
+ const [copy_state, set_copy_state] = useState("idle");
32
+ const can_speak = typeof props.onSpeakToggle === "function" && role_ui.variant === "assistant" && Boolean(String(m.content || "").trim());
33
+ const speak_state = can_speak ? props.getSpeakState?.(m) || "idle" : "idle";
34
+ const speak_icon = speak_state === "loading" ? "loader" : speak_state === "playing" ? "pause" : "speaker";
35
+ const speak_title = speak_state === "loading"
36
+ ? "Generating audio…"
37
+ : speak_state === "playing"
38
+ ? "Pause"
39
+ : speak_state === "paused"
40
+ ? "Resume"
41
+ : "Speak (TTS)";
42
+ const show_copy = props.showCopy !== false;
43
+ const attachments = Array.isArray(props.attachments) ? props.attachments : [];
44
+ const stats = Array.isArray(props.stats) ? props.stats : [];
45
+ return (_jsxs("div", { className: ["pc-chat-item", `pc-chat-item--${role_ui.variant}`, props.className].filter(Boolean).join(" "), children: [_jsxs("div", { className: "pc-chat-header", children: [_jsx("div", { className: "pc-chat-avatar", "aria-hidden": "true", children: _jsx(Icon, { name: role_ui.icon, size: 14 }) }), _jsx("span", { className: "pc-chat-role", children: role_ui.label }), _jsx("span", { className: "pc-chat-header-spacer" }), time_label ? _jsx("span", { className: "pc-chat-time", children: time_label }) : null, _jsxs("div", { className: "pc-chat-header-actions", children: [can_speak ? (_jsx("button", { className: "pc-chat-icon-btn", type: "button", "aria-label": speak_title, title: speak_title, onClick: () => props.onSpeakToggle?.(m), children: _jsx(Icon, { name: speak_icon, size: 22 }) })) : null, show_copy ? (_jsx("button", { className: `pc-chat-icon-btn ${copy_state !== "idle" ? `pc-chat-icon-btn--${copy_state}` : ""}`.trim(), type: "button", "aria-label": "Copy message", title: copy_state === "idle" ? "Copy" : copy_state === "copied" ? "Copied" : "Copy failed", onClick: async () => {
46
+ const ok = await copyText(String(m.content || ""));
47
+ set_copy_state(ok ? "copied" : "failed");
48
+ window.setTimeout(() => set_copy_state("idle"), 900);
49
+ }, children: _jsx(Icon, { name: "copy", size: 22 }) })) : null] })] }), _jsx("div", { className: "pc-chat-body", children: _jsx(ChatMessageContent, { text: String(m.content || ""), renderMarkdown: props.renderMarkdown, jsonCollapseAfterDepth: props.jsonCollapseAfterDepth }) }), attachments.length ? (_jsx("div", { className: "pc-chat-attachments", "aria-label": "Attachments", children: attachments.slice(0, 24).map((a, idx) => {
50
+ const label = String(a.label || "").trim() || "attachment";
51
+ const target = String(a.target || "").trim();
52
+ const title = String(a.title || "").trim();
53
+ const disabled = Boolean(a.disabled) || typeof a.onClick !== "function";
54
+ return (_jsxs("button", { type: "button", className: "pc-chat-attachment-chip", title: title || label, onClick: disabled ? undefined : a.onClick, disabled: disabled, children: [_jsx(Icon, { name: "paperclip", size: 14 }), target ? _jsx("span", { className: `pc-chat-attachment-target pc-chat-attachment-target--${target}`, children: target }) : null, _jsx("span", { className: "pc-chat-attachment-name", children: label })] }, a.id || `${label}:${idx}`));
55
+ }) })) : null, stats.length ? (_jsx("div", { className: "pc-chat-stats", "aria-label": "Stats", children: stats.slice(0, 12).map((s, idx) => {
56
+ const label = String(s.label || "").trim();
57
+ if (!label)
58
+ return null;
59
+ const title = String(s.title || "").trim();
60
+ const clickable = typeof s.onClick === "function";
61
+ return (_jsxs("button", { type: "button", className: ["pc-chat-stat", clickable ? "pc-chat-stat--clickable" : ""].filter(Boolean).join(" "), title: title || undefined, onClick: clickable ? () => s.onClick?.() : undefined, disabled: !clickable, children: [s.icon ? _jsx("span", { className: "pc-chat-stat-icon", "aria-hidden": "true", children: s.icon }) : null, _jsx("span", { className: "pc-chat-stat-label", children: label })] }, s.id || `${label}:${idx}`));
62
+ }) })) : null] }));
63
+ }
64
+ export default ChatMessageCard;
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import "./panel_chat.css";
3
+ import { type ChatMessage, type ChatMessageCardProps } from "./chat_message_card";
4
+ export type ChatThreadProps = {
5
+ messages: ChatMessage[];
6
+ className?: string;
7
+ empty?: React.ReactNode;
8
+ autoScroll?: boolean;
9
+ autoScrollThresholdPx?: number;
10
+ messageProps?: Omit<ChatMessageCardProps, "message">;
11
+ };
12
+ export declare function ChatThread(props: ChatThreadProps): React.ReactElement;
13
+ export default ChatThread;
14
+ //# sourceMappingURL=chat_thread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat_thread.d.ts","sourceRoot":"","sources":["../src/chat_thread.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAEpE,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EAAmB,KAAK,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEnG,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,YAAY,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,CAAC;CACtD,CAAC;AAQF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAoCrE;AAED,eAAe,UAAU,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import "./panel_chat.css";
4
+ import { ChatMessageCard } from "./chat_message_card";
5
+ function is_near_bottom(el, thresholdPx) {
6
+ const thr = Math.max(0, Math.trunc(thresholdPx));
7
+ const remaining = el.scrollHeight - el.scrollTop - el.clientHeight;
8
+ return remaining <= thr;
9
+ }
10
+ export function ChatThread(props) {
11
+ const threshold = typeof props.autoScrollThresholdPx === "number" && Number.isFinite(props.autoScrollThresholdPx) ? props.autoScrollThresholdPx : 120;
12
+ const auto = props.autoScroll !== false;
13
+ const list_ref = useRef(null);
14
+ const bottom_ref = useRef(null);
15
+ const [stick, set_stick] = useState(true);
16
+ const msgs = useMemo(() => (Array.isArray(props.messages) ? props.messages : []), [props.messages]);
17
+ useEffect(() => {
18
+ if (!auto)
19
+ return;
20
+ const el = list_ref.current;
21
+ if (!el)
22
+ return;
23
+ const on_scroll = () => {
24
+ set_stick(is_near_bottom(el, threshold));
25
+ };
26
+ el.addEventListener("scroll", on_scroll, { passive: true });
27
+ on_scroll();
28
+ return () => el.removeEventListener("scroll", on_scroll);
29
+ }, [auto, threshold]);
30
+ useEffect(() => {
31
+ if (!auto)
32
+ return;
33
+ if (!stick)
34
+ return;
35
+ bottom_ref.current?.scrollIntoView({ block: "end" });
36
+ }, [auto, stick, msgs.length]);
37
+ return (_jsxs("div", { ref: list_ref, className: ["pc-chat-thread", props.className].filter(Boolean).join(" "), children: [!msgs.length ? props.empty || null : null, msgs.map((m, idx) => (_jsx(ChatMessageCard, { message: m, ...(props.messageProps || {}) }, String(m.id || "") || String(m.ts || "") + ":" + String(m.role || "") + ":" + idx))), _jsx("div", { ref: bottom_ref })] }));
38
+ }
39
+ export default ChatThread;
@@ -0,0 +1,10 @@
1
+ export { ChatMessageContent } from "./message_content";
2
+ export { ChatComposer } from "./chat_composer";
3
+ export { Markdown } from "./markdown";
4
+ export { JsonViewer } from "./json_viewer";
5
+ export { ChatMessageCard } from "./chat_message_card";
6
+ export { ChatThread } from "./chat_thread";
7
+ export type { PanelChatMessage } from "./types";
8
+ export type { ChatAttachment, ChatMessage, ChatMessageLevel, ChatStat } from "./chat_message_card";
9
+ export { chatToMarkdown, copyText, downloadTextFile, tryParseJson } from "./utils";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACnG,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { ChatMessageContent } from "./message_content";
2
+ export { ChatComposer } from "./chat_composer";
3
+ export { Markdown } from "./markdown";
4
+ export { JsonViewer } from "./json_viewer";
5
+ export { ChatMessageCard } from "./chat_message_card";
6
+ export { ChatThread } from "./chat_thread";
7
+ export { chatToMarkdown, copyText, downloadTextFile, tryParseJson } from "./utils";
@@ -0,0 +1,8 @@
1
+ import "./panel_chat.css";
2
+ export declare function JsonViewer(props: {
3
+ value: unknown;
4
+ className?: string;
5
+ collapseAfterDepth?: number;
6
+ showCopy?: boolean;
7
+ }): React.ReactElement;
8
+ //# sourceMappingURL=json_viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json_viewer.d.ts","sourceRoot":"","sources":["../src/json_viewer.tsx"],"names":[],"mappings":"AAEA,OAAO,kBAAkB,CAAC;AAoJ1B,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,YAAY,CAsB7I"}
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import "./panel_chat.css";
4
+ function isJsonObject(value) {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ function isJsonArray(value) {
8
+ return Array.isArray(value);
9
+ }
10
+ function copyStringForJson(value) {
11
+ if (value == null)
12
+ return "";
13
+ if (typeof value === "string")
14
+ return value;
15
+ try {
16
+ return JSON.stringify(value, null, 2);
17
+ }
18
+ catch {
19
+ return String(value);
20
+ }
21
+ }
22
+ async function copyText(text) {
23
+ const value = String(text || "");
24
+ if (!value)
25
+ return;
26
+ try {
27
+ await navigator.clipboard.writeText(value);
28
+ }
29
+ catch {
30
+ const el = document.createElement("textarea");
31
+ el.value = value;
32
+ document.body.appendChild(el);
33
+ el.select();
34
+ document.execCommand("copy");
35
+ document.body.removeChild(el);
36
+ }
37
+ }
38
+ function Token({ kind, children, }) {
39
+ return _jsx("span", { className: `pc-json-token ${kind}`, children: children });
40
+ }
41
+ function renderPrimitive(value) {
42
+ if (value === null)
43
+ return _jsx(Token, { kind: "null", children: "null" });
44
+ if (typeof value === "string")
45
+ return _jsx(Token, { kind: "string", children: JSON.stringify(value) });
46
+ if (typeof value === "number")
47
+ return _jsx(Token, { kind: "number", children: String(value) });
48
+ if (typeof value === "boolean")
49
+ return _jsx(Token, { kind: "boolean", children: value ? "true" : "false" });
50
+ return _jsx(Token, { kind: "string", children: JSON.stringify(String(value)) });
51
+ }
52
+ function countSummary(value) {
53
+ if (isJsonArray(value))
54
+ return value.length === 1 ? "1 item" : `${value.length} items`;
55
+ if (isJsonObject(value))
56
+ return Object.keys(value).length === 1 ? "1 key" : `${Object.keys(value).length} keys`;
57
+ return "";
58
+ }
59
+ function JsonNode(props) {
60
+ const { label, value, depth, collapseAfterDepth, trailingComma } = props;
61
+ const indentPx = depth * 14;
62
+ const isObject = isJsonObject(value);
63
+ const isArray = isJsonArray(value);
64
+ const isContainer = isObject || isArray;
65
+ const defaultOpen = depth < collapseAfterDepth;
66
+ const [open, setOpen] = useState(defaultOpen);
67
+ const prefix = label ? (_jsxs(_Fragment, { children: [_jsx(Token, { kind: "key", children: JSON.stringify(label) }), ": "] })) : null;
68
+ if (!isContainer) {
69
+ return (_jsxs("div", { className: "pc-json-viewer__line", style: { paddingLeft: indentPx }, children: [prefix, renderPrimitive(value), trailingComma ? "," : ""] }));
70
+ }
71
+ const containerOpen = isArray ? "[" : "{";
72
+ const containerClose = isArray ? "]" : "}";
73
+ const summaryText = countSummary(value);
74
+ const summaryValue = open ? `${containerOpen}` : `${containerOpen}…${containerClose}${summaryText ? ` (${summaryText})` : ""}`;
75
+ const entries = isArray
76
+ ? value.map((v, i) => ({ key: String(i), label: undefined, value: v }))
77
+ : Object.entries(value).map(([k, v]) => ({ key: k, label: k, value: v }));
78
+ return (_jsxs("details", { className: "pc-json-viewer__details", open: open, onToggle: (e) => setOpen(e.currentTarget.open), children: [_jsxs("summary", { className: "pc-json-viewer__summary", style: { paddingLeft: indentPx }, children: [prefix, _jsx("span", { className: "pc-json-viewer__caret", "aria-hidden": "true", children: open ? "▾" : "▸" }), _jsx("span", { className: "pc-json-viewer__punct", children: summaryValue }), !open && trailingComma ? _jsx("span", { className: "pc-json-viewer__punct", children: "," }) : null] }), open ? (_jsx("div", { className: "pc-json-viewer__children", children: entries.length === 0 ? (_jsxs("div", { className: "pc-json-viewer__line", style: { paddingLeft: indentPx }, children: [_jsx("span", { className: "pc-json-viewer__punct", children: containerClose }), trailingComma ? "," : ""] })) : (_jsxs(_Fragment, { children: [entries.map((e, idx) => {
79
+ const isLast = idx === entries.length - 1;
80
+ return (_jsx(JsonNode, { label: e.label, value: e.value, depth: depth + 1, collapseAfterDepth: collapseAfterDepth, trailingComma: !isLast }, `${depth}:${label || "root"}:${e.key}`));
81
+ }), _jsxs("div", { className: "pc-json-viewer__line", style: { paddingLeft: indentPx }, children: [_jsx("span", { className: "pc-json-viewer__punct", children: containerClose }), trailingComma ? "," : ""] })] })) })) : null] }));
82
+ }
83
+ export function JsonViewer(props) {
84
+ const { value, className, showCopy = true } = props;
85
+ const collapseAfterDepth = Number.isFinite(props.collapseAfterDepth ?? NaN) ? Number(props.collapseAfterDepth) : 3;
86
+ const copyValue = useMemo(() => copyStringForJson(value), [value]);
87
+ const cls = ["pc-json-viewer", className].filter(Boolean).join(" ");
88
+ return (_jsxs("div", { className: cls, children: [showCopy ? (_jsx("div", { className: "pc-json-viewer__toolbar", children: _jsx("button", { type: "button", className: "pc-btn pc-json-viewer__copy", onClick: () => void copyText(copyValue), children: "Copy" }) })) : null, _jsx("div", { className: "pc-json-viewer__tree", role: "tree", "aria-label": "JSON viewer", children: _jsx(JsonNode, { value: value, depth: 0, collapseAfterDepth: collapseAfterDepth, trailingComma: false }) })] }));
89
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import "./panel_chat.css";
3
+ export declare function Markdown({ text, className, highlight, highlights, highlightClassName, highlightId, }: {
4
+ text: string;
5
+ className?: string;
6
+ highlight?: string;
7
+ highlights?: string[];
8
+ highlightClassName?: string;
9
+ highlightId?: string;
10
+ }): React.ReactElement;
11
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,kBAAkB,CAAC;AAqI1B,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,SAAS,EACT,SAAS,EACT,UAAU,EACV,kBAAkB,EAClB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,KAAK,CAAC,YAAY,CAuLrB"}