whisper-design
Version:
A React component library for building AI chat room UIs.
706 lines (705 loc) • 24 kB
JavaScript
import O, { useState as y, useRef as Y, useEffect as ee, useMemo as de } from "react";
import { v4 as se } from "uuid";
import fe from "dayjs";
var C = { exports: {} }, k = {};
/**
* @license React
* react-jsx-runtime.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var J;
function me() {
if (J) return k;
J = 1;
var t = Symbol.for("react.transitional.element"), a = Symbol.for("react.fragment");
function o(i, r, c) {
var l = null;
if (c !== void 0 && (l = "" + c), r.key !== void 0 && (l = "" + r.key), "key" in r) {
c = {};
for (var u in r)
u !== "key" && (c[u] = r[u]);
} else c = r;
return r = c.ref, {
$$typeof: t,
type: i,
key: l,
ref: r !== void 0 ? r : null,
props: c
};
}
return k.Fragment = a, k.jsx = o, k.jsxs = o, k;
}
var S = {};
/**
* @license React
* react-jsx-runtime.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var q;
function pe() {
return q || (q = 1, process.env.NODE_ENV !== "production" && function() {
function t(e) {
if (e == null) return null;
if (typeof e == "function")
return e.$$typeof === le ? null : e.displayName || e.name || null;
if (typeof e == "string") return e;
switch (e) {
case v:
return "Fragment";
case N:
return "Profiler";
case T:
return "StrictMode";
case re:
return "Suspense";
case ne:
return "SuspenseList";
case ce:
return "Activity";
}
if (typeof e == "object")
switch (typeof e.tag == "number" && console.error(
"Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."
), e.$$typeof) {
case j:
return "Portal";
case W:
return (e.displayName || "Context") + ".Provider";
case P:
return (e._context.displayName || "Context") + ".Consumer";
case b:
var n = e.render;
return e = e.displayName, e || (e = n.displayName || n.name || "", e = e !== "" ? "ForwardRef(" + e + ")" : "ForwardRef"), e;
case ie:
return n = e.displayName || null, n !== null ? n : t(e.type) || "Memo";
case D:
n = e._payload, e = e._init;
try {
return t(e(n));
} catch {
}
}
return null;
}
function a(e) {
return "" + e;
}
function o(e) {
try {
a(e);
var n = !1;
} catch {
n = !0;
}
if (n) {
n = console;
var m = n.error, p = typeof Symbol == "function" && Symbol.toStringTag && e[Symbol.toStringTag] || e.constructor.name || "Object";
return m.call(
n,
"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",
p
), a(e);
}
}
function i(e) {
if (e === v) return "<>";
if (typeof e == "object" && e !== null && e.$$typeof === D)
return "<...>";
try {
var n = t(e);
return n ? "<" + n + ">" : "<...>";
} catch {
return "<...>";
}
}
function r() {
var e = M.A;
return e === null ? null : e.getOwner();
}
function c() {
return Error("react-stack-top-frame");
}
function l(e) {
if (U.call(e, "key")) {
var n = Object.getOwnPropertyDescriptor(e, "key").get;
if (n && n.isReactWarning) return !1;
}
return e.key !== void 0;
}
function u(e, n) {
function m() {
B || (B = !0, console.error(
"%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",
n
));
}
m.isReactWarning = !0, Object.defineProperty(e, "key", {
get: m,
configurable: !0
});
}
function x() {
var e = t(this.type);
return G[e] || (G[e] = !0, console.error(
"Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."
)), e = this.props.ref, e !== void 0 ? e : null;
}
function g(e, n, m, p, R, E, I, L) {
return m = E.ref, e = {
$$typeof: f,
type: e,
key: n,
props: E,
_owner: R
}, (m !== void 0 ? m : null) !== null ? Object.defineProperty(e, "ref", {
enumerable: !1,
get: x
}) : Object.defineProperty(e, "ref", { enumerable: !1, value: null }), e._store = {}, Object.defineProperty(e._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
}), Object.defineProperty(e, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
}), Object.defineProperty(e, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: I
}), Object.defineProperty(e, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: L
}), Object.freeze && (Object.freeze(e.props), Object.freeze(e)), e;
}
function _(e, n, m, p, R, E, I, L) {
var h = n.children;
if (h !== void 0)
if (p)
if (oe(h)) {
for (p = 0; p < h.length; p++)
w(h[p]);
Object.freeze && Object.freeze(h);
} else
console.error(
"React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead."
);
else w(h);
if (U.call(n, "key")) {
h = t(e);
var A = Object.keys(n).filter(function(ue) {
return ue !== "key";
});
p = 0 < A.length ? "{key: someKey, " + A.join(": ..., ") + ": ...}" : "{key: someKey}", H[h + p] || (A = 0 < A.length ? "{" + A.join(": ..., ") + ": ...}" : "{}", console.error(
`A props object containing a "key" prop is being spread into JSX:
let props = %s;
<%s {...props} />
React keys must be passed directly to JSX without using spread:
let props = %s;
<%s key={someKey} {...props} />`,
p,
h,
A,
h
), H[h + p] = !0);
}
if (h = null, m !== void 0 && (o(m), h = "" + m), l(n) && (o(n.key), h = "" + n.key), "key" in n) {
m = {};
for (var $ in n)
$ !== "key" && (m[$] = n[$]);
} else m = n;
return h && u(
m,
typeof e == "function" ? e.displayName || e.name || "Unknown" : e
), g(
e,
h,
E,
R,
r(),
m,
I,
L
);
}
function w(e) {
typeof e == "object" && e !== null && e.$$typeof === f && e._store && (e._store.validated = 1);
}
var d = O, f = Symbol.for("react.transitional.element"), j = Symbol.for("react.portal"), v = Symbol.for("react.fragment"), T = Symbol.for("react.strict_mode"), N = Symbol.for("react.profiler"), P = Symbol.for("react.consumer"), W = Symbol.for("react.context"), b = Symbol.for("react.forward_ref"), re = Symbol.for("react.suspense"), ne = Symbol.for("react.suspense_list"), ie = Symbol.for("react.memo"), D = Symbol.for("react.lazy"), ce = Symbol.for("react.activity"), le = Symbol.for("react.client.reference"), M = d.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, U = Object.prototype.hasOwnProperty, oe = Array.isArray, F = console.createTask ? console.createTask : function() {
return null;
};
d = {
"react-stack-bottom-frame": function(e) {
return e();
}
};
var B, G = {}, V = d["react-stack-bottom-frame"].bind(
d,
c
)(), X = F(i(c)), H = {};
S.Fragment = v, S.jsx = function(e, n, m, p, R) {
var E = 1e4 > M.recentlyCreatedOwnerStacks++;
return _(
e,
n,
m,
!1,
p,
R,
E ? Error("react-stack-top-frame") : V,
E ? F(i(e)) : X
);
}, S.jsxs = function(e, n, m, p, R) {
var E = 1e4 > M.recentlyCreatedOwnerStacks++;
return _(
e,
n,
m,
!0,
p,
R,
E ? Error("react-stack-top-frame") : V,
E ? F(i(e)) : X
);
};
}()), S;
}
var Q;
function he() {
return Q || (Q = 1, process.env.NODE_ENV === "production" ? C.exports = me() : C.exports = pe()), C.exports;
}
var s = he();
const xe = O.memo(
({ src: t, alt: a, size: o = "medium", shape: i = "circle", gradient: r, children: c }) => {
const l = r ? `avatar--${r}-gradient` : "";
return /* @__PURE__ */ s.jsx(
"div",
{
className: `avatar avatar--${o} avatar--${i} ${l}`,
role: "img",
"aria-label": a,
children: t ? /* @__PURE__ */ s.jsx("img", { src: t, alt: a }) : c
}
);
}
), z = ({
onClick: t,
children: a,
variant: o = "primary",
disabled: i = !1,
type: r = "button",
isCircle: c = !1,
className: l = ""
}) => {
const x = `button ${c ? "button--circle" : `button--${o}`} ${l}`.trim();
return /* @__PURE__ */ s.jsx("button", { type: r, className: x, onClick: t, disabled: i, children: a });
}, te = (t) => {
switch (t.split(".").pop()?.toLowerCase()) {
case "pdf":
return "fas fa-file-pdf";
case "xlsx":
case "xls":
return "fas fa-file-excel";
case "docx":
case "doc":
return "fas fa-file-word";
case "pptx":
case "ppt":
return "fas fa-file-powerpoint";
case "jpg":
case "jpeg":
case "png":
case "gif":
case "bmp":
case "svg":
case "webp":
return "fas fa-file-image";
case "mp4":
case "avi":
case "mov":
case "wmv":
case "flv":
return "fas fa-file-video";
case "mp3":
case "wav":
case "flac":
case "aac":
return "fas fa-file-audio";
case "zip":
case "rar":
case "7z":
case "tar":
case "gz":
return "fas fa-file-archive";
case "txt":
case "md":
return "fas fa-file-alt";
case "js":
case "ts":
case "jsx":
case "tsx":
case "html":
case "css":
case "scss":
case "json":
case "xml":
case "py":
case "java":
case "cpp":
case "c":
return "fas fa-file-code";
default:
return "fas fa-file";
}
}, ae = (t) => {
if (t === 0) return "0 B";
const a = 1024, o = ["B", "KB", "MB", "GB"], i = Math.floor(Math.log(t) / Math.log(a));
return `${parseFloat((t / Math.pow(a, i)).toFixed(2))} ${o[i]}`;
}, ve = O.memo(
({ id: t, content: a, sender: o, type: i = "text", isLoading: r, files: c }) => {
const l = () => {
switch (i) {
case "image":
return /* @__PURE__ */ s.jsx("img", { src: a, alt: "" });
case "code":
return /* @__PURE__ */ s.jsx("pre", { children: /* @__PURE__ */ s.jsx("code", { children: a }) });
case "text":
default:
return /* @__PURE__ */ s.jsx("p", { children: a });
}
};
return /* @__PURE__ */ s.jsxs("div", { className: `message-bubble ${o} message-bubble--${i}`, children: [
l(),
c && c.length > 0 && /* @__PURE__ */ s.jsx("div", { className: "file-attachments", children: c.map((u) => /* @__PURE__ */ s.jsxs("div", { className: "file-attachment", children: [
/* @__PURE__ */ s.jsx("div", { className: "file-icon", children: /* @__PURE__ */ s.jsx("i", { className: te(u.name) }) }),
/* @__PURE__ */ s.jsxs("div", { className: "file-info", children: [
/* @__PURE__ */ s.jsx("div", { className: "file-name", children: u.name }),
/* @__PURE__ */ s.jsx("div", { className: "file-size", children: ae(u.size) })
] })
] }, u.id)) }),
r && /* @__PURE__ */ s.jsx("span", { className: "loading-indicator", children: "..." }),
" "
] }, t);
}
), je = ({
onSendMessage: t,
isSending: a = !1,
placeholder: o = "输入消息...",
onFilesChange: i
}) => {
const [r, c] = y(""), [l, u] = y([]), x = Y(null), g = (d) => {
u(d), i && i(d);
}, _ = (d) => {
d.preventDefault(), (r.trim() || l.length > 0) && (t(r.trim(), l), c(""), u([]));
}, w = (d) => {
d.key === "Enter" && !d.shiftKey && (d.preventDefault(), _(d));
};
return /* @__PURE__ */ s.jsx("form", { className: "chat-input-area", onSubmit: _, children: /* @__PURE__ */ s.jsxs("div", { className: "input-container", children: [
/* @__PURE__ */ s.jsx("div", { className: "input-toolbar", children: /* @__PURE__ */ s.jsx(z, { isCircle: !0, onClick: () => x.current?.click(), disabled: a, children: /* @__PURE__ */ s.jsx("i", { className: "fas fa-paperclip" }) }) }),
/* @__PURE__ */ s.jsx(
"input",
{
type: "file",
ref: x,
multiple: !0,
accept: "image/*,.pdf,.xlsx,.docx",
onChange: (d) => {
const j = Array.from(d.target.files || []).map(
(v) => Object.assign(v, { id: se() })
);
g([...l, ...j]), d.target.value = "";
},
style: { display: "none" }
}
),
l.length > 0 && /* @__PURE__ */ s.jsxs("div", { className: "file-list", children: [
" ",
l.map((d) => /* @__PURE__ */ s.jsxs("div", { className: "file-card", children: [
/* @__PURE__ */ s.jsx("span", { className: "file-icon", children: /* @__PURE__ */ s.jsx("i", { className: te(d.name) }) }),
/* @__PURE__ */ s.jsxs("div", { className: "file-info", children: [
/* @__PURE__ */ s.jsx("span", { className: "file-name", children: d.name }),
/* @__PURE__ */ s.jsx("span", { className: "file-size", children: ae(d.size) })
] }),
/* @__PURE__ */ s.jsx(
"button",
{
type: "button",
className: "file-remove-button",
onClick: () => g(l.filter((f) => f.id !== d.id)),
disabled: a,
children: "×"
}
)
] }, d.id))
] }),
/* @__PURE__ */ s.jsx(
"textarea",
{
className: "message-input",
placeholder: o,
value: r,
onChange: (d) => c(d.target.value),
onKeyDown: w,
"aria-label": "消息输入框",
disabled: a
}
),
/* @__PURE__ */ s.jsxs("div", { className: "input-actions", children: [
/* @__PURE__ */ s.jsxs("div", { className: "input-hints", children: [
/* @__PURE__ */ s.jsx("i", { className: "fas fa-lightbulb" }),
" 按 Enter 发送,Shift + Enter 换行"
] }),
/* @__PURE__ */ s.jsxs(z, { type: "submit", variant: "primary", disabled: a, onClick: () => {
}, children: [
/* @__PURE__ */ s.jsx("i", { className: "fas fa-paper-plane" }),
"发送"
] })
] })
] }) });
}, we = ({
onFilesChange: t,
acceptedFileTypes: a = "image/*,.pdf,.xlsx,.docx",
maxFiles: o = 5,
value: i,
// Controlled component value
disabled: r = !1
}) => {
const c = Y(null), [l, u] = y(i || []);
ee(() => {
i !== void 0 && u(i);
}, [i]);
const x = (f) => {
if (r) return;
const j = Array.from(f.target.files || []), v = [];
j.forEach((N) => {
if (l.length + v.length < o) {
const P = "." + N.name.split(".").pop()?.toLowerCase();
if (a.split(",").some((b) => (b = b.trim().toLowerCase(), b.startsWith(".") ? P === b : b.endsWith("/*") ? N.type.startsWith(b.slice(0, -1)) : N.type === b))) {
const b = {
...N,
id: se(),
...N.type.startsWith("image/") && { preview: URL.createObjectURL(N) }
};
v.push(b);
} else
console.warn(`File type not accepted: ${N.name} (${N.type})`);
} else
console.warn(`Max files reached (${o}). Skipping ${N.name}.`);
});
const T = [...l, ...v];
u(T), t?.(T), c.current && (c.current.value = "");
}, g = (f) => {
if (r) return;
const j = l.filter((v) => v.id !== f);
u(j), t?.(j);
}, _ = (f) => f.type.startsWith("image/") ? "🖼️" : f.type === "application/pdf" ? "📄" : f.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || f.name.endsWith(".xlsx") ? "📊" : "📎", w = (f) => {
if (f === 0) return "0 Bytes";
const j = 1024, v = ["Bytes", "KB", "MB", "GB", "TB"], T = Math.floor(Math.log(f) / Math.log(j));
return parseFloat((f / Math.pow(j, T)).toFixed(2)) + " " + v[T];
}, d = l.length >= o;
return /* @__PURE__ */ s.jsxs("div", { className: "file-upload-container", children: [
/* @__PURE__ */ s.jsx(
"input",
{
type: "file",
ref: c,
multiple: !0,
accept: a,
onChange: x,
disabled: r || d,
style: { display: "none" }
}
),
/* @__PURE__ */ s.jsx(
"button",
{
type: "button",
className: "file-upload-button",
onClick: () => c.current?.click(),
disabled: r || d,
children: d ? "Max files reached" : "Select Files"
}
),
l.length > 0 && /* @__PURE__ */ s.jsx("div", { className: "file-list", children: l.map((f) => /* @__PURE__ */ s.jsxs("div", { className: "file-card", children: [
/* @__PURE__ */ s.jsx("span", { className: "file-icon", children: _(f) }),
f.preview && /* @__PURE__ */ s.jsx("img", { src: f.preview, alt: "preview", className: "file-preview" }),
/* @__PURE__ */ s.jsxs("div", { className: "file-info", children: [
/* @__PURE__ */ s.jsx("span", { className: "file-name", children: f.name }),
/* @__PURE__ */ s.jsx("span", { className: "file-size", children: w(f.size) })
] }),
/* @__PURE__ */ s.jsx(
"button",
{
type: "button",
className: "file-remove-button",
onClick: () => g(f.id),
disabled: r,
children: "×"
}
)
] }, f.id)) })
] });
}, Ne = O.memo(({ message: t, config: a }) => {
const o = t.sender === "user", i = o ? a?.userAvatar : a?.aiAvatar, r = o ? "User Avatar" : "AI Avatar", c = o ? "primary" : "secondary", l = (u) => new Date(u).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return /* @__PURE__ */ s.jsxs("div", { className: `message-row ${o ? "user" : ""}`, children: [
/* @__PURE__ */ s.jsx(
xe,
{
...i && { src: i },
alt: r,
size: "medium",
gradient: c,
children: !i && /* @__PURE__ */ s.jsx("i", { className: `fas fa-${o ? "user" : "robot"}` })
}
),
/* @__PURE__ */ s.jsxs("div", { className: "message-content", children: [
/* @__PURE__ */ s.jsx(
ve,
{
id: t.id,
content: t.content,
sender: t.sender,
type: t.type || "text",
...t.isLoading !== void 0 && { isLoading: t.isLoading },
...t.files && { files: t.files }
}
),
/* @__PURE__ */ s.jsx("div", { className: "message-time", children: l(t.timestamp) })
] })
] });
}), be = ({ messages: t, config: a, isLoadingMore: o }) => {
const i = Y(null), r = () => {
i.current?.scrollIntoView({ behavior: "smooth" });
};
return ee(() => {
r();
}, [t]), /* @__PURE__ */ s.jsxs("div", { className: "chat-messages", children: [
" ",
t.map((c) => /* @__PURE__ */ s.jsx(Ne, { message: c, ...a && { config: a } }, c.id)),
o && /* @__PURE__ */ s.jsx("div", { className: "loading-more", children: "Loading more messages..." }),
/* @__PURE__ */ s.jsx("div", { ref: i }),
" "
] });
}, ge = (t, a = "HH:mm") => fe(t).format(a), K = (t, a) => t.length <= a ? t : t.substring(0, a) + "...", Z = {
CONVERSATION_TITLE_MAX_LENGTH: 30,
MESSAGE_PREVIEW_MAX_LENGTH: 50
}, Ee = ({
conversation: t,
isActive: a,
onClick: o
// onDelete, // Removed
}) => {
const i = t.messages.length > 0 && t.messages[0] ? K(t.messages[0].content, Z.CONVERSATION_TITLE_MAX_LENGTH) : t.title, r = t.messages[t.messages.length - 1], c = t.messages.length > 0 && r ? K(r.content, Z.MESSAGE_PREVIEW_MAX_LENGTH) : "No messages yet.", l = ge(t.lastUpdated);
return /* @__PURE__ */ s.jsx(
"div",
{
className: `conversation-item ${a ? "active" : ""}`,
onClick: () => o(t.id),
children: /* @__PURE__ */ s.jsxs("div", { className: "conversation-info", children: [
/* @__PURE__ */ s.jsxs("div", { className: "conversation-title", children: [
/* @__PURE__ */ s.jsx("span", { children: i }),
/* @__PURE__ */ s.jsx("span", { className: "conversation-time", children: l })
] }),
/* @__PURE__ */ s.jsx("div", { className: "conversation-preview", children: c })
] })
}
);
}, Ae = ({
messages: t,
onSendMessage: a,
isAITyping: o = !1,
config: i,
conversationTitle: r
}) => {
const [c, l] = y(!1), u = async (x, g) => {
l(!0), await a(x, g), l(!1);
};
return /* @__PURE__ */ s.jsxs("div", { className: "chat-container", children: [
" ",
/* @__PURE__ */ s.jsx("div", { className: "chat-header", children: /* @__PURE__ */ s.jsx("div", { className: "chat-title", children: r || "新对话" }) }),
/* @__PURE__ */ s.jsx(be, { messages: t, ...i && { config: i } }),
/* @__PURE__ */ s.jsx(
je,
{
onSendMessage: u,
isSending: c || o,
placeholder: "输入消息..."
}
)
] });
}, ke = ({
conversations: t,
activeConversationId: a,
onSelectConversation: o,
onNewConversation: i
}) => {
const [r, c] = y(""), l = de(() => {
if (!r)
return t;
const u = r.toLowerCase();
return t.filter(
(x) => x.title.toLowerCase().includes(u) || x.messages.some((g) => g.content.toLowerCase().includes(u))
);
}, [t, r]);
return /* @__PURE__ */ s.jsxs("div", { className: "conversations-panel", children: [
" ",
/* @__PURE__ */ s.jsxs("div", { className: "panel-header", children: [
" ",
/* @__PURE__ */ s.jsxs("div", { className: "panel-title", children: [
/* @__PURE__ */ s.jsx("i", { className: "fas fa-comments" }),
"对话列表",
/* @__PURE__ */ s.jsx(z, { isCircle: !0, onClick: i, className: "new-conversation-btn", children: /* @__PURE__ */ s.jsx("i", { className: "fas fa-plus" }) })
] }),
/* @__PURE__ */ s.jsxs("div", { className: "search-container", children: [
/* @__PURE__ */ s.jsx("i", { className: "fas fa-search search-icon" }),
/* @__PURE__ */ s.jsx(
"input",
{
type: "text",
className: "search-input",
placeholder: "搜索对话...",
value: r,
onChange: (u) => c(u.target.value)
}
)
] })
] }),
/* @__PURE__ */ s.jsxs("div", { className: "conversation-list", children: [
" ",
l.length > 0 ? l.map((u) => /* @__PURE__ */ s.jsx(
Ee,
{
conversation: u,
isActive: u.id === a,
onClick: o
},
u.id
)) : /* @__PURE__ */ s.jsx("div", { className: "conversation-list__empty", children: "No conversations found." })
] })
] });
};
export {
Ae as AIChatRoom,
xe as Avatar,
z as Button,
je as ChatInputArea,
Ne as ChatMessage,
be as ChatMessagesList,
Ee as ConversationItem,
ke as ConversationList,
we as FileUpload,
ve as MessageBubble
};
//# sourceMappingURL=ai-chat-room-components.mjs.map