@xenide-io/the-old-ui-theme 0.9.7 → 0.9.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/{chunk-2EO5VCXP.mjs → chunk-6SVJAU5K.mjs} +165 -1
- package/dist/index.mjs +1 -1
- package/dist/suite.d.mts +14 -1
- package/dist/suite.d.ts +14 -1
- package/dist/suite.js +147 -1
- package/dist/suite.mjs +153 -4
- package/dist/ui.d.mts +37 -1
- package/dist/ui.d.ts +37 -1
- package/dist/ui.js +170 -2
- package/dist/ui.mjs +11 -3
- package/package.json +1 -1
- package/src/components/ui/Charts.tsx +178 -0
- package/src/components/ui/charts.test.tsx +30 -0
- package/src/components/ui/index.ts +11 -0
- package/src/suite/components/ai-panel.tsx +166 -2
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
ArrowDown,
|
|
18
18
|
ArrowUp,
|
|
19
|
+
Check,
|
|
20
|
+
Copy,
|
|
21
|
+
EditPencil,
|
|
19
22
|
Erase,
|
|
20
23
|
Sparks,
|
|
21
24
|
Square,
|
|
@@ -157,6 +160,8 @@ export function SuiteAiPanel({
|
|
|
157
160
|
title = "Shelly AI",
|
|
158
161
|
fetchChat,
|
|
159
162
|
sendMessage,
|
|
163
|
+
editMessage,
|
|
164
|
+
cancelMessage,
|
|
160
165
|
clearChat,
|
|
161
166
|
brandIcon: BrandIcon,
|
|
162
167
|
spinner: Spinner,
|
|
@@ -180,6 +185,14 @@ export function SuiteAiPanel({
|
|
|
180
185
|
prompt: string;
|
|
181
186
|
signal?: AbortSignal;
|
|
182
187
|
}) => Promise<{ messages: SuiteAiChatMessage[]; reply: string }>;
|
|
188
|
+
/** Rewrite the thread from a user message. Falls back to sendMessage. */
|
|
189
|
+
editMessage?: (params: {
|
|
190
|
+
index: number;
|
|
191
|
+
prompt: string;
|
|
192
|
+
signal?: AbortSignal;
|
|
193
|
+
}) => Promise<{ messages: SuiteAiChatMessage[]; reply: string }>;
|
|
194
|
+
/** Stop the in-flight reply on the server. */
|
|
195
|
+
cancelMessage?: () => Promise<{ messages?: SuiteAiChatMessage[] }>;
|
|
183
196
|
clearChat: () => Promise<void>;
|
|
184
197
|
brandIcon?: ComponentType<{ className?: string }>;
|
|
185
198
|
spinner: ComponentType<{ className?: string }>;
|
|
@@ -220,6 +233,9 @@ export function SuiteAiPanel({
|
|
|
220
233
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
221
234
|
const abortRef = useRef<AbortController | null>(null);
|
|
222
235
|
const pendingPromptRef = useRef<string | null>(null);
|
|
236
|
+
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
237
|
+
const [editDraft, setEditDraft] = useState("");
|
|
238
|
+
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
|
223
239
|
const waiting = loading || isWaitingForReply;
|
|
224
240
|
|
|
225
241
|
useEffect(() => {
|
|
@@ -356,6 +372,14 @@ export function SuiteAiPanel({
|
|
|
356
372
|
abortRef.current?.abort();
|
|
357
373
|
abortRef.current = null;
|
|
358
374
|
setLoading(false);
|
|
375
|
+
if (!cancelMessage) return;
|
|
376
|
+
void cancelMessage()
|
|
377
|
+
.then((result) => {
|
|
378
|
+
if (result?.messages) setMessages(result.messages);
|
|
379
|
+
})
|
|
380
|
+
.catch(() => {
|
|
381
|
+
// Best-effort server cancel; the local abort already stopped the UI.
|
|
382
|
+
});
|
|
359
383
|
}
|
|
360
384
|
|
|
361
385
|
function retry() {
|
|
@@ -367,6 +391,58 @@ export function SuiteAiPanel({
|
|
|
367
391
|
else void hydrate();
|
|
368
392
|
}
|
|
369
393
|
|
|
394
|
+
async function saveEdit(index: number) {
|
|
395
|
+
const trimmed = editDraft.trim();
|
|
396
|
+
if (!trimmed || waiting) return;
|
|
397
|
+
setEditingIndex(null);
|
|
398
|
+
setEditDraft("");
|
|
399
|
+
setError("");
|
|
400
|
+
setAtBottom(true);
|
|
401
|
+
setMessages((prev) => prev.slice(0, index));
|
|
402
|
+
const controller = new AbortController();
|
|
403
|
+
abortRef.current = controller;
|
|
404
|
+
setLoading(true);
|
|
405
|
+
try {
|
|
406
|
+
const result = editMessage
|
|
407
|
+
? await editMessage({
|
|
408
|
+
index,
|
|
409
|
+
prompt: trimmed,
|
|
410
|
+
signal: controller.signal,
|
|
411
|
+
})
|
|
412
|
+
: await sendMessage({ prompt: trimmed, signal: controller.signal });
|
|
413
|
+
if (controller.signal.aborted) return;
|
|
414
|
+
const nextMessages = result.messages ?? [];
|
|
415
|
+
setMessages(nextMessages);
|
|
416
|
+
notifySuiteMutated(
|
|
417
|
+
nextMessages.flatMap((message) => message.actions ?? []),
|
|
418
|
+
);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (controller.signal.aborted) return;
|
|
421
|
+
setError(
|
|
422
|
+
err instanceof Error
|
|
423
|
+
? err.message
|
|
424
|
+
: "The AI request failed. Try again.",
|
|
425
|
+
);
|
|
426
|
+
void hydrate();
|
|
427
|
+
} finally {
|
|
428
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
429
|
+
setLoading(false);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function handleCopy(index: number, content: string) {
|
|
434
|
+
try {
|
|
435
|
+
await navigator.clipboard.writeText(content);
|
|
436
|
+
setCopiedIndex(index);
|
|
437
|
+
window.setTimeout(
|
|
438
|
+
() => setCopiedIndex((current) => (current === index ? null : current)),
|
|
439
|
+
1500,
|
|
440
|
+
);
|
|
441
|
+
} catch {
|
|
442
|
+
// clipboard unavailable
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
370
446
|
async function handleClear() {
|
|
371
447
|
if (loading) return;
|
|
372
448
|
try {
|
|
@@ -516,14 +592,102 @@ export function SuiteAiPanel({
|
|
|
516
592
|
{messages.map((message, index) => {
|
|
517
593
|
const isUser = message.role === "user";
|
|
518
594
|
if (isUser) {
|
|
595
|
+
if (editingIndex === index) {
|
|
596
|
+
return (
|
|
597
|
+
<div
|
|
598
|
+
key={`user-${index}`}
|
|
599
|
+
className="suite-msg-in suite-msg-in--user flex justify-end"
|
|
600
|
+
data-test="ask-ai-user"
|
|
601
|
+
>
|
|
602
|
+
<div className="w-full max-w-[85%] space-y-2">
|
|
603
|
+
<textarea
|
|
604
|
+
autoFocus
|
|
605
|
+
value={editDraft}
|
|
606
|
+
onChange={(event) => setEditDraft(event.target.value)}
|
|
607
|
+
onKeyDown={(event) => {
|
|
608
|
+
if (
|
|
609
|
+
event.key === "Enter" &&
|
|
610
|
+
!event.shiftKey &&
|
|
611
|
+
!event.nativeEvent.isComposing
|
|
612
|
+
) {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
void saveEdit(index);
|
|
615
|
+
}
|
|
616
|
+
if (event.key === "Escape") {
|
|
617
|
+
setEditingIndex(null);
|
|
618
|
+
setEditDraft("");
|
|
619
|
+
}
|
|
620
|
+
}}
|
|
621
|
+
rows={2}
|
|
622
|
+
data-test="ask-ai-edit-input"
|
|
623
|
+
className="w-full resize-none rounded-2xl rounded-br-md border border-ph-border bg-ph-surface px-3 py-2 text-sm leading-relaxed text-ph-ink outline-none focus-visible:border-ph-brand/45 focus-visible:ring-2 focus-visible:ring-ph-focus/35"
|
|
624
|
+
/>
|
|
625
|
+
<div className="flex justify-end gap-2">
|
|
626
|
+
<button
|
|
627
|
+
type="button"
|
|
628
|
+
onClick={() => {
|
|
629
|
+
setEditingIndex(null);
|
|
630
|
+
setEditDraft("");
|
|
631
|
+
}}
|
|
632
|
+
className="inline-flex min-h-8 items-center rounded-lg border border-ph-border bg-ph-surface px-2.5 text-xs font-medium text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
|
|
633
|
+
>
|
|
634
|
+
Cancel
|
|
635
|
+
</button>
|
|
636
|
+
<button
|
|
637
|
+
type="button"
|
|
638
|
+
data-test="ask-ai-edit-save"
|
|
639
|
+
disabled={!editDraft.trim() || waiting}
|
|
640
|
+
onClick={() => void saveEdit(index)}
|
|
641
|
+
className="inline-flex min-h-8 items-center rounded-lg bg-ph-brand px-2.5 text-xs font-medium text-[color:var(--ph-on-accent)] transition-opacity hover:bg-ph-brand-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
|
|
642
|
+
>
|
|
643
|
+
Save
|
|
644
|
+
</button>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
519
650
|
return (
|
|
520
651
|
<div
|
|
521
652
|
key={`user-${index}`}
|
|
522
653
|
className="suite-msg-in suite-msg-in--user flex justify-end"
|
|
523
654
|
data-test="ask-ai-user"
|
|
524
655
|
>
|
|
525
|
-
<div className="
|
|
526
|
-
{
|
|
656
|
+
<div className="group flex items-end gap-1.5">
|
|
657
|
+
{!waiting ? (
|
|
658
|
+
<div className="mb-0.5 flex items-center gap-0.5 opacity-100 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within:opacity-100 [@media(hover:hover)]:group-hover:opacity-100">
|
|
659
|
+
<button
|
|
660
|
+
type="button"
|
|
661
|
+
onClick={() => void handleCopy(index, message.content)}
|
|
662
|
+
aria-label={
|
|
663
|
+
copiedIndex === index ? "Copied" : "Copy message"
|
|
664
|
+
}
|
|
665
|
+
data-test="ask-ai-copy"
|
|
666
|
+
className="flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
|
|
667
|
+
>
|
|
668
|
+
{copiedIndex === index ? (
|
|
669
|
+
<Check className="h-3.5 w-3.5" />
|
|
670
|
+
) : (
|
|
671
|
+
<Copy className="h-3.5 w-3.5" />
|
|
672
|
+
)}
|
|
673
|
+
</button>
|
|
674
|
+
<button
|
|
675
|
+
type="button"
|
|
676
|
+
onClick={() => {
|
|
677
|
+
setEditDraft(message.content);
|
|
678
|
+
setEditingIndex(index);
|
|
679
|
+
}}
|
|
680
|
+
aria-label="Edit message"
|
|
681
|
+
data-test="ask-ai-edit"
|
|
682
|
+
className="flex h-7 w-7 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus"
|
|
683
|
+
>
|
|
684
|
+
<EditPencil className="h-3.5 w-3.5" />
|
|
685
|
+
</button>
|
|
686
|
+
</div>
|
|
687
|
+
) : null}
|
|
688
|
+
<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]">
|
|
689
|
+
{message.content}
|
|
690
|
+
</div>
|
|
527
691
|
</div>
|
|
528
692
|
</div>
|
|
529
693
|
);
|