@xenide-io/the-old-ui-theme 0.9.5 → 0.9.7
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/dist/ai-message-markdown-AUEMCO7K.mjs +65 -0
- package/dist/suite.d.mts +5 -1
- package/dist/suite.d.ts +5 -1
- package/dist/suite.js +83 -25
- package/dist/suite.mjs +39 -24
- package/package.json +1 -1
- package/src/styles/suite-skin.css +158 -0
- package/src/suite/components/ai-message-markdown.test.tsx +40 -0
- package/src/suite/components/ai-message-markdown.tsx +75 -1
- package/src/suite/components/ai-panel.tsx +30 -17
- package/src/suite/components/suite-sidebar.tsx +1 -3
- package/src/suite/components/suite-user-menu.tsx +27 -1
- package/dist/ai-message-markdown-TSPYON2F.mjs +0 -22
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import "./chunk-FWCSY2DS.mjs";
|
|
3
|
+
|
|
4
|
+
// src/suite/components/ai-message-markdown.tsx
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
var WORD_STEP_MS = 18;
|
|
9
|
+
var MAX_DELAY_MS = 600;
|
|
10
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["code", "pre", "style", "script"]);
|
|
11
|
+
function rehypeStaggerWords() {
|
|
12
|
+
return (tree) => {
|
|
13
|
+
let wordIndex = 0;
|
|
14
|
+
const visit = (node) => {
|
|
15
|
+
if (!node.children) return;
|
|
16
|
+
if (node.tagName && SKIP_TAGS.has(node.tagName)) return;
|
|
17
|
+
const next = [];
|
|
18
|
+
for (const child of node.children) {
|
|
19
|
+
const isText = child.type === "text" && typeof child.value === "string" && /\S/.test(child.value);
|
|
20
|
+
if (!isText) {
|
|
21
|
+
visit(child);
|
|
22
|
+
next.push(child);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const part of child.value.split(/(\s+)/)) {
|
|
26
|
+
if (!part) continue;
|
|
27
|
+
if (/^\s+$/.test(part)) {
|
|
28
|
+
next.push({ type: "text", value: part });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const delay = Math.min(wordIndex * WORD_STEP_MS, MAX_DELAY_MS);
|
|
32
|
+
wordIndex += 1;
|
|
33
|
+
next.push({
|
|
34
|
+
type: "element",
|
|
35
|
+
tagName: "span",
|
|
36
|
+
properties: {
|
|
37
|
+
className: ["suite-word-in"],
|
|
38
|
+
style: `animation-delay:${delay}ms`
|
|
39
|
+
},
|
|
40
|
+
children: [{ type: "text", value: part }]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
node.children = next;
|
|
45
|
+
};
|
|
46
|
+
visit(tree);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function SuiteAiMarkdownMessage({
|
|
50
|
+
markdown,
|
|
51
|
+
animate = false
|
|
52
|
+
}) {
|
|
53
|
+
return /* @__PURE__ */ jsx("div", { className: "suite-ai-markdown", children: /* @__PURE__ */ jsx(
|
|
54
|
+
ReactMarkdown,
|
|
55
|
+
{
|
|
56
|
+
remarkPlugins: [remarkGfm],
|
|
57
|
+
rehypePlugins: animate ? [rehypeStaggerWords] : void 0,
|
|
58
|
+
components: { img: () => null },
|
|
59
|
+
children: markdown
|
|
60
|
+
}
|
|
61
|
+
) });
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
SuiteAiMarkdownMessage as default
|
|
65
|
+
};
|
package/dist/suite.d.mts
CHANGED
|
@@ -339,6 +339,10 @@ interface SuiteUserMenuProps {
|
|
|
339
339
|
onSignOut: () => void;
|
|
340
340
|
/** Fallback letter(s) when there is no avatar image. */
|
|
341
341
|
fallbackInitials?: string;
|
|
342
|
+
/** Second line under the name, e.g. the workspace subscription ("Plus"). */
|
|
343
|
+
subtitle?: string | null;
|
|
344
|
+
/** Show the name + subtitle beside the avatar, for the sidebar footer. */
|
|
345
|
+
showDetails?: boolean;
|
|
342
346
|
/** Show the old desktop sidebar sign-out action beside the avatar. */
|
|
343
347
|
showSignOutAction?: boolean;
|
|
344
348
|
dataTest?: string;
|
|
@@ -351,7 +355,7 @@ interface SuiteUserMenuProps {
|
|
|
351
355
|
* profile settings and sign out. Desktop sidebars can also expose the legacy
|
|
352
356
|
* adjacent sign-out action without adding it to mobile chrome.
|
|
353
357
|
*/
|
|
354
|
-
declare function SuiteUserMenu({ name, email, image, settingsHref, onSignOut, fallbackInitials, showSignOutAction, dataTest, triggerId, triggerDataTest, className, }: SuiteUserMenuProps): react.JSX.Element;
|
|
358
|
+
declare function SuiteUserMenu({ name, email, image, settingsHref, onSignOut, fallbackInitials, subtitle, showDetails, showSignOutAction, dataTest, triggerId, triggerDataTest, className, }: SuiteUserMenuProps): react.JSX.Element;
|
|
355
359
|
|
|
356
360
|
/** Any icon component that accepts sizing/stroke props (Lucide, theme icons). */
|
|
357
361
|
type SuiteNavIcon = ComponentType<{
|
package/dist/suite.d.ts
CHANGED
|
@@ -339,6 +339,10 @@ interface SuiteUserMenuProps {
|
|
|
339
339
|
onSignOut: () => void;
|
|
340
340
|
/** Fallback letter(s) when there is no avatar image. */
|
|
341
341
|
fallbackInitials?: string;
|
|
342
|
+
/** Second line under the name, e.g. the workspace subscription ("Plus"). */
|
|
343
|
+
subtitle?: string | null;
|
|
344
|
+
/** Show the name + subtitle beside the avatar, for the sidebar footer. */
|
|
345
|
+
showDetails?: boolean;
|
|
342
346
|
/** Show the old desktop sidebar sign-out action beside the avatar. */
|
|
343
347
|
showSignOutAction?: boolean;
|
|
344
348
|
dataTest?: string;
|
|
@@ -351,7 +355,7 @@ interface SuiteUserMenuProps {
|
|
|
351
355
|
* profile settings and sign out. Desktop sidebars can also expose the legacy
|
|
352
356
|
* adjacent sign-out action without adding it to mobile chrome.
|
|
353
357
|
*/
|
|
354
|
-
declare function SuiteUserMenu({ name, email, image, settingsHref, onSignOut, fallbackInitials, showSignOutAction, dataTest, triggerId, triggerDataTest, className, }: SuiteUserMenuProps): react.JSX.Element;
|
|
358
|
+
declare function SuiteUserMenu({ name, email, image, settingsHref, onSignOut, fallbackInitials, subtitle, showDetails, showSignOutAction, dataTest, triggerId, triggerDataTest, className, }: SuiteUserMenuProps): react.JSX.Element;
|
|
355
359
|
|
|
356
360
|
/** Any icon component that accepts sizing/stroke props (Lucide, theme icons). */
|
|
357
361
|
type SuiteNavIcon = ComponentType<{
|
package/dist/suite.js
CHANGED
|
@@ -64,19 +64,59 @@ var ai_message_markdown_exports = {};
|
|
|
64
64
|
__export(ai_message_markdown_exports, {
|
|
65
65
|
default: () => SuiteAiMarkdownMessage
|
|
66
66
|
});
|
|
67
|
+
function rehypeStaggerWords() {
|
|
68
|
+
return (tree) => {
|
|
69
|
+
let wordIndex = 0;
|
|
70
|
+
const visit = (node) => {
|
|
71
|
+
if (!node.children) return;
|
|
72
|
+
if (node.tagName && SKIP_TAGS.has(node.tagName)) return;
|
|
73
|
+
const next = [];
|
|
74
|
+
for (const child of node.children) {
|
|
75
|
+
const isText = child.type === "text" && typeof child.value === "string" && /\S/.test(child.value);
|
|
76
|
+
if (!isText) {
|
|
77
|
+
visit(child);
|
|
78
|
+
next.push(child);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const part of child.value.split(/(\s+)/)) {
|
|
82
|
+
if (!part) continue;
|
|
83
|
+
if (/^\s+$/.test(part)) {
|
|
84
|
+
next.push({ type: "text", value: part });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const delay = Math.min(wordIndex * WORD_STEP_MS, MAX_DELAY_MS);
|
|
88
|
+
wordIndex += 1;
|
|
89
|
+
next.push({
|
|
90
|
+
type: "element",
|
|
91
|
+
tagName: "span",
|
|
92
|
+
properties: {
|
|
93
|
+
className: ["suite-word-in"],
|
|
94
|
+
style: `animation-delay:${delay}ms`
|
|
95
|
+
},
|
|
96
|
+
children: [{ type: "text", value: part }]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
node.children = next;
|
|
101
|
+
};
|
|
102
|
+
visit(tree);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
67
105
|
function SuiteAiMarkdownMessage({
|
|
68
|
-
markdown
|
|
106
|
+
markdown,
|
|
107
|
+
animate = false
|
|
69
108
|
}) {
|
|
70
109
|
return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "suite-ai-markdown", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
71
110
|
import_react_markdown.default,
|
|
72
111
|
{
|
|
73
112
|
remarkPlugins: [import_remark_gfm.default],
|
|
113
|
+
rehypePlugins: animate ? [rehypeStaggerWords] : void 0,
|
|
74
114
|
components: { img: () => null },
|
|
75
115
|
children: markdown
|
|
76
116
|
}
|
|
77
117
|
) });
|
|
78
118
|
}
|
|
79
|
-
var import_react_markdown, import_remark_gfm, import_jsx_runtime23;
|
|
119
|
+
var import_react_markdown, import_remark_gfm, import_jsx_runtime23, WORD_STEP_MS, MAX_DELAY_MS, SKIP_TAGS;
|
|
80
120
|
var init_ai_message_markdown = __esm({
|
|
81
121
|
"src/suite/components/ai-message-markdown.tsx"() {
|
|
82
122
|
"use strict";
|
|
@@ -84,6 +124,9 @@ var init_ai_message_markdown = __esm({
|
|
|
84
124
|
import_react_markdown = __toESM(require("react-markdown"));
|
|
85
125
|
import_remark_gfm = __toESM(require("remark-gfm"));
|
|
86
126
|
import_jsx_runtime23 = require("react/jsx-runtime");
|
|
127
|
+
WORD_STEP_MS = 18;
|
|
128
|
+
MAX_DELAY_MS = 600;
|
|
129
|
+
SKIP_TAGS = /* @__PURE__ */ new Set(["code", "pre", "style", "script"]);
|
|
87
130
|
}
|
|
88
131
|
});
|
|
89
132
|
|
|
@@ -2741,6 +2784,8 @@ function SuiteUserMenu({
|
|
|
2741
2784
|
settingsHref,
|
|
2742
2785
|
onSignOut,
|
|
2743
2786
|
fallbackInitials,
|
|
2787
|
+
subtitle,
|
|
2788
|
+
showDetails = false,
|
|
2744
2789
|
showSignOutAction = false,
|
|
2745
2790
|
dataTest = "suite-user-menu",
|
|
2746
2791
|
triggerId,
|
|
@@ -2770,10 +2815,17 @@ function SuiteUserMenu({
|
|
|
2770
2815
|
children: initials
|
|
2771
2816
|
}
|
|
2772
2817
|
) }) });
|
|
2818
|
+
const trigger = showDetails ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
2819
|
+
avatar,
|
|
2820
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "flex min-w-0 flex-col text-left", children: [
|
|
2821
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-sm font-medium text-ph-ink", children: name || email || "Account" }),
|
|
2822
|
+
subtitle ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "truncate text-xs text-ph-mutedtext", children: subtitle }) : null
|
|
2823
|
+
] })
|
|
2824
|
+
] }) : avatar;
|
|
2773
2825
|
const accountMenu = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
2774
2826
|
DropdownMenu,
|
|
2775
2827
|
{
|
|
2776
|
-
trigger
|
|
2828
|
+
trigger,
|
|
2777
2829
|
triggerId: triggerId != null ? triggerId : `${dataTest}-trigger`,
|
|
2778
2830
|
triggerDataTest: triggerDataTest != null ? triggerDataTest : `${dataTest}-trigger`,
|
|
2779
2831
|
"aria-label": "Account menu",
|
|
@@ -2784,6 +2836,7 @@ function SuiteUserMenu({
|
|
|
2784
2836
|
modal: false,
|
|
2785
2837
|
className: cn(
|
|
2786
2838
|
"[&_.ph-dropdown-trigger]:rounded-full",
|
|
2839
|
+
showDetails && "min-w-0 flex-1 [&_.ph-dropdown-trigger]:w-full [&_.ph-dropdown-trigger]:justify-start [&_.ph-dropdown-trigger]:rounded-lg",
|
|
2787
2840
|
showSignOutAction ? void 0 : className
|
|
2788
2841
|
),
|
|
2789
2842
|
"data-test": dataTest,
|
|
@@ -3541,6 +3594,11 @@ function SuiteAiPanel({
|
|
|
3541
3594
|
var _a;
|
|
3542
3595
|
if (open && !hydrating) (_a = textareaRef.current) == null ? void 0 : _a.focus();
|
|
3543
3596
|
}, [open, hydrating]);
|
|
3597
|
+
(0, import_react12.useEffect)(() => {
|
|
3598
|
+
if (!open) return;
|
|
3599
|
+
void Promise.resolve().then(() => (init_ai_message_markdown(), ai_message_markdown_exports)).catch(() => {
|
|
3600
|
+
});
|
|
3601
|
+
}, [open]);
|
|
3544
3602
|
(0, import_react12.useLayoutEffect)(() => {
|
|
3545
3603
|
const el = textareaRef.current;
|
|
3546
3604
|
if (!el) return;
|
|
@@ -3665,6 +3723,10 @@ function SuiteAiPanel({
|
|
|
3665
3723
|
else window.location.assign(new URL(openPath, href).toString());
|
|
3666
3724
|
}
|
|
3667
3725
|
if (!open) return null;
|
|
3726
|
+
const lastAssistantIndex = messages.reduce(
|
|
3727
|
+
(found, message, index) => message.role === "assistant" ? index : found,
|
|
3728
|
+
-1
|
|
3729
|
+
);
|
|
3668
3730
|
const Icon = BrandIcon != null ? BrandIcon : import_iconoir_react8.Sparks;
|
|
3669
3731
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
3670
3732
|
"aside",
|
|
@@ -3733,7 +3795,7 @@ function SuiteAiPanel({
|
|
|
3733
3795
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3734
3796
|
"div",
|
|
3735
3797
|
{
|
|
3736
|
-
className: "flex justify-end",
|
|
3798
|
+
className: "suite-msg-in suite-msg-in--user flex justify-end",
|
|
3737
3799
|
"data-test": "ask-ai-user",
|
|
3738
3800
|
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
|
|
3739
3801
|
},
|
|
@@ -3749,30 +3811,28 @@ function SuiteAiPanel({
|
|
|
3749
3811
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3750
3812
|
"span",
|
|
3751
3813
|
{
|
|
3752
|
-
className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
3814
|
+
className: "suite-msg-avatar mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
3753
3815
|
"aria-hidden": true,
|
|
3754
3816
|
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Icon, { className: "h-3.5 w-3.5" })
|
|
3755
3817
|
}
|
|
3756
3818
|
),
|
|
3757
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "min-w-0 max-w-[85%] space-y-2", children: [
|
|
3819
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "suite-msg-bubble min-w-0 max-w-[85%] space-y-2", children: [
|
|
3758
3820
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3759
3821
|
import_react12.Suspense,
|
|
3760
3822
|
{
|
|
3761
|
-
fallback:
|
|
3762
|
-
|
|
3823
|
+
fallback: (
|
|
3824
|
+
// The chunk is pre-warmed above, so this only shows on a
|
|
3825
|
+
// cold cache. Keep it invisible — a skeleton here reads
|
|
3826
|
+
// as if it were part of the answer.
|
|
3827
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", role: "status", children: "Rendering response\u2026" })
|
|
3828
|
+
),
|
|
3829
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3830
|
+
SuiteAiMarkdownMessage2,
|
|
3763
3831
|
{
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
"aria-label": "Rendering response",
|
|
3767
|
-
children: [
|
|
3768
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", children: "Rendering response\u2026" }),
|
|
3769
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "h-3 w-11/12 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" }),
|
|
3770
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "h-3 w-full rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" }),
|
|
3771
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "h-3 w-3/5 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" })
|
|
3772
|
-
]
|
|
3832
|
+
markdown: message.content,
|
|
3833
|
+
animate: index === lastAssistantIndex
|
|
3773
3834
|
}
|
|
3774
|
-
)
|
|
3775
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SuiteAiMarkdownMessage2, { markdown: message.content })
|
|
3835
|
+
)
|
|
3776
3836
|
}
|
|
3777
3837
|
) }),
|
|
3778
3838
|
((_a = message.actions) != null ? _a : []).map((action) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
@@ -3836,9 +3896,9 @@ function SuiteAiPanel({
|
|
|
3836
3896
|
waiting ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-end gap-2", "aria-hidden": true, children: [
|
|
3837
3897
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Icon, { className: "h-3.5 w-3.5" }) }),
|
|
3838
3898
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-center gap-1.5 rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2.5", children: [
|
|
3839
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
3840
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
3841
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
3899
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "suite-think-dot h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
3900
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "suite-think-dot suite-think-dot--2 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
3901
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "suite-think-dot suite-think-dot--3 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" })
|
|
3842
3902
|
] })
|
|
3843
3903
|
] }) : null,
|
|
3844
3904
|
waiting ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", "aria-live": "polite", children: "Assistant is responding\u2026" }) : null,
|
|
@@ -4730,9 +4790,7 @@ function SuiteSidebar({
|
|
|
4730
4790
|
className: cn(
|
|
4731
4791
|
"shrink-0 border-t border-ph-border",
|
|
4732
4792
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
4733
|
-
|
|
4734
|
-
// trigger, so even padding read top-heavy once the avatar shrank.
|
|
4735
|
-
collapsed ? "px-1.5 pt-1.5 pb-1" : "px-3 pt-3 pb-2"
|
|
4793
|
+
collapsed ? "p-1.5" : "p-3"
|
|
4736
4794
|
),
|
|
4737
4795
|
children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
|
|
4738
4796
|
"div",
|
package/dist/suite.mjs
CHANGED
|
@@ -1897,6 +1897,8 @@ function SuiteUserMenu({
|
|
|
1897
1897
|
settingsHref,
|
|
1898
1898
|
onSignOut,
|
|
1899
1899
|
fallbackInitials,
|
|
1900
|
+
subtitle,
|
|
1901
|
+
showDetails = false,
|
|
1900
1902
|
showSignOutAction = false,
|
|
1901
1903
|
dataTest = "suite-user-menu",
|
|
1902
1904
|
triggerId,
|
|
@@ -1926,10 +1928,17 @@ function SuiteUserMenu({
|
|
|
1926
1928
|
children: initials
|
|
1927
1929
|
}
|
|
1928
1930
|
) }) });
|
|
1931
|
+
const trigger = showDetails ? /* @__PURE__ */ jsxs11("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
1932
|
+
avatar,
|
|
1933
|
+
/* @__PURE__ */ jsxs11("span", { className: "flex min-w-0 flex-col text-left", children: [
|
|
1934
|
+
/* @__PURE__ */ jsx13("span", { className: "truncate text-sm font-medium text-ph-ink", children: name || email || "Account" }),
|
|
1935
|
+
subtitle ? /* @__PURE__ */ jsx13("span", { className: "truncate text-xs text-ph-mutedtext", children: subtitle }) : null
|
|
1936
|
+
] })
|
|
1937
|
+
] }) : avatar;
|
|
1929
1938
|
const accountMenu = /* @__PURE__ */ jsxs11(
|
|
1930
1939
|
DropdownMenu,
|
|
1931
1940
|
{
|
|
1932
|
-
trigger
|
|
1941
|
+
trigger,
|
|
1933
1942
|
triggerId: triggerId != null ? triggerId : `${dataTest}-trigger`,
|
|
1934
1943
|
triggerDataTest: triggerDataTest != null ? triggerDataTest : `${dataTest}-trigger`,
|
|
1935
1944
|
"aria-label": "Account menu",
|
|
@@ -1940,6 +1949,7 @@ function SuiteUserMenu({
|
|
|
1940
1949
|
modal: false,
|
|
1941
1950
|
className: cn2(
|
|
1942
1951
|
"[&_.ph-dropdown-trigger]:rounded-full",
|
|
1952
|
+
showDetails && "min-w-0 flex-1 [&_.ph-dropdown-trigger]:w-full [&_.ph-dropdown-trigger]:justify-start [&_.ph-dropdown-trigger]:rounded-lg",
|
|
1943
1953
|
showSignOutAction ? void 0 : className
|
|
1944
1954
|
),
|
|
1945
1955
|
"data-test": dataTest,
|
|
@@ -2386,7 +2396,7 @@ import {
|
|
|
2386
2396
|
WarningTriangle
|
|
2387
2397
|
} from "iconoir-react";
|
|
2388
2398
|
import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2389
|
-
var SuiteAiMarkdownMessage = lazy(() => import("./ai-message-markdown-
|
|
2399
|
+
var SuiteAiMarkdownMessage = lazy(() => import("./ai-message-markdown-AUEMCO7K.mjs"));
|
|
2390
2400
|
var SUITE_OPEN_ASK_AI_EVENT = "shellstack:open-ask-ai";
|
|
2391
2401
|
var SUITE_ASK_AI_OPEN_KEY = "shellstack:shelly-open";
|
|
2392
2402
|
var SUITE_ASK_AI_OPEN_QUERY = "shelly";
|
|
@@ -2561,6 +2571,11 @@ function SuiteAiPanel({
|
|
|
2561
2571
|
var _a;
|
|
2562
2572
|
if (open && !hydrating) (_a = textareaRef.current) == null ? void 0 : _a.focus();
|
|
2563
2573
|
}, [open, hydrating]);
|
|
2574
|
+
useEffect7(() => {
|
|
2575
|
+
if (!open) return;
|
|
2576
|
+
void import("./ai-message-markdown-AUEMCO7K.mjs").catch(() => {
|
|
2577
|
+
});
|
|
2578
|
+
}, [open]);
|
|
2564
2579
|
useLayoutEffect(() => {
|
|
2565
2580
|
const el = textareaRef.current;
|
|
2566
2581
|
if (!el) return;
|
|
@@ -2685,6 +2700,10 @@ function SuiteAiPanel({
|
|
|
2685
2700
|
else window.location.assign(new URL(openPath, href).toString());
|
|
2686
2701
|
}
|
|
2687
2702
|
if (!open) return null;
|
|
2703
|
+
const lastAssistantIndex = messages.reduce(
|
|
2704
|
+
(found, message, index) => message.role === "assistant" ? index : found,
|
|
2705
|
+
-1
|
|
2706
|
+
);
|
|
2688
2707
|
const Icon = BrandIcon != null ? BrandIcon : Sparks;
|
|
2689
2708
|
return /* @__PURE__ */ jsxs14(
|
|
2690
2709
|
"aside",
|
|
@@ -2753,7 +2772,7 @@ function SuiteAiPanel({
|
|
|
2753
2772
|
return /* @__PURE__ */ jsx17(
|
|
2754
2773
|
"div",
|
|
2755
2774
|
{
|
|
2756
|
-
className: "flex justify-end",
|
|
2775
|
+
className: "suite-msg-in suite-msg-in--user flex justify-end",
|
|
2757
2776
|
"data-test": "ask-ai-user",
|
|
2758
2777
|
children: /* @__PURE__ */ jsx17("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
|
|
2759
2778
|
},
|
|
@@ -2769,30 +2788,28 @@ function SuiteAiPanel({
|
|
|
2769
2788
|
/* @__PURE__ */ jsx17(
|
|
2770
2789
|
"span",
|
|
2771
2790
|
{
|
|
2772
|
-
className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
2791
|
+
className: "suite-msg-avatar mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
2773
2792
|
"aria-hidden": true,
|
|
2774
2793
|
children: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5" })
|
|
2775
2794
|
}
|
|
2776
2795
|
),
|
|
2777
|
-
/* @__PURE__ */ jsxs14("div", { className: "min-w-0 max-w-[85%] space-y-2", children: [
|
|
2796
|
+
/* @__PURE__ */ jsxs14("div", { className: "suite-msg-bubble min-w-0 max-w-[85%] space-y-2", children: [
|
|
2778
2797
|
/* @__PURE__ */ jsx17("div", { className: "rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink", children: /* @__PURE__ */ jsx17(
|
|
2779
2798
|
Suspense,
|
|
2780
2799
|
{
|
|
2781
|
-
fallback:
|
|
2782
|
-
|
|
2800
|
+
fallback: (
|
|
2801
|
+
// The chunk is pre-warmed above, so this only shows on a
|
|
2802
|
+
// cold cache. Keep it invisible — a skeleton here reads
|
|
2803
|
+
// as if it were part of the answer.
|
|
2804
|
+
/* @__PURE__ */ jsx17("span", { className: "sr-only", role: "status", children: "Rendering response\u2026" })
|
|
2805
|
+
),
|
|
2806
|
+
children: /* @__PURE__ */ jsx17(
|
|
2807
|
+
SuiteAiMarkdownMessage,
|
|
2783
2808
|
{
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
"aria-label": "Rendering response",
|
|
2787
|
-
children: [
|
|
2788
|
-
/* @__PURE__ */ jsx17("span", { className: "sr-only", children: "Rendering response\u2026" }),
|
|
2789
|
-
/* @__PURE__ */ jsx17("div", { className: "h-3 w-11/12 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" }),
|
|
2790
|
-
/* @__PURE__ */ jsx17("div", { className: "h-3 w-full rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" }),
|
|
2791
|
-
/* @__PURE__ */ jsx17("div", { className: "h-3 w-3/5 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" })
|
|
2792
|
-
]
|
|
2809
|
+
markdown: message.content,
|
|
2810
|
+
animate: index === lastAssistantIndex
|
|
2793
2811
|
}
|
|
2794
|
-
)
|
|
2795
|
-
children: /* @__PURE__ */ jsx17(SuiteAiMarkdownMessage, { markdown: message.content })
|
|
2812
|
+
)
|
|
2796
2813
|
}
|
|
2797
2814
|
) }),
|
|
2798
2815
|
((_a = message.actions) != null ? _a : []).map((action) => /* @__PURE__ */ jsxs14(
|
|
@@ -2856,9 +2873,9 @@ function SuiteAiPanel({
|
|
|
2856
2873
|
waiting ? /* @__PURE__ */ jsxs14("div", { className: "flex items-end gap-2", "aria-hidden": true, children: [
|
|
2857
2874
|
/* @__PURE__ */ jsx17("span", { className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand", children: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5" }) }),
|
|
2858
2875
|
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1.5 rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2.5", children: [
|
|
2859
|
-
/* @__PURE__ */ jsx17("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
2860
|
-
/* @__PURE__ */ jsx17("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
2861
|
-
/* @__PURE__ */ jsx17("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
2876
|
+
/* @__PURE__ */ jsx17("span", { className: "suite-think-dot h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
2877
|
+
/* @__PURE__ */ jsx17("span", { className: "suite-think-dot suite-think-dot--2 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
2878
|
+
/* @__PURE__ */ jsx17("span", { className: "suite-think-dot suite-think-dot--3 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" })
|
|
2862
2879
|
] })
|
|
2863
2880
|
] }) : null,
|
|
2864
2881
|
waiting ? /* @__PURE__ */ jsx17("span", { className: "sr-only", "aria-live": "polite", children: "Assistant is responding\u2026" }) : null,
|
|
@@ -3754,9 +3771,7 @@ function SuiteSidebar({
|
|
|
3754
3771
|
className: cn2(
|
|
3755
3772
|
"shrink-0 border-t border-ph-border",
|
|
3756
3773
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
3757
|
-
|
|
3758
|
-
// trigger, so even padding read top-heavy once the avatar shrank.
|
|
3759
|
-
collapsed ? "px-1.5 pt-1.5 pb-1" : "px-3 pt-3 pb-2"
|
|
3774
|
+
collapsed ? "p-1.5" : "p-3"
|
|
3760
3775
|
),
|
|
3761
3776
|
children: /* @__PURE__ */ jsxs19(
|
|
3762
3777
|
"div",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|
|
@@ -797,6 +797,164 @@ button:hover > .ph-card {
|
|
|
797
797
|
border-top: 1px solid var(--ph-border-subtle);
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
+
/* ── Ask AI conversation motion ─────────────────────────────────────────
|
|
801
|
+
Replies arrive whole (the suite chat is polled, not streamed), so the
|
|
802
|
+
motion carries the "alive" signal instead: a soft one-shot entrance per
|
|
803
|
+
message, a breathing thinking indicator, and a shimmer for the skeleton.
|
|
804
|
+
Only opacity + transform animate — height/margin would fight the
|
|
805
|
+
scroller. All of it collapses under prefers-reduced-motion. */
|
|
806
|
+
@keyframes suite-msg-in {
|
|
807
|
+
from {
|
|
808
|
+
opacity: 0;
|
|
809
|
+
transform: translateY(8px) scale(0.97);
|
|
810
|
+
filter: blur(4px);
|
|
811
|
+
}
|
|
812
|
+
60% {
|
|
813
|
+
opacity: 1;
|
|
814
|
+
}
|
|
815
|
+
to {
|
|
816
|
+
opacity: 1;
|
|
817
|
+
transform: none;
|
|
818
|
+
filter: blur(0);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
@keyframes suite-msg-in-user {
|
|
822
|
+
/* The user's own turn grows out of the composer corner, per the chat
|
|
823
|
+
scroller guidance, so sending feels anchored to where they typed. */
|
|
824
|
+
from {
|
|
825
|
+
opacity: 0;
|
|
826
|
+
transform: translateY(12px) scale(0.96);
|
|
827
|
+
filter: blur(4px);
|
|
828
|
+
}
|
|
829
|
+
60% {
|
|
830
|
+
opacity: 1;
|
|
831
|
+
}
|
|
832
|
+
to {
|
|
833
|
+
opacity: 1;
|
|
834
|
+
transform: none;
|
|
835
|
+
filter: blur(0);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
.suite-msg-in {
|
|
839
|
+
/* Slight overshoot rather than a flat ease-out: the bubble reads as
|
|
840
|
+
landing rather than fading up. Origin sits at the tail corner so it
|
|
841
|
+
appears to grow from the avatar (assistant) / composer (user). */
|
|
842
|
+
animation: suite-msg-in 280ms cubic-bezier(0.34, 1.4, 0.64, 1) both;
|
|
843
|
+
transform-origin: 0 100%;
|
|
844
|
+
}
|
|
845
|
+
.suite-msg-in--user {
|
|
846
|
+
animation-name: suite-msg-in-user;
|
|
847
|
+
animation-duration: 240ms;
|
|
848
|
+
transform-origin: 100% 100%;
|
|
849
|
+
}
|
|
850
|
+
/* Avatar lands first, bubble follows — a small layer separation. The bubble
|
|
851
|
+
only settles (no blur) because the words themselves carry the reveal. */
|
|
852
|
+
@keyframes suite-msg-settle {
|
|
853
|
+
from {
|
|
854
|
+
opacity: 0;
|
|
855
|
+
transform: translateY(4px);
|
|
856
|
+
}
|
|
857
|
+
to {
|
|
858
|
+
opacity: 1;
|
|
859
|
+
transform: none;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
.suite-msg-avatar {
|
|
863
|
+
animation: suite-msg-in 260ms cubic-bezier(0.34, 1.4, 0.64, 1) both;
|
|
864
|
+
}
|
|
865
|
+
.suite-msg-bubble {
|
|
866
|
+
animation: suite-msg-settle 200ms ease-out both;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* ── Answer reveal ──────────────────────────────────────────────────────
|
|
870
|
+
Replies land in one payload (the suite chat is polled), so the words
|
|
871
|
+
themselves are staggered to read as if they were being written. Delay is
|
|
872
|
+
applied per word inline by the rehype plugin in ai-message-markdown.tsx
|
|
873
|
+
and capped so a long answer still finishes settling in well under a
|
|
874
|
+
second. `both` fill keeps each word hidden until its turn. */
|
|
875
|
+
@keyframes suite-word-in {
|
|
876
|
+
from {
|
|
877
|
+
opacity: 0;
|
|
878
|
+
filter: blur(2px);
|
|
879
|
+
}
|
|
880
|
+
to {
|
|
881
|
+
opacity: 1;
|
|
882
|
+
filter: blur(0);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
.suite-word-in {
|
|
886
|
+
animation: suite-word-in 240ms ease-out both;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@keyframes suite-think-breathe {
|
|
890
|
+
0%,
|
|
891
|
+
100% {
|
|
892
|
+
opacity: 0.3;
|
|
893
|
+
transform: scale(0.8);
|
|
894
|
+
}
|
|
895
|
+
50% {
|
|
896
|
+
opacity: 1;
|
|
897
|
+
transform: scale(1);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
.suite-think-dot {
|
|
901
|
+
animation: suite-think-breathe 1.4s ease-in-out infinite;
|
|
902
|
+
}
|
|
903
|
+
.suite-think-dot--2 {
|
|
904
|
+
animation-delay: 0.16s;
|
|
905
|
+
}
|
|
906
|
+
.suite-think-dot--3 {
|
|
907
|
+
animation-delay: 0.32s;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/* Sweep a translateX band rather than background-position: the latter
|
|
911
|
+
repaints every frame, the former stays on the compositor. */
|
|
912
|
+
@keyframes suite-shimmer {
|
|
913
|
+
from {
|
|
914
|
+
transform: translateX(-100%);
|
|
915
|
+
}
|
|
916
|
+
to {
|
|
917
|
+
transform: translateX(200%);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
.suite-shimmer {
|
|
921
|
+
position: relative;
|
|
922
|
+
overflow: hidden;
|
|
923
|
+
}
|
|
924
|
+
.suite-shimmer::after {
|
|
925
|
+
content: "";
|
|
926
|
+
position: absolute;
|
|
927
|
+
inset: 0 auto 0 0;
|
|
928
|
+
width: 60%;
|
|
929
|
+
background-image: linear-gradient(
|
|
930
|
+
90deg,
|
|
931
|
+
transparent,
|
|
932
|
+
color-mix(in oklab, var(--ph-text-primary) 9%, transparent),
|
|
933
|
+
transparent
|
|
934
|
+
);
|
|
935
|
+
animation: suite-shimmer 1.6s linear infinite;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
@media (prefers-reduced-motion: reduce) {
|
|
939
|
+
.suite-msg-in,
|
|
940
|
+
.suite-msg-in--user,
|
|
941
|
+
.suite-msg-avatar,
|
|
942
|
+
.suite-msg-bubble,
|
|
943
|
+
.suite-word-in {
|
|
944
|
+
animation: none;
|
|
945
|
+
filter: none;
|
|
946
|
+
}
|
|
947
|
+
.suite-think-dot {
|
|
948
|
+
animation: none;
|
|
949
|
+
opacity: 0.7;
|
|
950
|
+
transform: none;
|
|
951
|
+
}
|
|
952
|
+
.suite-shimmer::after {
|
|
953
|
+
animation: none;
|
|
954
|
+
opacity: 0;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
800
958
|
/* ── Auth / OAuth / launch handoff ──────────────────────────────────────
|
|
801
959
|
One calm opacity fade — no stacked entrance reveals on bridge pages. */
|
|
802
960
|
.suite-auth-handoff {
|
|
@@ -38,4 +38,44 @@ describe("SuiteAiMarkdownMessage", () => {
|
|
|
38
38
|
expect(container.querySelector('[contenteditable]')).toBeNull();
|
|
39
39
|
expect(container.querySelector("img")).toBeNull();
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
it("leaves words untouched unless animate is set", () => {
|
|
43
|
+
const { container } = render(
|
|
44
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
45
|
+
<SuiteAiMarkdownMessage markdown={"Hello there world"} />
|
|
46
|
+
</SuiteThemeProvider>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(container.querySelectorAll(".suite-word-in")).toHaveLength(0);
|
|
50
|
+
expect(container.textContent).toContain("Hello there world");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("staggers each word when animate is set", () => {
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
56
|
+
<SuiteAiMarkdownMessage markdown={"Hello there world"} animate />
|
|
57
|
+
</SuiteThemeProvider>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const words = container.querySelectorAll<HTMLElement>(".suite-word-in");
|
|
61
|
+
expect(words).toHaveLength(3);
|
|
62
|
+
expect(words[0].textContent).toBe("Hello");
|
|
63
|
+
expect(words[0].style.animationDelay).toBe("0ms");
|
|
64
|
+
expect(words[1].style.animationDelay).toBe("18ms");
|
|
65
|
+
expect(words[2].style.animationDelay).toBe("36ms");
|
|
66
|
+
// Copy still yields the original prose.
|
|
67
|
+
expect(container.textContent).toBe("Hello there world");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not stagger code spans", () => {
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
73
|
+
<SuiteAiMarkdownMessage markdown={"Run `npm run dev` now"} animate />
|
|
74
|
+
</SuiteThemeProvider>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const code = container.querySelector("code");
|
|
78
|
+
expect(code).toBeTruthy();
|
|
79
|
+
expect(code?.querySelector(".suite-word-in")).toBeNull();
|
|
80
|
+
});
|
|
41
81
|
});
|
|
@@ -1,17 +1,91 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import ReactMarkdown from "react-markdown";
|
|
3
|
+
import ReactMarkdown, { type Options } from "react-markdown";
|
|
4
4
|
import remarkGfm from "remark-gfm";
|
|
5
5
|
|
|
6
|
+
/** Per-word stagger step, and the ceiling so a long reply still settles fast. */
|
|
7
|
+
const WORD_STEP_MS = 18;
|
|
8
|
+
const MAX_DELAY_MS = 600;
|
|
9
|
+
|
|
10
|
+
/** Tags whose text is left alone — animating code word by word reads badly. */
|
|
11
|
+
const SKIP_TAGS = new Set(["code", "pre", "style", "script"]);
|
|
12
|
+
|
|
13
|
+
interface HastNode {
|
|
14
|
+
type: string;
|
|
15
|
+
tagName?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
properties?: Record<string, unknown>;
|
|
18
|
+
children?: HastNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wrap each word of prose in a span that fades in on a stagger, so a reply
|
|
23
|
+
* that arrived in one payload reads as if it were being written.
|
|
24
|
+
*
|
|
25
|
+
* Delay lives inline on the span, so React re-rendering the same tree reuses
|
|
26
|
+
* the same elements and the animation does not replay.
|
|
27
|
+
*/
|
|
28
|
+
function rehypeStaggerWords() {
|
|
29
|
+
return (tree: HastNode) => {
|
|
30
|
+
let wordIndex = 0;
|
|
31
|
+
|
|
32
|
+
const visit = (node: HastNode) => {
|
|
33
|
+
if (!node.children) return;
|
|
34
|
+
if (node.tagName && SKIP_TAGS.has(node.tagName)) return;
|
|
35
|
+
|
|
36
|
+
const next: HastNode[] = [];
|
|
37
|
+
for (const child of node.children) {
|
|
38
|
+
const isText =
|
|
39
|
+
child.type === "text" &&
|
|
40
|
+
typeof child.value === "string" &&
|
|
41
|
+
/\S/.test(child.value);
|
|
42
|
+
|
|
43
|
+
if (!isText) {
|
|
44
|
+
visit(child);
|
|
45
|
+
next.push(child);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const part of child.value!.split(/(\s+)/)) {
|
|
50
|
+
if (!part) continue;
|
|
51
|
+
if (/^\s+$/.test(part)) {
|
|
52
|
+
next.push({ type: "text", value: part });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const delay = Math.min(wordIndex * WORD_STEP_MS, MAX_DELAY_MS);
|
|
56
|
+
wordIndex += 1;
|
|
57
|
+
next.push({
|
|
58
|
+
type: "element",
|
|
59
|
+
tagName: "span",
|
|
60
|
+
properties: {
|
|
61
|
+
className: ["suite-word-in"],
|
|
62
|
+
style: `animation-delay:${delay}ms`,
|
|
63
|
+
},
|
|
64
|
+
children: [{ type: "text", value: part }],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
node.children = next;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
visit(tree);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
6
75
|
export default function SuiteAiMarkdownMessage({
|
|
7
76
|
markdown,
|
|
77
|
+
animate = false,
|
|
8
78
|
}: {
|
|
9
79
|
markdown: string;
|
|
80
|
+
animate?: boolean;
|
|
10
81
|
}) {
|
|
11
82
|
return (
|
|
12
83
|
<div className="suite-ai-markdown">
|
|
13
84
|
<ReactMarkdown
|
|
14
85
|
remarkPlugins={[remarkGfm]}
|
|
86
|
+
rehypePlugins={
|
|
87
|
+
(animate ? [rehypeStaggerWords] : undefined) as Options["rehypePlugins"]
|
|
88
|
+
}
|
|
15
89
|
components={{ img: () => null }}
|
|
16
90
|
>
|
|
17
91
|
{markdown}
|
|
@@ -297,6 +297,13 @@ export function SuiteAiPanel({
|
|
|
297
297
|
if (open && !hydrating) textareaRef.current?.focus();
|
|
298
298
|
}, [open, hydrating]);
|
|
299
299
|
|
|
300
|
+
// Warm the lazily-imported Markdown renderer as soon as the panel opens, so a
|
|
301
|
+
// reply never flashes a placeholder while its chunk is still downloading.
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (!open) return;
|
|
304
|
+
void import("./ai-message-markdown").catch(() => {});
|
|
305
|
+
}, [open]);
|
|
306
|
+
|
|
300
307
|
useLayoutEffect(() => {
|
|
301
308
|
const el = textareaRef.current;
|
|
302
309
|
if (!el) return;
|
|
@@ -425,6 +432,13 @@ export function SuiteAiPanel({
|
|
|
425
432
|
|
|
426
433
|
if (!open) return null;
|
|
427
434
|
|
|
435
|
+
// Only the newest reply gets the staggered word reveal; animating the whole
|
|
436
|
+
// transcript would replay whenever the panel remounts mid-conversation.
|
|
437
|
+
const lastAssistantIndex = messages.reduce(
|
|
438
|
+
(found, message, index) => (message.role === "assistant" ? index : found),
|
|
439
|
+
-1,
|
|
440
|
+
);
|
|
441
|
+
|
|
428
442
|
const Icon = BrandIcon ?? Sparks;
|
|
429
443
|
|
|
430
444
|
return (
|
|
@@ -505,7 +519,7 @@ export function SuiteAiPanel({
|
|
|
505
519
|
return (
|
|
506
520
|
<div
|
|
507
521
|
key={`user-${index}`}
|
|
508
|
-
className="flex justify-end"
|
|
522
|
+
className="suite-msg-in suite-msg-in--user flex justify-end"
|
|
509
523
|
data-test="ask-ai-user"
|
|
510
524
|
>
|
|
511
525
|
<div className="w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]">
|
|
@@ -521,28 +535,27 @@ export function SuiteAiPanel({
|
|
|
521
535
|
data-test="ask-ai-assistant"
|
|
522
536
|
>
|
|
523
537
|
<span
|
|
524
|
-
className="mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand"
|
|
538
|
+
className="suite-msg-avatar mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand"
|
|
525
539
|
aria-hidden
|
|
526
540
|
>
|
|
527
541
|
<Icon className="h-3.5 w-3.5" />
|
|
528
542
|
</span>
|
|
529
|
-
<div className="min-w-0 max-w-[85%] space-y-2">
|
|
543
|
+
<div className="suite-msg-bubble min-w-0 max-w-[85%] space-y-2">
|
|
530
544
|
<div className="rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink">
|
|
531
545
|
<Suspense
|
|
532
546
|
fallback={
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
<div className="h-3 w-11/12 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" />
|
|
540
|
-
<div className="h-3 w-full rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" />
|
|
541
|
-
<div className="h-3 w-3/5 rounded bg-ph-mutedtext/20 motion-safe:animate-pulse" />
|
|
542
|
-
</div>
|
|
547
|
+
// The chunk is pre-warmed above, so this only shows on a
|
|
548
|
+
// cold cache. Keep it invisible — a skeleton here reads
|
|
549
|
+
// as if it were part of the answer.
|
|
550
|
+
<span className="sr-only" role="status">
|
|
551
|
+
Rendering response…
|
|
552
|
+
</span>
|
|
543
553
|
}
|
|
544
554
|
>
|
|
545
|
-
<SuiteAiMarkdownMessage
|
|
555
|
+
<SuiteAiMarkdownMessage
|
|
556
|
+
markdown={message.content}
|
|
557
|
+
animate={index === lastAssistantIndex}
|
|
558
|
+
/>
|
|
546
559
|
</Suspense>
|
|
547
560
|
</div>
|
|
548
561
|
{(message.actions ?? []).map((action) => (
|
|
@@ -612,9 +625,9 @@ export function SuiteAiPanel({
|
|
|
612
625
|
<Icon className="h-3.5 w-3.5" />
|
|
613
626
|
</span>
|
|
614
627
|
<div className="flex items-center gap-1.5 rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2.5">
|
|
615
|
-
<span className="h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
616
|
-
<span className="h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
617
|
-
<span className="h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
628
|
+
<span className="suite-think-dot h-1.5 w-1.5 rounded-full bg-ph-mutedtext" />
|
|
629
|
+
<span className="suite-think-dot suite-think-dot--2 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" />
|
|
630
|
+
<span className="suite-think-dot suite-think-dot--3 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" />
|
|
618
631
|
</div>
|
|
619
632
|
</div>
|
|
620
633
|
) : null}
|
|
@@ -320,9 +320,7 @@ export function SuiteSidebar({
|
|
|
320
320
|
className={cn(
|
|
321
321
|
"shrink-0 border-t border-ph-border",
|
|
322
322
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
323
|
-
|
|
324
|
-
// trigger, so even padding read top-heavy once the avatar shrank.
|
|
325
|
-
collapsed ? "px-1.5 pt-1.5 pb-1" : "px-3 pt-3 pb-2",
|
|
323
|
+
collapsed ? "p-1.5" : "p-3",
|
|
326
324
|
)}
|
|
327
325
|
>
|
|
328
326
|
<div
|
|
@@ -16,6 +16,10 @@ export interface SuiteUserMenuProps {
|
|
|
16
16
|
onSignOut: () => void;
|
|
17
17
|
/** Fallback letter(s) when there is no avatar image. */
|
|
18
18
|
fallbackInitials?: string;
|
|
19
|
+
/** Second line under the name, e.g. the workspace subscription ("Plus"). */
|
|
20
|
+
subtitle?: string | null;
|
|
21
|
+
/** Show the name + subtitle beside the avatar, for the sidebar footer. */
|
|
22
|
+
showDetails?: boolean;
|
|
19
23
|
/** Show the old desktop sidebar sign-out action beside the avatar. */
|
|
20
24
|
showSignOutAction?: boolean;
|
|
21
25
|
dataTest?: string;
|
|
@@ -72,6 +76,8 @@ export function SuiteUserMenu({
|
|
|
72
76
|
settingsHref,
|
|
73
77
|
onSignOut,
|
|
74
78
|
fallbackInitials,
|
|
79
|
+
subtitle,
|
|
80
|
+
showDetails = false,
|
|
75
81
|
showSignOutAction = false,
|
|
76
82
|
dataTest = "suite-user-menu",
|
|
77
83
|
triggerId,
|
|
@@ -110,9 +116,27 @@ export function SuiteUserMenu({
|
|
|
110
116
|
</span>
|
|
111
117
|
);
|
|
112
118
|
|
|
119
|
+
// Sidebar footer shows who is signed in and which plan the workspace is on;
|
|
120
|
+
// the mobile header keeps the bare avatar so its action row stays compact.
|
|
121
|
+
const trigger = showDetails ? (
|
|
122
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
123
|
+
{avatar}
|
|
124
|
+
<span className="flex min-w-0 flex-col text-left">
|
|
125
|
+
<span className="truncate text-sm font-medium text-ph-ink">
|
|
126
|
+
{name || email || "Account"}
|
|
127
|
+
</span>
|
|
128
|
+
{subtitle ? (
|
|
129
|
+
<span className="truncate text-xs text-ph-mutedtext">{subtitle}</span>
|
|
130
|
+
) : null}
|
|
131
|
+
</span>
|
|
132
|
+
</span>
|
|
133
|
+
) : (
|
|
134
|
+
avatar
|
|
135
|
+
);
|
|
136
|
+
|
|
113
137
|
const accountMenu = (
|
|
114
138
|
<DropdownMenu
|
|
115
|
-
trigger={
|
|
139
|
+
trigger={trigger}
|
|
116
140
|
triggerId={triggerId ?? `${dataTest}-trigger`}
|
|
117
141
|
triggerDataTest={triggerDataTest ?? `${dataTest}-trigger`}
|
|
118
142
|
aria-label="Account menu"
|
|
@@ -123,6 +147,8 @@ export function SuiteUserMenu({
|
|
|
123
147
|
modal={false}
|
|
124
148
|
className={cn(
|
|
125
149
|
"[&_.ph-dropdown-trigger]:rounded-full",
|
|
150
|
+
showDetails &&
|
|
151
|
+
"min-w-0 flex-1 [&_.ph-dropdown-trigger]:w-full [&_.ph-dropdown-trigger]:justify-start [&_.ph-dropdown-trigger]:rounded-lg",
|
|
126
152
|
showSignOutAction ? undefined : className,
|
|
127
153
|
)}
|
|
128
154
|
data-test={dataTest}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import "./chunk-FWCSY2DS.mjs";
|
|
3
|
-
|
|
4
|
-
// src/suite/components/ai-message-markdown.tsx
|
|
5
|
-
import ReactMarkdown from "react-markdown";
|
|
6
|
-
import remarkGfm from "remark-gfm";
|
|
7
|
-
import { jsx } from "react/jsx-runtime";
|
|
8
|
-
function SuiteAiMarkdownMessage({
|
|
9
|
-
markdown
|
|
10
|
-
}) {
|
|
11
|
-
return /* @__PURE__ */ jsx("div", { className: "suite-ai-markdown", children: /* @__PURE__ */ jsx(
|
|
12
|
-
ReactMarkdown,
|
|
13
|
-
{
|
|
14
|
-
remarkPlugins: [remarkGfm],
|
|
15
|
-
components: { img: () => null },
|
|
16
|
-
children: markdown
|
|
17
|
-
}
|
|
18
|
-
) });
|
|
19
|
-
}
|
|
20
|
-
export {
|
|
21
|
-
SuiteAiMarkdownMessage as default
|
|
22
|
-
};
|