@asgard-js/react 0.0.40 → 0.0.41-canary.1

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 (137) hide show
  1. package/dist/components/chatbot/chatbot-body/conversation-message-renderer.d.ts.map +1 -1
  2. package/dist/components/chatbot/chatbot-footer/chatbot-footer.d.ts.map +1 -1
  3. package/dist/components/templates/index.d.ts +1 -0
  4. package/dist/components/templates/index.d.ts.map +1 -1
  5. package/dist/components/templates/user-image-template/index.d.ts +2 -0
  6. package/dist/components/templates/user-image-template/index.d.ts.map +1 -0
  7. package/dist/components/templates/user-image-template/user-image-template.d.ts +12 -0
  8. package/dist/components/templates/user-image-template/user-image-template.d.ts.map +1 -0
  9. package/dist/context/asgard-service-context.d.ts +1 -0
  10. package/dist/context/asgard-service-context.d.ts.map +1 -1
  11. package/dist/hooks/use-channel.d.ts +1 -1
  12. package/dist/hooks/use-channel.d.ts.map +1 -1
  13. package/dist/index.js +18353 -18684
  14. package/dist/style.css +1 -1
  15. package/dist/utils/file-validation.d.ts +12 -0
  16. package/dist/utils/file-validation.d.ts.map +1 -0
  17. package/package.json +2 -2
  18. package/.babelrc +0 -12
  19. package/eslint.config.cjs +0 -12
  20. package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +0 -13
  21. package/src/components/chatbot/chatbot-body/chatbot-body.tsx +0 -45
  22. package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +0 -55
  23. package/src/components/chatbot/chatbot-body/index.ts +0 -1
  24. package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +0 -41
  25. package/src/components/chatbot/chatbot-container/chatbot-container.tsx +0 -49
  26. package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +0 -54
  27. package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +0 -67
  28. package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +0 -140
  29. package/src/components/chatbot/chatbot-footer/index.ts +0 -1
  30. package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +0 -132
  31. package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +0 -48
  32. package/src/components/chatbot/chatbot-header/chatbot-header.tsx +0 -98
  33. package/src/components/chatbot/chatbot-header/index.ts +0 -1
  34. package/src/components/chatbot/chatbot.spec.tsx +0 -8
  35. package/src/components/chatbot/chatbot.tsx +0 -121
  36. package/src/components/chatbot/profile-icon.tsx +0 -26
  37. package/src/components/index.ts +0 -2
  38. package/src/components/templates/avatar/avatar.module.scss +0 -6
  39. package/src/components/templates/avatar/avatar.tsx +0 -28
  40. package/src/components/templates/avatar/index.ts +0 -1
  41. package/src/components/templates/button-template/button-template.module.scss +0 -0
  42. package/src/components/templates/button-template/button-template.tsx +0 -45
  43. package/src/components/templates/button-template/card.module.scss +0 -58
  44. package/src/components/templates/button-template/card.spec.tsx +0 -213
  45. package/src/components/templates/button-template/card.tsx +0 -123
  46. package/src/components/templates/button-template/index.ts +0 -1
  47. package/src/components/templates/carousel-template/carousel-template.module.scss +0 -15
  48. package/src/components/templates/carousel-template/carousel-template.tsx +0 -49
  49. package/src/components/templates/carousel-template/index.ts +0 -1
  50. package/src/components/templates/chart-template/chart-template.module.scss +0 -52
  51. package/src/components/templates/chart-template/chart-template.tsx +0 -75
  52. package/src/components/templates/chart-template/index.ts +0 -1
  53. package/src/components/templates/hint-template/hint-template.module.scss +0 -43
  54. package/src/components/templates/hint-template/hint-template.tsx +0 -76
  55. package/src/components/templates/hint-template/index.ts +0 -1
  56. package/src/components/templates/image-template/image-template.module.scss +0 -67
  57. package/src/components/templates/image-template/image-template.tsx +0 -58
  58. package/src/components/templates/image-template/index.ts +0 -1
  59. package/src/components/templates/index.ts +0 -10
  60. package/src/components/templates/quick-replies/index.ts +0 -1
  61. package/src/components/templates/quick-replies/quick-replies.module.scss +0 -16
  62. package/src/components/templates/quick-replies/quick-replies.tsx +0 -47
  63. package/src/components/templates/template-box/index.ts +0 -2
  64. package/src/components/templates/template-box/template-box-content.module.scss +0 -13
  65. package/src/components/templates/template-box/template-box-content.tsx +0 -30
  66. package/src/components/templates/template-box/template-box.module.scss +0 -19
  67. package/src/components/templates/template-box/template-box.tsx +0 -48
  68. package/src/components/templates/text-template/bot-typing-box.tsx +0 -81
  69. package/src/components/templates/text-template/bot-typing-placeholder.tsx +0 -28
  70. package/src/components/templates/text-template/index.ts +0 -3
  71. package/src/components/templates/text-template/text-template.module.scss +0 -131
  72. package/src/components/templates/text-template/text-template.tsx +0 -94
  73. package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +0 -758
  74. package/src/components/templates/time/index.ts +0 -1
  75. package/src/components/templates/time/time.module.scss +0 -6
  76. package/src/components/templates/time/time.tsx +0 -34
  77. package/src/context/asgard-app-initialization-context.tsx +0 -154
  78. package/src/context/asgard-service-context.tsx +0 -145
  79. package/src/context/asgard-template-context.tsx +0 -83
  80. package/src/context/asgard-theme-context.tsx +0 -546
  81. package/src/context/index.ts +0 -4
  82. package/src/hooks/index.ts +0 -11
  83. package/src/hooks/use-asgard-service-client.ts +0 -68
  84. package/src/hooks/use-channel.ts +0 -154
  85. package/src/hooks/use-debounce.ts +0 -18
  86. package/src/hooks/use-deep-compare-memo.ts +0 -19
  87. package/src/hooks/use-is-on-screen-keyboard-open.ts +0 -43
  88. package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +0 -15
  89. package/src/hooks/use-prevent-over-scrolling.ts +0 -77
  90. package/src/hooks/use-react-markdown-renderer.tsx +0 -278
  91. package/src/hooks/use-resize-observer.tsx +0 -27
  92. package/src/hooks/use-update-vh.ts +0 -30
  93. package/src/hooks/use-viewport-size.ts +0 -51
  94. package/src/icons/add_a_photo.svg +0 -3
  95. package/src/icons/bot.svg +0 -14
  96. package/src/icons/close.svg +0 -3
  97. package/src/icons/distance.svg +0 -3
  98. package/src/icons/mic.svg +0 -3
  99. package/src/icons/photo_library.svg +0 -3
  100. package/src/icons/profile.svg +0 -28
  101. package/src/icons/refresh.svg +0 -3
  102. package/src/icons/send.svg +0 -3
  103. package/src/icons/stop.svg +0 -22
  104. package/src/icons/volume_up.svg +0 -3
  105. package/src/index.ts +0 -4
  106. package/src/models/bot-provider.ts +0 -108
  107. package/src/styles/_index.scss +0 -1
  108. package/src/styles/_styles.scss +0 -11
  109. package/src/styles/colors/_colors.scss +0 -10
  110. package/src/styles/colors/_index.scss +0 -1
  111. package/src/styles/colors/_variables.scss +0 -72
  112. package/src/styles/palette/_index.scss +0 -1
  113. package/src/styles/palette/_palette.scss +0 -42
  114. package/src/styles/palette/_variables.scss +0 -40
  115. package/src/styles/radius/_index.scss +0 -1
  116. package/src/styles/radius/_radius.scss +0 -8
  117. package/src/styles/radius/_variables.scss +0 -12
  118. package/src/styles/spacing/_index.scss +0 -1
  119. package/src/styles/spacing/_spacing.scss +0 -8
  120. package/src/styles/spacing/_variables.scss +0 -13
  121. package/src/styles/utils/_index.scss +0 -1
  122. package/src/styles/utils/_map.scss +0 -22
  123. package/src/test-setup.ts +0 -1
  124. package/src/utils/color-utils.ts +0 -52
  125. package/src/utils/deep-merge.ts +0 -26
  126. package/src/utils/extractors.ts +0 -20
  127. package/src/utils/format-time.ts +0 -8
  128. package/src/utils/index.ts +0 -1
  129. package/src/utils/is.ts +0 -72
  130. package/src/utils/selectors.ts +0 -7
  131. package/src/utils/uri-validation.spec.ts +0 -208
  132. package/src/utils/uri-validation.ts +0 -103
  133. package/tsconfig.json +0 -16
  134. package/tsconfig.lib.json +0 -63
  135. package/tsconfig.spec.json +0 -36
  136. package/tsconfig.tsbuildinfo +0 -1
  137. package/vite.config.ts +0 -63
@@ -0,0 +1,12 @@
1
+ export declare const SUPPORTED_IMAGE_TYPES: string[];
2
+ export declare const MAX_FILE_SIZE: number;
3
+ export interface FileValidationResult {
4
+ isValid: boolean;
5
+ error?: string;
6
+ }
7
+ export declare function validateImageFile(file: File): FileValidationResult;
8
+ export declare function validateImageFiles(files: FileList | File[]): {
9
+ validFiles: File[];
10
+ errors: string[];
11
+ };
12
+ //# sourceMappingURL=file-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-validation.d.ts","sourceRoot":"","sources":["../../src/utils/file-validation.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,UAMjC,CAAC;AAEF,eAAO,MAAM,aAAa,QAAmB,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,oBAAoB,CAsBlE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG;IAC5D,UAAU,EAAE,IAAI,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAgBA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asgard-js/react",
3
- "version": "0.0.40",
3
+ "version": "0.0.41-canary.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -54,7 +54,7 @@
54
54
  "vitest": "^1.6.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "@asgard-js/core": "^0.0.40",
57
+ "@asgard-js/core": "^0.0.41-canary.1",
58
58
  "react": "^18.0.0",
59
59
  "react-dom": "^18.0.0"
60
60
  },
package/.babelrc DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "presets": [
3
- [
4
- "@nx/react/babel",
5
- {
6
- "runtime": "automatic",
7
- "useBuiltIns": "usage"
8
- }
9
- ]
10
- ],
11
- "plugins": []
12
- }
package/eslint.config.cjs DELETED
@@ -1,12 +0,0 @@
1
- const nx = require('@nx/eslint-plugin');
2
- const baseConfig = require('../../eslint.config.cjs');
3
-
4
- module.exports = [
5
- ...baseConfig,
6
- ...nx.configs['flat/react'],
7
- {
8
- files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
9
- // Override or add rules here
10
- rules: {},
11
- },
12
- ];
@@ -1,13 +0,0 @@
1
- .chatbot_body {
2
- overflow-x: hidden;
3
- overflow-y: scroll;
4
-
5
- .chatbot_body__content {
6
- margin: 0 auto;
7
- display: flex;
8
- flex-direction: column;
9
- padding: 16px;
10
- gap: 16px;
11
- max-width: 1200px;
12
- }
13
- }
@@ -1,45 +0,0 @@
1
- import { ReactNode, useEffect, useMemo } from 'react';
2
- import { useAsgardContext } from '../../../context/asgard-service-context';
3
- import styles from './chatbot-body.module.scss';
4
- import { ConversationMessageRenderer } from './conversation-message-renderer';
5
- import { BotTypingPlaceholder } from '../../templates';
6
- import { useAsgardThemeContext } from '../../../context/asgard-theme-context';
7
- import clsx from 'clsx';
8
-
9
- export function ChatbotBody(): ReactNode {
10
- const { chatbot } = useAsgardThemeContext();
11
-
12
- const { messages, messageBoxBottomRef, botTypingPlaceholder } =
13
- useAsgardContext();
14
-
15
- useEffect(() => {
16
- messageBoxBottomRef.current?.scrollIntoView({ behavior: 'smooth' });
17
- }, [messages, messageBoxBottomRef]);
18
-
19
- const contentStyles = useMemo(
20
- () => ({
21
- maxWidth: chatbot?.contentMaxWidth ?? '1200px',
22
- }),
23
- [chatbot]
24
- );
25
-
26
- return (
27
- <div
28
- className={clsx('asgard-chatbot-body', styles.chatbot_body)}
29
- style={chatbot?.body?.style}
30
- >
31
- <div className={styles.chatbot_body__content} style={contentStyles}>
32
- {Array.from(messages?.values() ?? []).map((message) => (
33
- <ConversationMessageRenderer
34
- key={message.messageId}
35
- message={message}
36
- />
37
- ))}
38
- <BotTypingPlaceholder
39
- placeholder={botTypingPlaceholder ?? '正在輸入訊息'}
40
- />
41
- <div ref={messageBoxBottomRef} />
42
- </div>
43
- </div>
44
- );
45
- }
@@ -1,55 +0,0 @@
1
- import { ReactNode } from 'react';
2
- import { ConversationMessage, MessageTemplateType } from '@asgard-js/core';
3
- import {
4
- BotTypingBox,
5
- ButtonTemplate,
6
- CarouselTemplate,
7
- HintTemplate,
8
- TextTemplate,
9
- ChartTemplate,
10
- ImageTemplate,
11
- } from '../../templates';
12
-
13
- interface ConversationMessageRendererProps {
14
- message: ConversationMessage;
15
- }
16
-
17
- export function ConversationMessageRenderer(
18
- props: ConversationMessageRendererProps
19
- ): ReactNode {
20
- const { message } = props;
21
-
22
- if (message.type === 'user') {
23
- return <TextTemplate message={message} />;
24
- }
25
-
26
- if (message.type === 'error') {
27
- return <HintTemplate message={message} />;
28
- }
29
-
30
- if (message.isTyping) {
31
- return (
32
- <BotTypingBox
33
- isTyping={message.isTyping}
34
- typingText={message.typingText}
35
- />
36
- );
37
- }
38
-
39
- switch (message.message.template?.type) {
40
- case MessageTemplateType.TEXT:
41
- return <TextTemplate message={message} />;
42
- case MessageTemplateType.HINT:
43
- return <HintTemplate message={message} />;
44
- case MessageTemplateType.BUTTON:
45
- return <ButtonTemplate message={message} />;
46
- case MessageTemplateType.CAROUSEL:
47
- return <CarouselTemplate message={message} />;
48
- case MessageTemplateType.CHART:
49
- return <ChartTemplate message={message} />;
50
- case MessageTemplateType.IMAGE:
51
- return <ImageTemplate message={message} />;
52
- default:
53
- return <div />;
54
- }
55
- }
@@ -1 +0,0 @@
1
- export * from './chatbot-body';
@@ -1,41 +0,0 @@
1
- @use '../../../styles';
2
-
3
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100..900&family=Space+Grotesk:wght@300..700&display=swap');
4
-
5
- .chatbot_root {
6
- @include styles.generate();
7
-
8
- font-family: 'Space Grotesk', 'Noto Sans TC', sans-serif;
9
-
10
- * {
11
- line-height: 1.5;
12
- box-sizing: border-box;
13
- }
14
- }
15
-
16
- .full_screen {
17
- position: fixed;
18
- top: 0;
19
- left: 0;
20
- width: 100%;
21
- height: calc(var(--vh, 1vh) * 100);
22
- background-color: var(--asg-color-bg);
23
-
24
- .chatbot_container {
25
- height: 100%;
26
- transition: all 0.1s;
27
- padding: env(safe-area-inset-top) env(safe-area-inset-right)
28
- env(safe-area-inset-bottom) env(safe-area-inset-left);
29
-
30
- &.screen_keyboard_open {
31
- padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
32
- env(safe-area-inset-left);
33
- }
34
- }
35
- }
36
-
37
- .chatbot_container {
38
- display: grid;
39
- grid-template-rows: max-content auto max-content;
40
- background-color: var(--asg-color-bg);
41
- }
@@ -1,49 +0,0 @@
1
- import { PropsWithChildren, ReactNode, useRef, CSSProperties } from 'react';
2
- import { useUpdateVh } from '../../../hooks';
3
- import { ChatbotFullScreenContainer } from './chatbot-full-screen-container';
4
- import classes from './chatbot-container.module.scss';
5
- import { useAsgardThemeContext } from '../../../context/asgard-theme-context';
6
- import clsx from 'clsx';
7
-
8
- interface ChatbotContainerProps extends PropsWithChildren {
9
- className?: string;
10
- style?: CSSProperties;
11
- fullScreen?: boolean;
12
- }
13
-
14
- export function ChatbotContainer(props: ChatbotContainerProps): ReactNode {
15
- const { fullScreen, children, className, style = {} } = props;
16
-
17
- const rootRef = useRef<HTMLDivElement>(null);
18
-
19
- useUpdateVh(rootRef);
20
-
21
- const {
22
- chatbot: {
23
- style: rootStyle,
24
- header,
25
- body,
26
- footer,
27
- ...chatbotInnerContainerStyle
28
- },
29
- } = useAsgardThemeContext();
30
-
31
- return (
32
- <div
33
- ref={rootRef}
34
- className={clsx(classes.chatbot_root, className)}
35
- style={Object.assign({}, rootStyle, style)}
36
- >
37
- {fullScreen ? (
38
- <ChatbotFullScreenContainer>{children}</ChatbotFullScreenContainer>
39
- ) : (
40
- <div
41
- className={classes.chatbot_container}
42
- style={chatbotInnerContainerStyle}
43
- >
44
- {children}
45
- </div>
46
- )}
47
- </div>
48
- );
49
- }
@@ -1,54 +0,0 @@
1
- import { PropsWithChildren, ReactNode, useMemo, useRef } from 'react';
2
- import clsx from 'clsx';
3
- import {
4
- useIsOnScreenKeyboardOpen,
5
- useOnScreenKeyboardScrollFix,
6
- usePreventOverScrolling,
7
- useViewportSize,
8
- } from '../../../hooks';
9
- import classes from './chatbot-container.module.scss';
10
- import { useAsgardThemeContext } from '../../../context/asgard-theme-context';
11
-
12
- export function ChatbotFullScreenContainer(
13
- props: PropsWithChildren
14
- ): ReactNode {
15
- const { children } = props;
16
-
17
- const containerRef = useRef<HTMLDivElement>(null);
18
-
19
- const theme = useAsgardThemeContext();
20
-
21
- usePreventOverScrolling(containerRef);
22
-
23
- useOnScreenKeyboardScrollFix();
24
-
25
- const [, height] = useViewportSize() ?? [];
26
-
27
- const isOnScreenKeyboardOpen = useIsOnScreenKeyboardOpen();
28
-
29
- const styles = useMemo(() => {
30
- return Object.assign(
31
- theme?.chatbot?.backgroundColor
32
- ? {
33
- backgroundColor: theme.chatbot?.backgroundColor,
34
- }
35
- : {},
36
- isOnScreenKeyboardOpen ? { height } : {}
37
- );
38
- }, [height, isOnScreenKeyboardOpen, theme]);
39
-
40
- return (
41
- <div className={classes.full_screen}>
42
- <div
43
- ref={containerRef}
44
- className={clsx(
45
- classes.chatbot_container,
46
- isOnScreenKeyboardOpen && classes.screen_keyboard_open
47
- )}
48
- style={styles}
49
- >
50
- {children}
51
- </div>
52
- </div>
53
- );
54
- }
@@ -1,67 +0,0 @@
1
- .chatbot_footer {
2
- border-top: 1px solid #434343;
3
-
4
- .chatbot_footer__content {
5
- display: grid;
6
- align-items: flex-end;
7
- grid-template-columns: auto 24px;
8
- margin: 0 auto;
9
- max-width: 1200px;
10
- padding: var(--asg-spacing-3) var(--asg-spacing-4);
11
- gap: var(--asg-spacing-2);
12
- }
13
- }
14
-
15
- .chatbot_textarea {
16
- font: inherit;
17
- font-size: 16px;
18
- height: 36px;
19
- max-height: 240px;
20
- padding: 8px;
21
- padding-left: 12px;
22
- background: var(--asg-color-bg);
23
- border: 1px solid rgba(67, 67, 67, 1);
24
- border-radius: 2px;
25
- color: rgba(140, 140, 140, 1);
26
- resize: none;
27
- overflow: hidden;
28
-
29
- &::placeholder {
30
- color: var(--asg-color-text-placeholder);
31
- }
32
- }
33
-
34
- .chatbot_submit_button {
35
- font: inherit;
36
- width: 24px;
37
- height: 100%;
38
- max-height: 38px;
39
- display: flex;
40
- justify-content: center;
41
- align-items: center;
42
- font-size: 16px;
43
- border: none;
44
- background: transparent;
45
- border-radius: 50%;
46
- color: white;
47
- cursor: pointer;
48
-
49
- > svg {
50
- -webkit-touch-callout: none;
51
- -webkit-user-select: none;
52
- user-select: none;
53
- width: auto;
54
- min-width: 16px;
55
- height: 60%;
56
- max-height: 38px;
57
- min-height: 16px;
58
- }
59
-
60
- &.chatbot_submit_button__disabled {
61
- > svg {
62
- > path {
63
- fill: rgba(140, 140, 140, 1);
64
- }
65
- }
66
- }
67
- }
@@ -1,140 +0,0 @@
1
- import {
2
- ChangeEventHandler,
3
- KeyboardEventHandler,
4
- ReactNode,
5
- useCallback,
6
- useEffect,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from 'react';
11
- import { useAsgardContext } from '../../../context/asgard-service-context';
12
- import styles from './chatbot-footer.module.scss';
13
- import SendSvg from '../../../icons/send.svg?react';
14
- import { SpeechInputButton } from './speech-input-button';
15
- import clsx from 'clsx';
16
- import { useAsgardThemeContext } from '../../../context/asgard-theme-context';
17
-
18
- export function ChatbotFooter(): ReactNode {
19
- const { sendMessage, isConnecting, inputPlaceholder } = useAsgardContext();
20
-
21
- const { chatbot } = useAsgardThemeContext();
22
-
23
- const [value, setValue] = useState('');
24
- const [isComposing, setIsComposing] = useState(false);
25
- const textareaRef = useRef<HTMLTextAreaElement>(null);
26
-
27
- const disabled = useMemo(
28
- () => isConnecting || !value.trim(),
29
- [isConnecting, value]
30
- );
31
-
32
- const contentStyles = useMemo(
33
- () => ({
34
- maxWidth: chatbot?.contentMaxWidth ?? '1200px',
35
- borderTopColor: chatbot?.borderColor,
36
- }),
37
- [chatbot]
38
- );
39
-
40
- const onChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
41
- (event) => {
42
- const element = event.target as HTMLTextAreaElement;
43
- const value = element.value;
44
-
45
- element.style.height = '36px';
46
-
47
- if (value) {
48
- element.style.height = `${element.scrollHeight}px`;
49
- }
50
-
51
- setValue(event.target.value);
52
- },
53
- []
54
- );
55
-
56
- const onSubmit = useCallback(() => {
57
- if (!isComposing && !isConnecting) {
58
- sendMessage?.({ text: value });
59
- setValue('');
60
-
61
- if (textareaRef.current) {
62
- textareaRef.current.style.height = '36px';
63
- }
64
- }
65
- }, [isComposing, isConnecting, sendMessage, value]);
66
-
67
- const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
68
- (event) => {
69
- if (
70
- event.key === 'Enter' &&
71
- !isComposing &&
72
- !isConnecting &&
73
- value.trim()
74
- ) {
75
- sendMessage?.({ text: value });
76
- setValue('');
77
-
78
- const element = event.target as HTMLTextAreaElement;
79
-
80
- element.style.height = '36px';
81
- }
82
- },
83
- [isComposing, isConnecting, sendMessage, value]
84
- );
85
-
86
- useEffect(() => {
87
- if (textareaRef.current) {
88
- textareaRef.current.style.setProperty(
89
- '--asg-color-text-placeholder',
90
- chatbot.footer?.textArea?.['::placeholder']?.color ??
91
- 'var(--asg-color-text-placeholder)'
92
- );
93
- }
94
- }, [chatbot.footer?.textArea]);
95
-
96
- return (
97
- <div
98
- className={clsx('asgard-chatbot-footer', styles.chatbot_footer)}
99
- style={chatbot.footer?.style}
100
- >
101
- <div className={styles.chatbot_footer__content} style={contentStyles}>
102
- <textarea
103
- ref={textareaRef}
104
- className={styles.chatbot_textarea}
105
- style={chatbot.footer?.textArea?.style}
106
- disabled={isConnecting}
107
- cols={40}
108
- value={value}
109
- placeholder={inputPlaceholder || "Enter message"}
110
- onChange={onChange}
111
- onKeyDown={onKeyDown}
112
- onCompositionStart={() => setIsComposing(true)}
113
- onCompositionEnd={() => setIsComposing(false)}
114
- />
115
- {value ? (
116
- <button
117
- className={clsx(
118
- styles.chatbot_submit_button,
119
- disabled && styles.chatbot_submit_button__disabled
120
- )}
121
- style={chatbot.footer?.submitButton?.style}
122
- disabled={disabled}
123
- onClick={onSubmit}
124
- >
125
- <SendSvg />
126
- </button>
127
- ) : (
128
- <SpeechInputButton
129
- setValue={setValue}
130
- className={clsx(
131
- styles.chatbot_submit_button,
132
- isConnecting && styles.chatbot_submit_button__disabled
133
- )}
134
- style={chatbot.footer?.speechInputButton?.style}
135
- />
136
- )}
137
- </div>
138
- </div>
139
- );
140
- }
@@ -1 +0,0 @@
1
- export * from './chatbot-footer';
@@ -1,132 +0,0 @@
1
- import {
2
- Dispatch,
3
- MouseEventHandler,
4
- ReactNode,
5
- SetStateAction,
6
- TouchEventHandler,
7
- useCallback,
8
- useEffect,
9
- useRef,
10
- useState,
11
- CSSProperties,
12
- } from 'react';
13
- import MicSvg from '../../../icons/mic.svg?react';
14
- import StopSvg from '../../../icons/stop.svg?react';
15
-
16
- interface SpeechInputButtonProps {
17
- setValue: Dispatch<SetStateAction<string>>;
18
- className?: string;
19
- style?: CSSProperties;
20
- }
21
-
22
- export function SpeechInputButton(props: SpeechInputButtonProps): ReactNode {
23
- const { setValue, className, style } = props;
24
-
25
- const [listening, setListening] = useState(false);
26
- const recognitionRef = useRef<SpeechRecognition | null>(null);
27
-
28
- useEffect(() => {
29
- const SpeechRecognition =
30
- window.SpeechRecognition || window.webkitSpeechRecognition;
31
-
32
- if (!SpeechRecognition) return;
33
-
34
- const recognition = new SpeechRecognition();
35
- recognition.lang = 'zh-TW';
36
- recognition.continuous = true;
37
- recognition.interimResults = true;
38
-
39
- recognition.onresult = (event: SpeechRecognitionEvent): void => {
40
- for (let i = event.resultIndex; i < event.results.length; i++) {
41
- if (event.results[i].isFinal) {
42
- setValue((prev) => prev + event.results[i][0].transcript);
43
- }
44
- }
45
- };
46
-
47
- recognition.onerror = (event: SpeechRecognitionErrorEvent): void => {
48
- alert(`語音識別錯誤: ${JSON.stringify(event.error)}`);
49
- };
50
-
51
- recognition.onend = (): void => {
52
- setListening(false);
53
- };
54
-
55
- recognitionRef.current = recognition;
56
- }, [setValue]);
57
-
58
- const startListening = useCallback(() => {
59
- if (!recognitionRef.current) {
60
- alert('無法開始辨識: 語音識別器未初始化,請檢查是否有授權使用麥克風');
61
-
62
- return;
63
- }
64
-
65
- try {
66
- recognitionRef.current.start();
67
- setListening(true);
68
- } catch (error) {
69
- alert(`無法開始辨識: ${JSON.stringify(error)}`);
70
- }
71
- }, []);
72
-
73
- const stopListening = useCallback(() => {
74
- if (!recognitionRef.current) return;
75
-
76
- recognitionRef.current.stop();
77
- setListening(false);
78
- }, []);
79
-
80
- const onMouseDown = useCallback<MouseEventHandler<HTMLDivElement>>(
81
- (event) => {
82
- if (!listening) {
83
- event.preventDefault();
84
- startListening();
85
- }
86
- },
87
- [listening, startListening]
88
- );
89
-
90
- const onMouseUp = useCallback<MouseEventHandler<HTMLDivElement>>(
91
- (event) => {
92
- if (listening) {
93
- event.preventDefault();
94
- stopListening();
95
- }
96
- },
97
- [listening, stopListening]
98
- );
99
-
100
- const onTouchStart = useCallback<TouchEventHandler<HTMLDivElement>>(
101
- (event) => {
102
- if (!listening) {
103
- event.preventDefault();
104
- startListening();
105
- }
106
- },
107
- [listening, startListening]
108
- );
109
-
110
- const onTouchEnd = useCallback<TouchEventHandler<HTMLDivElement>>(
111
- (event) => {
112
- if (listening) {
113
- event.preventDefault();
114
- stopListening();
115
- }
116
- },
117
- [listening, stopListening]
118
- );
119
-
120
- return (
121
- <div
122
- className={className}
123
- style={style}
124
- onMouseDown={onMouseDown}
125
- onMouseUp={onMouseUp}
126
- onTouchStart={onTouchStart}
127
- onTouchEnd={onTouchEnd}
128
- >
129
- {listening ? <StopSvg /> : <MicSvg />}
130
- </div>
131
- );
132
- }