@asgard-js/react 0.0.38-canary.6 → 0.0.39-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 (29) hide show
  1. package/README.md +2 -0
  2. package/dist/components/chatbot/api-key-input/api-key-input.d.ts +11 -0
  3. package/dist/components/chatbot/api-key-input/api-key-input.d.ts.map +1 -0
  4. package/dist/components/chatbot/api-key-input/index.d.ts +2 -0
  5. package/dist/components/chatbot/api-key-input/index.d.ts.map +1 -0
  6. package/dist/components/chatbot/chatbot.d.ts +2 -0
  7. package/dist/components/chatbot/chatbot.d.ts.map +1 -1
  8. package/dist/components/templates/hint-template/hint-template.d.ts.map +1 -1
  9. package/dist/components/templates/quick-replies/quick-replies.d.ts.map +1 -1
  10. package/dist/components/templates/text-template/text-template.d.ts.map +1 -1
  11. package/dist/context/asgard-theme-context.d.ts +6 -1
  12. package/dist/context/asgard-theme-context.d.ts.map +1 -1
  13. package/dist/hooks/use-react-markdown-renderer.d.ts.map +1 -1
  14. package/dist/index.js +15952 -15650
  15. package/dist/style.css +1 -1
  16. package/dist/utils/color-utils.d.ts +15 -0
  17. package/dist/utils/color-utils.d.ts.map +1 -0
  18. package/package.json +2 -2
  19. package/src/components/chatbot/api-key-input/api-key-input.module.scss +192 -0
  20. package/src/components/chatbot/api-key-input/api-key-input.tsx +119 -0
  21. package/src/components/chatbot/api-key-input/index.ts +1 -0
  22. package/src/components/chatbot/chatbot.tsx +82 -10
  23. package/src/components/templates/hint-template/hint-template.module.scss +4 -0
  24. package/src/components/templates/hint-template/hint-template.tsx +6 -1
  25. package/src/components/templates/quick-replies/quick-replies.tsx +5 -2
  26. package/src/components/templates/text-template/text-template.tsx +6 -2
  27. package/src/context/asgard-theme-context.tsx +146 -1
  28. package/src/hooks/use-react-markdown-renderer.tsx +14 -2
  29. package/src/utils/color-utils.ts +52 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Darkens a color by a given percentage
3
+ * @param color - Hex color string (e.g., "#640000")
4
+ * @param percentage - Percentage to darken (e.g., 0.2 for 20%)
5
+ * @returns Darkened hex color string
6
+ */
7
+ export declare function darkenColor(color: string, percentage: number): string;
8
+ /**
9
+ * Lightens a color by mixing with white
10
+ * @param color - Hex color string (e.g., "#640000")
11
+ * @param percentage - Percentage to lighten (e.g., 0.8 for 20% original, 80% white)
12
+ * @returns Lightened hex color string
13
+ */
14
+ export declare function lightenColor(color: string, percentage: number): string;
15
+ //# sourceMappingURL=color-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"color-utils.d.ts","sourceRoot":"","sources":["../../src/utils/color-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAmBrE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBtE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asgard-js/react",
3
- "version": "0.0.38-canary.6",
3
+ "version": "0.0.39-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.38-canary.6",
57
+ "@asgard-js/core": "^0.0.38",
58
58
  "react": "^18.0.0",
59
59
  "react-dom": "^18.0.0"
60
60
  },
@@ -0,0 +1,192 @@
1
+ .container {
2
+ width: 220px;
3
+ background: rgba(45, 45, 45, 0.95);
4
+ border-radius: 12px;
5
+ padding: 24px 20px 20px;
6
+ backdrop-filter: blur(10px);
7
+ border: 1px solid rgba(255, 255, 255, 0.1);
8
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
9
+ color: white;
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
11
+ }
12
+
13
+ .header {
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ margin-bottom: 24px;
18
+ }
19
+
20
+ .icon {
21
+ width: 48px;
22
+ height: 48px;
23
+ margin-bottom: 12px;
24
+ opacity: 0.7;
25
+ }
26
+
27
+ .title {
28
+ margin: 0;
29
+ font-size: 18px;
30
+ font-weight: 500;
31
+ color: white;
32
+ text-align: center;
33
+ }
34
+
35
+ .form {
36
+ width: 100%;
37
+ }
38
+
39
+ .inputGroup {
40
+ margin-bottom: 20px;
41
+ }
42
+
43
+ .label {
44
+ display: block;
45
+ font-size: 14px;
46
+ font-weight: 400;
47
+ color: rgba(255, 255, 255, 0.7);
48
+ margin-bottom: 8px;
49
+ }
50
+
51
+ .inputWrapper {
52
+ position: relative;
53
+ width: 100%;
54
+ }
55
+
56
+ .input {
57
+ width: 100%;
58
+ height: 42px;
59
+ padding: 0 40px 0 12px;
60
+ font-size: 14px;
61
+ font-weight: 400;
62
+ color: rgba(255, 255, 255, 0.8);
63
+ background: rgba(255, 255, 255, 0.05);
64
+ border: 1px solid rgba(255, 255, 255, 0.1);
65
+ border-radius: 6px;
66
+ outline: none;
67
+ transition: all 0.2s ease;
68
+ box-sizing: border-box;
69
+
70
+ &::placeholder {
71
+ color: rgba(255, 255, 255, 0.4);
72
+ }
73
+
74
+ &:focus {
75
+ border-color: rgba(255, 255, 255, 0.3);
76
+ background: rgba(255, 255, 255, 0.08);
77
+ }
78
+
79
+ &.error {
80
+ border-color: rgba(255, 69, 58, 0.6);
81
+ background: rgba(255, 69, 58, 0.05);
82
+ }
83
+
84
+ &.disabled {
85
+ opacity: 0.5;
86
+ cursor: not-allowed;
87
+ }
88
+ }
89
+
90
+ .toggleButton {
91
+ position: absolute;
92
+ right: 8px;
93
+ top: 50%;
94
+ transform: translateY(-50%);
95
+ background: transparent;
96
+ border: none;
97
+ color: rgba(255, 255, 255, 0.5);
98
+ cursor: pointer;
99
+ padding: 4px;
100
+ border-radius: 4px;
101
+ transition: all 0.2s ease;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+
106
+ &:hover:not(:disabled) {
107
+ color: rgba(255, 255, 255, 0.7);
108
+ background: rgba(255, 255, 255, 0.05);
109
+ }
110
+
111
+ &:disabled {
112
+ opacity: 0.3;
113
+ cursor: not-allowed;
114
+ }
115
+ }
116
+
117
+ .toggleIcon {
118
+ width: 16px;
119
+ height: 16px;
120
+ }
121
+
122
+ .errorMessage {
123
+ margin-top: 6px;
124
+ font-size: 12px;
125
+ color: rgba(255, 69, 58, 0.8);
126
+ }
127
+
128
+ .submitButton {
129
+ width: 100%;
130
+ height: 42px;
131
+ background: #5856D6;
132
+ border: none;
133
+ border-radius: 6px;
134
+ color: white;
135
+ font-size: 14px;
136
+ font-weight: 500;
137
+ cursor: pointer;
138
+ transition: all 0.2s ease;
139
+ outline: none;
140
+
141
+ &:hover:not(:disabled) {
142
+ background: #4845C7;
143
+ transform: translateY(-1px);
144
+ }
145
+
146
+ &:active:not(:disabled) {
147
+ transform: translateY(0);
148
+ }
149
+
150
+ &:disabled {
151
+ opacity: 0.5;
152
+ cursor: not-allowed;
153
+ transform: none;
154
+ }
155
+
156
+ &.loading {
157
+ position: relative;
158
+ color: transparent;
159
+
160
+ &::after {
161
+ content: '';
162
+ position: absolute;
163
+ top: 50%;
164
+ left: 50%;
165
+ width: 16px;
166
+ height: 16px;
167
+ margin-left: -8px;
168
+ margin-top: -8px;
169
+ border: 2px solid transparent;
170
+ border-top: 2px solid white;
171
+ border-radius: 50%;
172
+ animation: spin 1s linear infinite;
173
+ }
174
+ }
175
+ }
176
+
177
+ @keyframes spin {
178
+ 0% {
179
+ transform: rotate(0deg);
180
+ }
181
+ 100% {
182
+ transform: rotate(360deg);
183
+ }
184
+ }
185
+
186
+ // 響應式設計
187
+ @media (max-width: 280px) {
188
+ .container {
189
+ width: 100%;
190
+ min-width: 200px;
191
+ }
192
+ }
@@ -0,0 +1,119 @@
1
+ import { useState, FormEvent, ChangeEvent } from 'react';
2
+ import clsx from 'clsx';
3
+ import ProfileSvg from '../../../icons/profile.svg?react';
4
+ import styles from './api-key-input.module.scss';
5
+
6
+ export interface ApiKeyInputProps {
7
+ onSubmit: (apiKey: string) => void;
8
+ loading?: boolean;
9
+ error?: string;
10
+ placeholder?: string;
11
+ title?: string;
12
+ showToggle?: boolean;
13
+ className?: string;
14
+ }
15
+
16
+ export function ApiKeyInput({
17
+ onSubmit,
18
+ loading = false,
19
+ error,
20
+ placeholder = 'Enter your key',
21
+ title = 'Preview',
22
+ showToggle = true,
23
+ className,
24
+ }: ApiKeyInputProps) {
25
+ const [apiKey, setApiKey] = useState('');
26
+ const [showPassword, setShowPassword] = useState(false);
27
+
28
+ const handleSubmit = (e: FormEvent) => {
29
+ e.preventDefault();
30
+ if (apiKey.trim() && !loading) {
31
+ onSubmit(apiKey.trim());
32
+ }
33
+ };
34
+
35
+ const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
36
+ setApiKey(e.target.value);
37
+ };
38
+
39
+ const togglePasswordVisibility = () => {
40
+ setShowPassword(!showPassword);
41
+ };
42
+
43
+ return (
44
+ <div className={clsx(styles.container, className)}>
45
+ <div className={styles.header}>
46
+ <ProfileSvg className={styles.icon} />
47
+ <h2 className={styles.title}>{title}</h2>
48
+ </div>
49
+
50
+ <form onSubmit={handleSubmit} className={styles.form}>
51
+ <div className={styles.inputGroup}>
52
+ <label className={styles.label}>Key</label>
53
+ <div className={styles.inputWrapper}>
54
+ <input
55
+ type={showPassword ? 'text' : 'password'}
56
+ value={apiKey}
57
+ onChange={handleInputChange}
58
+ placeholder={placeholder}
59
+ className={clsx(styles.input, {
60
+ [styles.error]: error,
61
+ [styles.disabled]: loading,
62
+ })}
63
+ disabled={loading}
64
+ autoComplete="off"
65
+ />
66
+ {showToggle && (
67
+ <button
68
+ type="button"
69
+ onClick={togglePasswordVisibility}
70
+ className={styles.toggleButton}
71
+ disabled={loading}
72
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
73
+ >
74
+ <svg
75
+ className={styles.toggleIcon}
76
+ width="16"
77
+ height="16"
78
+ viewBox="0 0 24 24"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ strokeWidth="2"
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ >
85
+ {showPassword ? (
86
+ <>
87
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
88
+ <line x1="1" y1="1" x2="23" y2="23"/>
89
+ </>
90
+ ) : (
91
+ <>
92
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
93
+ <circle cx="12" cy="12" r="3"/>
94
+ </>
95
+ )}
96
+ </svg>
97
+ </button>
98
+ )}
99
+ </div>
100
+ {error && (
101
+ <div className={styles.errorMessage}>
102
+ {error}
103
+ </div>
104
+ )}
105
+ </div>
106
+
107
+ <button
108
+ type="submit"
109
+ disabled={!apiKey.trim() || loading}
110
+ className={clsx(styles.submitButton, {
111
+ [styles.loading]: loading,
112
+ })}
113
+ >
114
+ {loading ? 'Loading...' : 'Continue'}
115
+ </button>
116
+ </form>
117
+ </div>
118
+ );
119
+ }
@@ -0,0 +1 @@
1
+ export * from './api-key-input';
@@ -1,4 +1,9 @@
1
- import { forwardRef, ForwardedRef, ReactNode, CSSProperties } from 'react';
1
+ import {
2
+ forwardRef,
3
+ ForwardedRef,
4
+ ReactNode,
5
+ CSSProperties,
6
+ } from 'react';
2
7
  import { ClientConfig, ConversationMessage } from '@asgard-js/core';
3
8
  import {
4
9
  AsgardThemeContextProvider,
@@ -12,11 +17,14 @@ import {
12
17
  AsgardAppInitializationContextProvider,
13
18
  AsgardServiceContextProviderProps,
14
19
  } from '../../context';
20
+ import { ApiKeyInput } from './api-key-input';
15
21
  import { ChatbotHeader } from './chatbot-header';
16
22
  import { ChatbotBody } from './chatbot-body';
17
23
  import { ChatbotFooter } from './chatbot-footer';
18
24
  import { ChatbotContainer } from './chatbot-container/chatbot-container';
19
25
 
26
+ type AuthState = 'loading' | 'needApiKey' | 'authenticated' | 'error';
27
+
20
28
  interface ChatbotProps extends AsgardTemplateContextValue {
21
29
  className?: string;
22
30
  style?: CSSProperties;
@@ -38,6 +46,9 @@ interface ChatbotProps extends AsgardTemplateContextValue {
38
46
  onClose?: () => void;
39
47
  loadingComponent?: ReactNode;
40
48
  defaultLinkTarget?: '_blank' | '_self' | '_parent' | '_top';
49
+
50
+ // Auth state prop
51
+ authState?: AuthState;
41
52
  }
42
53
 
43
54
  export interface ChatbotRef {
@@ -72,8 +83,77 @@ export const Chatbot = forwardRef(function Chatbot(
72
83
  className,
73
84
  style,
74
85
  defaultLinkTarget,
86
+ authState = 'authenticated',
75
87
  } = props;
76
88
 
89
+ // Render different content based on authState
90
+ const renderContent = () => {
91
+ switch (authState) {
92
+ case 'loading':
93
+ return (
94
+ <div style={{
95
+ display: 'flex',
96
+ alignItems: 'center',
97
+ justifyContent: 'center',
98
+ flex: 1,
99
+ padding: '20px'
100
+ }}>
101
+ {loadingComponent || <div>Loading...</div>}
102
+ </div>
103
+ );
104
+
105
+ case 'needApiKey':
106
+ return (
107
+ <div style={{
108
+ display: 'flex',
109
+ alignItems: 'center',
110
+ justifyContent: 'center',
111
+ flex: 1,
112
+ padding: '20px'
113
+ }}>
114
+ <ApiKeyInput
115
+ title={title}
116
+ onSubmit={() => {}} // Will be handled by parent
117
+ placeholder="Enter your key"
118
+ />
119
+ </div>
120
+ );
121
+
122
+ case 'error':
123
+ return (
124
+ <div style={{
125
+ display: 'flex',
126
+ alignItems: 'center',
127
+ justifyContent: 'center',
128
+ flex: 1,
129
+ padding: '20px',
130
+ color: '#ff6b6b'
131
+ }}>
132
+ <div style={{ textAlign: 'center' }}>
133
+ <div style={{ fontSize: '16px', marginBottom: '8px' }}>⚠️</div>
134
+ <div>Something went wrong. Please try again later.</div>
135
+ </div>
136
+ </div>
137
+ );
138
+
139
+ case 'authenticated':
140
+ default:
141
+ return (
142
+ <>
143
+ <AsgardTemplateContextProvider
144
+ onErrorClick={onErrorClick}
145
+ errorMessageRenderer={errorMessageRenderer}
146
+ onTemplateBtnClick={onTemplateBtnClick}
147
+ defaultLinkTarget={defaultLinkTarget}
148
+ >
149
+ <ChatbotBody />
150
+ </AsgardTemplateContextProvider>
151
+ <ChatbotFooter />
152
+ </>
153
+ );
154
+ }
155
+ };
156
+
77
157
  return (
78
158
  <AsgardAppInitializationContextProvider
79
159
  enabled={enableLoadConfigFromService}
@@ -104,15 +184,7 @@ export const Chatbot = forwardRef(function Chatbot(
104
184
  customActions={customActions}
105
185
  maintainConnectionWhenClosed={maintainConnectionWhenClosed}
106
186
  />
107
- <AsgardTemplateContextProvider
108
- onErrorClick={onErrorClick}
109
- errorMessageRenderer={errorMessageRenderer}
110
- onTemplateBtnClick={onTemplateBtnClick}
111
- defaultLinkTarget={defaultLinkTarget}
112
- >
113
- <ChatbotBody />
114
- </AsgardTemplateContextProvider>
115
- <ChatbotFooter />
187
+ {renderContent()}
116
188
  </ChatbotContainer>
117
189
  </AsgardServiceContextProvider>
118
190
  </AsgardThemeContextProvider>
@@ -33,6 +33,10 @@
33
33
  }
34
34
  }
35
35
 
36
+ .hint_text {
37
+ color: white;
38
+ }
39
+
36
40
  .time {
37
41
  font-size: 12px;
38
42
  color: rgba(140, 140, 140, 1);
@@ -65,7 +65,12 @@ export function HintTemplate(props: HintTemplateProps): ReactNode {
65
65
  style={themeTemplate?.HintMessageTemplate?.style}
66
66
  >
67
67
  <div className={classes.time}>{formatTime(message.time)}</div>
68
- {template.text}
68
+ <div
69
+ className={classes.hint_text}
70
+ style={themeTemplate?.HintMessageTemplate?.style}
71
+ >
72
+ {template.text}
73
+ </div>
69
74
  </div>
70
75
  );
71
76
  }
@@ -11,7 +11,7 @@ interface QuickRepliesProps {
11
11
  export function QuickReplies(props: QuickRepliesProps): ReactNode {
12
12
  const { quickReplies } = props;
13
13
 
14
- const { template } = useAsgardThemeContext();
14
+ const { template, botMessage } = useAsgardThemeContext();
15
15
  const { sendMessage, isConnecting } = useAsgardContext();
16
16
 
17
17
  const onClick = useCallback(
@@ -32,7 +32,10 @@ export function QuickReplies(props: QuickRepliesProps): ReactNode {
32
32
  <button
33
33
  key={quickReply.text}
34
34
  className={styles.quick_reply}
35
- style={template?.quickReplies?.button?.style}
35
+ style={{
36
+ ...template?.quickReplies?.button?.style,
37
+ backgroundColor: botMessage?.quickReplyBackgroundColor || template?.quickReplies?.button?.style?.backgroundColor,
38
+ }}
36
39
  disabled={isConnecting}
37
40
  onClick={() => onClick(quickReply.text)}
38
41
  >
@@ -19,6 +19,7 @@ export function TextTemplate(props: TextTemplateProps): ReactNode {
19
19
  const { avatar } = useAsgardContext();
20
20
 
21
21
  const theme = useAsgardThemeContext();
22
+ const { botMessage } = theme;
22
23
 
23
24
  const { htmlBlocks, lastTypingText } = useMarkdownRenderer(
24
25
  (message as ConversationBotMessage)?.message?.text || '',
@@ -40,9 +41,12 @@ export function TextTemplate(props: TextTemplateProps): ReactNode {
40
41
  backgroundColor: theme?.botMessage?.backgroundColor,
41
42
  };
42
43
  default:
43
- return {};
44
+ return {
45
+ color: theme?.chatbot?.primaryComponent?.secondaryColor || theme?.template?.TextMessageTemplate?.style?.color,
46
+ backgroundColor: botMessage?.unsentBackgroundColor,
47
+ };
44
48
  }
45
- }, [message, theme]);
49
+ }, [message, theme, botMessage]);
46
50
 
47
51
  if (message.type === 'error') return null;
48
52