@admin-layout/tailwind-ui 12.2.4-alpha.56 → 12.2.4-alpha.57
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/lib/components/AIToolbar/components/completion/CharacterPicker.d.ts +3 -0
- package/lib/components/AIToolbar/components/completion/CharacterPicker.d.ts.map +1 -0
- package/lib/components/AIToolbar/components/completion/CharacterPicker.js +90 -0
- package/lib/components/AIToolbar/components/completion/CharacterPicker.js.map +1 -0
- package/lib/components/AIToolbar/components/completion/Input.d.ts.map +1 -1
- package/lib/components/AIToolbar/components/completion/Input.js +5 -5
- package/lib/components/AIToolbar/hooks/useActiveCharacter.d.ts +21 -6
- package/lib/components/AIToolbar/hooks/useActiveCharacter.d.ts.map +1 -1
- package/lib/components/AIToolbar/hooks/useActiveCharacter.js +179 -25
- package/lib/components/AIToolbar/hooks/useActiveCharacter.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CharacterPicker.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/CharacterPicker.tsx"],"names":[],"mappings":"AAgDA,wBAAgB,eAAe,4CAiD9B;AAED,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import {ChevronDownIcon,CheckIcon}from'lucide-react';import'../../../../shardui/accordion.js';import'../../../../shardui/alert.js';import'../../../../shardui/alert-dialog.js';import'@radix-ui/react-aspect-ratio';import'../../../../shardui/avatar.js';import'../../../../shardui/badge.js';import'../../../../shardui/breadcrumb.js';import {Button}from'../../../../shardui/button.js';import'../../../../shardui/calendar.js';import'../../../../shardui/card.js';import'../../../../shardui/carousel.js';import'../../../../shardui/chart.js';import'../../../../shardui/checkbox.js';import'@radix-ui/react-collapsible';import'../../../../shardui/command.js';import'../../../../shardui/context-menu.js';import'../../../../shardui/dialog.js';import'../../../../shardui/drawer.js';import'../../../../shardui/dropdown-menu.js';import'../../../../shardui/form.js';import'../../../../shardui/hover-card.js';import'../../../../shardui/input.js';import'../../../../shardui/input-otp.js';import'../../../../shardui/label.js';import'../../../../shardui/menubar.js';import'../../../../shardui/navigation-menu.js';import'../../../../shardui/pagination.js';import {Popover,PopoverTrigger,PopoverContent}from'../../../../shardui/popover.js';import'../../../../shardui/progress.js';import'../../../../shardui/radio-group.js';import'react-resizable-panels';import'../../../../utils/isBrowser/index.js';import {cn}from'../../../../utils/util.js';import'fast-deep-equal/react.js';import'../../../../shardui/scroll-area.js';import'../../../../shardui/select.js';import'../../../../shardui/separator.js';import'../../../../shardui/sheet.js';import'../../../../shardui/sidebar.js';import'../../../../shardui/slider.js';import'../../../ThemeProvider/ThemeProvider.js';import'sonner';import'../../../../shardui/switch.js';import'../../../../shardui/table.js';import'../../../../shardui/tabs.js';import'../../../../shardui/textarea.js';import'../../../../shardui/toast.js';import'../../../../shardui/toggle.js';import'../../../../shardui/toggle-group.js';import'../../../../shardui/tooltip.js';import {useCharacterPicker}from'../../hooks/useActiveCharacter.js';function AvatarBadge({
|
|
2
|
+
entry,
|
|
3
|
+
size
|
|
4
|
+
}) {
|
|
5
|
+
if (entry?.avatarSrc) {
|
|
6
|
+
return jsx("img", {
|
|
7
|
+
src: entry.avatarSrc,
|
|
8
|
+
alt: "",
|
|
9
|
+
"aria-hidden": "true",
|
|
10
|
+
draggable: false,
|
|
11
|
+
width: size,
|
|
12
|
+
height: size,
|
|
13
|
+
className: "rounded-full object-cover ring-1 ring-border/60",
|
|
14
|
+
style: {
|
|
15
|
+
width: size,
|
|
16
|
+
height: size
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return jsx("div", {
|
|
21
|
+
"aria-hidden": "true",
|
|
22
|
+
className: "flex items-center justify-center rounded-full bg-muted text-[9px] font-semibold text-muted-foreground ring-1 ring-border/60",
|
|
23
|
+
style: {
|
|
24
|
+
width: size,
|
|
25
|
+
height: size
|
|
26
|
+
},
|
|
27
|
+
children: "AI"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// main component
|
|
31
|
+
function CharacterPicker() {
|
|
32
|
+
const {
|
|
33
|
+
characters,
|
|
34
|
+
activeName,
|
|
35
|
+
setActive
|
|
36
|
+
} = useCharacterPicker();
|
|
37
|
+
const [open, setOpen] = React.useState(false);
|
|
38
|
+
if (characters.length < 2) return null;
|
|
39
|
+
const active = characters.find(c => c.name === activeName) ?? characters[0] ?? null;
|
|
40
|
+
return jsxs(Popover, {
|
|
41
|
+
open: open,
|
|
42
|
+
onOpenChange: setOpen,
|
|
43
|
+
children: [jsx(PopoverTrigger, {
|
|
44
|
+
asChild: true,
|
|
45
|
+
children: jsxs(Button, {
|
|
46
|
+
size: "sm",
|
|
47
|
+
variant: "ghost",
|
|
48
|
+
className: "h-7 gap-1.5 rounded-full border border-border/50 bg-muted/30 px-1.5 py-0 text-[11px] font-medium",
|
|
49
|
+
title: "Switch character",
|
|
50
|
+
children: [jsx(AvatarBadge, {
|
|
51
|
+
entry: active,
|
|
52
|
+
size: 20
|
|
53
|
+
}), jsx("span", {
|
|
54
|
+
className: "max-w-[8rem] truncate",
|
|
55
|
+
children: active?.name ?? 'AI'
|
|
56
|
+
}), jsx(ChevronDownIcon, {
|
|
57
|
+
className: "h-3 w-3 opacity-60"
|
|
58
|
+
})]
|
|
59
|
+
})
|
|
60
|
+
}), jsx(PopoverContent, {
|
|
61
|
+
align: "start",
|
|
62
|
+
side: "bottom",
|
|
63
|
+
sideOffset: 6,
|
|
64
|
+
className: "z-[210] w-56 p-1",
|
|
65
|
+
children: jsx("div", {
|
|
66
|
+
className: "max-h-64 overflow-y-auto",
|
|
67
|
+
children: characters.map(c => {
|
|
68
|
+
const isActive = c.name === active?.name;
|
|
69
|
+
return jsxs("button", {
|
|
70
|
+
type: "button",
|
|
71
|
+
onClick: () => {
|
|
72
|
+
setActive(c.name);
|
|
73
|
+
setOpen(false);
|
|
74
|
+
},
|
|
75
|
+
className: cn('flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-muted', isActive ? 'bg-muted/60' : ''),
|
|
76
|
+
children: [jsx(AvatarBadge, {
|
|
77
|
+
entry: c,
|
|
78
|
+
size: 24
|
|
79
|
+
}), jsx("span", {
|
|
80
|
+
className: "flex-1 truncate",
|
|
81
|
+
children: c.name
|
|
82
|
+
}), isActive ? jsx(CheckIcon, {
|
|
83
|
+
className: "h-3.5 w-3.5 opacity-70"
|
|
84
|
+
}) : null]
|
|
85
|
+
}, c.name);
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})]
|
|
89
|
+
});
|
|
90
|
+
}export{CharacterPicker};//# sourceMappingURL=CharacterPicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CharacterPicker.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/Input.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/Input.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAQ/C,eAAO,MAAM,KAAK,GAAI,sTAsBnB,mBAAmB,GAAG;IACrB,QAAQ,EAAE,OAAO,CAAC;CACrB,4CAyTA,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {useRef,useMemo,useLayoutEffect}from'react';import {AIToolbarInputSurface}from'../AIToolbarPrimitives.js';import {XIcon,Loader2}from'lucide-react';import'../../../../shardui/accordion.js';import'../../../../shardui/alert.js';import'../../../../shardui/alert-dialog.js';import'@radix-ui/react-aspect-ratio';import'../../../../shardui/avatar.js';import'../../../../shardui/badge.js';import'../../../../shardui/breadcrumb.js';import {Button}from'../../../../shardui/button.js';import'../../../../shardui/calendar.js';import'../../../../shardui/card.js';import'../../../../shardui/carousel.js';import'../../../../shardui/chart.js';import'../../../../shardui/checkbox.js';import'@radix-ui/react-collapsible';import'../../../../shardui/command.js';import'../../../../shardui/context-menu.js';import'../../../../shardui/dialog.js';import'../../../../shardui/drawer.js';import'../../../../shardui/dropdown-menu.js';import'../../../../shardui/form.js';import'../../../../shardui/hover-card.js';import {Input as Input$1}from'../../../../shardui/input.js';import'../../../../shardui/input-otp.js';import'../../../../shardui/label.js';import'../../../../shardui/menubar.js';import'../../../../shardui/navigation-menu.js';import'../../../../shardui/pagination.js';import {Popover,PopoverContent,PopoverTrigger}from'../../../../shardui/popover.js';import'../../../../shardui/progress.js';import'../../../../shardui/radio-group.js';import'react-resizable-panels';import'../../../../utils/isBrowser/index.js';import {cn}from'../../../../utils/util.js';import'fast-deep-equal/react.js';import'../../../../shardui/scroll-area.js';import'../../../../shardui/select.js';import'../../../../shardui/separator.js';import'../../../../shardui/sheet.js';import'../../../../shardui/sidebar.js';import'../../../../shardui/slider.js';import'../../../ThemeProvider/ThemeProvider.js';import'sonner';import {Switch}from'../../../../shardui/switch.js';import'../../../../shardui/table.js';import'../../../../shardui/tabs.js';import'../../../../shardui/textarea.js';import'../../../../shardui/toast.js';import'../../../../shardui/toggle.js';import'../../../../shardui/toggle-group.js';import'../../../../shardui/tooltip.js';import {Markdown}from'../Markdown/index.js';import {AssistantMessageActions}from'./AssistantMessageActions.js';import {CopyButton}from'../Markdown/copy-button.js';import {MessageHistory}from'./MessageHistory.js';import {useActiveCharacter}from'../../hooks/useActiveCharacter.js';import {useChatBackgroundImage}from'../../hooks/useChatBackgroundImage.js';import {AssistantAvatar}from'./AssistantAvatar.js';const Input = ({
|
|
1
|
+
import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {useRef,useMemo,useLayoutEffect}from'react';import {AIToolbarInputSurface}from'../AIToolbarPrimitives.js';import {XIcon,Loader2}from'lucide-react';import'../../../../shardui/accordion.js';import'../../../../shardui/alert.js';import'../../../../shardui/alert-dialog.js';import'@radix-ui/react-aspect-ratio';import'../../../../shardui/avatar.js';import'../../../../shardui/badge.js';import'../../../../shardui/breadcrumb.js';import {Button}from'../../../../shardui/button.js';import'../../../../shardui/calendar.js';import'../../../../shardui/card.js';import'../../../../shardui/carousel.js';import'../../../../shardui/chart.js';import'../../../../shardui/checkbox.js';import'@radix-ui/react-collapsible';import'../../../../shardui/command.js';import'../../../../shardui/context-menu.js';import'../../../../shardui/dialog.js';import'../../../../shardui/drawer.js';import'../../../../shardui/dropdown-menu.js';import'../../../../shardui/form.js';import'../../../../shardui/hover-card.js';import {Input as Input$1}from'../../../../shardui/input.js';import'../../../../shardui/input-otp.js';import'../../../../shardui/label.js';import'../../../../shardui/menubar.js';import'../../../../shardui/navigation-menu.js';import'../../../../shardui/pagination.js';import {Popover,PopoverContent,PopoverTrigger}from'../../../../shardui/popover.js';import'../../../../shardui/progress.js';import'../../../../shardui/radio-group.js';import'react-resizable-panels';import'../../../../utils/isBrowser/index.js';import {cn}from'../../../../utils/util.js';import'fast-deep-equal/react.js';import'../../../../shardui/scroll-area.js';import'../../../../shardui/select.js';import'../../../../shardui/separator.js';import'../../../../shardui/sheet.js';import'../../../../shardui/sidebar.js';import'../../../../shardui/slider.js';import'../../../ThemeProvider/ThemeProvider.js';import'sonner';import {Switch}from'../../../../shardui/switch.js';import'../../../../shardui/table.js';import'../../../../shardui/tabs.js';import'../../../../shardui/textarea.js';import'../../../../shardui/toast.js';import'../../../../shardui/toggle.js';import'../../../../shardui/toggle-group.js';import'../../../../shardui/tooltip.js';import {Markdown}from'../Markdown/index.js';import {AssistantMessageActions}from'./AssistantMessageActions.js';import {CopyButton}from'../Markdown/copy-button.js';import {MessageHistory}from'./MessageHistory.js';import {useActiveCharacter}from'../../hooks/useActiveCharacter.js';import {useChatBackgroundImage}from'../../hooks/useChatBackgroundImage.js';import {AssistantAvatar}from'./AssistantAvatar.js';import {CharacterPicker}from'./CharacterPicker.js';const Input = ({
|
|
2
2
|
isPopoverOpen,
|
|
3
3
|
isLoading,
|
|
4
4
|
reset,
|
|
@@ -97,11 +97,11 @@ import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {useRef,useMemo,useLayo
|
|
|
97
97
|
}) : null, jsxs("div", {
|
|
98
98
|
className: "relative z-10 flex shrink-0 items-center justify-between border-b bg-muted/30 px-4 py-2",
|
|
99
99
|
children: [jsxs("div", {
|
|
100
|
-
className: "flex flex-row items-center gap-
|
|
100
|
+
className: "flex flex-row items-center gap-2",
|
|
101
101
|
children: [jsx("h3", {
|
|
102
102
|
className: "select-none text-xs font-semibold",
|
|
103
103
|
children: keepEngaged ? 'Conversation Mode' : 'AI Response'
|
|
104
|
-
}), jsx("div", {
|
|
104
|
+
}), jsx(CharacterPicker, {}), jsx("div", {
|
|
105
105
|
className: "text-[10px] text-muted-foreground/70",
|
|
106
106
|
children: "(Use arrow keys to scroll)"
|
|
107
107
|
})]
|
|
@@ -155,9 +155,9 @@ import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {useRef,useMemo,useLayo
|
|
|
155
155
|
children: "Error:"
|
|
156
156
|
}), " ", error]
|
|
157
157
|
}) : null, sortedAsc.map((msg, idx) => jsx("div", {
|
|
158
|
-
className: cn('flex w-full', msg.role === 'user' ? 'justify-end' : 'justify-start'),
|
|
158
|
+
className: cn('flex w-full gap-2', msg.role === 'user' ? 'justify-end' : 'justify-start'),
|
|
159
159
|
children: jsx("div", {
|
|
160
|
-
className: cn('rounded-xl px-3 py-2 text-sm shadow-sm', msg.role === 'user' ? 'max-w-[min(100%,40rem)] border border-border/60 bg-muted/70 text-foreground' : '
|
|
160
|
+
className: cn('rounded-xl px-3 py-2 text-sm shadow-sm', msg.role === 'user' ? 'max-w-[min(100%,40rem)] border border-border/60 bg-muted/70 text-foreground' : 'min-w-0 max-w-full flex-1 border border-border/60 bg-muted/70'),
|
|
161
161
|
children: msg.role === 'user' ? jsxs(Fragment, {
|
|
162
162
|
children: [jsxs("div", {
|
|
163
163
|
className: "mb-1.5 flex items-center gap-2",
|
|
@@ -6,13 +6,28 @@ export interface ActiveCharacter {
|
|
|
6
6
|
/** Pre-baked `data:` URL for the avatar PNG, or null if not yet loaded. */
|
|
7
7
|
avatarSrc: string | null;
|
|
8
8
|
}
|
|
9
|
+
export interface CharacterListEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
fileName?: string;
|
|
12
|
+
avatarSrc: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface CharacterPicker {
|
|
15
|
+
/** All installed characters with resolved avatars. */
|
|
16
|
+
characters: CharacterListEntry[];
|
|
17
|
+
/** Currently active character name, or null if none picked / none installed. */
|
|
18
|
+
activeName: string | null;
|
|
19
|
+
/** Switch the active character by name. Pass null to clear (revert to first). */
|
|
20
|
+
setActive: (name: string | null) => void;
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
|
-
* Return the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Today the desktop ships at most one active character at a time, so we just
|
|
14
|
-
* pick the first entry. When multi-character support lands we'll switch this
|
|
15
|
-
* to read from a presence bus.
|
|
23
|
+
* Return the active character (name + avatar) or null when none is installed
|
|
24
|
+
* / we're not running under Tauri. Selection persists via localStorage and
|
|
25
|
+
* follows changes made by `useCharacterPicker`.
|
|
16
26
|
*/
|
|
17
27
|
export declare function useActiveCharacter(): ActiveCharacter | null;
|
|
28
|
+
/**
|
|
29
|
+
* Enumerate installed characters and switch the active one. Empty list and
|
|
30
|
+
* `activeName === null` outside Tauri / when nothing is installed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useCharacterPicker(): CharacterPicker;
|
|
18
33
|
//# sourceMappingURL=useActiveCharacter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useActiveCharacter.d.ts","sourceRoot":"","sources":["../../../../src/components/AIToolbar/hooks/useActiveCharacter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useActiveCharacter.d.ts","sourceRoot":"","sources":["../../../../src/components/AIToolbar/hooks/useActiveCharacter.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,eAAe;IAC5B,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC5B,sDAAsD;IACtD,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,gFAAgF;IAChF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iFAAiF;IACjF,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC5C;AA2GD;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CA8D3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAqFpD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import*as React from'react';// Active-character lookup for the AIToolbar.
|
|
2
2
|
//
|
|
3
|
-
// Surfaces the currently-
|
|
3
|
+
// Surfaces the currently-selected character (name + avatar data URL) so the
|
|
4
4
|
// completion popover and message history can show "Seraphina" with her avatar
|
|
5
5
|
// instead of a generic "AI" label. Backed by the desktop Tauri commands
|
|
6
6
|
// `character_list` and `character_get_avatar` (see
|
|
@@ -11,11 +11,25 @@ import*as React from'react';// Active-character lookup for the AIToolbar.
|
|
|
11
11
|
// - Tauri without any character-contributing extension installed: returns null.
|
|
12
12
|
// - Tauri with an extension but no avatar: returns name-only (avatarSrc = null).
|
|
13
13
|
//
|
|
14
|
-
// Re-fetches on the `yantra:character-refresh` Tauri event
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Re-fetches on the `yantra:character-refresh` Tauri event + the
|
|
15
|
+
// `yantra:extensions-changed` window event (marketplace install/uninstall).
|
|
16
|
+
//
|
|
17
|
+
// When multiple characters are installed, the active one is chosen by name
|
|
18
|
+
// from localStorage (`yantra:active-character-name`). Use `useCharacterPicker`
|
|
19
|
+
// to enumerate and switch.
|
|
17
20
|
// constants
|
|
18
21
|
const CHARACTER_REFRESH_EVENT = 'yantra:character-refresh';
|
|
22
|
+
/**
|
|
23
|
+
* Window event dispatched by `yantra-app/portable-devices/desktop/src/utils/marketplaceInstall.ts`
|
|
24
|
+
* after the marketplace install / uninstall path triggers `character_refresh`.
|
|
25
|
+
* Listening here lets the avatar swap in without an app restart.
|
|
26
|
+
*/
|
|
27
|
+
const EXTENSIONS_CHANGED_EVENT = 'yantra:extensions-changed';
|
|
28
|
+
/** Window event dispatched by `useCharacterPicker().setActive(...)` to notify subscribers. */
|
|
29
|
+
const ACTIVE_CHARACTER_CHANGED_EVENT = 'yantra:active-character-changed';
|
|
30
|
+
const ACTIVE_CHARACTER_STORAGE_KEY = 'yantra:active-character-name';
|
|
31
|
+
/** Manifest contribution key emitted by extensions that ship character cards. */
|
|
32
|
+
const CHARACTER_CONTRIBUTION = 'characters';
|
|
19
33
|
// helpers
|
|
20
34
|
function isTauri() {
|
|
21
35
|
if (typeof window === 'undefined') return false;
|
|
@@ -43,23 +57,63 @@ async function safeListen(event, cb) {
|
|
|
43
57
|
return () => {};
|
|
44
58
|
}
|
|
45
59
|
}
|
|
60
|
+
function readStoredActiveName() {
|
|
61
|
+
if (typeof window === 'undefined') return null;
|
|
62
|
+
try {
|
|
63
|
+
return window.localStorage.getItem(ACTIVE_CHARACTER_STORAGE_KEY);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decide whether an `extensions-changed` event is relevant to characters.
|
|
70
|
+
* Detail with no `contributes` array is treated as relevant (defensive
|
|
71
|
+
* reload) so we never miss a real change.
|
|
72
|
+
*/
|
|
73
|
+
function isCharacterRelevant(event) {
|
|
74
|
+
const detail = event.detail;
|
|
75
|
+
if (!detail || !Array.isArray(detail.contributes)) return true;
|
|
76
|
+
return detail.contributes.includes(CHARACTER_CONTRIBUTION);
|
|
77
|
+
}
|
|
78
|
+
function writeStoredActiveName(name) {
|
|
79
|
+
if (typeof window === 'undefined') return;
|
|
80
|
+
try {
|
|
81
|
+
if (name == null) window.localStorage.removeItem(ACTIVE_CHARACTER_STORAGE_KEY);else window.localStorage.setItem(ACTIVE_CHARACTER_STORAGE_KEY, name);
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore quota / privacy-mode errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function fetchAvatarSrc(fileName) {
|
|
87
|
+
if (!fileName) return null;
|
|
88
|
+
const dto = await safeInvoke('character_get_avatar', {
|
|
89
|
+
fileName
|
|
90
|
+
});
|
|
91
|
+
if (!dto || !dto.base64 || dto.base64.length <= 100) return null;
|
|
92
|
+
return `data:${dto.mime_type || 'image/png'};base64,${dto.base64}`;
|
|
93
|
+
}
|
|
94
|
+
function pickActive(list, desiredName) {
|
|
95
|
+
if (list.length === 0) return null;
|
|
96
|
+
if (desiredName) {
|
|
97
|
+
const match = list.find(c => c.name === desiredName);
|
|
98
|
+
if (match) return match;
|
|
99
|
+
}
|
|
100
|
+
return list[0];
|
|
101
|
+
}
|
|
46
102
|
// main API
|
|
47
103
|
/**
|
|
48
|
-
* Return the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Today the desktop ships at most one active character at a time, so we just
|
|
52
|
-
* pick the first entry. When multi-character support lands we'll switch this
|
|
53
|
-
* to read from a presence bus.
|
|
104
|
+
* Return the active character (name + avatar) or null when none is installed
|
|
105
|
+
* / we're not running under Tauri. Selection persists via localStorage and
|
|
106
|
+
* follows changes made by `useCharacterPicker`.
|
|
54
107
|
*/
|
|
55
108
|
function useActiveCharacter() {
|
|
56
|
-
const [
|
|
109
|
+
const [list, setList] = React.useState([]);
|
|
110
|
+
const [activeName, setActiveName] = React.useState(() => readStoredActiveName());
|
|
57
111
|
const [avatarSrc, setAvatarSrc] = React.useState(null);
|
|
58
112
|
const reload = React.useCallback(async () => {
|
|
59
|
-
const
|
|
113
|
+
const next = await safeInvoke('character_list', {
|
|
60
114
|
shallow: false
|
|
61
115
|
});
|
|
62
|
-
|
|
116
|
+
setList(next ?? []);
|
|
63
117
|
}, []);
|
|
64
118
|
React.useEffect(() => {
|
|
65
119
|
void reload();
|
|
@@ -72,32 +126,132 @@ function useActiveCharacter() {
|
|
|
72
126
|
}).then(fn => {
|
|
73
127
|
if (cancelled) fn();else unlisten = fn;
|
|
74
128
|
});
|
|
129
|
+
function onExtensionsChanged(e) {
|
|
130
|
+
if (!isCharacterRelevant(e)) return;
|
|
131
|
+
void reload();
|
|
132
|
+
}
|
|
133
|
+
function onActiveChanged() {
|
|
134
|
+
setActiveName(readStoredActiveName());
|
|
135
|
+
}
|
|
136
|
+
if (typeof window !== 'undefined') {
|
|
137
|
+
window.addEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
|
|
138
|
+
window.addEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
|
|
139
|
+
}
|
|
75
140
|
return () => {
|
|
76
141
|
cancelled = true;
|
|
77
142
|
if (unlisten) unlisten();
|
|
143
|
+
if (typeof window !== 'undefined') {
|
|
144
|
+
window.removeEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
|
|
145
|
+
window.removeEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
|
|
146
|
+
}
|
|
78
147
|
};
|
|
79
148
|
}, [reload]);
|
|
149
|
+
const active = React.useMemo(() => pickActive(list, activeName), [list, activeName]);
|
|
80
150
|
React.useEffect(() => {
|
|
81
|
-
if (!
|
|
151
|
+
if (!active?.file_name) {
|
|
82
152
|
setAvatarSrc(null);
|
|
83
153
|
return;
|
|
84
154
|
}
|
|
85
155
|
let alive = true;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}).then(dto => {
|
|
89
|
-
if (!alive) return;
|
|
90
|
-
const valid = dto && dto.base64 && dto.base64.length > 100;
|
|
91
|
-
setAvatarSrc(valid ? `data:${dto.mime_type || 'image/png'};base64,${dto.base64}` : null);
|
|
156
|
+
fetchAvatarSrc(active.file_name).then(src => {
|
|
157
|
+
if (alive) setAvatarSrc(src);
|
|
92
158
|
});
|
|
93
159
|
return () => {
|
|
94
160
|
alive = false;
|
|
95
161
|
};
|
|
96
|
-
}, [
|
|
97
|
-
if (!
|
|
162
|
+
}, [active?.file_name]);
|
|
163
|
+
if (!active) return null;
|
|
98
164
|
return {
|
|
99
|
-
name:
|
|
100
|
-
fileName:
|
|
165
|
+
name: active.name,
|
|
166
|
+
fileName: active.file_name,
|
|
101
167
|
avatarSrc
|
|
102
168
|
};
|
|
103
|
-
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Enumerate installed characters and switch the active one. Empty list and
|
|
172
|
+
* `activeName === null` outside Tauri / when nothing is installed.
|
|
173
|
+
*/
|
|
174
|
+
function useCharacterPicker() {
|
|
175
|
+
const [list, setList] = React.useState([]);
|
|
176
|
+
const [avatars, setAvatars] = React.useState({});
|
|
177
|
+
const [activeName, setActiveNameState] = React.useState(() => readStoredActiveName());
|
|
178
|
+
const reload = React.useCallback(async () => {
|
|
179
|
+
const next = await safeInvoke('character_list', {
|
|
180
|
+
shallow: false
|
|
181
|
+
});
|
|
182
|
+
setList(next ?? []);
|
|
183
|
+
}, []);
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
void reload();
|
|
186
|
+
}, [reload]);
|
|
187
|
+
React.useEffect(() => {
|
|
188
|
+
let unlisten = null;
|
|
189
|
+
let cancelled = false;
|
|
190
|
+
safeListen(CHARACTER_REFRESH_EVENT, () => {
|
|
191
|
+
void reload();
|
|
192
|
+
}).then(fn => {
|
|
193
|
+
if (cancelled) fn();else unlisten = fn;
|
|
194
|
+
});
|
|
195
|
+
function onExtensionsChanged(e) {
|
|
196
|
+
if (!isCharacterRelevant(e)) return;
|
|
197
|
+
void reload();
|
|
198
|
+
}
|
|
199
|
+
function onActiveChanged() {
|
|
200
|
+
setActiveNameState(readStoredActiveName());
|
|
201
|
+
}
|
|
202
|
+
if (typeof window !== 'undefined') {
|
|
203
|
+
window.addEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
|
|
204
|
+
window.addEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
|
|
205
|
+
}
|
|
206
|
+
return () => {
|
|
207
|
+
cancelled = true;
|
|
208
|
+
if (unlisten) unlisten();
|
|
209
|
+
if (typeof window !== 'undefined') {
|
|
210
|
+
window.removeEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
|
|
211
|
+
window.removeEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}, [reload]);
|
|
215
|
+
// Resolve avatars for every character in the list (cached by file_name).
|
|
216
|
+
React.useEffect(() => {
|
|
217
|
+
let alive = true;
|
|
218
|
+
async function loadAll() {
|
|
219
|
+
const pending = list.filter(c => c.file_name && !(c.file_name in avatars));
|
|
220
|
+
if (pending.length === 0) return;
|
|
221
|
+
const entries = await Promise.all(pending.map(async c => [c.file_name, await fetchAvatarSrc(c.file_name)]));
|
|
222
|
+
if (!alive) return;
|
|
223
|
+
setAvatars(prev => {
|
|
224
|
+
const next = {
|
|
225
|
+
...prev
|
|
226
|
+
};
|
|
227
|
+
for (const [key, src] of entries) next[key] = src;
|
|
228
|
+
return next;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
void loadAll();
|
|
232
|
+
return () => {
|
|
233
|
+
alive = false;
|
|
234
|
+
};
|
|
235
|
+
}, [list, avatars]);
|
|
236
|
+
const characters = React.useMemo(() => list.map(c => ({
|
|
237
|
+
name: c.name,
|
|
238
|
+
fileName: c.file_name,
|
|
239
|
+
avatarSrc: c.file_name ? avatars[c.file_name] ?? null : null
|
|
240
|
+
})), [list, avatars]);
|
|
241
|
+
const setActive = React.useCallback(name => {
|
|
242
|
+
writeStoredActiveName(name);
|
|
243
|
+
setActiveNameState(name);
|
|
244
|
+
if (typeof window !== 'undefined') {
|
|
245
|
+
window.dispatchEvent(new CustomEvent(ACTIVE_CHARACTER_CHANGED_EVENT, {
|
|
246
|
+
detail: {
|
|
247
|
+
name
|
|
248
|
+
}
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
}, []);
|
|
252
|
+
return {
|
|
253
|
+
characters,
|
|
254
|
+
activeName,
|
|
255
|
+
setActive
|
|
256
|
+
};
|
|
257
|
+
}export{useActiveCharacter,useCharacterPicker};//# sourceMappingURL=useActiveCharacter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useActiveCharacter.js","sources":["../../../../src/components/AIToolbar/hooks/useActiveCharacter.ts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useActiveCharacter.js","sources":["../../../../src/components/AIToolbar/hooks/useActiveCharacter.ts"],"sourcesContent":[null],"names":[],"mappings":"4BAwBA;;;;;;AAMI;AACH;AAED;;;AAGI;AACH;AAED;;;;AAII;;AAGH;AA2GD,MAAA,uBAAA,GAAA,0BAAA;;;;AAIG;AACH;AAgEA,MAAA,wBAAA,GAAA,2BAAA;;;AAGG,MAAA,4BAAA,GAAA,8BAAA;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@admin-layout/tailwind-ui",
|
|
3
|
-
"version": "12.2.4-alpha.
|
|
3
|
+
"version": "12.2.4-alpha.57",
|
|
4
4
|
"description": "Sample core for higher packages to depend on",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "CDMBase LLC",
|
|
@@ -96,5 +96,5 @@
|
|
|
96
96
|
"typescript": {
|
|
97
97
|
"definition": "lib/index.d.ts"
|
|
98
98
|
},
|
|
99
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "134adf9207d6cad519b1fd25f4c39615b3682354"
|
|
100
100
|
}
|