@asgard-js/react 0.0.42-canary.5 → 0.0.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asgard-js/react",
3
- "version": "0.0.42-canary.5",
3
+ "version": "0.0.42",
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.42-canary.1",
57
+ "@asgard-js/core": "^0.0.42",
58
58
  "react": "^18.0.0",
59
59
  "react-dom": "^18.0.0"
60
60
  },
Binary file
@@ -1,61 +1,61 @@
1
- .container {
2
- width: 220px;
3
- background: rgba(45, 45, 45, 0.95);
1
+ .api_key_input {
2
+ width: 360px;
3
+ max-width: calc(100vw - 32px);
4
+ background-color: var(--asg-color-bg);
4
5
  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;
6
+ padding: 24px;
7
+ border: 0.5px solid var(--asg-color-border);
8
+
9
+ @media (max-width: 480px) {
10
+ width: 100%;
11
+ max-width: none;
12
+ padding: 20px;
13
+ margin: 0 16px;
14
+ }
11
15
  }
12
16
 
13
- .header {
17
+ .api_key_input__header {
14
18
  display: flex;
15
19
  flex-direction: column;
20
+ gap: 8px;
16
21
  align-items: center;
17
- margin-bottom: 24px;
22
+ margin-bottom: 20px;
18
23
  }
19
24
 
20
- .icon {
21
- width: 48px;
22
- height: 48px;
23
- margin-bottom: 12px;
24
- opacity: 0.7;
25
+ .api_key_input__icon {
26
+ width: 40px;
27
+ height: 40px;
25
28
  }
26
29
 
27
- .title {
30
+ .api_key_input__title {
28
31
  margin: 0;
29
- font-size: 18px;
32
+ font-size: 20px;
30
33
  font-weight: 500;
31
34
  color: white;
32
- text-align: center;
33
35
  }
34
36
 
35
- .form {
37
+ .api_key_input__form {
36
38
  width: 100%;
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 20px;
37
42
  }
38
43
 
39
- .inputGroup {
40
- margin-bottom: 20px;
41
- }
42
-
43
- .label {
44
+ .api_key_input__label {
44
45
  display: block;
45
46
  font-size: 14px;
46
- font-weight: 400;
47
47
  color: rgba(255, 255, 255, 0.7);
48
48
  margin-bottom: 8px;
49
49
  }
50
50
 
51
- .inputWrapper {
51
+ .api_key_input__input_wrapper {
52
52
  position: relative;
53
53
  width: 100%;
54
54
  }
55
55
 
56
- .input {
56
+ .api_key_input__input {
57
57
  width: 100%;
58
- height: 42px;
58
+ height: 48px;
59
59
  padding: 0 40px 0 12px;
60
60
  font-size: 14px;
61
61
  font-weight: 400;
@@ -67,6 +67,11 @@
67
67
  transition: all 0.2s ease;
68
68
  box-sizing: border-box;
69
69
 
70
+ @media (max-width: 480px) {
71
+ height: 44px;
72
+ font-size: 16px;
73
+ }
74
+
70
75
  &::placeholder {
71
76
  color: rgba(255, 255, 255, 0.4);
72
77
  }
@@ -76,18 +81,18 @@
76
81
  background: rgba(255, 255, 255, 0.08);
77
82
  }
78
83
 
79
- &.error {
80
- border-color: rgba(255, 69, 58, 0.6);
81
- background: rgba(255, 69, 58, 0.05);
84
+ &--error {
85
+ border-color: #FF4D4F;
86
+ background: rgba(255, 77, 79, 0.05);
82
87
  }
83
88
 
84
- &.disabled {
89
+ &--disabled {
85
90
  opacity: 0.5;
86
91
  cursor: not-allowed;
87
92
  }
88
93
  }
89
94
 
90
- .toggleButton {
95
+ .api_key_input__toggle_button {
91
96
  position: absolute;
92
97
  right: 8px;
93
98
  top: 50%;
@@ -114,23 +119,23 @@
114
119
  }
115
120
  }
116
121
 
117
- .toggleIcon {
122
+ .api_key_input__toggle_icon {
118
123
  width: 16px;
119
124
  height: 16px;
120
125
  }
121
126
 
122
- .errorMessage {
127
+ .api_key_input__error_message {
123
128
  margin-top: 6px;
124
129
  font-size: 12px;
125
- color: rgba(255, 69, 58, 0.8);
130
+ color: #FF4D4F;
126
131
  }
127
132
 
128
- .submitButton {
133
+ .api_key_input__submit_button {
129
134
  width: 100%;
130
- height: 42px;
131
- background: #5856D6;
135
+ height: 48px;
132
136
  border: none;
133
137
  border-radius: 6px;
138
+ background-color: #5856d6;
134
139
  color: white;
135
140
  font-size: 14px;
136
141
  font-weight: 500;
@@ -138,13 +143,9 @@
138
143
  transition: all 0.2s ease;
139
144
  outline: none;
140
145
 
141
- &:hover:not(:disabled) {
142
- background: #4845C7;
143
- transform: translateY(-1px);
144
- }
145
-
146
- &:active:not(:disabled) {
147
- transform: translateY(0);
146
+ @media (max-width: 480px) {
147
+ height: 44px;
148
+ font-size: 16px;
148
149
  }
149
150
 
150
151
  &:disabled {
@@ -152,41 +153,4 @@
152
153
  cursor: not-allowed;
153
154
  transform: none;
154
155
  }
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
156
  }
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
- }
@@ -1,6 +1,10 @@
1
1
  import { useState, FormEvent, ChangeEvent } from 'react';
2
2
  import clsx from 'clsx';
3
- import ProfileSvg from '../../../icons/profile.svg?react';
3
+ import { useAsgardThemeContext } from '../../../context/asgard-theme-context';
4
+ import { useAsgardContext } from '../../../context/asgard-service-context';
5
+ import { ProfileIcon } from '../profile-icon';
6
+ import EyeSvg from '../../../icons/eye.svg?react';
7
+ import EyeOffSvg from '../../../icons/eye-off.svg?react';
4
8
  import styles from './api-key-input.module.scss';
5
9
 
6
10
  export interface ApiKeyInputProps {
@@ -21,44 +25,52 @@ export function ApiKeyInput({
21
25
  title = 'Preview',
22
26
  showToggle = true,
23
27
  className,
24
- }: ApiKeyInputProps) {
28
+ }: ApiKeyInputProps): JSX.Element {
25
29
  const [apiKey, setApiKey] = useState('');
26
30
  const [showPassword, setShowPassword] = useState(false);
31
+ const { chatbot } = useAsgardThemeContext();
32
+ const { avatar } = useAsgardContext();
27
33
 
28
- const handleSubmit = (e: FormEvent) => {
34
+ const handleSubmit = (e: FormEvent): void => {
29
35
  e.preventDefault();
30
36
  if (apiKey.trim() && !loading) {
31
37
  onSubmit(apiKey.trim());
32
38
  }
33
39
  };
34
40
 
35
- const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
41
+ const handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
36
42
  setApiKey(e.target.value);
37
43
  };
38
44
 
39
- const togglePasswordVisibility = () => {
45
+ const togglePasswordVisibility = (): void => {
40
46
  setShowPassword(!showPassword);
41
47
  };
42
48
 
43
49
  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>
50
+ <div
51
+ className={clsx(styles.api_key_input, className)}
52
+ style={{
53
+ backgroundColor: chatbot.backgroundColor,
54
+ borderColor: chatbot.borderColor,
55
+ }}
56
+ >
57
+ <div className={styles.api_key_input__header}>
58
+ <ProfileIcon avatar={avatar} />
59
+ <h2 className={styles.api_key_input__title} style={chatbot?.header?.title?.style}>{title}</h2>
48
60
  </div>
49
61
 
50
- <form onSubmit={handleSubmit} className={styles.form}>
51
- <div className={styles.inputGroup}>
52
- <label className={styles.label}>Key</label>
53
- <div className={styles.inputWrapper}>
62
+ <form onSubmit={handleSubmit} className={styles.api_key_input__form}>
63
+ <div>
64
+ <label className={styles.api_key_input__label}>Key</label>
65
+ <div className={styles.api_key_input__input_wrapper}>
54
66
  <input
55
67
  type={showPassword ? 'text' : 'password'}
56
68
  value={apiKey}
57
69
  onChange={handleInputChange}
58
70
  placeholder={placeholder}
59
- className={clsx(styles.input, {
60
- [styles.error]: error,
61
- [styles.disabled]: loading,
71
+ className={clsx(styles.api_key_input__input, {
72
+ [styles['api_key_input__input--error']]: error,
73
+ [styles['api_key_input__input--disabled']]: loading,
62
74
  })}
63
75
  disabled={loading}
64
76
  autoComplete="off"
@@ -67,49 +79,29 @@ export function ApiKeyInput({
67
79
  <button
68
80
  type="button"
69
81
  onClick={togglePasswordVisibility}
70
- className={styles.toggleButton}
82
+ className={styles.api_key_input__toggle_button}
71
83
  disabled={loading}
72
84
  aria-label={showPassword ? 'Hide password' : 'Show password'}
73
85
  >
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>
86
+ {showPassword ? (
87
+ <EyeOffSvg className={styles.api_key_input__toggle_icon} />
88
+ ) : (
89
+ <EyeSvg className={styles.api_key_input__toggle_icon} />
90
+ )}
97
91
  </button>
98
92
  )}
99
93
  </div>
100
- {error && (
101
- <div className={styles.errorMessage}>
102
- {error}
103
- </div>
104
- )}
94
+ {error && <div className={styles.api_key_input__error_message}>{error}</div>}
105
95
  </div>
106
96
 
107
97
  <button
108
98
  type="submit"
109
99
  disabled={!apiKey.trim() || loading}
110
- className={clsx(styles.submitButton, {
111
- [styles.loading]: loading,
112
- })}
100
+ className={styles.api_key_input__submit_button}
101
+ style={{
102
+ backgroundColor: chatbot?.mainColor,
103
+ color: chatbot?.secondaryColor,
104
+ }}
113
105
  >
114
106
  {loading ? 'Loading...' : 'Continue'}
115
107
  </button>
@@ -0,0 +1,24 @@
1
+ .chatbot__auth_state_container {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ flex: 1;
6
+ padding: 20px;
7
+ }
8
+
9
+ .chatbot__error_state {
10
+ color: #ff6b6b;
11
+ }
12
+
13
+ .chatbot__error_state__content {
14
+ text-align: center;
15
+ }
16
+
17
+ .chatbot__error_state__icon {
18
+ font-size: 16px;
19
+ margin-bottom: 8px;
20
+ }
21
+
22
+ .chatbot__error_state__message {
23
+ // No additional styles needed - uses default text styling
24
+ }
@@ -17,13 +17,14 @@ import {
17
17
  AsgardAppInitializationContextProvider,
18
18
  AsgardServiceContextProviderProps,
19
19
  } from '../../context';
20
+ import { AuthState } from '@asgard-js/core';
21
+ import clsx from 'clsx';
20
22
  import { ApiKeyInput } from './api-key-input';
21
23
  import { ChatbotHeader } from './chatbot-header';
22
24
  import { ChatbotBody } from './chatbot-body';
23
25
  import { ChatbotFooter } from './chatbot-footer';
24
26
  import { ChatbotContainer } from './chatbot-container/chatbot-container';
25
-
26
- type AuthState = 'loading' | 'needApiKey' | 'authenticated' | 'error';
27
+ import styles from './chatbot.module.scss';
27
28
 
28
29
  interface ChatbotProps extends AsgardTemplateContextValue {
29
30
  className?: string;
@@ -95,26 +96,14 @@ export const Chatbot = forwardRef(function Chatbot(
95
96
  switch (authState) {
96
97
  case 'loading':
97
98
  return (
98
- <div style={{
99
- display: 'flex',
100
- alignItems: 'center',
101
- justifyContent: 'center',
102
- flex: 1,
103
- padding: '20px'
104
- }}>
99
+ <div className={styles.chatbot__auth_state_container}>
105
100
  {loadingComponent || <div>Loading...</div>}
106
101
  </div>
107
102
  );
108
103
 
109
104
  case 'needApiKey':
110
105
  return (
111
- <div style={{
112
- display: 'flex',
113
- alignItems: 'center',
114
- justifyContent: 'center',
115
- flex: 1,
116
- padding: '20px'
117
- }}>
106
+ <div className={styles.chatbot__auth_state_container}>
118
107
  <ApiKeyInput
119
108
  title={title}
120
109
  onSubmit={onApiKeySubmit || (() => {})}
@@ -123,19 +112,24 @@ export const Chatbot = forwardRef(function Chatbot(
123
112
  </div>
124
113
  );
125
114
 
115
+ case 'invalidApiKey':
116
+ return (
117
+ <div className={styles.chatbot__auth_state_container}>
118
+ <ApiKeyInput
119
+ title={title}
120
+ onSubmit={onApiKeySubmit || (() => {})}
121
+ placeholder="Enter your key"
122
+ error="Please check if the key is correct."
123
+ />
124
+ </div>
125
+ );
126
+
126
127
  case 'error':
127
128
  return (
128
- <div style={{
129
- display: 'flex',
130
- alignItems: 'center',
131
- justifyContent: 'center',
132
- flex: 1,
133
- padding: '20px',
134
- color: '#ff6b6b'
135
- }}>
136
- <div style={{ textAlign: 'center' }}>
137
- <div style={{ fontSize: '16px', marginBottom: '8px' }}>⚠️</div>
138
- <div>Something went wrong. Please try again later.</div>
129
+ <div className={clsx(styles.chatbot__auth_state_container, styles.chatbot__error_state)}>
130
+ <div className={styles.chatbot__error_state__content}>
131
+ <div className={styles.chatbot__error_state__icon}>⚠️</div>
132
+ <div className={styles.chatbot__error_state__message}>Something went wrong. Please try again later.</div>
139
133
  </div>
140
134
  </div>
141
135
  );
@@ -159,7 +153,7 @@ export const Chatbot = forwardRef(function Chatbot(
159
153
  };
160
154
 
161
155
  // Don't initialize SSE connection when explicitly needing API key or in error state
162
- if (authState !== 'needApiKey' && authState !== 'error') {
156
+ if (authState !== 'needApiKey' && authState !== 'error' && authState !== 'invalidApiKey') {
163
157
  return (
164
158
  <AsgardAppInitializationContextProvider
165
159
  enabled={enableLoadConfigFromService}
@@ -199,23 +193,35 @@ export const Chatbot = forwardRef(function Chatbot(
199
193
  );
200
194
  }
201
195
 
202
- // For non-authenticated states, render without AsgardServiceContextProvider
196
+ // For non-authenticated states, provide AsgardServiceContextProvider but without SSE connection
203
197
  return (
204
198
  <AsgardThemeContextProvider theme={theme}>
205
- <ChatbotContainer
206
- fullScreen={fullScreen}
207
- className={className}
208
- style={style}
199
+ <AsgardServiceContextProvider
200
+ parentRef={ref}
201
+ avatar={avatar}
202
+ config={config}
203
+ customChannelId={customChannelId}
204
+ initMessages={initMessages}
205
+ onSseMessage={onSseMessage}
206
+ onAuthError={onAuthError}
207
+ botTypingPlaceholder={botTypingPlaceholder}
208
+ inputPlaceholder={inputPlaceholder}
209
209
  >
210
- <ChatbotHeader
211
- title={title}
212
- onReset={onReset}
213
- onClose={onClose}
214
- customActions={customActions}
215
- maintainConnectionWhenClosed={maintainConnectionWhenClosed}
216
- />
217
- {renderContent()}
218
- </ChatbotContainer>
210
+ <ChatbotContainer
211
+ fullScreen={fullScreen}
212
+ className={className}
213
+ style={style}
214
+ >
215
+ <ChatbotHeader
216
+ title={title}
217
+ onReset={onReset}
218
+ onClose={onClose}
219
+ customActions={customActions}
220
+ maintainConnectionWhenClosed={maintainConnectionWhenClosed}
221
+ />
222
+ {renderContent()}
223
+ </ChatbotContainer>
224
+ </AsgardServiceContextProvider>
219
225
  </AsgardThemeContextProvider>
220
226
  );
221
227
  });
@@ -31,6 +31,8 @@ export interface AsgardThemeContextValue {
31
31
  backgroundColor?: CSSProperties['backgroundColor'];
32
32
  borderColor?: CSSProperties['borderColor'];
33
33
  inactiveColor?: CSSProperties['color'];
34
+ mainColor?: CSSProperties['color'];
35
+ secondaryColor?: CSSProperties['color'];
34
36
  primaryComponent?: {
35
37
  mainColor?: CSSProperties['color'];
36
38
  secondaryColor?: CSSProperties['color'];
@@ -267,6 +269,9 @@ export function AsgardThemeContextProvider(
267
269
  chatbot: {
268
270
  backgroundColor: themeFromAnnotations.chatbot?.backgroundColor,
269
271
  borderColor: themeFromAnnotations.chatbot?.borderColor,
272
+ mainColor: themeFromAnnotations.chatbot?.primaryComponent?.mainColor,
273
+ secondaryColor: themeFromAnnotations.chatbot?.primaryComponent?.secondaryColor,
274
+
270
275
  header: {
271
276
  style: {
272
277
  borderBottomColor: themeFromAnnotations.chatbot?.borderColor,
@@ -407,6 +412,8 @@ export function AsgardThemeContextProvider(
407
412
  chatbot: {
408
413
  backgroundColor: propsTheme.chatbot?.backgroundColor,
409
414
  borderColor: propsTheme.chatbot?.borderColor,
415
+ mainColor: propsTheme.chatbot?.primaryComponent?.mainColor,
416
+ secondaryColor: propsTheme.chatbot?.primaryComponent?.secondaryColor,
410
417
  header: {
411
418
  style: {
412
419
  borderBottomColor: propsTheme.chatbot?.borderColor,
@@ -0,0 +1,4 @@
1
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <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"/>
3
+ <line x1="1" y1="1" x2="23" y2="23"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
3
+ <circle cx="12" cy="12" r="3"/>
4
+ </svg>
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import 'highlight.js/styles/atom-one-dark.css';
2
2
  export * from './components';
3
3
  export * from './context';
4
- export * from './hooks';
4
+ export * from './hooks';