@admin-layout/tailwind-ui 12.2.4-alpha.56 → 12.2.4-alpha.58

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.
Files changed (27) hide show
  1. package/lib/components/AIToolbar/components/completion/AssistantAvatar.d.ts +13 -1
  2. package/lib/components/AIToolbar/components/completion/AssistantAvatar.d.ts.map +1 -1
  3. package/lib/components/AIToolbar/components/completion/AssistantAvatar.js +17 -5
  4. package/lib/components/AIToolbar/components/completion/AssistantAvatar.js.map +1 -1
  5. package/lib/components/AIToolbar/components/completion/CharacterPicker.d.ts +3 -0
  6. package/lib/components/AIToolbar/components/completion/CharacterPicker.d.ts.map +1 -0
  7. package/lib/components/AIToolbar/components/completion/CharacterPicker.js +90 -0
  8. package/lib/components/AIToolbar/components/completion/CharacterPicker.js.map +1 -0
  9. package/lib/components/AIToolbar/components/completion/Input.d.ts.map +1 -1
  10. package/lib/components/AIToolbar/components/completion/Input.js +11 -7
  11. package/lib/components/AIToolbar/components/completion/Input.js.map +1 -1
  12. package/lib/components/AIToolbar/components/completion/MessageHistory.d.ts.map +1 -1
  13. package/lib/components/AIToolbar/components/completion/MessageHistory.js +3 -1
  14. package/lib/components/AIToolbar/components/completion/MessageHistory.js.map +1 -1
  15. package/lib/components/AIToolbar/hooks/useActiveCharacter.d.ts +21 -6
  16. package/lib/components/AIToolbar/hooks/useActiveCharacter.d.ts.map +1 -1
  17. package/lib/components/AIToolbar/hooks/useActiveCharacter.js +183 -26
  18. package/lib/components/AIToolbar/hooks/useActiveCharacter.js.map +1 -1
  19. package/lib/components/AIToolbar/hooks/useCharacterSprite.d.ts +14 -0
  20. package/lib/components/AIToolbar/hooks/useCharacterSprite.d.ts.map +1 -0
  21. package/lib/components/AIToolbar/hooks/useCharacterSprite.js +103 -0
  22. package/lib/components/AIToolbar/hooks/useCharacterSprite.js.map +1 -0
  23. package/lib/components/AIToolbar/utils/emotionClassifier.d.ts +19 -0
  24. package/lib/components/AIToolbar/utils/emotionClassifier.d.ts.map +1 -0
  25. package/lib/components/AIToolbar/utils/emotionClassifier.js +142 -0
  26. package/lib/components/AIToolbar/utils/emotionClassifier.js.map +1 -0
  27. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  interface AssistantAvatarProps {
2
+ /** Static fallback avatar (default portrait). Used when no sprite is resolved. */
2
3
  avatarSrc: string | null;
3
4
  /** Optional message sequence number rendered as "#N" beneath the avatar. */
4
5
  index?: number;
@@ -6,7 +7,18 @@ interface AssistantAvatarProps {
6
7
  size?: number;
7
8
  /** Tailwind classes applied to the column wrapper. */
8
9
  className?: string;
10
+ /**
11
+ * Character `file_name` (i.e. the PNG card stem — "Seraphina"). When set
12
+ * together with `text`, the avatar switches to the matching expression
13
+ * sprite from the character's sprite folder.
14
+ */
15
+ characterFileName?: string | null;
16
+ /**
17
+ * Message text used to pick an emotion sprite. The classifier maps the
18
+ * text to one of 28 GoEmotions labels (`neutral`, `joy`, `caring`, ...).
19
+ */
20
+ text?: string | null;
9
21
  }
10
- export declare function AssistantAvatar({ avatarSrc, index, size, className }: AssistantAvatarProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function AssistantAvatar({ avatarSrc, index, size, className, characterFileName, text, }: AssistantAvatarProps): import("react/jsx-runtime").JSX.Element;
11
23
  export default AssistantAvatar;
12
24
  //# sourceMappingURL=AssistantAvatar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantAvatar.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/AssistantAvatar.tsx"],"names":[],"mappings":"AASA,UAAU,oBAAoB;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAS,EAAE,SAAS,EAAE,EAAE,oBAAoB,2CA6B/F;AAED,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"AssistantAvatar.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/AssistantAvatar.tsx"],"names":[],"mappings":"AAgBA,UAAU,oBAAoB;IAC1B,kFAAkF;IAClF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAC5B,SAAS,EACT,KAAK,EACL,IAAS,EACT,SAAS,EACT,iBAAiB,EACjB,IAAI,GACP,EAAE,oBAAoB,2CA2CtB;AAED,eAAe,eAAe,CAAC"}
@@ -1,15 +1,27 @@
1
- import {jsxs,jsx}from'react/jsx-runtime';import'../../../../utils/isBrowser/index.js';import {cn}from'../../../../utils/util.js';import'react';import'fast-deep-equal/react.js';function AssistantAvatar({
1
+ import {jsxs,jsx}from'react/jsx-runtime';import*as React from'react';import'../../../../utils/isBrowser/index.js';import {cn}from'../../../../utils/util.js';import'fast-deep-equal/react.js';import {useCharacterSprite}from'../../hooks/useCharacterSprite.js';import {classifyEmotion}from'../../utils/emotionClassifier.js';function AssistantAvatar({
2
2
  avatarSrc,
3
3
  index,
4
- size = 44,
5
- className
4
+ size = 60,
5
+ className,
6
+ characterFileName,
7
+ text
6
8
  }) {
7
9
  const hasNumber = typeof index === 'number';
10
+ // Pick an emotion only when we actually have a character that can supply
11
+ // a sprite — avoids burning a Tauri round-trip for non-Tauri builds.
12
+ const emotion = React.useMemo(() => {
13
+ if (!characterFileName || !text) return null;
14
+ return classifyEmotion(text);
15
+ }, [characterFileName, text]);
16
+ const spriteSrc = useCharacterSprite(characterFileName ?? null, emotion);
17
+ // Prefer the emotion sprite; fall back to the static avatar while it loads
18
+ // or when sprite resolution returned null (no character / outside Tauri).
19
+ const renderedSrc = spriteSrc ?? avatarSrc;
8
20
  return jsxs("div", {
9
21
  className: cn('flex shrink-0 flex-col items-center gap-1 pt-0.5', className),
10
22
  "aria-hidden": !hasNumber,
11
- children: [avatarSrc ? jsx("img", {
12
- src: avatarSrc,
23
+ children: [renderedSrc ? jsx("img", {
24
+ src: renderedSrc,
13
25
  alt: "",
14
26
  "aria-hidden": "true",
15
27
  draggable: false,
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantAvatar.js","sources":["../../../../../src/components/AIToolbar/components/completion/AssistantAvatar.tsx"],"sourcesContent":[null],"names":["_jsxs"],"mappings":"gLAWI,SAAA,eAAA,CAAA;WACM;OACN;MACA,GAAK,EAAE;;;AAGV,EAAA,MAAA,SAAA,GAAA,OAAA,KAAA,KAAA,QAAA;AAED,EAAA,OAAAA,IAAA,CAAA,KAAA,EAAA;AA+BA,IAAA,SAAA,EAAe,qDAAgB,EAAA,SAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"AssistantAvatar.js","sources":["../../../../../src/components/AIToolbar/components/completion/AssistantAvatar.tsx"],"sourcesContent":[null],"names":[],"mappings":"gUA+BI,SAAA,eAAA,CAAA;;;AAGG,EAAA,IAAA,GAAA,EAAA;AACH,EAAA,SAAO;AACV,EAAA,iBAAA;AAED,EAAA;AAoDA,CAAA,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,3 @@
1
+ export declare function CharacterPicker(): import("react/jsx-runtime").JSX.Element;
2
+ export default CharacterPicker;
3
+ //# sourceMappingURL=CharacterPicker.d.ts.map
@@ -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;AAO/C,eAAO,MAAM,KAAK,GAAI,sTAsBnB,mBAAmB,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,4CAoT7C,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,4CA6TA,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-1",
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' : 'w-full min-w-0 max-w-full border border-border/60 bg-muted/70'),
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",
@@ -178,7 +178,9 @@ import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {useRef,useMemo,useLayo
178
178
  className: "flex w-full gap-3",
179
179
  children: [jsx(AssistantAvatar, {
180
180
  avatarSrc: character?.avatarSrc ?? null,
181
- index: idx + 1
181
+ index: idx + 1,
182
+ characterFileName: character?.fileName ?? null,
183
+ text: msg.content
182
184
  }), jsxs("div", {
183
185
  className: "min-w-0 flex-1",
184
186
  children: [jsxs("div", {
@@ -211,7 +213,9 @@ import {jsx,jsxs,Fragment}from'react/jsx-runtime';import {useRef,useMemo,useLayo
211
213
  className: "flex w-full gap-3",
212
214
  children: [jsx(AssistantAvatar, {
213
215
  avatarSrc: character?.avatarSrc ?? null,
214
- index: sortedAsc.length + 1
216
+ index: sortedAsc.length + 1,
217
+ characterFileName: character?.fileName ?? null,
218
+ text: response
215
219
  }), jsxs("div", {
216
220
  className: "min-w-0 flex-1",
217
221
  children: [jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"Input.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"Input.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"MessageHistory.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/MessageHistory.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM5C,UAAU,mBAAmB;IACzB,mBAAmB,EAAE,WAAW,EAAE,CAAC;IACnC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAED,eAAO,MAAM,cAAc,GAAI,6FAK5B,mBAAmB,4CAuIrB,CAAC"}
1
+ {"version":3,"file":"MessageHistory.d.ts","sourceRoot":"","sources":["../../../../../src/components/AIToolbar/components/completion/MessageHistory.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM5C,UAAU,mBAAmB;IACzB,mBAAmB,EAAE,WAAW,EAAE,CAAC;IACnC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAED,eAAO,MAAM,cAAc,GAAI,6FAK5B,mBAAmB,4CAyIrB,CAAC"}
@@ -101,7 +101,9 @@ import {jsxs,jsx,Fragment}from'react/jsx-runtime';import {MessageSquareText,Chev
101
101
  className: "flex w-full gap-3",
102
102
  children: [jsx(AssistantAvatar, {
103
103
  avatarSrc: character?.avatarSrc ?? null,
104
- index: arr.length - descIdx
104
+ index: arr.length - descIdx,
105
+ characterFileName: character?.fileName ?? null,
106
+ text: message.content
105
107
  }), jsxs("div", {
106
108
  className: "min-w-0 flex-1",
107
109
  children: [jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"MessageHistory.js","sources":["../../../../../src/components/AIToolbar/components/completion/MessageHistory.tsx"],"sourcesContent":[null],"names":[],"mappings":"i1EAcC,MAAA,cAAA,GAAA,CAAA;AAED,EAAA,mBAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"MessageHistory.js","sources":["../../../../../src/components/AIToolbar/components/completion/MessageHistory.tsx"],"sourcesContent":[null],"names":[],"mappings":"i1EAcC,MAAA,cAAA,GAAA,CAAA;AAED,EAAA,mBAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -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 first discovered character (name + avatar) or null when no
11
- * character extension is installed / we're not running under Tauri.
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":"AAqBA,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;AAkDD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CA8C3D"}
1
+ {"version":3,"file":"useActiveCharacter.d.ts","sourceRoot":"","sources":["../../../../src/components/AIToolbar/hooks/useActiveCharacter.ts"],"names":[],"mappings":"AAyBA,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,CAiE3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAqFpD"}
@@ -1,6 +1,6 @@
1
- import*as React from'react';// Active-character lookup for the AIToolbar.
1
+ import*as React from'react';import {clearCharacterSpriteCache}from'./useCharacterSprite.js';// Active-character lookup for the AIToolbar.
2
2
  //
3
- // Surfaces the currently-installed character (name + avatar data URL) so the
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, which the desktop
15
- // shell emits after the marketplace install/uninstall path triggers
16
- // `character_refresh`. Hook will quietly stay current as extensions come and go.
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 first discovered character (name + avatar) or null when no
49
- * character extension is installed / we're not running under Tauri.
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 [character, setCharacter] = React.useState(null);
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 list = await safeInvoke('character_list', {
113
+ const next = await safeInvoke('character_list', {
60
114
  shallow: false
61
115
  });
62
- setCharacter(list?.[0] ?? null);
116
+ setList(next ?? []);
63
117
  }, []);
64
118
  React.useEffect(() => {
65
119
  void reload();
@@ -68,36 +122,139 @@ function useActiveCharacter() {
68
122
  let unlisten = null;
69
123
  let cancelled = false;
70
124
  safeListen(CHARACTER_REFRESH_EVENT, () => {
125
+ // Sprites may have changed on disk (new install / uninstall); drop
126
+ // cached data URLs so the next render re-fetches.
127
+ clearCharacterSpriteCache();
71
128
  void reload();
72
129
  }).then(fn => {
73
130
  if (cancelled) fn();else unlisten = fn;
74
131
  });
132
+ function onExtensionsChanged(e) {
133
+ if (!isCharacterRelevant(e)) return;
134
+ void reload();
135
+ }
136
+ function onActiveChanged() {
137
+ setActiveName(readStoredActiveName());
138
+ }
139
+ if (typeof window !== 'undefined') {
140
+ window.addEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
141
+ window.addEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
142
+ }
75
143
  return () => {
76
144
  cancelled = true;
77
145
  if (unlisten) unlisten();
146
+ if (typeof window !== 'undefined') {
147
+ window.removeEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
148
+ window.removeEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
149
+ }
78
150
  };
79
151
  }, [reload]);
152
+ const active = React.useMemo(() => pickActive(list, activeName), [list, activeName]);
80
153
  React.useEffect(() => {
81
- if (!character?.file_name) {
154
+ if (!active?.file_name) {
82
155
  setAvatarSrc(null);
83
156
  return;
84
157
  }
85
158
  let alive = true;
86
- safeInvoke('character_get_avatar', {
87
- fileName: character.file_name
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);
159
+ fetchAvatarSrc(active.file_name).then(src => {
160
+ if (alive) setAvatarSrc(src);
92
161
  });
93
162
  return () => {
94
163
  alive = false;
95
164
  };
96
- }, [character?.file_name]);
97
- if (!character) return null;
165
+ }, [active?.file_name]);
166
+ if (!active) return null;
98
167
  return {
99
- name: character.name,
100
- fileName: character.file_name,
168
+ name: active.name,
169
+ fileName: active.file_name,
101
170
  avatarSrc
102
171
  };
103
- }export{useActiveCharacter};//# sourceMappingURL=useActiveCharacter.js.map
172
+ }
173
+ /**
174
+ * Enumerate installed characters and switch the active one. Empty list and
175
+ * `activeName === null` outside Tauri / when nothing is installed.
176
+ */
177
+ function useCharacterPicker() {
178
+ const [list, setList] = React.useState([]);
179
+ const [avatars, setAvatars] = React.useState({});
180
+ const [activeName, setActiveNameState] = React.useState(() => readStoredActiveName());
181
+ const reload = React.useCallback(async () => {
182
+ const next = await safeInvoke('character_list', {
183
+ shallow: false
184
+ });
185
+ setList(next ?? []);
186
+ }, []);
187
+ React.useEffect(() => {
188
+ void reload();
189
+ }, [reload]);
190
+ React.useEffect(() => {
191
+ let unlisten = null;
192
+ let cancelled = false;
193
+ safeListen(CHARACTER_REFRESH_EVENT, () => {
194
+ void reload();
195
+ }).then(fn => {
196
+ if (cancelled) fn();else unlisten = fn;
197
+ });
198
+ function onExtensionsChanged(e) {
199
+ if (!isCharacterRelevant(e)) return;
200
+ void reload();
201
+ }
202
+ function onActiveChanged() {
203
+ setActiveNameState(readStoredActiveName());
204
+ }
205
+ if (typeof window !== 'undefined') {
206
+ window.addEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
207
+ window.addEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
208
+ }
209
+ return () => {
210
+ cancelled = true;
211
+ if (unlisten) unlisten();
212
+ if (typeof window !== 'undefined') {
213
+ window.removeEventListener(EXTENSIONS_CHANGED_EVENT, onExtensionsChanged);
214
+ window.removeEventListener(ACTIVE_CHARACTER_CHANGED_EVENT, onActiveChanged);
215
+ }
216
+ };
217
+ }, [reload]);
218
+ // Resolve avatars for every character in the list (cached by file_name).
219
+ React.useEffect(() => {
220
+ let alive = true;
221
+ async function loadAll() {
222
+ const pending = list.filter(c => c.file_name && !(c.file_name in avatars));
223
+ if (pending.length === 0) return;
224
+ const entries = await Promise.all(pending.map(async c => [c.file_name, await fetchAvatarSrc(c.file_name)]));
225
+ if (!alive) return;
226
+ setAvatars(prev => {
227
+ const next = {
228
+ ...prev
229
+ };
230
+ for (const [key, src] of entries) next[key] = src;
231
+ return next;
232
+ });
233
+ }
234
+ void loadAll();
235
+ return () => {
236
+ alive = false;
237
+ };
238
+ }, [list, avatars]);
239
+ const characters = React.useMemo(() => list.map(c => ({
240
+ name: c.name,
241
+ fileName: c.file_name,
242
+ avatarSrc: c.file_name ? avatars[c.file_name] ?? null : null
243
+ })), [list, avatars]);
244
+ const setActive = React.useCallback(name => {
245
+ writeStoredActiveName(name);
246
+ setActiveNameState(name);
247
+ if (typeof window !== 'undefined') {
248
+ window.dispatchEvent(new CustomEvent(ACTIVE_CHARACTER_CHANGED_EVENT, {
249
+ detail: {
250
+ name
251
+ }
252
+ }));
253
+ }
254
+ }, []);
255
+ return {
256
+ characters,
257
+ activeName,
258
+ setActive
259
+ };
260
+ }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":"4BAqBA;;;;;;AAMI;AACH;AAkDD;;;;;;;AAOG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"useActiveCharacter.js","sources":["../../../../src/components/AIToolbar/hooks/useActiveCharacter.ts"],"sourcesContent":[null],"names":[],"mappings":"4FAyBA;;;;;;AAMI;AACH;AAED;;;AAGI;AACH;AAED;;;;AAII;;AA8GJ;;;;AAIG;AACH;AAmEA;;;AAGG,MAAA,8BAAA,GAAA,iCAAA;AACH,MAAA,4BAAgB,GAAA,8BAqFf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Returns the data URL for `(characterFileName, label)` or `null` while the
3
+ * sprite is loading / when running outside Tauri / when no character is set.
4
+ *
5
+ * Re-evaluates whenever either input changes. Cached results resolve in the
6
+ * same render (no transient `null`).
7
+ */
8
+ export declare function useCharacterSprite(characterFileName: string | null | undefined, label: string | null | undefined): string | null;
9
+ /**
10
+ * Clears the sprite cache. Call after `character_refresh` so newly-installed
11
+ * sprite packs aren't shadowed by stale data URLs.
12
+ */
13
+ export declare function clearCharacterSpriteCache(): void;
14
+ //# sourceMappingURL=useCharacterSprite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCharacterSprite.d.ts","sourceRoot":"","sources":["../../../../src/components/AIToolbar/hooks/useCharacterSprite.ts"],"names":[],"mappings":"AAqFA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACjC,MAAM,GAAG,IAAI,CAoBf;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD"}
@@ -0,0 +1,103 @@
1
+ import*as React from'react';// Expression-sprite lookup for assistant message bubbles.
2
+ //
3
+ // Resolves `(characterFileName, label)` → `data:image/png;base64,...` via the
4
+ // Tauri command `character_get_expression`, with a module-level cache so the
5
+ // same sprite is fetched at most once per session even when many messages map
6
+ // to the same emotion.
7
+ //
8
+ // The Rust side (`character_get_expression`) already implements graceful
9
+ // fallback through `neutral`/`joy`/avatar when the requested label is missing,
10
+ // so callers can pass any of the 28 GoEmotions labels without first checking
11
+ // which sprites the character actually ships.
12
+ //
13
+ // Degrades to `null` outside Tauri / when no character file_name is provided.
14
+ function isTauri() {
15
+ if (typeof window === 'undefined') return false;
16
+ const w = window;
17
+ return Boolean(w.__TAURI_INTERNALS__ ?? w.__TAURI__);
18
+ }
19
+ async function safeInvoke(cmd, args) {
20
+ if (!isTauri()) return null;
21
+ try {
22
+ // @ts-ignore — optional dynamic import, present as a peer dep in Tauri builds.
23
+ const mod = await import('@tauri-apps/api/core');
24
+ return await mod.invoke(cmd, args);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ // Two layered caches keyed by `${fileName}::${label}`:
30
+ //
31
+ // - `urlCache` resolves synchronously on subsequent calls (no re-render flicker).
32
+ // - `pendingCache` deduplicates concurrent fetches for the same key when
33
+ // several messages on screen happen to share the same emotion.
34
+ const urlCache = new Map();
35
+ const pendingCache = new Map();
36
+ function cacheKey(fileName, label) {
37
+ return `${fileName}::${label}`;
38
+ }
39
+ async function fetchSprite(fileName, label) {
40
+ const key = cacheKey(fileName, label);
41
+ const cached = urlCache.get(key);
42
+ if (cached !== undefined) return cached;
43
+ const pending = pendingCache.get(key);
44
+ if (pending) return pending;
45
+ const promise = (async () => {
46
+ const dto = await safeInvoke('character_get_expression', {
47
+ fileName,
48
+ label
49
+ });
50
+ if (!dto || !dto.base64 || dto.base64.length < 100) {
51
+ urlCache.set(key, '');
52
+ return null;
53
+ }
54
+ const url = `data:${dto.mime_type || 'image/png'};base64,${dto.base64}`;
55
+ urlCache.set(key, url);
56
+ // If Rust fell back to a different label (sprite missing), also cache
57
+ // the resolved label so a subsequent call for it short-circuits.
58
+ if (dto.label && dto.label !== label) {
59
+ urlCache.set(cacheKey(fileName, dto.label), url);
60
+ }
61
+ return url;
62
+ })();
63
+ pendingCache.set(key, promise);
64
+ try {
65
+ return await promise;
66
+ } finally {
67
+ pendingCache.delete(key);
68
+ }
69
+ }
70
+ /**
71
+ * Returns the data URL for `(characterFileName, label)` or `null` while the
72
+ * sprite is loading / when running outside Tauri / when no character is set.
73
+ *
74
+ * Re-evaluates whenever either input changes. Cached results resolve in the
75
+ * same render (no transient `null`).
76
+ */
77
+ function useCharacterSprite(characterFileName, label) {
78
+ const key = characterFileName && label ? cacheKey(characterFileName, label) : null;
79
+ const cachedInitial = key ? urlCache.get(key) ?? null : null;
80
+ const [src, setSrc] = React.useState(cachedInitial && cachedInitial !== '' ? cachedInitial : null);
81
+ React.useEffect(() => {
82
+ if (!characterFileName || !label) {
83
+ setSrc(null);
84
+ return;
85
+ }
86
+ let alive = true;
87
+ fetchSprite(characterFileName, label).then(url => {
88
+ if (alive) setSrc(url);
89
+ });
90
+ return () => {
91
+ alive = false;
92
+ };
93
+ }, [characterFileName, label]);
94
+ return src;
95
+ }
96
+ /**
97
+ * Clears the sprite cache. Call after `character_refresh` so newly-installed
98
+ * sprite packs aren't shadowed by stale data URLs.
99
+ */
100
+ function clearCharacterSpriteCache() {
101
+ urlCache.clear();
102
+ pendingCache.clear();
103
+ }export{clearCharacterSpriteCache,useCharacterSprite};//# sourceMappingURL=useCharacterSprite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCharacterSprite.js","sources":["../../../../src/components/AIToolbar/hooks/useCharacterSprite.ts"],"sourcesContent":[null],"names":[],"mappings":"4BAqFA;;;;;;AAMG;AACH;AAyBA;;;AAGG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,19 @@
1
+ export type EmotionLabel = 'admiration' | 'amusement' | 'anger' | 'annoyance' | 'approval' | 'caring' | 'confusion' | 'curiosity' | 'desire' | 'disappointment' | 'disapproval' | 'disgust' | 'embarrassment' | 'excitement' | 'fear' | 'gratitude' | 'grief' | 'joy' | 'love' | 'nervousness' | 'neutral' | 'optimism' | 'pride' | 'realization' | 'relief' | 'remorse' | 'sadness' | 'surprise';
2
+ /** All 28 labels, in the SillyTavern canonical order. */
3
+ export declare const ALL_EMOTIONS: EmotionLabel[];
4
+ /**
5
+ * Classify a chat-message string into one of the 28 GoEmotions labels.
6
+ *
7
+ * Returns `'neutral'` when no rule fires, which is what every sprite pack
8
+ * uses as the fallback (and what `character_get_expression` falls back to
9
+ * server-side when the requested sprite is missing).
10
+ *
11
+ * @param text - The assistant's message text. Markdown formatting is fine —
12
+ * the regexes only care about word content.
13
+ * @param available - Optional restrict-set: only return labels present here.
14
+ * Useful when the caller knows which sprites the
15
+ * character actually ships and wants to avoid a Tauri
16
+ * round-trip for labels that will fall back anyway.
17
+ */
18
+ export declare function classifyEmotion(text: string | null | undefined, available?: readonly string[]): EmotionLabel;
19
+ //# sourceMappingURL=emotionClassifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emotionClassifier.d.ts","sourceRoot":"","sources":["../../../../src/components/AIToolbar/utils/emotionClassifier.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,YAAY,GAClB,YAAY,GACZ,WAAW,GACX,OAAO,GACP,WAAW,GACX,UAAU,GACV,QAAQ,GACR,WAAW,GACX,WAAW,GACX,QAAQ,GACR,gBAAgB,GAChB,aAAa,GACb,SAAS,GACT,eAAe,GACf,YAAY,GACZ,MAAM,GACN,WAAW,GACX,OAAO,GACP,KAAK,GACL,MAAM,GACN,aAAa,GACb,SAAS,GACT,UAAU,GACV,OAAO,GACP,aAAa,GACb,QAAQ,GACR,SAAS,GACT,SAAS,GACT,UAAU,CAAC;AAEjB,yDAAyD;AACzD,eAAO,MAAM,YAAY,EAAE,YAAY,EA6BtC,CAAC;AAgNF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,CAuB5G"}
@@ -0,0 +1,142 @@
1
+ // Lightweight emotion classifier for assistant message text.
2
+ //
3
+ // Picks one of the 28 SillyTavern / GoEmotions labels so the AIToolbar can
4
+ // switch the assistant's avatar to the matching expression sprite (see
5
+ // `packages-extensions/seraphina-character-extension/characters/Seraphina/`
6
+ // for the canonical sprite layout: `neutral.png`, `joy.png`, `caring.png`,
7
+ // `surprise.png`, etc.).
8
+ //
9
+ // This is intentionally a heuristic (regex/keyword scoring) — fast, offline,
10
+ // no model dependency — and is the same shape (`text → label`) as a real
11
+ // GoEmotions classifier, so it can be transparently swapped for an ONNX
12
+ // transformer later without touching the call sites.
13
+ //
14
+ // Limitations:
15
+ // - English only; would need rework for i18n responses.
16
+ // - Misses subtle nuance (sarcasm, mixed emotions) — picks the *strongest*
17
+ // signal and never returns more than one label.
18
+ // - Stage directions in italics ("smiles warmly") count the same as inline
19
+ // dialogue; that's actually desirable for character-card style replies
20
+ // where mood lives in the prose.
21
+ /** All 28 labels, in the SillyTavern canonical order. */
22
+ const RULES = [{
23
+ label: 'joy',
24
+ patterns: [/\b(happy|happiness|joy(ful)?|delight(ed|ful)?|cheerful|glad)\b/gi, /\b(smile[sd]?|grin(s|ned|ning)?|beam(s|ing|ed)?)\b/gi, /:\)|=\)|<3+/g]
25
+ }, {
26
+ label: 'amusement',
27
+ patterns: [/\b(laugh(s|ed|ing|ter)?|chuckle[sd]?|giggle[sd]?|snicker(s|ed|ing)?)\b/gi, /\b(amus(ed|ing|ement)|funny|hilarious|teasing)\b/gi, /\bha+ha+\b/gi, /lol\b/gi]
28
+ }, {
29
+ label: 'love',
30
+ patterns: [/\b(i love you|love you|my love|beloved|darling|sweetheart|dearest)\b/gi, /\b(adore[sd]?|cherish(ed|es)?|smitten)\b/gi],
31
+ weight: 2
32
+ }, {
33
+ label: 'caring',
34
+ patterns: [/\b(gentle(ly)?|tender(ly)?|softly|kind(ly|ness)?|warm(ly|th)?|reassuring(ly)?)\b/gi, /\b(comfort(s|ed|ing)?|soothing(ly)?|protect(s|ing|ed)?|nurturing)\b/gi, /\b(are you (ok|okay|alright)|how are you feeling|don'?t worry)\b/gi]
35
+ }, {
36
+ label: 'admiration',
37
+ patterns: [/\b(impressive|impressed|brilliant|magnificent|incredible|amazing|remarkable|astonishing|stunning|breathtaking)\b/gi, /\b(well done|great job|so proud of you)\b/gi]
38
+ }, {
39
+ label: 'approval',
40
+ patterns: [/\b(approve|agree|exactly|precisely|that'?s right|good idea|sounds good|works for me)\b/gi, /\b(yes,?\s+(of course|absolutely|certainly))/gi]
41
+ }, {
42
+ label: 'disapproval',
43
+ patterns: [/\b(disapprove|that'?s wrong|i don'?t agree|i wouldn'?t|shouldn'?t have)\b/gi, /\b(unacceptable|not okay)\b/gi]
44
+ }, {
45
+ label: 'anger',
46
+ patterns: [/\b(angry|furious|enraged|seething|livid|outraged)\b/gi, /\b(scowl(s|ed|ing)?|glare[sd]?|snarl(s|ed|ing)?|growl(s|ed|ing)?)\b/gi, /\bhow dare you\b/gi],
47
+ weight: 2
48
+ }, {
49
+ label: 'annoyance',
50
+ patterns: [/\b(annoy(ed|ing|ance)?|irritat(ed|ing|ion)|frustrat(ed|ing|ion))\b/gi, /\b(sigh(s|ed|ing)?|huff(s|ed|ing)?|rolls? (her|his|their) eyes)\b/gi, /\b(ugh|tsk)\b/gi]
51
+ }, {
52
+ label: 'fear',
53
+ patterns: [/\b(afraid|scared|terrified|frightened|fearful|horrified)\b/gi, /\b(trembl(es|ed|ing)|shiver(s|ed|ing)?|flinch(es|ed|ing)?)\b/gi, /\b(panic(ked|king)?|dread)\b/gi],
54
+ weight: 2
55
+ }, {
56
+ label: 'nervousness',
57
+ patterns: [/\b(nervous(ly|ness)?|anxious(ly)?|uneasy|jittery|tense(ly)?|on edge)\b/gi, /\b(fidget(s|ed|ing)?|bites? (her|his|their) lip|wrings? (her|his|their) hands)\b/gi]
58
+ }, {
59
+ label: 'sadness',
60
+ patterns: [/\b(sad(ness|ly)?|unhappy|sorrow(ful)?|heartbroken|miserable|melancholy|forlorn)\b/gi, /\b(tear(s|ful|y)|crying|weep(s|ing|ed)?|sob(s|bing|bed)?)\b/gi, /:\(/g],
61
+ weight: 2
62
+ }, {
63
+ label: 'grief',
64
+ patterns: [/\b(grief|grieving|mourn(s|ed|ing)?|bereaved|devastated)\b/gi, /\b(loss of|passed away|gone forever)\b/gi],
65
+ weight: 2
66
+ }, {
67
+ label: 'remorse',
68
+ patterns: [/\b(i'?m sorry|i am sorry|my apologies|apologi[sz]e|forgive me|regret(s|ted|ting)?|guilty|guilt)\b/gi, /\b(shouldn'?t have|wish i hadn'?t)\b/gi],
69
+ weight: 2
70
+ }, {
71
+ label: 'disappointment',
72
+ patterns: [/\b(disappointed|disappointing|let down|underwhelm(ed|ing)?|expected (more|better))\b/gi]
73
+ }, {
74
+ label: 'embarrassment',
75
+ patterns: [/\b(embarrass(ed|ing|ment)?|flustered|mortified|sheepish(ly)?)\b/gi, /\b(blush(es|ed|ing)?|cheeks (turn|burn|flush))\b/gi]
76
+ }, {
77
+ label: 'disgust',
78
+ patterns: [/\b(disgust(ed|ing)?|revolted|repulsed|nauseat(ed|ing)|gross|yuck|ew)\b/gi, /\b(grimace[sd]?|wrinkles? (her|his|their) nose)\b/gi]
79
+ }, {
80
+ label: 'excitement',
81
+ patterns: [/\b(excited(ly)?|thrilled|exhilarat(ed|ing)|exhilaration|elated|ecstatic|stoked|pumped)\b/gi, /\b(can'?t wait|bouncing|jumps? up)\b/gi]
82
+ }, {
83
+ label: 'gratitude',
84
+ patterns: [/\b(thank you|thanks|thank(ful|fully)?|grateful(ly)?|appreciat(e|ed|ing|ion))\b/gi],
85
+ weight: 2
86
+ }, {
87
+ label: 'pride',
88
+ patterns: [/\b(proud(ly)?|pride)\b/gi, /\b(chin (held )?high|stands? tall)\b/gi]
89
+ }, {
90
+ label: 'relief',
91
+ patterns: [/\b(relief|relieved|reliev(es|ed|ing)?)\b/gi, /\b(exhale[sd]? (deeply|slowly)|breathes? out)\b/gi]
92
+ }, {
93
+ label: 'optimism',
94
+ patterns: [/\b(hopeful(ly)?|optimist(ic)?|things will (work|turn) out|look(ing)? forward|we'?ll figure)\b/gi]
95
+ }, {
96
+ label: 'desire',
97
+ patterns: [/\b(crave[sd]?|yearn(s|ed|ing)?|long(s|ed|ing) for|want(s|ed|ing)? you|need(s|ed|ing)? you)\b/gi, /\b(lean(s|ed|ing)? in close|breathless)\b/gi]
98
+ }, {
99
+ label: 'curiosity',
100
+ patterns: [/\b(curious(ly)?|curiosity|intrigu(ed|ing)|wonder(s|ed|ing)? (about|what|why|how))\b/gi, /\btell me more\b/gi]
101
+ }, {
102
+ label: 'confusion',
103
+ patterns: [/\b(confus(ed|ing|ion)|puzzl(ed|ing)|baffl(ed|ing)|perplex(ed|ing))\b/gi, /\b(tilts? (her|his|their) head|raises? an eyebrow|i don'?t understand|what do you mean)\b/gi, /\?\?+/g]
104
+ }, {
105
+ label: 'realization',
106
+ patterns: [/\b(realiz(es|ed|ing)|realisation|i see now|of course!|that makes sense|finally understand)\b/gi, /\b(aha|eureka)\b/gi]
107
+ }, {
108
+ label: 'surprise',
109
+ patterns: [/\b(surpris(ed|ing|e)|astonish(ed|ing)|astounded|shocked|stunned|taken aback)\b/gi, /\b(gasp(s|ed|ing)?|eyes (widen|go wide))\b/gi, /\b(oh!|wha[at]?\?!)/gi]
110
+ }];
111
+ /**
112
+ * Classify a chat-message string into one of the 28 GoEmotions labels.
113
+ *
114
+ * Returns `'neutral'` when no rule fires, which is what every sprite pack
115
+ * uses as the fallback (and what `character_get_expression` falls back to
116
+ * server-side when the requested sprite is missing).
117
+ *
118
+ * @param text - The assistant's message text. Markdown formatting is fine —
119
+ * the regexes only care about word content.
120
+ * @param available - Optional restrict-set: only return labels present here.
121
+ * Useful when the caller knows which sprites the
122
+ * character actually ships and wants to avoid a Tauri
123
+ * round-trip for labels that will fall back anyway.
124
+ */
125
+ function classifyEmotion(text, available) {
126
+ if (!text) return 'neutral';
127
+ let bestLabel = 'neutral';
128
+ let bestScore = 0;
129
+ for (const rule of RULES) {
130
+ const weight = rule.weight ?? 1;
131
+ let score = 0;
132
+ for (const pattern of rule.patterns) {
133
+ const matches = text.match(pattern);
134
+ if (matches) score += matches.length * weight;
135
+ }
136
+ if (score > bestScore) {
137
+ bestScore = score;
138
+ bestLabel = rule.label;
139
+ }
140
+ }
141
+ return bestLabel;
142
+ }export{classifyEmotion};//# sourceMappingURL=emotionClassifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emotionClassifier.js","sources":["../../../../src/components/AIToolbar/utils/emotionClassifier.ts"],"sourcesContent":[null],"names":[],"mappings":"AAqBA;AA8BA;AACA;AA6OA;;;;;;;;;;;;;AAaG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@admin-layout/tailwind-ui",
3
- "version": "12.2.4-alpha.56",
3
+ "version": "12.2.4-alpha.58",
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": "61757b2c770347ae8d91008b4d2b366fd363e299"
99
+ "gitHead": "4cf5febc99804338d85d52ec12787d7bbb661bad"
100
100
  }