@xenide-io/the-old-ui-theme 0.4.6 → 0.4.9
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-blocknote-4Q4U2T2W.mjs +63 -0
- package/dist/chunk-IAH7Z46I.mjs +104 -0
- package/dist/suite.js +501 -240
- package/dist/suite.mjs +291 -199
- package/package.json +3 -1
- package/src/styles/suite-skin.css +22 -0
- package/src/suite/components/ai-message-blocknote.test.tsx +40 -0
- package/src/suite/components/ai-message-blocknote.tsx +61 -0
- package/src/suite/components/ai-panel.test.ts +20 -0
- package/src/suite/components/ai-panel.tsx +255 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
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",
|
|
@@ -131,6 +131,8 @@
|
|
|
131
131
|
"access": "public"
|
|
132
132
|
},
|
|
133
133
|
"dependencies": {
|
|
134
|
+
"@blocknote/core": "0.52.1",
|
|
135
|
+
"@blocknote/react": "0.52.1",
|
|
134
136
|
"@radix-ui/react-dialog": "^1.1.23",
|
|
135
137
|
"@radix-ui/react-dropdown-menu": "^2.1.24",
|
|
136
138
|
"clsx": "^2.1.1",
|
|
@@ -475,6 +475,28 @@ button:hover > .ph-card {
|
|
|
475
475
|
-webkit-overflow-scrolling: touch;
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
/* ── Ask AI answer content ───────────────────────────────────────────── */
|
|
479
|
+
.suite-ai-blocknote .bn-editor {
|
|
480
|
+
min-height: 0;
|
|
481
|
+
padding: 0;
|
|
482
|
+
background: transparent;
|
|
483
|
+
color: var(--ph-text-primary);
|
|
484
|
+
font-family: inherit;
|
|
485
|
+
font-size: 0.875rem;
|
|
486
|
+
}
|
|
487
|
+
.suite-ai-blocknote .bn-block-content {
|
|
488
|
+
padding-block: 2px;
|
|
489
|
+
}
|
|
490
|
+
.suite-ai-blocknote [data-content-type="heading"] {
|
|
491
|
+
padding-top: 8px;
|
|
492
|
+
}
|
|
493
|
+
.suite-ai-blocknote [data-content-type="codeBlock"] {
|
|
494
|
+
overflow-x: auto;
|
|
495
|
+
border: 1px solid var(--ph-border);
|
|
496
|
+
border-radius: 6px;
|
|
497
|
+
background: var(--ph-surface);
|
|
498
|
+
}
|
|
499
|
+
|
|
478
500
|
/* ── Auth / OAuth / launch handoff ──────────────────────────────────────
|
|
479
501
|
One calm opacity fade — no stacked entrance reveals on bridge pages. */
|
|
480
502
|
.suite-auth-handoff {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import SuiteAiBlockNoteMessage from "./ai-message-blocknote";
|
|
5
|
+
import { SuiteThemeProvider } from "./theme-provider";
|
|
6
|
+
|
|
7
|
+
const themeConfig = {
|
|
8
|
+
storageKey: "suite-ai-test-theme",
|
|
9
|
+
lightThemeId: "test-light",
|
|
10
|
+
darkThemeId: "test-dark",
|
|
11
|
+
fallbackTheme: "light" as const,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("SuiteAiBlockNoteMessage", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.stubGlobal("localStorage", {
|
|
17
|
+
getItem: () => "light",
|
|
18
|
+
setItem: vi.fn(),
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup();
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders Markdown as restricted read-only BlockNote content", async () => {
|
|
27
|
+
const { container } = render(
|
|
28
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
29
|
+
<SuiteAiBlockNoteMessage markdown={"## Summary\n\n- First item"} />
|
|
30
|
+
</SuiteThemeProvider>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await waitFor(() =>
|
|
34
|
+
expect(screen.getByText("Summary")).toBeInTheDocument(),
|
|
35
|
+
);
|
|
36
|
+
expect(screen.getByText("First item")).toBeInTheDocument();
|
|
37
|
+
expect(container.querySelector('[contenteditable="false"]')).toBeTruthy();
|
|
38
|
+
expect(container.querySelector("img")).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
|
|
5
|
+
import { BlockNoteViewRaw, useCreateBlockNote } from "@blocknote/react";
|
|
6
|
+
import "@blocknote/react/style.css";
|
|
7
|
+
|
|
8
|
+
import { useSuiteTheme } from "./theme-provider";
|
|
9
|
+
|
|
10
|
+
const assistantSchema = BlockNoteSchema.create({
|
|
11
|
+
blockSpecs: {
|
|
12
|
+
paragraph: defaultBlockSpecs.paragraph,
|
|
13
|
+
heading: defaultBlockSpecs.heading,
|
|
14
|
+
bulletListItem: defaultBlockSpecs.bulletListItem,
|
|
15
|
+
numberedListItem: defaultBlockSpecs.numberedListItem,
|
|
16
|
+
checkListItem: defaultBlockSpecs.checkListItem,
|
|
17
|
+
codeBlock: defaultBlockSpecs.codeBlock,
|
|
18
|
+
quote: defaultBlockSpecs.quote,
|
|
19
|
+
table: defaultBlockSpecs.table,
|
|
20
|
+
divider: defaultBlockSpecs.divider,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default function SuiteAiBlockNoteMessage({
|
|
25
|
+
markdown,
|
|
26
|
+
}: {
|
|
27
|
+
markdown: string;
|
|
28
|
+
}) {
|
|
29
|
+
const { resolvedTheme } = useSuiteTheme();
|
|
30
|
+
const editor = useCreateBlockNote({ schema: assistantSchema });
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
try {
|
|
34
|
+
const blocks = editor.tryParseMarkdownToBlocks(markdown);
|
|
35
|
+
editor.replaceBlocks(
|
|
36
|
+
editor.document,
|
|
37
|
+
blocks.length > 0 ? blocks : [{ type: "paragraph", content: markdown }],
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
editor.replaceBlocks(editor.document, [
|
|
41
|
+
{ type: "paragraph", content: markdown },
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
}, [editor, markdown]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<BlockNoteViewRaw
|
|
48
|
+
editor={editor}
|
|
49
|
+
editable={false}
|
|
50
|
+
theme={resolvedTheme}
|
|
51
|
+
className="suite-ai-blocknote"
|
|
52
|
+
formattingToolbar={false}
|
|
53
|
+
sideMenu={false}
|
|
54
|
+
slashMenu={false}
|
|
55
|
+
linkToolbar={false}
|
|
56
|
+
filePanel={false}
|
|
57
|
+
tableHandles={false}
|
|
58
|
+
comments={false}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { resolveSuiteAiLauncherPosition } from './ai-panel';
|
|
4
|
+
|
|
5
|
+
describe('resolveSuiteAiLauncherPosition', () => {
|
|
6
|
+
it('docks to either edge and keeps the launcher inside the viewport', () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveSuiteAiLauncherPosition('left', -20, 1000, 800, 120, 40),
|
|
9
|
+
).toEqual({
|
|
10
|
+
x: 12,
|
|
11
|
+
y: 12,
|
|
12
|
+
});
|
|
13
|
+
expect(
|
|
14
|
+
resolveSuiteAiLauncherPosition('right', 900, 1000, 800, 120, 40),
|
|
15
|
+
).toEqual({
|
|
16
|
+
x: 868,
|
|
17
|
+
y: 748,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
lazy,
|
|
5
|
+
Suspense,
|
|
4
6
|
useCallback,
|
|
5
7
|
useEffect,
|
|
6
8
|
useRef,
|
|
7
9
|
useState,
|
|
8
10
|
type ComponentType,
|
|
11
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
12
|
+
type PointerEvent as ReactPointerEvent,
|
|
9
13
|
type ReactNode,
|
|
10
14
|
} from 'react';
|
|
11
15
|
import { Check, Copy, Sparks, Xmark as X } from 'iconoir-react';
|
|
12
16
|
|
|
17
|
+
const SuiteAiBlockNoteMessage = lazy(
|
|
18
|
+
() => import('./ai-message-blocknote'),
|
|
19
|
+
);
|
|
20
|
+
|
|
13
21
|
export const SUITE_OPEN_ASK_AI_EVENT = 'shellstack:open-ask-ai';
|
|
14
22
|
|
|
15
23
|
export type SuiteAskAiOpenDetail = {
|
|
@@ -37,6 +45,34 @@ export interface SuiteAiChatMessage {
|
|
|
37
45
|
created_at?: string;
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
type LauncherSide = 'left' | 'right';
|
|
49
|
+
type LauncherPosition = { x: number; y: number };
|
|
50
|
+
|
|
51
|
+
const LAUNCHER_EDGE_GAP = 12;
|
|
52
|
+
const LAUNCHER_STORAGE_KEY = 'suite-ask-ai-launcher-position';
|
|
53
|
+
|
|
54
|
+
export function resolveSuiteAiLauncherPosition(
|
|
55
|
+
side: LauncherSide,
|
|
56
|
+
desiredY: number,
|
|
57
|
+
viewportWidth: number,
|
|
58
|
+
viewportHeight: number,
|
|
59
|
+
launcherWidth: number,
|
|
60
|
+
launcherHeight: number,
|
|
61
|
+
): LauncherPosition {
|
|
62
|
+
const maxX = Math.max(
|
|
63
|
+
LAUNCHER_EDGE_GAP,
|
|
64
|
+
viewportWidth - launcherWidth - LAUNCHER_EDGE_GAP,
|
|
65
|
+
);
|
|
66
|
+
const maxY = Math.max(
|
|
67
|
+
LAUNCHER_EDGE_GAP,
|
|
68
|
+
viewportHeight - launcherHeight - LAUNCHER_EDGE_GAP,
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
x: side === 'left' ? LAUNCHER_EDGE_GAP : maxX,
|
|
72
|
+
y: Math.min(Math.max(desiredY, LAUNCHER_EDGE_GAP), maxY),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
/**
|
|
41
77
|
* Suite-wide Ask AI slide-over — Notion-style continuous chat.
|
|
42
78
|
*
|
|
@@ -77,9 +113,67 @@ export function SuiteAiPanel({
|
|
|
77
113
|
const [hydrating, setHydrating] = useState(false);
|
|
78
114
|
const [error, setError] = useState('');
|
|
79
115
|
const [copiedId, setCopiedId] = useState<number | null>(null);
|
|
116
|
+
const [launcherSide, setLauncherSide] = useState<LauncherSide>('right');
|
|
117
|
+
const [launcherPosition, setLauncherPosition] =
|
|
118
|
+
useState<LauncherPosition | null>(null);
|
|
119
|
+
const [draggingLauncher, setDraggingLauncher] = useState(false);
|
|
80
120
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
121
|
+
const launcherRef = useRef<HTMLButtonElement>(null);
|
|
122
|
+
const launcherSideRef = useRef<LauncherSide>('right');
|
|
123
|
+
const launcherPositionRef = useRef<LauncherPosition | null>(null);
|
|
124
|
+
const launcherDragRef = useRef<{
|
|
125
|
+
pointerId: number;
|
|
126
|
+
offsetX: number;
|
|
127
|
+
offsetY: number;
|
|
128
|
+
startX: number;
|
|
129
|
+
startY: number;
|
|
130
|
+
} | null>(null);
|
|
131
|
+
const suppressLauncherClickRef = useRef(false);
|
|
81
132
|
const pendingPromptRef = useRef<string | null>(null);
|
|
82
133
|
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const launcher = launcherRef.current;
|
|
136
|
+
if (!launcher) return;
|
|
137
|
+
|
|
138
|
+
let side: LauncherSide = 'right';
|
|
139
|
+
let desiredY = window.innerHeight - launcher.offsetHeight - 20;
|
|
140
|
+
try {
|
|
141
|
+
const stored = JSON.parse(
|
|
142
|
+
localStorage.getItem(LAUNCHER_STORAGE_KEY) || 'null',
|
|
143
|
+
) as {
|
|
144
|
+
side?: LauncherSide;
|
|
145
|
+
y?: number;
|
|
146
|
+
} | null;
|
|
147
|
+
if (stored?.side === 'left' || stored?.side === 'right')
|
|
148
|
+
side = stored.side;
|
|
149
|
+
if (typeof stored?.y === 'number') desiredY = stored.y;
|
|
150
|
+
} catch {
|
|
151
|
+
// Ignore invalid saved launcher positions.
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
launcherSideRef.current = side;
|
|
155
|
+
setLauncherSide(side);
|
|
156
|
+
|
|
157
|
+
const placeLauncher = () => {
|
|
158
|
+
const element = launcherRef.current;
|
|
159
|
+
if (!element) return;
|
|
160
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
161
|
+
launcherSideRef.current,
|
|
162
|
+
launcherPositionRef.current?.y ?? desiredY,
|
|
163
|
+
window.innerWidth,
|
|
164
|
+
window.innerHeight,
|
|
165
|
+
element.offsetWidth,
|
|
166
|
+
element.offsetHeight,
|
|
167
|
+
);
|
|
168
|
+
launcherPositionRef.current = next;
|
|
169
|
+
setLauncherPosition(next);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
placeLauncher();
|
|
173
|
+
window.addEventListener('resize', placeLauncher);
|
|
174
|
+
return () => window.removeEventListener('resize', placeLauncher);
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
83
177
|
const scrollToBottom = useCallback(() => {
|
|
84
178
|
const el = scrollerRef.current;
|
|
85
179
|
if (!el) return;
|
|
@@ -185,21 +279,158 @@ export function SuiteAiPanel({
|
|
|
185
279
|
}
|
|
186
280
|
}
|
|
187
281
|
|
|
282
|
+
function saveLauncherPosition(
|
|
283
|
+
side: LauncherSide,
|
|
284
|
+
position: LauncherPosition,
|
|
285
|
+
) {
|
|
286
|
+
try {
|
|
287
|
+
localStorage.setItem(
|
|
288
|
+
LAUNCHER_STORAGE_KEY,
|
|
289
|
+
JSON.stringify({ side, y: position.y }),
|
|
290
|
+
);
|
|
291
|
+
} catch {
|
|
292
|
+
// Storage may be unavailable; dragging still works for this session.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function dockLauncher(side: LauncherSide, desiredY: number) {
|
|
297
|
+
const launcher = launcherRef.current;
|
|
298
|
+
if (!launcher) return;
|
|
299
|
+
const next = resolveSuiteAiLauncherPosition(
|
|
300
|
+
side,
|
|
301
|
+
desiredY,
|
|
302
|
+
window.innerWidth,
|
|
303
|
+
window.innerHeight,
|
|
304
|
+
launcher.offsetWidth,
|
|
305
|
+
launcher.offsetHeight,
|
|
306
|
+
);
|
|
307
|
+
launcherSideRef.current = side;
|
|
308
|
+
launcherPositionRef.current = next;
|
|
309
|
+
setLauncherSide(side);
|
|
310
|
+
setLauncherPosition(next);
|
|
311
|
+
saveLauncherPosition(side, next);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function startLauncherDrag(event: ReactPointerEvent<HTMLButtonElement>) {
|
|
315
|
+
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
316
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
317
|
+
launcherDragRef.current = {
|
|
318
|
+
pointerId: event.pointerId,
|
|
319
|
+
offsetX: event.clientX - rect.left,
|
|
320
|
+
offsetY: event.clientY - rect.top,
|
|
321
|
+
startX: event.clientX,
|
|
322
|
+
startY: event.clientY,
|
|
323
|
+
};
|
|
324
|
+
suppressLauncherClickRef.current = false;
|
|
325
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
326
|
+
setDraggingLauncher(true);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function moveLauncher(event: ReactPointerEvent<HTMLButtonElement>) {
|
|
330
|
+
const drag = launcherDragRef.current;
|
|
331
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
332
|
+
const launcher = event.currentTarget;
|
|
333
|
+
const maxX = Math.max(
|
|
334
|
+
LAUNCHER_EDGE_GAP,
|
|
335
|
+
window.innerWidth - launcher.offsetWidth - LAUNCHER_EDGE_GAP,
|
|
336
|
+
);
|
|
337
|
+
const maxY = Math.max(
|
|
338
|
+
LAUNCHER_EDGE_GAP,
|
|
339
|
+
window.innerHeight - launcher.offsetHeight - LAUNCHER_EDGE_GAP,
|
|
340
|
+
);
|
|
341
|
+
const next = {
|
|
342
|
+
x: Math.min(
|
|
343
|
+
Math.max(event.clientX - drag.offsetX, LAUNCHER_EDGE_GAP),
|
|
344
|
+
maxX,
|
|
345
|
+
),
|
|
346
|
+
y: Math.min(
|
|
347
|
+
Math.max(event.clientY - drag.offsetY, LAUNCHER_EDGE_GAP),
|
|
348
|
+
maxY,
|
|
349
|
+
),
|
|
350
|
+
};
|
|
351
|
+
if (
|
|
352
|
+
Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 4
|
|
353
|
+
) {
|
|
354
|
+
suppressLauncherClickRef.current = true;
|
|
355
|
+
}
|
|
356
|
+
launcherPositionRef.current = next;
|
|
357
|
+
setLauncherPosition(next);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function finishLauncherDrag(event: ReactPointerEvent<HTMLButtonElement>) {
|
|
361
|
+
const drag = launcherDragRef.current;
|
|
362
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
363
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
364
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
365
|
+
}
|
|
366
|
+
launcherDragRef.current = null;
|
|
367
|
+
setDraggingLauncher(false);
|
|
368
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
369
|
+
const currentX = launcherPositionRef.current?.x ?? rect.left;
|
|
370
|
+
const side: LauncherSide =
|
|
371
|
+
currentX + rect.width / 2 < window.innerWidth / 2 ? 'left' : 'right';
|
|
372
|
+
dockLauncher(side, launcherPositionRef.current?.y ?? rect.top);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function moveLauncherWithKeyboard(
|
|
376
|
+
event: ReactKeyboardEvent<HTMLButtonElement>,
|
|
377
|
+
) {
|
|
378
|
+
if (
|
|
379
|
+
!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)
|
|
380
|
+
)
|
|
381
|
+
return;
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
const currentY =
|
|
384
|
+
launcherPositionRef.current?.y ??
|
|
385
|
+
event.currentTarget.getBoundingClientRect().top;
|
|
386
|
+
if (event.key === 'ArrowLeft') dockLauncher('left', currentY);
|
|
387
|
+
else if (event.key === 'ArrowRight') dockLauncher('right', currentY);
|
|
388
|
+
else
|
|
389
|
+
dockLauncher(
|
|
390
|
+
launcherSideRef.current,
|
|
391
|
+
currentY + (event.key === 'ArrowUp' ? -24 : 24),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
188
395
|
const Icon = BrandIcon ?? Sparks;
|
|
189
396
|
|
|
190
397
|
return (
|
|
191
398
|
<>
|
|
192
399
|
{!hideLauncher ? (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
400
|
+
<>
|
|
401
|
+
<button
|
|
402
|
+
ref={launcherRef}
|
|
403
|
+
type="button"
|
|
404
|
+
data-test="ai-panel-launcher"
|
|
405
|
+
onClick={() => {
|
|
406
|
+
if (suppressLauncherClickRef.current) {
|
|
407
|
+
suppressLauncherClickRef.current = false;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
setOpen(true);
|
|
411
|
+
}}
|
|
412
|
+
onPointerDown={startLauncherDrag}
|
|
413
|
+
onPointerMove={moveLauncher}
|
|
414
|
+
onPointerUp={finishLauncherDrag}
|
|
415
|
+
onPointerCancel={finishLauncherDrag}
|
|
416
|
+
onKeyDown={moveLauncherWithKeyboard}
|
|
417
|
+
className={`fixed z-30 flex touch-none select-none items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-4 py-2.5 text-sm font-medium text-ph-ink shadow-ph hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand ${draggingLauncher ? 'cursor-grabbing' : 'cursor-grab'}`}
|
|
418
|
+
style={
|
|
419
|
+
launcherPosition
|
|
420
|
+
? { left: launcherPosition.x, top: launcherPosition.y }
|
|
421
|
+
: { bottom: 20, right: 20 }
|
|
422
|
+
}
|
|
423
|
+
aria-label="Open Ask AI"
|
|
424
|
+
aria-describedby="suite-ask-ai-launcher-help"
|
|
425
|
+
>
|
|
426
|
+
<Icon className="h-4 w-4 text-ph-brand" />
|
|
427
|
+
Ask AI
|
|
428
|
+
</button>
|
|
429
|
+
<span id="suite-ask-ai-launcher-help" className="sr-only">
|
|
430
|
+
Drag to reposition, or use arrow keys. The launcher docks to the
|
|
431
|
+
nearest screen edge.
|
|
432
|
+
</span>
|
|
433
|
+
</>
|
|
203
434
|
) : null}
|
|
204
435
|
|
|
205
436
|
{open ? (
|
|
@@ -211,7 +442,7 @@ export function SuiteAiPanel({
|
|
|
211
442
|
aria-label="Close Ask AI"
|
|
212
443
|
/>
|
|
213
444
|
<aside
|
|
214
|
-
className=
|
|
445
|
+
className={`absolute inset-y-0 flex w-full max-w-md flex-col bg-ph-surface shadow-2xl ${launcherSide === 'left' ? 'left-0 border-r border-ph-border' : 'right-0 border-l border-ph-border'}`}
|
|
215
446
|
data-test="suite-ask-ai-panel"
|
|
216
447
|
>
|
|
217
448
|
<div className="flex items-center justify-between border-b border-ph-border px-4 py-3">
|
|
@@ -305,7 +536,19 @@ export function SuiteAiPanel({
|
|
|
305
536
|
}
|
|
306
537
|
data-test={isUser ? 'ask-ai-user' : 'ask-ai-assistant'}
|
|
307
538
|
>
|
|
308
|
-
|
|
539
|
+
{isUser ? (
|
|
540
|
+
<p className="whitespace-pre-wrap">{message.content}</p>
|
|
541
|
+
) : (
|
|
542
|
+
<Suspense
|
|
543
|
+
fallback={
|
|
544
|
+
<p className="whitespace-pre-wrap">
|
|
545
|
+
{message.content}
|
|
546
|
+
</p>
|
|
547
|
+
}
|
|
548
|
+
>
|
|
549
|
+
<SuiteAiBlockNoteMessage markdown={message.content} />
|
|
550
|
+
</Suspense>
|
|
551
|
+
)}
|
|
309
552
|
{!isUser ? (
|
|
310
553
|
<button
|
|
311
554
|
type="button"
|