react-ai-chat-actions 0.1.0
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/LICENSE +5 -0
- package/README.md +93 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# react-ai-chat-actions
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-ai-chat-actions)
|
|
4
|
+
[](https://www.npmjs.com/package/react-ai-chat-actions)
|
|
5
|
+
[](https://bundlephobia.com/package/react-ai-chat-actions)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
<img src="https://i.ibb.co/fVNC9PSx/aichatlogo.png" alt="react-ai-chat-actions" width="400" />
|
|
9
|
+
|
|
10
|
+
Action bar for AI chat messages. Like, dislike, copy, regenerate, speak, pin — with themes, tooltips, and loading states out of the box.
|
|
11
|
+
|
|
12
|
+
**[Live demo →](https://react-ai-chat-actions.vercel.app/)**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install react-ai-chat-actions
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<img src="https://i.ibb.co/Wvtvyvwc/image.png" alt="react-ai-chat-actions" width="400" />
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { ActionBar } from "react-ai-chat-actions";
|
|
29
|
+
import "react-ai-chat-actions/dist/style.css";
|
|
30
|
+
|
|
31
|
+
<ActionBar
|
|
32
|
+
messageId="msg-1"
|
|
33
|
+
visible={true}
|
|
34
|
+
actions={["like", "dislike", "divider", "copy", "regenerate"]}
|
|
35
|
+
onAction={(type, messageId) => console.log(type, messageId)}
|
|
36
|
+
/>;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Props
|
|
42
|
+
|
|
43
|
+
| Prop | Type | Default | Description |
|
|
44
|
+
| ----------- | --------------------------- | ------- | ----------------------------------------- |
|
|
45
|
+
| `messageId` | `string` | — | Required. Passed back in `onAction` |
|
|
46
|
+
| `visible` | `boolean` | `true` | Show or hide the bar |
|
|
47
|
+
| `actions` | `ActionType[]` | — | Which buttons to render and in what order |
|
|
48
|
+
| `onAction` | `(type, messageId) => void` | — | Callback on any button click |
|
|
49
|
+
| `loading` | `ActionType[]` | `[]` | Buttons in loading state |
|
|
50
|
+
| `disabled` | `ActionType[]` | `[]` | Buttons in disabled state |
|
|
51
|
+
|
|
52
|
+
### ActionType
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
type ActionType =
|
|
56
|
+
| "like"
|
|
57
|
+
| "dislike"
|
|
58
|
+
| "copy"
|
|
59
|
+
| "regenerate"
|
|
60
|
+
| "speak"
|
|
61
|
+
| "options"
|
|
62
|
+
| "pin"
|
|
63
|
+
| "divider";
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Themes
|
|
69
|
+
|
|
70
|
+
Four built-in themes: `light`, `dark`, `neon`, `square`.
|
|
71
|
+
|
|
72
|
+
Apply via `data-theme` on any parent element:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<div data-theme="dark">
|
|
76
|
+
<ActionBar ... />
|
|
77
|
+
</div>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Roadmap
|
|
83
|
+
|
|
84
|
+
- [ ] `ChatMessageWrapper` — wrapper mode with built-in hover visibility
|
|
85
|
+
- [ ] Custom actions support
|
|
86
|
+
- [ ] More themes
|
|
87
|
+
- [ ] Animations
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`lucide-react`),t=require(`react/jsx-runtime`),n=require(`react`);const r=({label:n,icon:r,disabled:i,loading:a,onClick:o,active:s})=>(0,t.jsx)(`button`,{className:`ca-btn`,"aria-pressed":s,disabled:i||a,onClick:o,"aria-label":n,children:a?(0,t.jsx)(e.Loader,{size:16,className:`ca-spinner`}):r}),i=(e,t)=>{if(e!==`like`&&e!==`dislike`)return t;let n=e===`like`?`dislike`:`like`;return console.log(t.filter(e=>e!==n),`activeActions.filter((a) => a !== filterOption);`),t.filter(e=>e!==n)},a=({messageId:e,onAction:t})=>{let[r,a]=(0,n.useState)([]),o=e=>r.includes(e);return console.log(r,`console.log(activeActions);`),{isActive:o,handleAction:n=>{if([`copy`,`regenerate`].includes(n)){t(e,n);return}if(o(n))a(e=>e.filter(e=>e!==n));else{let o=i(n,r);t(e,n),a([...o,n])}}}},o=({label:e,children:r,disabled:i=!1})=>{if(i)return r;let a=(0,n.useRef)(null),o=(0,n.useRef)(null),[s,c]=(0,n.useState)(`top`);return(0,t.jsxs)(`div`,{className:`ca-tooltip-wrapper ca-tooltip--${s}`,ref:a,onMouseEnter:()=>{!a.current||!o.current||setTimeout(()=>{if(!a.current||!o.current)return;let e=a.current.getBoundingClientRect(),t={top:e.top,bottom:window.innerHeight-e.bottom,left:e.left,right:window.innerWidth-e.right};c(t.top>=t.bottom?`top`:`bottom`)},0)},children:[r,(0,t.jsx)(`span`,{ref:o,className:`ca-tooltip`,children:e})]})},s={like:{icon:(0,t.jsx)(e.ThumbsUp,{size:16}),label:`Like`},dislike:{icon:(0,t.jsx)(e.ThumbsDown,{size:16}),label:`Dislike`},copy:{icon:(0,t.jsx)(e.Copy,{size:16}),label:`Copy`},regenerate:{icon:(0,t.jsx)(e.RefreshCw,{size:16}),label:`Regenerate`},speak:{icon:(0,t.jsx)(e.Volume2,{size:16}),label:`Speak`},options:{icon:(0,t.jsx)(e.MoreHorizontal,{size:16}),label:`Options`},pin:{icon:(0,t.jsx)(e.Pin,{size:16}),label:`pin`},divider:{icon:(0,t.jsx)(`div`,{className:`ca-divider`}),label:``}},c=({messageId:e,actions:n,onAction:i,visible:c,loading:l,disabled:u})=>{let{isActive:d,handleAction:f}=a({messageId:e,onAction:i});return c?(0,t.jsx)(`div`,{className:`ca-bar`,children:n.map(e=>{if(e===`divider`)return(0,t.jsx)(`div`,{className:`ca-divider`},e);let n=s[e],i=d(e);return(0,t.jsx)(o,{disabled:u?.includes(e),label:n.label,children:(0,t.jsx)(r,{...n,active:i,loading:l?.includes(e),disabled:u?.includes(e),onClick:()=>f(e)})},e)})}):null};exports.ActionBar=c;
|
|
3
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["Loader","ThumbsUp","ThumbsDown","Copy","RefreshCw","Volume2","MoreHorizontal","Pin"],"sources":["../src/components/action-button/action-button.tsx","../src/hooks/use-chat-actions.tsx","../src/components/tooltip/tooltip.tsx","../src/components/action-bar/action-bar.tsx"],"sourcesContent":["import { Loader } from \"lucide-react\";\nimport { ActionButtonProps } from \"../../types\";\n\nexport const ActionButton = ({\n label,\n icon,\n disabled,\n loading,\n onClick,\n active,\n}: ActionButtonProps) => {\n return (\n <button\n className=\"ca-btn\"\n aria-pressed={active}\n disabled={disabled || loading}\n onClick={onClick}\n aria-label={label}\n >\n {loading ? <Loader size={16} className=\"ca-spinner\" /> : icon}\n </button>\n );\n};\n\nexport default ActionButton;\n","import { useState } from \"react\";\nimport { ActionType } from \"src/types\";\n\nconst reconcile = (currentAction: ActionType, activeActions: ActionType[]) => {\n if (currentAction !== \"like\" && currentAction !== \"dislike\") {\n return activeActions;\n }\n const filterOption = currentAction === \"like\" ? \"dislike\" : \"like\";\n console.log(\n activeActions.filter((a) => a !== filterOption),\n \"activeActions.filter((a) => a !== filterOption);\",\n );\n\n return activeActions.filter((a) => a !== filterOption);\n};\n\nconst useChatActions = ({\n messageId,\n onAction,\n}: {\n messageId: string;\n onAction: (messageId: string, action: ActionType) => void;\n}) => {\n const [activeActions, setActiveActions] = useState<ActionType[]>([]);\n\n const isActive = (action: ActionType) => activeActions.includes(action);\n\n const handleAction = (action: ActionType) => {\n const noStateActions: ActionType[] = [\"copy\", \"regenerate\"];\n\n if (noStateActions.includes(action)) {\n onAction(messageId, action);\n return;\n }\n\n if (isActive(action)) {\n setActiveActions((prev) => prev.filter((a) => a !== action));\n } else {\n let reconciledActions = reconcile(action, activeActions);\n onAction(messageId, action);\n setActiveActions([...reconciledActions, action]);\n }\n };\n\n console.log(activeActions, \"console.log(activeActions);\");\n return { isActive, handleAction };\n};\n\nexport default useChatActions;\n","import { ReactNode, useRef, useState } from \"react\";\n\nexport const Tooltip = ({\n label,\n children,\n disabled = false,\n}: {\n label: string;\n children: ReactNode;\n disabled?: boolean;\n}) => {\n if (disabled) return children;\n const wrapperRef = useRef<HTMLDivElement>(null);\n const tooltipRef = useRef<HTMLSpanElement>(null);\n\n const [side, setSide] = useState<\"top\" | \"bottom\">(\"top\");\n\n const handleMouseEnter = () => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n setTimeout(() => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n const rect = wrapperRef.current.getBoundingClientRect();\n\n const spaces = {\n top: rect.top,\n bottom: window.innerHeight - rect.bottom,\n left: rect.left,\n right: window.innerWidth - rect.right,\n };\n\n const side = spaces.top >= spaces.bottom ? \"top\" : \"bottom\";\n\n setSide(side);\n }, 0);\n };\n\n return (\n <div\n className={`ca-tooltip-wrapper ca-tooltip--${side}`}\n ref={wrapperRef}\n onMouseEnter={handleMouseEnter}\n >\n {children}\n <span ref={tooltipRef} className=\"ca-tooltip\">\n {label}\n </span>\n </div>\n );\n};\n","import {\n Copy,\n MoreHorizontal,\n Pin,\n RefreshCw,\n ThumbsDown,\n ThumbsUp,\n Volume2,\n} from \"lucide-react\";\nimport { ActionBarProps, ActionButtonMeta, ActionType } from \"../../types\";\nimport ActionButton from \"../action-button/action-button\";\nimport useChatActions from \"../../hooks/use-chat-actions\";\nimport { Tooltip } from \"../tooltip/tooltip\";\n\nconst buttonsMeta: Record<ActionType, ActionButtonMeta> = {\n like: { icon: <ThumbsUp size={16} />, label: \"Like\" },\n dislike: { icon: <ThumbsDown size={16} />, label: \"Dislike\" },\n copy: { icon: <Copy size={16} />, label: \"Copy\" },\n regenerate: { icon: <RefreshCw size={16} />, label: \"Regenerate\" },\n speak: { icon: <Volume2 size={16} />, label: \"Speak\" },\n options: { icon: <MoreHorizontal size={16} />, label: \"Options\" },\n pin: { icon: <Pin size={16} />, label: \"pin\" },\n divider: { icon: <div className=\"ca-divider\" />, label: \"\" },\n};\n\nexport const ActionBar = ({\n messageId,\n actions,\n onAction,\n visible,\n loading,\n disabled,\n}: ActionBarProps) => {\n const { isActive, handleAction } = useChatActions({ messageId, onAction });\n\n if (!visible) return null;\n return (\n <div className=\"ca-bar\">\n {actions.map((action) => {\n if (action === \"divider\") {\n return <div key={action} className=\"ca-divider\" />;\n }\n let meta = buttonsMeta[action];\n let active = isActive(action);\n\n return (\n <Tooltip\n disabled={disabled?.includes(action)}\n label={meta.label}\n key={action}\n >\n <ActionButton\n {...meta}\n active={active}\n loading={loading?.includes(action)}\n disabled={disabled?.includes(action)}\n onClick={() => handleAction(action)}\n />\n </Tooltip>\n );\n })}\n </div>\n );\n};\n\nexport default ActionBar;\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
//#region src/theme.d.ts
|
|
3
|
+
type ThemeToken = '--ca-barBg' | '--ca-barShadow' | '--ca-barBorder' | '--ca-barRadius' | '--ca-btnColor' | '--ca-btnHoverBg' | '--ca-btnActiveBg' | '--ca-btnActiveColor' | '--ca-btnActiveGlow' | '--ca-btnRadius' | '--ca-dividerColor' | '--ca-tooltipBg' | '--ca-tooltipColor';
|
|
4
|
+
type ThemeName = 'square' | 'light' | 'dark' | 'neon';
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/types/index.d.ts
|
|
7
|
+
type ActionType = "like" | "dislike" | "copy" | "regenerate" | "speak" | "options" | "pin" | "divider";
|
|
8
|
+
type ActionTypeFiltered = Exclude<ActionType, "divider">;
|
|
9
|
+
type ActionBarProps = {
|
|
10
|
+
messageId: string;
|
|
11
|
+
visible?: boolean;
|
|
12
|
+
actions: ActionType[];
|
|
13
|
+
loading?: ActionTypeFiltered[];
|
|
14
|
+
disabled?: ActionTypeFiltered[];
|
|
15
|
+
onAction: (messageId: string, action: ActionType) => void;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/components/action-bar/action-bar.d.ts
|
|
19
|
+
declare const ActionBar: ({
|
|
20
|
+
messageId,
|
|
21
|
+
actions,
|
|
22
|
+
onAction,
|
|
23
|
+
visible,
|
|
24
|
+
loading,
|
|
25
|
+
disabled
|
|
26
|
+
}: ActionBarProps) => react_jsx_runtime0.JSX.Element | null;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { ActionBar, type ActionBarProps, type ActionType, type ThemeName, type ThemeToken };
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
import{Copy as e,Loader as t,MoreHorizontal as n,Pin as r,RefreshCw as i,ThumbsDown as a,ThumbsUp as o,Volume2 as s}from"lucide-react";import{jsx as c,jsxs as l}from"react/jsx-runtime";import{useRef as u,useState as d}from"react";const f=({label:e,icon:n,disabled:r,loading:i,onClick:a,active:o})=>c(`button`,{className:`ca-btn`,"aria-pressed":o,disabled:r||i,onClick:a,"aria-label":e,children:i?c(t,{size:16,className:`ca-spinner`}):n}),p=(e,t)=>{if(e!==`like`&&e!==`dislike`)return t;let n=e===`like`?`dislike`:`like`;return console.log(t.filter(e=>e!==n),`activeActions.filter((a) => a !== filterOption);`),t.filter(e=>e!==n)},m=({messageId:e,onAction:t})=>{let[n,r]=d([]),i=e=>n.includes(e);return console.log(n,`console.log(activeActions);`),{isActive:i,handleAction:a=>{if([`copy`,`regenerate`].includes(a)){t(e,a);return}if(i(a))r(e=>e.filter(e=>e!==a));else{let i=p(a,n);t(e,a),r([...i,a])}}}},h=({label:e,children:t,disabled:n=!1})=>{if(n)return t;let r=u(null),i=u(null),[a,o]=d(`top`);return l(`div`,{className:`ca-tooltip-wrapper ca-tooltip--${a}`,ref:r,onMouseEnter:()=>{!r.current||!i.current||setTimeout(()=>{if(!r.current||!i.current)return;let e=r.current.getBoundingClientRect(),t={top:e.top,bottom:window.innerHeight-e.bottom,left:e.left,right:window.innerWidth-e.right};o(t.top>=t.bottom?`top`:`bottom`)},0)},children:[t,c(`span`,{ref:i,className:`ca-tooltip`,children:e})]})},g={like:{icon:c(o,{size:16}),label:`Like`},dislike:{icon:c(a,{size:16}),label:`Dislike`},copy:{icon:c(e,{size:16}),label:`Copy`},regenerate:{icon:c(i,{size:16}),label:`Regenerate`},speak:{icon:c(s,{size:16}),label:`Speak`},options:{icon:c(n,{size:16}),label:`Options`},pin:{icon:c(r,{size:16}),label:`pin`},divider:{icon:c(`div`,{className:`ca-divider`}),label:``}},_=({messageId:e,actions:t,onAction:n,visible:r,loading:i,disabled:a})=>{let{isActive:o,handleAction:s}=m({messageId:e,onAction:n});return r?c(`div`,{className:`ca-bar`,children:t.map(e=>{if(e===`divider`)return c(`div`,{className:`ca-divider`},e);let t=g[e],n=o(e);return c(h,{disabled:a?.includes(e),label:t.label,children:c(f,{...t,active:n,loading:i?.includes(e),disabled:a?.includes(e),onClick:()=>s(e)})},e)})}):null};export{_ as ActionBar};
|
|
3
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/components/action-button/action-button.tsx","../src/hooks/use-chat-actions.tsx","../src/components/tooltip/tooltip.tsx","../src/components/action-bar/action-bar.tsx"],"sourcesContent":["import { Loader } from \"lucide-react\";\nimport { ActionButtonProps } from \"../../types\";\n\nexport const ActionButton = ({\n label,\n icon,\n disabled,\n loading,\n onClick,\n active,\n}: ActionButtonProps) => {\n return (\n <button\n className=\"ca-btn\"\n aria-pressed={active}\n disabled={disabled || loading}\n onClick={onClick}\n aria-label={label}\n >\n {loading ? <Loader size={16} className=\"ca-spinner\" /> : icon}\n </button>\n );\n};\n\nexport default ActionButton;\n","import { useState } from \"react\";\nimport { ActionType } from \"src/types\";\n\nconst reconcile = (currentAction: ActionType, activeActions: ActionType[]) => {\n if (currentAction !== \"like\" && currentAction !== \"dislike\") {\n return activeActions;\n }\n const filterOption = currentAction === \"like\" ? \"dislike\" : \"like\";\n console.log(\n activeActions.filter((a) => a !== filterOption),\n \"activeActions.filter((a) => a !== filterOption);\",\n );\n\n return activeActions.filter((a) => a !== filterOption);\n};\n\nconst useChatActions = ({\n messageId,\n onAction,\n}: {\n messageId: string;\n onAction: (messageId: string, action: ActionType) => void;\n}) => {\n const [activeActions, setActiveActions] = useState<ActionType[]>([]);\n\n const isActive = (action: ActionType) => activeActions.includes(action);\n\n const handleAction = (action: ActionType) => {\n const noStateActions: ActionType[] = [\"copy\", \"regenerate\"];\n\n if (noStateActions.includes(action)) {\n onAction(messageId, action);\n return;\n }\n\n if (isActive(action)) {\n setActiveActions((prev) => prev.filter((a) => a !== action));\n } else {\n let reconciledActions = reconcile(action, activeActions);\n onAction(messageId, action);\n setActiveActions([...reconciledActions, action]);\n }\n };\n\n console.log(activeActions, \"console.log(activeActions);\");\n return { isActive, handleAction };\n};\n\nexport default useChatActions;\n","import { ReactNode, useRef, useState } from \"react\";\n\nexport const Tooltip = ({\n label,\n children,\n disabled = false,\n}: {\n label: string;\n children: ReactNode;\n disabled?: boolean;\n}) => {\n if (disabled) return children;\n const wrapperRef = useRef<HTMLDivElement>(null);\n const tooltipRef = useRef<HTMLSpanElement>(null);\n\n const [side, setSide] = useState<\"top\" | \"bottom\">(\"top\");\n\n const handleMouseEnter = () => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n setTimeout(() => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n const rect = wrapperRef.current.getBoundingClientRect();\n\n const spaces = {\n top: rect.top,\n bottom: window.innerHeight - rect.bottom,\n left: rect.left,\n right: window.innerWidth - rect.right,\n };\n\n const side = spaces.top >= spaces.bottom ? \"top\" : \"bottom\";\n\n setSide(side);\n }, 0);\n };\n\n return (\n <div\n className={`ca-tooltip-wrapper ca-tooltip--${side}`}\n ref={wrapperRef}\n onMouseEnter={handleMouseEnter}\n >\n {children}\n <span ref={tooltipRef} className=\"ca-tooltip\">\n {label}\n </span>\n </div>\n );\n};\n","import {\n Copy,\n MoreHorizontal,\n Pin,\n RefreshCw,\n ThumbsDown,\n ThumbsUp,\n Volume2,\n} from \"lucide-react\";\nimport { ActionBarProps, ActionButtonMeta, ActionType } from \"../../types\";\nimport ActionButton from \"../action-button/action-button\";\nimport useChatActions from \"../../hooks/use-chat-actions\";\nimport { Tooltip } from \"../tooltip/tooltip\";\n\nconst buttonsMeta: Record<ActionType, ActionButtonMeta> = {\n like: { icon: <ThumbsUp size={16} />, label: \"Like\" },\n dislike: { icon: <ThumbsDown size={16} />, label: \"Dislike\" },\n copy: { icon: <Copy size={16} />, label: \"Copy\" },\n regenerate: { icon: <RefreshCw size={16} />, label: \"Regenerate\" },\n speak: { icon: <Volume2 size={16} />, label: \"Speak\" },\n options: { icon: <MoreHorizontal size={16} />, label: \"Options\" },\n pin: { icon: <Pin size={16} />, label: \"pin\" },\n divider: { icon: <div className=\"ca-divider\" />, label: \"\" },\n};\n\nexport const ActionBar = ({\n messageId,\n actions,\n onAction,\n visible,\n loading,\n disabled,\n}: ActionBarProps) => {\n const { isActive, handleAction } = useChatActions({ messageId, onAction });\n\n if (!visible) return null;\n return (\n <div className=\"ca-bar\">\n {actions.map((action) => {\n if (action === \"divider\") {\n return <div key={action} className=\"ca-divider\" />;\n }\n let meta = buttonsMeta[action];\n let active = isActive(action);\n\n return (\n <Tooltip\n disabled={disabled?.includes(action)}\n label={meta.label}\n key={action}\n >\n <ActionButton\n {...meta}\n active={active}\n loading={loading?.includes(action)}\n disabled={disabled?.includes(action)}\n onClick={() => handleAction(action)}\n />\n </Tooltip>\n );\n })}\n </div>\n );\n};\n\nexport default ActionBar;\n"],"mappings":"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"}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root,[data-theme=square]{--ca-barBg:#fff;--ca-barShadow:0 2px 8px #0001;--ca-barBorder:none;--ca-barRadius:0px;--ca-btnColor:#374151;--ca-btnHoverBg:#f3f4f6;--ca-btnActiveBg:#0e7490;--ca-btnActiveColor:#fff;--ca-btnActiveGlow:0 0 4px #0e749044;--ca-btnRadius:0px;--ca-dividerColor:#e5e7eb;--ca-tooltipBg:#1f2937;--ca-tooltipColor:#fff}[data-theme=light]{--ca-barBg:#fff;--ca-barShadow:0 2px 8px #0001;--ca-barBorder:none;--ca-barRadius:999px;--ca-btnColor:#374151;--ca-btnHoverBg:#f3f4f6;--ca-btnActiveBg:#0e7490;--ca-btnActiveColor:#fff;--ca-btnActiveGlow:0 0 4px #0e749044;--ca-btnRadius:999px;--ca-dividerColor:#e5e7eb;--ca-tooltipBg:#1f2937;--ca-tooltipColor:#fff}[data-theme=dark]{--ca-barBg:#1e1e1e;--ca-barShadow:0 2px 8px #0004;--ca-barBorder:none;--ca-barRadius:999px;--ca-btnColor:#d1d5db;--ca-btnHoverBg:#ffffff14;--ca-btnActiveBg:#0e7490;--ca-btnActiveColor:#fff;--ca-btnActiveGlow:0 0 4px #0e749066;--ca-btnRadius:999px;--ca-dividerColor:#ffffff1a;--ca-tooltipBg:#374151;--ca-tooltipColor:#f9fafb}[data-theme=neon]{--ca-barBg:#0d0d0d;--ca-barShadow:0 2px 12px #0006;--ca-barBorder:1px solid #4fffcc66;--ca-barRadius:999px;--ca-btnColor:#aaa;--ca-btnHoverBg:#ffffff0f;--ca-btnActiveBg:#4fffcc;--ca-btnActiveColor:#0d0d0d;--ca-btnActiveGlow:0 0 12px #4fffcc, 0 0 24px #4fffcc88;--ca-btnRadius:999px;--ca-dividerColor:#ffffff1a;--ca-tooltipBg:#0d2e28;--ca-tooltipColor:#4fffcc}.ca-bar{background:var(--ca-barBg);box-shadow:var(--ca-barShadow);border-radius:var(--ca-barRadius);border:var(--ca-barBorder);align-items:center;gap:2px;padding:4px;display:inline-flex}.ca-divider{background:var(--ca-dividerColor);width:1px;height:16px;margin:0 4px}.ca-btn{width:32px;height:32px;color:var(--ca-btnColor);border-radius:var(--ca-btnRadius);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:background .15s,color .15s,box-shadow .15s;display:flex}.ca-btn:hover:not(:disabled){background:var(--ca-btnHoverBg)}.ca-btn[aria-pressed=true],.ca-btn[aria-pressed=true]:hover{background:var(--ca-btnActiveBg);color:var(--ca-btnActiveColor);box-shadow:var(--ca-btnActiveGlow)}.ca-btn:active{opacity:.8;transform:scale(.9)}.ca-btn:disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.ca-spinner{animation:.8s linear infinite ca-spin}.ca-tooltip-wrapper{display:inline-flex;position:relative}.ca-tooltip{background:var(--ca-tooltipBg);color:var(--ca-tooltipColor);white-space:nowrap;pointer-events:none;opacity:0;border-radius:4px;padding:4px 8px;font-size:11px;transition:opacity .15s;position:absolute}.ca-tooltip--top .ca-tooltip{bottom:calc(100% + 6px);left:50%;transform:translate(-50%)}.ca-tooltip--bottom .ca-tooltip{top:calc(100% + 6px);left:50%;transform:translate(-50%)}.ca-tooltip-wrapper:hover .ca-tooltip{opacity:1}@keyframes ca-spin{to{transform:rotate(360deg)}}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-ai-chat-actions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React actions bar for AI chat messages",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsdown",
|
|
20
|
+
"dev": "vite dev",
|
|
21
|
+
"storybook": "storybook dev -p 6006",
|
|
22
|
+
"build-storybook": "npx storybook build"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/kirilinsky/react-ai-chat-actions.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"react",
|
|
30
|
+
"next",
|
|
31
|
+
"ai",
|
|
32
|
+
"chat",
|
|
33
|
+
"actions",
|
|
34
|
+
"like",
|
|
35
|
+
"dislike",
|
|
36
|
+
"copy",
|
|
37
|
+
"message",
|
|
38
|
+
"toolbar",
|
|
39
|
+
"reactions"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"type": "module",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/kirilinsky/react-ai-chat-actions/issues"
|
|
45
|
+
},
|
|
46
|
+
"author": "Kirilinsky (https://github.com/kirilinsky)",
|
|
47
|
+
"homepage": "https://github.com/kirilinsky/react-ai-chat-actions#readme",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=18",
|
|
50
|
+
"react-dom": ">=18",
|
|
51
|
+
"var-th": ">=0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@tsdown/css": "^0.21.4",
|
|
55
|
+
"@types/node": "^25.5.0",
|
|
56
|
+
"@types/react": "^19.2.14",
|
|
57
|
+
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"react": "^19.2.4",
|
|
59
|
+
"react-dom": "^19.2.4",
|
|
60
|
+
"tsdown": "^0.21.3",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"storybook": "^10.3.1",
|
|
63
|
+
"@storybook/react-vite": "^10.3.1"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"lucide-react": "^0.577.0"
|
|
67
|
+
}
|
|
68
|
+
}
|