@pmate/chat-sdk 0.1.5 → 0.1.7

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.
@@ -0,0 +1 @@
1
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--spacing:.25rem;--ease-out:cubic-bezier(0,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.w-full{width:100%}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.border{border-style:var(--tw-border-style);border-width:1px}.uppercase{text-transform:uppercase}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -1 +1 @@
1
- import{R as e}from"./iframe-fcxekwVB.js";import"./preload-helper-PPVm8Dsz.js";const o={},c=e.createContext(o);function u(n){const t=e.useContext(c);return e.useMemo(function(){return typeof n=="function"?n(t):{...t,...n}},[t,n])}function r(n){let t;return n.disableParentContext?t=typeof n.components=="function"?n.components(o):n.components||o:t=u(n.components),e.createElement(c.Provider,{value:t},n.children)}export{r as MDXProvider,u as useMDXComponents};
1
+ import{R as e}from"./iframe-DVJDoCiO.js";import"./preload-helper-PPVm8Dsz.js";const o={},c=e.createContext(o);function u(n){const t=e.useContext(c);return e.useMemo(function(){return typeof n=="function"?n(t):{...t,...n}},[t,n])}function r(n){let t;return n.disableParentContext?t=typeof n.components=="function"?n.components(o):n.components||o:t=u(n.components),e.createElement(c.Provider,{value:t},n.children)}export{r as MDXProvider,u as useMDXComponents};
@@ -1 +1 @@
1
- import{r as s}from"./iframe-fcxekwVB.js";import{c as l}from"./client-BG97-bzh.js";import"./preload-helper-PPVm8Dsz.js";var n=new Map;function m(){return globalThis.IS_REACT_ACT_ENVIRONMENT}var a=({callback:e,children:r})=>{let t=s.useRef();return s.useLayoutEffect(()=>{t.current!==e&&(t.current=e,e())},[e]),r};typeof Promise.withResolvers>"u"&&(Promise.withResolvers=()=>{let e=null,r=null;return{promise:new Promise((t,o)=>{e=t,r=o}),resolve:e,reject:r}});var v=async(e,r,t)=>{let o=await c(r,t);if(m()){o.render(e);return}let{promise:i,resolve:u}=Promise.withResolvers();return o.render(s.createElement(a,{callback:u},e)),i},f=(e,r)=>{let t=n.get(e);t&&(t.unmount(),n.delete(e))},c=async(e,r)=>{let t=n.get(e);return t||(t=l.createRoot(e,r),n.set(e,t)),t};export{v as renderElement,f as unmountElement};
1
+ import{r as s}from"./iframe-DVJDoCiO.js";import{c as l}from"./client-De2ACSn_.js";import"./preload-helper-PPVm8Dsz.js";var n=new Map;function m(){return globalThis.IS_REACT_ACT_ENVIRONMENT}var a=({callback:e,children:r})=>{let t=s.useRef();return s.useLayoutEffect(()=>{t.current!==e&&(t.current=e,e())},[e]),r};typeof Promise.withResolvers>"u"&&(Promise.withResolvers=()=>{let e=null,r=null;return{promise:new Promise((t,o)=>{e=t,r=o}),resolve:e,reject:r}});var v=async(e,r,t)=>{let o=await c(r,t);if(m()){o.render(e);return}let{promise:i,resolve:u}=Promise.withResolvers();return o.render(s.createElement(a,{callback:u},e)),i},f=(e,r)=>{let t=n.get(e);t&&(t.unmount(),n.delete(e))},c=async(e,r)=>{let t=n.get(e);return t||(t=l.createRoot(e,r),n.set(e,t)),t};export{v as renderElement,f as unmountElement};
package/dist/iframe.html CHANGED
@@ -529,9 +529,9 @@
529
529
  }
530
530
  </script>
531
531
 
532
- <script type="module" crossorigin src="./assets/iframe-fcxekwVB.js"></script>
532
+ <script type="module" crossorigin src="./assets/iframe-DVJDoCiO.js"></script>
533
533
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-PPVm8Dsz.js">
534
- <link rel="stylesheet" crossorigin href="./assets/iframe-CWturJ6d.css">
534
+ <link rel="stylesheet" crossorigin href="./assets/iframe-q28pAgAa.css">
535
535
  </head>
536
536
 
537
537
  <body>
package/dist/index.json CHANGED
@@ -1 +1 @@
1
- {"v":5,"entries":{"pmate-chat-sdk-chatinput--mobile-idle":{"type":"story","id":"pmate-chat-sdk-chatinput--mobile-idle","name":"Mobile Idle","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--mobile-voice-ready":{"type":"story","id":"pmate-chat-sdk-chatinput--mobile-voice-ready","name":"Mobile Voice Ready","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--mobile-recording":{"type":"story","id":"pmate-chat-sdk-chatinput--mobile-recording","name":"Mobile Recording","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--desktop-typing":{"type":"story","id":"pmate-chat-sdk-chatinput--desktop-typing","name":"Desktop Typing","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--desktop-attachment":{"type":"story","id":"pmate-chat-sdk-chatinput--desktop-attachment","name":"Desktop Attachment","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]},"pmate-chat-sdk-chatinput--waveform-active":{"type":"story","id":"pmate-chat-sdk-chatinput--waveform-active","name":"Waveform Active","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]}}}
1
+ {"v":5,"entries":{"pmate-chat-sdk-chatinput--playground":{"type":"story","id":"pmate-chat-sdk-chatinput--playground","name":"Playground","title":"@pmate/chat-sdk/ChatInput","importPath":"./src/stories/ChatInput.stories.tsx","tags":["dev","test"]}}}
package/dist/project.json CHANGED
@@ -1 +1 @@
1
- {"generatedAt":1774594436837,"userSince":1774065191593,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":false,"refCount":0,"testPackages":{"vitest":"4.0.17"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"pnpm","version":"10.6.5","agent":"pnpm","nodeLinker":"undefined"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"9.1.17","language":"typescript","storybookPackages":{"@storybook/addon-docs":{"version":"9.1.17"},"@storybook/react-vite":{"version":"9.0.17"},"storybook":{"version":"9.1.17"}},"addons":{"$SNIP/node_modules/.pnpm/@storybook+addon-docs@9.1.17_@types+react@19.2.8_storybook@9.1.17_@testing-library+dom@_6282815dfd5a1a048c68a92ca86a2d86/node_modules/@storybook/addon-docs":{}}}
1
+ {"generatedAt":1777466785658,"userSince":1774065191593,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":false,"refCount":0,"testPackages":{"vitest":"4.0.17"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"pnpm","version":"10.6.5","agent":"pnpm","nodeLinker":"undefined"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"9.1.17","language":"typescript","storybookPackages":{"@storybook/addon-docs":{"version":"9.1.17"},"@storybook/react-vite":{"version":"9.0.17"},"storybook":{"version":"9.1.17"}},"addons":{"$SNIP/node_modules/.pnpm/@storybook+addon-docs@9.1.17_@types+react@19.2.8_storybook@9.1.17_@testing-library+dom@_6282815dfd5a1a048c68a92ca86a2d86/node_modules/@storybook/addon-docs":{}}}
@@ -1,13 +1,29 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  export function AttachmentPreview({ attachments, tokens, }) {
4
4
  if (attachments.length === 0) {
5
5
  return null;
6
6
  }
7
- return (_jsx("div", { className: "flex flex-wrap", style: { gap: tokens.spacing.attachmentGap }, children: attachments.map((attachment) => (_jsx("span", { style: {
7
+ return (_jsx("div", { className: "flex flex-wrap", style: { gap: tokens.spacing.attachmentGap }, children: attachments.map((attachment) => (_jsxs("span", { style: {
8
+ display: "inline-flex",
9
+ alignItems: "center",
10
+ gap: 6,
8
11
  borderRadius: tokens.radius.attachmentPreview,
9
12
  background: tokens.color.surfaceInputFocus,
10
13
  color: tokens.color.textSecondary,
11
- padding: "4px 10px",
12
- }, title: attachment.url ?? attachment.name, children: attachment.name }, attachment.id))) }));
14
+ border: `1px solid ${tokens.color.borderDefault}`,
15
+ padding: "6px 10px",
16
+ fontSize: 12,
17
+ lineHeight: "16px",
18
+ fontFamily: tokens.typography.familyBase,
19
+ boxShadow: tokens.shadow.inputElevated,
20
+ }, title: attachment.url ?? attachment.name, children: [_jsx("span", { "aria-hidden": "true", style: {
21
+ width: 6,
22
+ height: 6,
23
+ borderRadius: 999,
24
+ background: attachment.kind === "image"
25
+ ? tokens.color.iconActive
26
+ : tokens.color.textSecondary,
27
+ opacity: 0.9,
28
+ } }), attachment.name] }, attachment.id))) }));
13
29
  }
@@ -11,21 +11,23 @@ import { useVoiceInputController } from "../hooks/useVoiceInputController";
11
11
  import { AttachmentPreview } from "./AttachmentPreview";
12
12
  import { EmojiPicker } from "./EmojiPicker";
13
13
  import { EmojiTrigger } from "./EmojiTrigger";
14
+ import { MoreTrigger } from "./ImageTrigger";
14
15
  import { InputSurface } from "./InputSurface";
15
- import { SendAction } from "./SendAction";
16
16
  import { StatusHint } from "./StatusHint";
17
17
  import { TextComposer } from "./TextComposer";
18
18
  import { WaveformFeedback } from "./WaveformFeedback";
19
- import { ImageOutlineIcon, MicOutlineIcon } from "./icons";
19
+ import { FileOutlineIcon, ImageOutlineIcon, KeyboardOutlineIcon, MicOutlineIcon, } from "./icons";
20
20
  function DesktopInner(props) {
21
21
  const tokens = mergeChatInputTokens(props.tokens);
22
22
  const state = useChatInputState();
23
23
  const attachments = useAttachmentState();
24
24
  const [isEmojiOpen, setIsEmojiOpen] = useState(false);
25
+ const [isMoreOpen, setIsMoreOpen] = useState(false);
25
26
  const [isUploadingImage, setIsUploadingImage] = useState(false);
26
27
  const textComposerRef = useRef(null);
27
28
  const emojiAnchorRef = useRef(null);
28
29
  const imageInputRef = useRef(null);
30
+ const fileInputRef = useRef(null);
29
31
  const voice = useVoiceInputController({
30
32
  ...props.voice,
31
33
  onVoiceSend: async (blob) => {
@@ -99,6 +101,13 @@ function DesktopInner(props) {
99
101
  setIsUploadingImage(false);
100
102
  }
101
103
  };
104
+ const handleFileAttachment = (file) => {
105
+ attachments.addAttachment({
106
+ kind: "file",
107
+ name: file.name,
108
+ file,
109
+ });
110
+ };
102
111
  const insertEmoji = (emoji) => {
103
112
  const textarea = textComposerRef.current;
104
113
  const start = textarea?.selectionStart ?? state.text.length;
@@ -113,53 +122,145 @@ function DesktopInner(props) {
113
122
  textarea?.setSelectionRange(caret, caret);
114
123
  });
115
124
  };
116
- return (_jsx(InputSurface, { tokens: tokens, status: state.status, children: _jsxs("div", { className: "grid gap-3", style: { position: "relative" }, children: [_jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
125
+ const attachmentActionStyle = {
126
+ display: "grid",
127
+ justifyItems: "center",
128
+ alignContent: "center",
129
+ gap: 7,
130
+ minWidth: 72,
131
+ minHeight: 78,
132
+ border: 0,
133
+ borderRadius: 18,
134
+ background: "transparent",
135
+ color: tokens.color.textSecondary,
136
+ cursor: "pointer",
137
+ fontSize: 12,
138
+ fontFamily: tokens.typography.familyBase,
139
+ };
140
+ const attachmentIconStyle = {
141
+ width: 52,
142
+ height: 52,
143
+ borderRadius: 16,
144
+ display: "inline-flex",
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ background: tokens.color.surfaceInputDefault,
148
+ color: tokens.color.iconDefault,
149
+ border: `1px solid ${tokens.color.borderDefault}`,
150
+ boxShadow: "0 10px 26px rgba(0, 0, 0, 0.10)",
151
+ };
152
+ const sideActionStyle = {
153
+ display: "inline-flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ width: 32,
157
+ height: 32,
158
+ border: 0,
159
+ borderRadius: 999,
160
+ background: "transparent",
161
+ color: tokens.color.iconDefault,
162
+ cursor: "pointer",
163
+ alignSelf: "center",
164
+ transition: `color ${tokens.motion.stateTransition}, transform ${tokens.motion.pressFeedback}`,
165
+ };
166
+ return (_jsx("div", { style: { position: "relative", width: "100%" }, children: _jsxs("div", { className: "grid gap-3", style: { position: "relative", width: "100%" }, children: [_jsx("input", { ref: imageInputRef, type: "file", accept: props.image?.accept ?? "image/*", style: { display: "none" }, onChange: (event) => {
117
167
  const file = event.target.files?.[0];
118
168
  if (file) {
119
169
  void handleImageFile(file);
170
+ setIsMoreOpen(false);
120
171
  }
121
172
  event.currentTarget.value = "";
122
- } }), _jsx(EmojiPicker, { open: isEmojiOpen, isMobile: false, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, placeholder: "Message PMate", onChange: (event) => {
123
- state.setText(event.target.value);
124
- state.setStatus(event.target.value.trim() ? "typing" : "idle");
125
- props.onChange?.(event.target.value);
126
- } }), _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), voice.status === "recording" ? (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(WaveformFeedback, { levels: voice.waveformLevels, tokens: tokens, compact: true }), _jsxs("span", { style: { color: tokens.color.textSecondary, fontSize: 12 }, children: [Math.max(1, Math.round(voice.durationMs / 1000)), "s"] })] })) : null, _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(EmojiTrigger, { ref: emojiAnchorRef, style: { color: "#616161" }, onClick: () => setIsEmojiOpen((prev) => !prev) }), _jsx("button", { type: "button", onClick: () => imageInputRef.current?.click(), style: {
127
- display: "inline-flex",
128
- alignItems: "center",
129
- justifyContent: "center",
130
- width: 32,
131
- height: 32,
132
- border: 0,
133
- background: "transparent",
134
- color: "#616161",
135
- cursor: props.disabled || isUploadingImage ? "not-allowed" : "pointer",
136
- opacity: props.disabled || isUploadingImage ? 0.4 : 1,
137
- }, disabled: props.disabled || isUploadingImage, children: _jsx(ImageOutlineIcon, { size: 22 }) }), _jsx("button", { type: "button", onClick: () => {
138
- if (props.disabled || props.enableVoice === false) {
139
- return;
140
- }
141
- if (voice.status === "recording") {
142
- voice.stopRecording("transcribe");
143
- return;
173
+ } }), _jsx("input", { ref: fileInputRef, type: "file", style: { display: "none" }, onChange: (event) => {
174
+ const file = event.target.files?.[0];
175
+ if (file) {
176
+ handleFileAttachment(file);
177
+ setIsMoreOpen(false);
178
+ }
179
+ event.currentTarget.value = "";
180
+ } }), _jsx(EmojiPicker, { open: isEmojiOpen, isMobile: false, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx(AttachmentPreview, { attachments: attachments.attachments, tokens: tokens }), isMoreOpen ? (_jsxs("div", { style: {
181
+ position: "absolute",
182
+ left: 0,
183
+ right: 0,
184
+ bottom: 66,
185
+ padding: "12px 14px",
186
+ borderRadius: 24,
187
+ background: `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 94%, transparent)`,
188
+ border: `1px solid ${tokens.color.borderDefault}`,
189
+ display: "flex",
190
+ gap: 12,
191
+ boxShadow: "0 18px 50px rgba(0, 0, 0, 0.16)",
192
+ backdropFilter: "blur(18px)",
193
+ zIndex: 3,
194
+ }, children: [_jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled || isUploadingImage, onClick: () => imageInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(ImageOutlineIcon, { size: 24 }) }), "\u56FE\u7247"] }), _jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled, onClick: () => fileInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(FileOutlineIcon, { size: 24 }) }), "\u6587\u4EF6"] })] })) : null, voice.status === "recording" ? (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(WaveformFeedback, { levels: voice.waveformLevels, tokens: tokens, compact: true }), _jsxs("span", { style: { color: tokens.color.textSecondary, fontSize: 12 }, children: [Math.max(1, Math.round(voice.durationMs / 1000)), "s"] })] })) : null, _jsxs("div", { style: {
195
+ display: "grid",
196
+ gridTemplateColumns: "32px minmax(0, 1fr) 32px 32px",
197
+ alignItems: "center",
198
+ gap: 6,
199
+ }, children: [_jsx("button", { type: "button", onClick: () => {
200
+ if (props.disabled || props.enableVoice === false) {
201
+ return;
202
+ }
203
+ if (voice.status === "recording") {
204
+ voice.stopRecording("transcribe");
205
+ return;
206
+ }
207
+ voice.beginVoiceMode();
208
+ voice.startRecording();
209
+ }, style: {
210
+ ...sideActionStyle,
211
+ color: voice.status === "recording" || voice.status === "transcribing"
212
+ ? tokens.color.iconActive
213
+ : tokens.color.iconDefault,
214
+ cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
215
+ opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
216
+ }, "aria-label": voice.status === "recording"
217
+ ? "停止录音并转文字"
218
+ : "开始语音输入", children: voice.status === "recording" ? (_jsx(KeyboardOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
219
+ minHeight: 52,
220
+ padding: "0 14px",
221
+ display: "flex",
222
+ alignItems: "center",
223
+ }, children: _jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "Message PMate", onKeyDown: (event) => {
224
+ if (event.key !== "Enter") {
225
+ return;
226
+ }
227
+ const shouldSend = props.submitShortcut === "meta-enter" ? event.metaKey || event.ctrlKey : !event.shiftKey;
228
+ if (shouldSend) {
229
+ event.preventDefault();
230
+ if (state.text.trim() || state.attachments.length > 0) {
231
+ void onSend();
144
232
  }
145
- voice.beginVoiceMode();
146
- voice.startRecording();
147
- }, style: {
148
- display: "inline-flex",
149
- alignItems: "center",
150
- justifyContent: "center",
151
- width: 32,
152
- height: 32,
153
- border: 0,
154
- background: "transparent",
155
- color: voice.status === "recording" || voice.status === "transcribing"
156
- ? tokens.color.iconActive
157
- : "#616161",
158
- cursor: props.disabled || props.enableVoice === false ? "not-allowed" : "pointer",
159
- opacity: props.disabled || props.enableVoice === false ? 0.4 : 1,
160
- }, "aria-label": voice.status === "recording"
161
- ? "停止录音并转文字"
162
- : "开始语音输入", children: _jsx(MicOutlineIcon, { size: 22 }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { style: { color: tokens.color.textSecondary, fontSize: 12 }, children: "Enter send / Shift+Enter newline" }), _jsx(SendAction, { tokens: tokens, disabled: props.disabled || (!state.text.trim() && state.attachments.length === 0), onClick: () => void onSend(), children: "Send" })] })] }), _jsx(StatusHint, { tokens: tokens, text: desktopStatusText, tone: state.error ? "error" : "secondary" })] }) }));
233
+ }
234
+ }, onChange: (event) => {
235
+ state.setText(event.target.value);
236
+ state.setStatus(event.target.value.trim() ? "typing" : "idle");
237
+ props.onChange?.(event.target.value);
238
+ }, style: {
239
+ minHeight: tokens.typography.inputText.lineHeight,
240
+ height: tokens.typography.inputText.lineHeight,
241
+ maxHeight: 128,
242
+ paddingTop: 0,
243
+ paddingBottom: 0,
244
+ display: "block",
245
+ overflow: "hidden",
246
+ caretColor: tokens.color.brandPrimary,
247
+ } }) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, style: {
248
+ ...sideActionStyle,
249
+ width: 32,
250
+ height: 32,
251
+ }, onClick: () => {
252
+ setIsMoreOpen(false);
253
+ setIsEmojiOpen((prev) => !prev);
254
+ } }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
255
+ setIsEmojiOpen(false);
256
+ setIsMoreOpen((prev) => !prev);
257
+ }, disabled: props.disabled, style: {
258
+ ...sideActionStyle,
259
+ color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
260
+ width: 32,
261
+ height: 32,
262
+ opacity: props.disabled ? 0.4 : 1,
263
+ } })] }), _jsx(StatusHint, { tokens: tokens, text: desktopStatusText, tone: state.error ? "error" : "secondary" })] }) }));
163
264
  }
164
265
  export function ChatInputDesktop(props) {
165
266
  const stateKey = JSON.stringify({
@@ -13,25 +13,26 @@ import { AttachmentPreview } from "./AttachmentPreview";
13
13
  import { EmojiPicker } from "./EmojiPicker";
14
14
  import { EmojiTrigger } from "./EmojiTrigger";
15
15
  import { HoldToTalkButton } from "./HoldToTalkButton";
16
- import { ImageTrigger } from "./ImageTrigger";
16
+ import { MoreTrigger } from "./ImageTrigger";
17
17
  import { InputSurface } from "./InputSurface";
18
- import { SendAction } from "./SendAction";
19
18
  import { StatusHint } from "./StatusHint";
20
19
  import { TextComposer } from "./TextComposer";
21
20
  import { WaveformFeedback } from "./WaveformFeedback";
22
- import { KeyboardOutlineIcon, MicOutlineIcon } from "./icons";
21
+ import { FileOutlineIcon, ImageOutlineIcon, KeyboardOutlineIcon, MicOutlineIcon, } from "./icons";
23
22
  function MobileInner(props) {
24
23
  const tokens = mergeChatInputTokens(props.tokens);
25
24
  const state = useChatInputState();
26
25
  const attachments = useAttachmentState();
27
26
  const [recordingChoice, setRecordingChoice] = useState("send");
28
27
  const [isEmojiOpen, setIsEmojiOpen] = useState(false);
28
+ const [isMoreOpen, setIsMoreOpen] = useState(false);
29
29
  const [isUploadingImage, setIsUploadingImage] = useState(false);
30
30
  const cancelZoneRef = useRef(null);
31
31
  const transcribeZoneRef = useRef(null);
32
32
  const textComposerRef = useRef(null);
33
33
  const emojiAnchorRef = useRef(null);
34
34
  const imageInputRef = useRef(null);
35
+ const fileInputRef = useRef(null);
35
36
  const voice = useVoiceInputController({
36
37
  ...props.voice,
37
38
  onVoiceSend: async (blob) => {
@@ -98,6 +99,13 @@ function MobileInner(props) {
98
99
  setIsUploadingImage(false);
99
100
  }
100
101
  };
102
+ const handleFileAttachment = (file) => {
103
+ attachments.addAttachment({
104
+ kind: "file",
105
+ name: file.name,
106
+ file,
107
+ });
108
+ };
101
109
  const isRecordingOverlayVisible = state.status === "recording" || state.status === "transcribing";
102
110
  const updateRecordingChoice = (clientX, clientY) => {
103
111
  const isInside = (node) => {
@@ -135,13 +143,55 @@ function MobileInner(props) {
135
143
  textarea?.setSelectionRange(caret, caret);
136
144
  });
137
145
  };
146
+ const attachmentActionStyle = {
147
+ display: "grid",
148
+ justifyItems: "center",
149
+ alignContent: "center",
150
+ gap: 7,
151
+ minWidth: 72,
152
+ minHeight: 78,
153
+ border: 0,
154
+ borderRadius: 18,
155
+ background: "transparent",
156
+ color: tokens.color.textSecondary,
157
+ cursor: "pointer",
158
+ fontSize: 12,
159
+ fontFamily: tokens.typography.familyBase,
160
+ };
161
+ const attachmentIconStyle = {
162
+ width: 52,
163
+ height: 52,
164
+ borderRadius: 16,
165
+ display: "inline-flex",
166
+ alignItems: "center",
167
+ justifyContent: "center",
168
+ background: tokens.color.surfaceInputDefault,
169
+ color: tokens.color.iconDefault,
170
+ border: `1px solid ${tokens.color.borderDefault}`,
171
+ boxShadow: "0 10px 26px rgba(0, 0, 0, 0.10)",
172
+ };
173
+ const sideActionStyle = {
174
+ display: "inline-flex",
175
+ alignItems: "center",
176
+ justifyContent: "center",
177
+ width: 30,
178
+ height: 30,
179
+ border: 0,
180
+ borderRadius: 999,
181
+ background: "transparent",
182
+ color: tokens.color.iconDefault,
183
+ cursor: "pointer",
184
+ alignSelf: "center",
185
+ WebkitTapHighlightColor: "transparent",
186
+ transition: `color ${tokens.motion.stateTransition}, transform ${tokens.motion.pressFeedback}`,
187
+ };
138
188
  return (_jsxs("div", { style: {
139
189
  position: "relative",
140
190
  width: "100%",
141
- maxWidth: 420,
191
+ maxWidth: "min(420px, calc(100vw - 16px))",
142
192
  padding: 0,
143
- background: "#ECE9E3",
144
- borderRadius: 0,
193
+ background: "transparent",
194
+ borderRadius: tokens.radius.inputSurface,
145
195
  border: 0,
146
196
  boxShadow: "none",
147
197
  overflow: "visible",
@@ -233,40 +283,67 @@ function MobileInner(props) {
233
283
  const file = event.target.files?.[0];
234
284
  if (file) {
235
285
  void handleImageFile(file);
286
+ setIsMoreOpen(false);
287
+ }
288
+ event.currentTarget.value = "";
289
+ } }), _jsx("input", { ref: fileInputRef, type: "file", style: { display: "none" }, onChange: (event) => {
290
+ const file = event.target.files?.[0];
291
+ if (file) {
292
+ handleFileAttachment(file);
293
+ setIsMoreOpen(false);
236
294
  }
237
295
  event.currentTarget.value = "";
238
- } }), _jsxs("div", { style: {
296
+ } }), isMoreOpen ? (_jsxs("div", { style: {
297
+ position: "absolute",
298
+ left: 0,
299
+ right: 0,
300
+ bottom: 64,
301
+ padding: "12px 14px",
302
+ borderRadius: 24,
303
+ background: `color-mix(in srgb, ${tokens.color.surfaceInputFocus} 94%, transparent)`,
304
+ border: `1px solid ${tokens.color.borderDefault}`,
305
+ display: "flex",
306
+ gap: 12,
307
+ boxShadow: "0 18px 50px rgba(0, 0, 0, 0.16)",
308
+ backdropFilter: "blur(18px)",
309
+ zIndex: 3,
310
+ }, children: [_jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled || isUploadingImage, onClick: () => imageInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(ImageOutlineIcon, { size: 24 }) }), "\u56FE\u7247"] }), _jsxs("button", { type: "button", style: attachmentActionStyle, disabled: props.disabled, onClick: () => fileInputRef.current?.click(), children: [_jsx("span", { style: attachmentIconStyle, children: _jsx(FileOutlineIcon, { size: 24 }) }), "\u6587\u4EF6"] })] })) : null, _jsxs("div", { style: {
239
311
  position: "relative",
240
312
  display: "grid",
241
- gridTemplateColumns: "28px minmax(0, 1fr) auto auto",
313
+ gridTemplateColumns: "30px minmax(0, 1fr) 30px 30px",
242
314
  gap: 4,
243
- alignItems: "end",
244
- padding: "4px 0 env(safe-area-inset-bottom, 0px)",
315
+ alignItems: "center",
316
+ padding: "0 0 env(safe-area-inset-bottom, 0px)",
245
317
  }, children: [_jsx(EmojiPicker, { open: isEmojiOpen, isMobile: true, anchorRef: emojiAnchorRef, onSelect: insertEmoji, onClose: () => setIsEmojiOpen(false) }), _jsx("button", { type: "button", "aria-label": state.mode === "voice" ? "切换到文字输入" : "切换到语音输入", disabled: props.disabled || props.enableVoice === false, onClick: () => {
246
318
  const next = state.mode === "voice" ? "text" : "voice";
247
319
  state.setMode(next);
248
320
  state.setStatus(next === "voice" ? "voice-mode-ready" : "idle");
249
321
  }, style: {
250
- display: "inline-flex",
251
- alignItems: "center",
252
- justifyContent: "center",
253
- width: 28,
254
- height: 28,
255
- border: 0,
256
- borderRadius: 999,
257
- background: "transparent",
258
- color: "#111111",
259
- cursor: "pointer",
260
- alignSelf: "center",
322
+ ...sideActionStyle,
323
+ color: state.mode === "voice"
324
+ ? tokens.color.iconActive
325
+ : tokens.color.iconDefault,
261
326
  opacity: props.disabled ? 0.45 : 1,
262
- }, children: state.mode === "voice" ? _jsx(KeyboardOutlineIcon, { size: 24 }) : _jsx(MicOutlineIcon, { size: 24 }) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
263
- borderRadius: 14,
264
- background: "#FFFFFF",
265
- borderColor: state.status === "error" ? tokens.color.borderError : "#E5E7EB",
266
- boxShadow: "none",
267
- minHeight: state.mode === "voice" ? 54 : 44,
327
+ }, children: state.mode === "voice" ? (_jsx(KeyboardOutlineIcon, { size: 27 })) : (_jsx(MicOutlineIcon, { size: 24, strokeWidth: 2 })) }), _jsx(InputSurface, { tokens: tokens, status: state.status, styleOverride: {
328
+ borderRadius: tokens.radius.inputSurface,
329
+ background: state.status === "error"
330
+ ? tokens.color.surfaceInputError
331
+ : state.status === "recording"
332
+ ? tokens.color.surfaceInputRecording
333
+ : tokens.color.surfaceInputDefault,
334
+ borderColor: state.status === "error"
335
+ ? tokens.color.borderError
336
+ : state.status === "typing"
337
+ ? tokens.color.borderFocus
338
+ : tokens.color.borderDefault,
339
+ boxShadow: state.status === "recording"
340
+ ? tokens.shadow.recordingActive
341
+ : tokens.shadow.inputElevated,
342
+ minHeight: 48,
343
+ display: "flex",
344
+ alignItems: "center",
268
345
  padding: state.mode === "voice" ? "0" : "0 12px",
269
- }, children: state.mode === "voice" ? (_jsx("div", { children: _jsx(HoldToTalkButton, { tokens: tokens, active: isRecordingOverlayVisible, disabled: props.disabled, text: "\u6309\u4F4F \u8BF4\u8BDD", activeText: "\u677E\u5F00 \u53D1\u9001", onPressStart: () => {
346
+ }, children: state.mode === "voice" ? (_jsx("div", { style: { minHeight: 48 }, children: _jsx(HoldToTalkButton, { tokens: tokens, active: isRecordingOverlayVisible, disabled: props.disabled, text: "\u6309\u4F4F \u8BF4\u8BDD", activeText: "\u677E\u5F00 \u53D1\u9001", onPressStart: () => {
270
347
  setRecordingChoice("send");
271
348
  voice.startRecording();
272
349
  }, onPressMove: (event) => {
@@ -285,36 +362,47 @@ function MobileInner(props) {
285
362
  }, onPressCancel: () => {
286
363
  setRecordingChoice("send");
287
364
  voice.cancelRecording();
288
- } }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, placeholder: "\u53D1\u6D88\u606F", className: "placeholder:text-[#8A8A8E]", onChange: (event) => {
365
+ } }) })) : (_jsx(TextComposer, { ref: textComposerRef, tokens: tokens, value: state.text, rows: 1, wrap: "off", placeholder: "Message", enterKeyHint: "send", onKeyDown: (event) => {
366
+ if (event.key === "Enter" && !event.shiftKey) {
367
+ event.preventDefault();
368
+ if (state.text.trim()) {
369
+ void onSend();
370
+ }
371
+ }
372
+ }, onChange: (event) => {
289
373
  state.setText(event.target.value);
290
374
  state.setStatus(event.target.value.trim() ? "typing" : "idle");
291
375
  props.onChange?.(event.target.value);
292
376
  }, style: {
293
- minHeight: 18,
294
- paddingTop: 10,
295
- paddingBottom: 10,
296
- color: "#171717",
297
- fontSize: 16,
298
- lineHeight: "22px",
377
+ minHeight: tokens.typography.inputText.lineHeight,
378
+ height: tokens.typography.inputText.lineHeight,
379
+ maxHeight: 92,
380
+ paddingTop: 0,
381
+ paddingBottom: 0,
382
+ display: "block",
383
+ overflow: "hidden",
384
+ whiteSpace: "pre",
385
+ color: tokens.color.textPrimary,
386
+ fontSize: tokens.typography.inputText.size,
387
+ lineHeight: `${tokens.typography.inputText.lineHeight}px`,
299
388
  caretColor: tokens.color.brandPrimary,
300
- } })) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, onClick: () => setIsEmojiOpen((prev) => !prev), style: {
301
- color: "#111111",
302
- alignSelf: "center",
303
- width: 28,
304
- height: 28,
305
- } }), state.mode === "text" && state.text.trim() ? (_jsx(SendAction, { tokens: tokens, disabled: props.disabled, onClick: () => void onSend(), style: {
306
- minWidth: 64,
307
- minHeight: 30,
308
- borderRadius: 12,
309
- alignSelf: "center",
310
- padding: "0 12px",
311
- }, children: "\u53D1\u9001" })) : (_jsx(ImageTrigger, { "data-icon-size": 24, onClick: () => imageInputRef.current?.click(), disabled: props.disabled || isUploadingImage, style: {
312
- color: "#111111",
313
- alignSelf: "center",
314
- width: 28,
315
- height: 28,
316
- opacity: props.disabled || isUploadingImage ? 0.4 : 1,
317
- } }))] }), _jsx(StatusHint, { tokens: tokens, text: state.status === "recording" || state.status === "transcribing" ? undefined : statusText, tone: state.status === "error" ? "error" : "secondary" })] }));
389
+ } })) }), _jsx(EmojiTrigger, { ref: emojiAnchorRef, "data-icon-size": 24, onClick: () => {
390
+ setIsMoreOpen(false);
391
+ setIsEmojiOpen((prev) => !prev);
392
+ }, style: {
393
+ ...sideActionStyle,
394
+ width: 30,
395
+ height: 30,
396
+ } }), _jsx(MoreTrigger, { "data-icon-size": 24, onClick: () => {
397
+ setIsEmojiOpen(false);
398
+ setIsMoreOpen((prev) => !prev);
399
+ }, disabled: props.disabled, style: {
400
+ ...sideActionStyle,
401
+ color: isMoreOpen ? tokens.color.iconActive : tokens.color.iconDefault,
402
+ width: 30,
403
+ height: 30,
404
+ opacity: props.disabled ? 0.4 : 1,
405
+ } })] }), _jsx(StatusHint, { tokens: tokens, text: state.status === "recording" || state.status === "transcribing" ? undefined : statusText, tone: state.status === "error" ? "error" : "secondary" })] }));
318
406
  }
319
407
  export function ChatInputMobile(props) {
320
408
  const stateKey = JSON.stringify({