@folio-agent/widget 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,6 @@ npm install @folio-agent/widget
18
18
  </script>
19
19
  ```
20
20
 
21
- 配色・フォントは CSS カスタムプロパティ6トークン(`--folio-agent-surface` / `text` / `muted` / `accent` / `accent-contrast` / `font`)で上書きできる。未指定時は既定デザインのまま動く。
21
+ 配色・フォントは CSS カスタムプロパティ6トークン(`--folio-agent-surface` / `text` / `muted` / `accent` / `accent-contrast` / `font`)で上書きできる。未指定時はホストの配色に合わせる。変える場合も `surface` / `text` / `accent` の3色で足り、残りはこの3色から導出する。
22
22
 
23
- UI 文言は `lang="en"` 属性で英語に切り替えられる(未指定は日本語)。
23
+ UI 文言は `lang="en"` 属性で英語に切り替えられる(未指定は日本語)。パネルの見出し・案内文・質問候補は `heading` / `greeting` / `suggestions`(`|` 区切り)属性で差し替えられる。
package/dist/styles.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const WIDGET_STYLES = "\n :host {\n all: initial;\n color: inherit;\n color-scheme: inherit;\n font-family: var(--folio-agent-font, inherit);\n }\n .toggle {\n position: fixed;\n right: 20px;\n bottom: 20px;\n width: 56px;\n height: 56px;\n border-radius: 50%;\n border: none;\n background: var(--folio-agent-accent, CanvasText);\n color: var(--folio-agent-accent-contrast, Canvas);\n font-size: 24px;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);\n z-index: 2147483000;\n }\n .panel {\n position: fixed;\n right: 20px;\n bottom: 88px;\n width: 320px;\n max-height: 420px;\n display: flex;\n flex-direction: column;\n background: var(--folio-agent-surface, Canvas);\n border-radius: 12px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);\n overflow: hidden;\n z-index: 2147483000;\n }\n .panel[hidden] {\n display: none;\n }\n .messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n font-size: 14px;\n line-height: 1.6;\n color: var(--folio-agent-text, CanvasText);\n }\n .message {\n white-space: pre-wrap;\n }\n .message.assistant {\n align-self: flex-start;\n background: color-mix(in srgb, var(--folio-agent-text, CanvasText) 8%, var(--folio-agent-surface, Canvas));\n border: 1px solid color-mix(in srgb, var(--folio-agent-text, CanvasText) 16%, var(--folio-agent-surface, Canvas));\n border-radius: 8px;\n padding: 10px 14px;\n }\n .message.user {\n align-self: flex-end;\n background: color-mix(in srgb, var(--folio-agent-text, CanvasText) 14%, var(--folio-agent-surface, Canvas));\n border: 1px solid color-mix(in srgb, var(--folio-agent-text, CanvasText) 24%, var(--folio-agent-surface, Canvas));\n border-radius: 8px;\n padding: 10px 14px;\n }\n .disclosure {\n font-size: 12px;\n color: var(--folio-agent-muted, color-mix(in srgb, CanvasText 55%, Canvas));\n padding: 0 0 4px;\n }\n .disclosure a {\n color: #2563eb;\n }\n form {\n display: flex;\n border-top: 1px solid color-mix(in srgb, CanvasText 16%, Canvas);\n }\n input {\n flex: 1;\n border: none;\n background: transparent;\n color: inherit;\n padding: 10px;\n font-size: 14px;\n }\n input:focus {\n outline: none;\n }\n button[type=\"submit\"] {\n border: none;\n background: none;\n padding: 0 14px;\n cursor: pointer;\n color: var(--folio-agent-accent, CanvasText);\n font-weight: 600;\n }\n";
1
+ export declare const WIDGET_STYLES = "\n :host {\n all: initial;\n color: inherit;\n color-scheme: inherit;\n font-family: var(--folio-agent-font, inherit);\n\n --_surface: var(--folio-agent-surface, Canvas);\n --_text: var(--folio-agent-text, CanvasText);\n --_accent: var(--folio-agent-accent, var(--_text));\n --_accent-contrast: var(--folio-agent-accent-contrast, var(--_surface));\n --_muted: var(--folio-agent-muted, color-mix(in srgb, var(--_text) 62%, var(--_surface)));\n --_hair: color-mix(in srgb, var(--_text) 12%, transparent);\n --_glass: color-mix(in srgb, var(--_surface) 76%, transparent);\n --_glass-strong: color-mix(in srgb, var(--_surface) 94%, transparent);\n --_field: color-mix(in srgb, var(--_text) 5%, color-mix(in srgb, var(--_surface) 80%, transparent));\n --_chip: color-mix(in srgb, var(--_surface) 60%, transparent);\n --_focus: color-mix(in srgb, var(--_accent) 70%, var(--_text));\n }\n * {\n box-sizing: border-box;\n }\n button,\n textarea {\n font: inherit;\n }\n svg {\n display: block;\n }\n /* \u30DB\u30B9\u30C8\u5074\u306E ::selection \u306F Shadow DOM \u306E\u4E2D\u3078\u5C4A\u304B\u306A\u3044\u306E\u3067\u3001\u3053\u3053\u3067 accent \u304B\u3089\u4F5C\u308B\u3002\n \u6307\u5B9A\u3057\u306A\u3044\u3068\u30D6\u30E9\u30A6\u30B6\u65E2\u5B9A\u306E\u9752\u306B\u306A\u308A\u3001\u30B5\u30A4\u30C8\u306E\u914D\u8272\u304B\u3089\u6D6E\u304F */\n ::selection {\n background: color-mix(in srgb, var(--_accent) 32%, transparent);\n color: var(--_text);\n }\n\n .toggle {\n position: fixed;\n right: 20px;\n bottom: calc(20px + env(safe-area-inset-bottom, 0px));\n z-index: 2147483000;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n height: 46px;\n padding: 0 18px 0 14px;\n border-radius: 999px;\n border: 1px solid color-mix(in srgb, var(--_accent-contrast) 14%, transparent);\n background: color-mix(in srgb, var(--_accent) 88%, transparent);\n -webkit-backdrop-filter: blur(16px) saturate(1.6);\n backdrop-filter: blur(16px) saturate(1.6);\n color: var(--_accent-contrast);\n font-size: 14px;\n font-weight: 600;\n letter-spacing: 0.02em;\n cursor: pointer;\n box-shadow: 0 10px 30px rgba(16, 20, 28, 0.22);\n transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.2s ease;\n }\n .toggle:hover {\n transform: translateY(-1px);\n }\n .toggle:active {\n transform: scale(0.97);\n }\n .toggle svg {\n width: 20px;\n height: 20px;\n flex: none;\n }\n .root.open .toggle {\n opacity: 0;\n transform: scale(0.85);\n pointer-events: none;\n }\n\n .panel {\n position: fixed;\n right: 20px;\n bottom: calc(20px + env(safe-area-inset-bottom, 0px));\n z-index: 2147483001;\n width: min(400px, calc(100vw - 40px));\n height: min(620px, calc(100dvh - 40px));\n display: flex;\n flex-direction: column;\n border-radius: 22px;\n border: 1px solid var(--_hair);\n background: var(--_glass);\n -webkit-backdrop-filter: blur(28px) saturate(1.8);\n backdrop-filter: blur(28px) saturate(1.8);\n box-shadow: 0 24px 64px rgba(16, 20, 28, 0.2), 0 2px 6px rgba(16, 20, 28, 0.08);\n color: var(--_text);\n overflow: hidden;\n transform-origin: calc(100% - 40px) calc(100% - 20px);\n opacity: 0;\n visibility: hidden;\n transform: translateY(10px) scale(0.9);\n transition:\n opacity 0.2s ease,\n transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),\n visibility 0s linear 0.32s;\n }\n .root.open .panel {\n opacity: 1;\n visibility: visible;\n transform: none;\n transition:\n opacity 0.22s ease,\n transform 0.38s cubic-bezier(0.2, 0.9, 0.25, 1.05),\n visibility 0s;\n }\n\n .header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 14px 12px 12px 18px;\n border-bottom: 1px solid var(--_hair);\n }\n .titles {\n flex: 1;\n min-width: 0;\n }\n .heading {\n font-size: 15px;\n font-weight: 700;\n line-height: 1.3;\n }\n .subheading {\n font-size: 12px;\n line-height: 1.4;\n color: var(--_muted);\n }\n .close,\n .reset {\n flex: none;\n display: grid;\n place-items: center;\n width: 34px;\n height: 34px;\n border: 0;\n border-radius: 50%;\n background: color-mix(in srgb, var(--_text) 7%, transparent);\n color: var(--_text);\n cursor: pointer;\n }\n .reset {\n background: transparent;\n color: var(--_muted);\n }\n .reset[hidden] {\n display: none;\n }\n .close:hover,\n .reset:hover {\n background: color-mix(in srgb, var(--_text) 12%, transparent);\n }\n .close svg,\n .reset svg {\n width: 16px;\n height: 16px;\n }\n\n .messages {\n flex: 1;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 18px 18px 8px;\n display: flex;\n flex-direction: column;\n gap: 14px;\n font-size: 15px;\n line-height: 1.7;\n }\n .message,\n .greeting {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n animation: fade-in 0.28s ease both;\n }\n .message.assistant,\n .greeting {\n align-self: stretch;\n }\n .message.user {\n align-self: flex-end;\n max-width: 85%;\n padding: 8px 14px;\n border-radius: 18px 18px 6px 18px;\n background: color-mix(in srgb, var(--_accent) 14%, var(--_surface));\n border: 1px solid color-mix(in srgb, var(--_accent) 28%, var(--_surface));\n }\n @keyframes fade-in {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: none; }\n }\n\n .suggestions {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n }\n .suggestions button {\n padding: 7px 14px;\n border: 1px solid var(--_hair);\n border-radius: 999px;\n background: var(--_chip);\n color: var(--_text);\n font-size: 14px;\n text-align: left;\n cursor: pointer;\n transition: background 0.15s ease;\n }\n .suggestions button:hover {\n background: color-mix(in srgb, var(--_text) 8%, transparent);\n }\n\n .typing {\n display: inline-flex;\n gap: 5px;\n padding: 8px 0;\n }\n .typing span {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--_muted);\n animation: typing-dot 1.1s ease-in-out infinite;\n }\n .typing span:nth-child(2) {\n animation-delay: 0.15s;\n }\n .typing span:nth-child(3) {\n animation-delay: 0.3s;\n }\n @keyframes typing-dot {\n 0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }\n 40% { opacity: 1; transform: translateY(-3px); }\n }\n\n form {\n padding: 8px 12px calc(10px + env(safe-area-inset-bottom, 0px));\n }\n .field {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 6px 6px 6px 14px;\n border: 1px solid var(--_hair);\n border-radius: 22px;\n background: var(--_field);\n transition: border-color 0.15s ease;\n }\n .field:focus-within {\n border-color: color-mix(in srgb, var(--_text) 32%, transparent);\n }\n textarea {\n flex: 1;\n min-width: 0;\n max-height: 144px;\n padding: 6px 0;\n border: 0;\n outline: 0;\n resize: none;\n background: transparent;\n color: var(--_text);\n caret-color: var(--_accent);\n /* 16px \u672A\u6E80\u3060\u3068 iOS Safari \u304C\u30D5\u30A9\u30FC\u30AB\u30B9\u6642\u306B\u30DA\u30FC\u30B8\u3092\u62E1\u5927\u3059\u308B */\n font-size: 16px;\n line-height: 1.5;\n }\n textarea::placeholder {\n color: var(--_muted);\n }\n .send {\n flex: none;\n display: grid;\n place-items: center;\n width: 34px;\n height: 34px;\n border: 0;\n border-radius: 50%;\n background: var(--_accent);\n color: var(--_accent-contrast);\n cursor: pointer;\n transition: opacity 0.15s ease, transform 0.15s ease;\n }\n .send:disabled {\n opacity: 0.25;\n cursor: default;\n }\n .send:not(:disabled):active {\n transform: scale(0.92);\n }\n .send svg {\n width: 18px;\n height: 18px;\n }\n .disclosure {\n margin: 8px 0 0;\n font-size: 11px;\n line-height: 1.5;\n text-align: center;\n color: var(--_muted);\n }\n .disclosure a {\n color: inherit;\n }\n\n .toggle:focus-visible,\n .close:focus-visible,\n .reset:focus-visible,\n .send:focus-visible,\n .suggestions button:focus-visible {\n outline: 2px solid var(--_focus);\n outline-offset: 2px;\n }\n\n @media (max-width: 640px) {\n .toggle {\n right: 16px;\n bottom: calc(16px + env(safe-area-inset-bottom, 0px));\n }\n /* \u9AD8\u3055\u3068\u4E0A\u7AEF\u306F visualViewport \u304B\u3089 JS \u304C\u6D41\u3057\u8FBC\u3080\u3002\u30AD\u30FC\u30DC\u30FC\u30C9\u8868\u793A\u4E2D\u3082\u5165\u529B\u6B04\u3092\u898B\u3048\u308B\u4F4D\u7F6E\u306B\u4FDD\u3064\u305F\u3081 */\n .panel {\n top: var(--vv-top, 0px);\n right: 0;\n bottom: auto;\n left: 0;\n width: 100%;\n height: var(--vv-height, 100dvh);\n border: 0;\n border-radius: 0;\n background: var(--_glass-strong);\n transform-origin: 50% 100%;\n transform: translateY(32px);\n }\n .header {\n padding-top: calc(12px + env(safe-area-inset-top, 0px));\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .toggle,\n .panel,\n .root.open .panel {\n transform: none;\n }\n .panel {\n transition: opacity 0.15s linear, visibility 0s linear 0.15s;\n }\n .root.open .panel {\n transition: opacity 0.15s linear;\n }\n .message,\n .greeting,\n .typing span {\n animation: none;\n }\n }\n";
package/dist/styles.js CHANGED
@@ -1,104 +1,368 @@
1
1
  // テーマ注入: 利用側サイトのグローバルCSSで
2
2
  // folio-agent-widget { --folio-agent-accent: #...; }
3
3
  // のようにカスタムプロパティを指定するとShadow DOM内に継承されテーマが反映される。
4
- // 未指定の場合は各プロパティがホスト配色(CSSシステムカラー)から導出した値を使い、
5
- // サイトのライト/ダークいずれの配色にも自動で馴染む。
4
+ // 決めるのは surface / text / accent の3色で足り、境界線・吹き出し・入力欄などは
5
+ // すべてこの3色から color-mix で導出する。未指定の場合はホスト配色(CSSシステムカラー)
6
+ // から導出し、サイトのライト/ダークいずれの配色にも自動で馴染む。
6
7
  export const WIDGET_STYLES = `
7
8
  :host {
8
9
  all: initial;
9
10
  color: inherit;
10
11
  color-scheme: inherit;
11
12
  font-family: var(--folio-agent-font, inherit);
13
+
14
+ --_surface: var(--folio-agent-surface, Canvas);
15
+ --_text: var(--folio-agent-text, CanvasText);
16
+ --_accent: var(--folio-agent-accent, var(--_text));
17
+ --_accent-contrast: var(--folio-agent-accent-contrast, var(--_surface));
18
+ --_muted: var(--folio-agent-muted, color-mix(in srgb, var(--_text) 62%, var(--_surface)));
19
+ --_hair: color-mix(in srgb, var(--_text) 12%, transparent);
20
+ --_glass: color-mix(in srgb, var(--_surface) 76%, transparent);
21
+ --_glass-strong: color-mix(in srgb, var(--_surface) 94%, transparent);
22
+ --_field: color-mix(in srgb, var(--_text) 5%, color-mix(in srgb, var(--_surface) 80%, transparent));
23
+ --_chip: color-mix(in srgb, var(--_surface) 60%, transparent);
24
+ --_focus: color-mix(in srgb, var(--_accent) 70%, var(--_text));
12
25
  }
26
+ * {
27
+ box-sizing: border-box;
28
+ }
29
+ button,
30
+ textarea {
31
+ font: inherit;
32
+ }
33
+ svg {
34
+ display: block;
35
+ }
36
+ /* ホスト側の ::selection は Shadow DOM の中へ届かないので、ここで accent から作る。
37
+ 指定しないとブラウザ既定の青になり、サイトの配色から浮く */
38
+ ::selection {
39
+ background: color-mix(in srgb, var(--_accent) 32%, transparent);
40
+ color: var(--_text);
41
+ }
42
+
13
43
  .toggle {
14
44
  position: fixed;
15
45
  right: 20px;
16
- bottom: 20px;
17
- width: 56px;
18
- height: 56px;
19
- border-radius: 50%;
20
- border: none;
21
- background: var(--folio-agent-accent, CanvasText);
22
- color: var(--folio-agent-accent-contrast, Canvas);
23
- font-size: 24px;
24
- cursor: pointer;
25
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
46
+ bottom: calc(20px + env(safe-area-inset-bottom, 0px));
26
47
  z-index: 2147483000;
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ height: 46px;
52
+ padding: 0 18px 0 14px;
53
+ border-radius: 999px;
54
+ border: 1px solid color-mix(in srgb, var(--_accent-contrast) 14%, transparent);
55
+ background: color-mix(in srgb, var(--_accent) 88%, transparent);
56
+ -webkit-backdrop-filter: blur(16px) saturate(1.6);
57
+ backdrop-filter: blur(16px) saturate(1.6);
58
+ color: var(--_accent-contrast);
59
+ font-size: 14px;
60
+ font-weight: 600;
61
+ letter-spacing: 0.02em;
62
+ cursor: pointer;
63
+ box-shadow: 0 10px 30px rgba(16, 20, 28, 0.22);
64
+ transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.2s ease;
65
+ }
66
+ .toggle:hover {
67
+ transform: translateY(-1px);
68
+ }
69
+ .toggle:active {
70
+ transform: scale(0.97);
71
+ }
72
+ .toggle svg {
73
+ width: 20px;
74
+ height: 20px;
75
+ flex: none;
27
76
  }
77
+ .root.open .toggle {
78
+ opacity: 0;
79
+ transform: scale(0.85);
80
+ pointer-events: none;
81
+ }
82
+
28
83
  .panel {
29
84
  position: fixed;
30
85
  right: 20px;
31
- bottom: 88px;
32
- width: 320px;
33
- max-height: 420px;
86
+ bottom: calc(20px + env(safe-area-inset-bottom, 0px));
87
+ z-index: 2147483001;
88
+ width: min(400px, calc(100vw - 40px));
89
+ height: min(620px, calc(100dvh - 40px));
34
90
  display: flex;
35
91
  flex-direction: column;
36
- background: var(--folio-agent-surface, Canvas);
37
- border-radius: 12px;
38
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
92
+ border-radius: 22px;
93
+ border: 1px solid var(--_hair);
94
+ background: var(--_glass);
95
+ -webkit-backdrop-filter: blur(28px) saturate(1.8);
96
+ backdrop-filter: blur(28px) saturate(1.8);
97
+ box-shadow: 0 24px 64px rgba(16, 20, 28, 0.2), 0 2px 6px rgba(16, 20, 28, 0.08);
98
+ color: var(--_text);
39
99
  overflow: hidden;
40
- z-index: 2147483000;
100
+ transform-origin: calc(100% - 40px) calc(100% - 20px);
101
+ opacity: 0;
102
+ visibility: hidden;
103
+ transform: translateY(10px) scale(0.9);
104
+ transition:
105
+ opacity 0.2s ease,
106
+ transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),
107
+ visibility 0s linear 0.32s;
41
108
  }
42
- .panel[hidden] {
109
+ .root.open .panel {
110
+ opacity: 1;
111
+ visibility: visible;
112
+ transform: none;
113
+ transition:
114
+ opacity 0.22s ease,
115
+ transform 0.38s cubic-bezier(0.2, 0.9, 0.25, 1.05),
116
+ visibility 0s;
117
+ }
118
+
119
+ .header {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 6px;
123
+ padding: 14px 12px 12px 18px;
124
+ border-bottom: 1px solid var(--_hair);
125
+ }
126
+ .titles {
127
+ flex: 1;
128
+ min-width: 0;
129
+ }
130
+ .heading {
131
+ font-size: 15px;
132
+ font-weight: 700;
133
+ line-height: 1.3;
134
+ }
135
+ .subheading {
136
+ font-size: 12px;
137
+ line-height: 1.4;
138
+ color: var(--_muted);
139
+ }
140
+ .close,
141
+ .reset {
142
+ flex: none;
143
+ display: grid;
144
+ place-items: center;
145
+ width: 34px;
146
+ height: 34px;
147
+ border: 0;
148
+ border-radius: 50%;
149
+ background: color-mix(in srgb, var(--_text) 7%, transparent);
150
+ color: var(--_text);
151
+ cursor: pointer;
152
+ }
153
+ .reset {
154
+ background: transparent;
155
+ color: var(--_muted);
156
+ }
157
+ .reset[hidden] {
43
158
  display: none;
44
159
  }
160
+ .close:hover,
161
+ .reset:hover {
162
+ background: color-mix(in srgb, var(--_text) 12%, transparent);
163
+ }
164
+ .close svg,
165
+ .reset svg {
166
+ width: 16px;
167
+ height: 16px;
168
+ }
169
+
45
170
  .messages {
46
171
  flex: 1;
47
172
  overflow-y: auto;
48
- padding: 16px;
173
+ overscroll-behavior: contain;
174
+ padding: 18px 18px 8px;
49
175
  display: flex;
50
176
  flex-direction: column;
51
- gap: 10px;
52
- font-size: 14px;
53
- line-height: 1.6;
54
- color: var(--folio-agent-text, CanvasText);
177
+ gap: 14px;
178
+ font-size: 15px;
179
+ line-height: 1.7;
55
180
  }
56
- .message {
181
+ .message,
182
+ .greeting {
57
183
  white-space: pre-wrap;
184
+ overflow-wrap: anywhere;
185
+ animation: fade-in 0.28s ease both;
58
186
  }
59
- .message.assistant {
60
- align-self: flex-start;
61
- background: color-mix(in srgb, var(--folio-agent-text, CanvasText) 8%, var(--folio-agent-surface, Canvas));
62
- border: 1px solid color-mix(in srgb, var(--folio-agent-text, CanvasText) 16%, var(--folio-agent-surface, Canvas));
63
- border-radius: 8px;
64
- padding: 10px 14px;
187
+ .message.assistant,
188
+ .greeting {
189
+ align-self: stretch;
65
190
  }
66
191
  .message.user {
67
192
  align-self: flex-end;
68
- background: color-mix(in srgb, var(--folio-agent-text, CanvasText) 14%, var(--folio-agent-surface, Canvas));
69
- border: 1px solid color-mix(in srgb, var(--folio-agent-text, CanvasText) 24%, var(--folio-agent-surface, Canvas));
70
- border-radius: 8px;
71
- padding: 10px 14px;
193
+ max-width: 85%;
194
+ padding: 8px 14px;
195
+ border-radius: 18px 18px 6px 18px;
196
+ background: color-mix(in srgb, var(--_accent) 14%, var(--_surface));
197
+ border: 1px solid color-mix(in srgb, var(--_accent) 28%, var(--_surface));
72
198
  }
73
- .disclosure {
74
- font-size: 12px;
75
- color: var(--folio-agent-muted, color-mix(in srgb, CanvasText 55%, Canvas));
76
- padding: 0 0 4px;
199
+ @keyframes fade-in {
200
+ from { opacity: 0; transform: translateY(4px); }
201
+ to { opacity: 1; transform: none; }
77
202
  }
78
- .disclosure a {
79
- color: #2563eb;
203
+
204
+ .suggestions {
205
+ display: flex;
206
+ flex-direction: column;
207
+ align-items: flex-start;
208
+ gap: 8px;
209
+ }
210
+ .suggestions button {
211
+ padding: 7px 14px;
212
+ border: 1px solid var(--_hair);
213
+ border-radius: 999px;
214
+ background: var(--_chip);
215
+ color: var(--_text);
216
+ font-size: 14px;
217
+ text-align: left;
218
+ cursor: pointer;
219
+ transition: background 0.15s ease;
220
+ }
221
+ .suggestions button:hover {
222
+ background: color-mix(in srgb, var(--_text) 8%, transparent);
80
223
  }
224
+
225
+ .typing {
226
+ display: inline-flex;
227
+ gap: 5px;
228
+ padding: 8px 0;
229
+ }
230
+ .typing span {
231
+ width: 7px;
232
+ height: 7px;
233
+ border-radius: 50%;
234
+ background: var(--_muted);
235
+ animation: typing-dot 1.1s ease-in-out infinite;
236
+ }
237
+ .typing span:nth-child(2) {
238
+ animation-delay: 0.15s;
239
+ }
240
+ .typing span:nth-child(3) {
241
+ animation-delay: 0.3s;
242
+ }
243
+ @keyframes typing-dot {
244
+ 0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
245
+ 40% { opacity: 1; transform: translateY(-3px); }
246
+ }
247
+
81
248
  form {
249
+ padding: 8px 12px calc(10px + env(safe-area-inset-bottom, 0px));
250
+ }
251
+ .field {
82
252
  display: flex;
83
- border-top: 1px solid color-mix(in srgb, CanvasText 16%, Canvas);
253
+ align-items: flex-end;
254
+ gap: 8px;
255
+ padding: 6px 6px 6px 14px;
256
+ border: 1px solid var(--_hair);
257
+ border-radius: 22px;
258
+ background: var(--_field);
259
+ transition: border-color 0.15s ease;
260
+ }
261
+ .field:focus-within {
262
+ border-color: color-mix(in srgb, var(--_text) 32%, transparent);
84
263
  }
85
- input {
264
+ textarea {
86
265
  flex: 1;
87
- border: none;
266
+ min-width: 0;
267
+ max-height: 144px;
268
+ padding: 6px 0;
269
+ border: 0;
270
+ outline: 0;
271
+ resize: none;
88
272
  background: transparent;
89
- color: inherit;
90
- padding: 10px;
91
- font-size: 14px;
273
+ color: var(--_text);
274
+ caret-color: var(--_accent);
275
+ /* 16px 未満だと iOS Safari がフォーカス時にページを拡大する */
276
+ font-size: 16px;
277
+ line-height: 1.5;
92
278
  }
93
- input:focus {
94
- outline: none;
279
+ textarea::placeholder {
280
+ color: var(--_muted);
95
281
  }
96
- button[type="submit"] {
97
- border: none;
98
- background: none;
99
- padding: 0 14px;
282
+ .send {
283
+ flex: none;
284
+ display: grid;
285
+ place-items: center;
286
+ width: 34px;
287
+ height: 34px;
288
+ border: 0;
289
+ border-radius: 50%;
290
+ background: var(--_accent);
291
+ color: var(--_accent-contrast);
100
292
  cursor: pointer;
101
- color: var(--folio-agent-accent, CanvasText);
102
- font-weight: 600;
293
+ transition: opacity 0.15s ease, transform 0.15s ease;
294
+ }
295
+ .send:disabled {
296
+ opacity: 0.25;
297
+ cursor: default;
298
+ }
299
+ .send:not(:disabled):active {
300
+ transform: scale(0.92);
301
+ }
302
+ .send svg {
303
+ width: 18px;
304
+ height: 18px;
305
+ }
306
+ .disclosure {
307
+ margin: 8px 0 0;
308
+ font-size: 11px;
309
+ line-height: 1.5;
310
+ text-align: center;
311
+ color: var(--_muted);
312
+ }
313
+ .disclosure a {
314
+ color: inherit;
315
+ }
316
+
317
+ .toggle:focus-visible,
318
+ .close:focus-visible,
319
+ .reset:focus-visible,
320
+ .send:focus-visible,
321
+ .suggestions button:focus-visible {
322
+ outline: 2px solid var(--_focus);
323
+ outline-offset: 2px;
324
+ }
325
+
326
+ @media (max-width: 640px) {
327
+ .toggle {
328
+ right: 16px;
329
+ bottom: calc(16px + env(safe-area-inset-bottom, 0px));
330
+ }
331
+ /* 高さと上端は visualViewport から JS が流し込む。キーボード表示中も入力欄を見える位置に保つため */
332
+ .panel {
333
+ top: var(--vv-top, 0px);
334
+ right: 0;
335
+ bottom: auto;
336
+ left: 0;
337
+ width: 100%;
338
+ height: var(--vv-height, 100dvh);
339
+ border: 0;
340
+ border-radius: 0;
341
+ background: var(--_glass-strong);
342
+ transform-origin: 50% 100%;
343
+ transform: translateY(32px);
344
+ }
345
+ .header {
346
+ padding-top: calc(12px + env(safe-area-inset-top, 0px));
347
+ }
348
+ }
349
+
350
+ @media (prefers-reduced-motion: reduce) {
351
+ .toggle,
352
+ .panel,
353
+ .root.open .panel {
354
+ transform: none;
355
+ }
356
+ .panel {
357
+ transition: opacity 0.15s linear, visibility 0s linear 0.15s;
358
+ }
359
+ .root.open .panel {
360
+ transition: opacity 0.15s linear;
361
+ }
362
+ .message,
363
+ .greeting,
364
+ .typing span {
365
+ animation: none;
366
+ }
103
367
  }
104
368
  `;
@@ -2,5 +2,6 @@ export declare class FolioAgentWidgetElement extends HTMLElement {
2
2
  #private;
3
3
  static readonly tagName = "folio-agent-widget";
4
4
  connectedCallback(): void;
5
+ disconnectedCallback(): void;
5
6
  }
6
7
  export declare function defineFolioAgentWidget(tagName?: string): void;
@@ -1,19 +1,33 @@
1
1
  import { WIDGET_STYLES } from "./styles.js";
2
2
  const WIDGET_TEXT = {
3
3
  ja: {
4
- toggleAriaLabel: "チャットを開く",
5
- inputPlaceholder: "メッセージを入力",
4
+ toggleLabel: "質問する",
5
+ headingText: "このサイトについて質問",
6
+ subheadingText: "公開している情報をもとに AI が答えます",
7
+ closeLabel: "閉じる",
8
+ resetLabel: "新しい会話",
9
+ greetingText: "制作実績・記事の内容・仕事の依頼について答えます。",
10
+ suggestions: ["どんな制作実績がありますか?", "どんな考え方で仕事をしていますか?", "仕事を依頼するには?"],
11
+ inputPlaceholder: "質問を入力",
6
12
  submitLabel: "送信",
13
+ typingLabel: "回答を作成中",
7
14
  disclosureText: "入力内容は品質改善のため記録されます。",
8
- policyLinkText: "利用データの扱いについて",
15
+ policyLinkText: "利用データの扱い",
9
16
  configErrorText: "設定エラー: endpoint属性が指定されていません。",
10
17
  answerFallbackText: "回答を取得できませんでした。しばらくしてから再度お試しください。",
11
18
  networkErrorText: "通信エラーが発生しました。しばらくしてから再度お試しください。",
12
19
  },
13
20
  en: {
14
- toggleAriaLabel: "Open chat",
15
- inputPlaceholder: "Type a message",
21
+ toggleLabel: "Ask",
22
+ headingText: "Ask about this site",
23
+ subheadingText: "AI answers from what this site publishes",
24
+ closeLabel: "Close",
25
+ resetLabel: "New conversation",
26
+ greetingText: "Ask about past work, articles, or how to request a project.",
27
+ suggestions: ["What have you built?", "How do you approach your work?", "How can I request a project?"],
28
+ inputPlaceholder: "Type a question",
16
29
  submitLabel: "Send",
30
+ typingLabel: "Writing an answer",
17
31
  disclosureText: "Your input is logged for quality improvement.",
18
32
  policyLinkText: "About data usage",
19
33
  configErrorText: "Configuration error: the endpoint attribute is not set.",
@@ -21,99 +35,314 @@ const WIDGET_TEXT = {
21
35
  networkErrorText: "A network error occurred. Please try again later.",
22
36
  },
23
37
  };
38
+ const CHAT_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 12.5c0 3.9-3.6 7-8 7-1.1 0-2.2-.2-3.1-.6L4.5 20l1.2-3.4C4.6 15.5 4 14 4 12.5c0-3.9 3.6-7 8-7s8 3.1 8 7Z"/><path d="M9 12.5h.01M12 12.5h.01M15 12.5h.01" stroke-width="2.4"/></svg>`;
39
+ const CLOSE_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>`;
40
+ const RESET_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h8"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>`;
41
+ const SEND_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg>`;
42
+ const COMPACT_QUERY = "(max-width: 640px)";
43
+ const FINE_POINTER_QUERY = "(pointer: fine)";
24
44
  function resolveLanguage(value) {
25
45
  return value === "en" ? "en" : "ja";
26
46
  }
47
+ function matches(query) {
48
+ return typeof window.matchMedia === "function" && window.matchMedia(query).matches;
49
+ }
50
+ function element(tag, className) {
51
+ const el = document.createElement(tag);
52
+ if (className)
53
+ el.className = className;
54
+ return el;
55
+ }
27
56
  export class FolioAgentWidgetElement extends HTMLElement {
28
57
  static tagName = "folio-agent-widget";
29
58
  #open = false;
30
- #hasShownDisclosure = false;
59
+ #pending = false;
31
60
  #messages = [];
61
+ #text = WIDGET_TEXT.ja;
62
+ #rootEl;
63
+ #toggleEl;
32
64
  #panelEl;
65
+ #closeEl;
66
+ #resetEl;
33
67
  #messagesEl;
68
+ #suggestionsEl;
69
+ #typingEl;
34
70
  #inputEl;
35
- #text = WIDGET_TEXT.ja;
71
+ #sendEl;
72
+ #scrollLocked = false;
73
+ // 会話をやり直すたびに進める。やり直す前に送った質問の回答が遅れて届いても、描画しない
74
+ #conversation = 0;
75
+ #previousOverflow = "";
76
+ #onViewportChange = () => this.#syncViewport();
36
77
  connectedCallback() {
78
+ if (this.shadowRoot)
79
+ return;
37
80
  this.#text = WIDGET_TEXT[resolveLanguage(this.getAttribute("lang"))];
38
81
  const root = this.attachShadow({ mode: "open" });
39
82
  const style = document.createElement("style");
40
83
  style.textContent = WIDGET_STYLES;
41
84
  root.appendChild(style);
42
- const toggle = document.createElement("button");
43
- toggle.className = "toggle";
85
+ const wrapper = element("div", "root");
86
+ wrapper.append(this.#buildToggle(), this.#buildPanel());
87
+ root.appendChild(wrapper);
88
+ this.#rootEl = wrapper;
89
+ }
90
+ disconnectedCallback() {
91
+ this.#watchViewport(false);
92
+ this.#lockScroll(false);
93
+ }
94
+ #buildToggle() {
95
+ const toggle = element("button", "toggle");
44
96
  toggle.type = "button";
45
- toggle.setAttribute("aria-label", this.#text.toggleAriaLabel);
46
- toggle.textContent = "💬";
47
- toggle.addEventListener("click", () => this.#togglePanel());
48
- root.appendChild(toggle);
49
- const panel = document.createElement("div");
50
- panel.className = "panel";
51
- panel.hidden = true;
52
- const messages = document.createElement("div");
53
- messages.className = "messages";
54
- panel.appendChild(messages);
55
- const form = document.createElement("form");
56
- const input = document.createElement("input");
57
- input.type = "text";
58
- input.placeholder = this.#text.inputPlaceholder;
59
- input.autocomplete = "off";
60
- const submit = document.createElement("button");
61
- submit.type = "submit";
62
- submit.textContent = this.#text.submitLabel;
63
- form.appendChild(input);
64
- form.appendChild(submit);
65
- form.addEventListener("submit", (event) => {
66
- event.preventDefault();
67
- const text = input.value;
68
- input.value = "";
69
- void this.#sendMessage(text);
97
+ toggle.setAttribute("aria-expanded", "false");
98
+ toggle.setAttribute("aria-controls", "panel");
99
+ toggle.innerHTML = CHAT_ICON;
100
+ const label = element("span");
101
+ label.textContent = this.#text.toggleLabel;
102
+ toggle.appendChild(label);
103
+ toggle.addEventListener("click", () => this.#setOpen(true));
104
+ this.#toggleEl = toggle;
105
+ return toggle;
106
+ }
107
+ #buildPanel() {
108
+ const heading = this.getAttribute("heading") ?? this.#text.headingText;
109
+ const panel = element("section", "panel");
110
+ panel.id = "panel";
111
+ panel.setAttribute("role", "dialog");
112
+ panel.setAttribute("aria-label", heading);
113
+ panel.setAttribute("aria-hidden", "true");
114
+ panel.inert = true;
115
+ panel.addEventListener("keydown", (event) => {
116
+ if (event.key === "Escape")
117
+ this.#setOpen(false);
70
118
  });
71
- panel.appendChild(form);
72
- root.appendChild(panel);
119
+ const header = element("header", "header");
120
+ const titles = element("div", "titles");
121
+ const headingEl = element("div", "heading");
122
+ headingEl.textContent = heading;
123
+ const subheading = element("div", "subheading");
124
+ subheading.textContent = this.#text.subheadingText;
125
+ titles.append(headingEl, subheading);
126
+ const close = element("button", "close");
127
+ close.type = "button";
128
+ close.setAttribute("aria-label", this.#text.closeLabel);
129
+ close.innerHTML = CLOSE_ICON;
130
+ close.addEventListener("click", () => this.#setOpen(false));
131
+ const reset = element("button", "reset");
132
+ reset.type = "button";
133
+ reset.hidden = true;
134
+ reset.setAttribute("aria-label", this.#text.resetLabel);
135
+ reset.title = this.#text.resetLabel;
136
+ reset.innerHTML = RESET_ICON;
137
+ reset.addEventListener("click", () => this.#resetConversation());
138
+ header.append(titles, reset, close);
139
+ const messages = element("div", "messages");
140
+ messages.setAttribute("aria-live", "polite");
141
+ panel.append(header, messages, this.#buildForm());
73
142
  this.#panelEl = panel;
143
+ this.#closeEl = close;
144
+ this.#resetEl = reset;
74
145
  this.#messagesEl = messages;
75
- this.#inputEl = input;
146
+ this.#renderIntro();
147
+ return panel;
76
148
  }
77
- #togglePanel() {
78
- this.#open = !this.#open;
79
- if (!this.#panelEl)
149
+ #renderIntro() {
150
+ if (!this.#messagesEl)
80
151
  return;
81
- this.#panelEl.hidden = !this.#open;
82
- if (this.#open && !this.#hasShownDisclosure) {
83
- this.#hasShownDisclosure = true;
84
- this.#renderDisclosure();
85
- }
86
- if (this.#open) {
87
- this.#inputEl?.focus();
152
+ const greeting = this.getAttribute("greeting") ?? this.#text.greetingText;
153
+ if (greeting.trim()) {
154
+ const greetingEl = element("div", "greeting");
155
+ greetingEl.textContent = greeting;
156
+ this.#messagesEl.appendChild(greetingEl);
88
157
  }
158
+ const suggestions = this.#buildSuggestions();
159
+ if (suggestions)
160
+ this.#messagesEl.appendChild(suggestions);
89
161
  }
90
- #renderDisclosure() {
162
+ #resetConversation() {
91
163
  if (!this.#messagesEl)
92
164
  return;
93
- const disclosure = document.createElement("div");
94
- disclosure.className = "disclosure";
95
- disclosure.append(`${this.#text.disclosureText} `);
165
+ this.#conversation += 1;
166
+ this.#messages = [];
167
+ this.#setPending(false);
168
+ this.#messagesEl.replaceChildren();
169
+ this.#suggestionsEl = undefined;
170
+ this.#renderIntro();
171
+ if (this.#resetEl)
172
+ this.#resetEl.hidden = true;
173
+ if (this.#inputEl)
174
+ this.#inputEl.value = "";
175
+ this.#syncInput();
176
+ if (matches(FINE_POINTER_QUERY))
177
+ this.#inputEl?.focus();
178
+ else
179
+ this.#closeEl?.focus({ preventScroll: true });
180
+ }
181
+ #buildSuggestions() {
182
+ const attr = this.getAttribute("suggestions");
183
+ const items = (attr === null ? this.#text.suggestions : attr.split("|"))
184
+ .map((item) => item.trim())
185
+ .filter((item) => item.length > 0);
186
+ if (items.length === 0)
187
+ return undefined;
188
+ const list = element("div", "suggestions");
189
+ for (const item of items) {
190
+ const button = element("button");
191
+ button.type = "button";
192
+ button.textContent = item;
193
+ button.addEventListener("click", () => void this.#sendMessage(item));
194
+ list.appendChild(button);
195
+ }
196
+ this.#suggestionsEl = list;
197
+ return list;
198
+ }
199
+ #buildForm() {
200
+ const form = element("form");
201
+ const field = element("div", "field");
202
+ const input = element("textarea");
203
+ input.rows = 1;
204
+ input.placeholder = this.#text.inputPlaceholder;
205
+ input.setAttribute("aria-label", this.#text.inputPlaceholder);
206
+ input.setAttribute("enterkeyhint", "send");
207
+ input.addEventListener("input", () => this.#syncInput());
208
+ input.addEventListener("keydown", (event) => {
209
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing)
210
+ return;
211
+ event.preventDefault();
212
+ form.requestSubmit();
213
+ });
214
+ const send = element("button", "send");
215
+ send.type = "submit";
216
+ send.disabled = true;
217
+ send.setAttribute("aria-label", this.#text.submitLabel);
218
+ send.innerHTML = SEND_ICON;
219
+ field.append(input, send);
220
+ form.append(field, this.#buildDisclosure());
221
+ form.addEventListener("submit", (event) => {
222
+ event.preventDefault();
223
+ const text = input.value;
224
+ input.value = "";
225
+ this.#syncInput();
226
+ void this.#sendMessage(text);
227
+ });
228
+ this.#inputEl = input;
229
+ this.#sendEl = send;
230
+ return form;
231
+ }
232
+ #buildDisclosure() {
233
+ const disclosure = element("p", "disclosure");
234
+ disclosure.append(this.#text.disclosureText);
96
235
  const policyHref = this.getAttribute("policy-href");
97
236
  if (policyHref) {
98
- const link = document.createElement("a");
237
+ disclosure.append(" ");
238
+ const link = element("a");
99
239
  link.href = policyHref;
100
240
  link.target = "_blank";
101
241
  link.rel = "noopener noreferrer";
102
242
  link.textContent = this.#text.policyLinkText;
103
243
  disclosure.appendChild(link);
104
244
  }
105
- this.#messagesEl.appendChild(disclosure);
245
+ return disclosure;
246
+ }
247
+ #setOpen(open) {
248
+ if (this.#open === open || !this.#rootEl || !this.#panelEl || !this.#toggleEl)
249
+ return;
250
+ this.#open = open;
251
+ this.#rootEl.classList.toggle("open", open);
252
+ this.#toggleEl.setAttribute("aria-expanded", String(open));
253
+ this.#toggleEl.inert = open;
254
+ this.#panelEl.setAttribute("aria-hidden", String(!open));
255
+ this.#panelEl.inert = !open;
256
+ this.#watchViewport(open);
257
+ this.#lockScroll(open && matches(COMPACT_QUERY));
258
+ if (open) {
259
+ // タッチ端末で開いた瞬間にキーボードを出すと、パネルの登場とキーボードの動きが重なる
260
+ if (matches(FINE_POINTER_QUERY))
261
+ this.#inputEl?.focus();
262
+ else
263
+ this.#closeEl?.focus({ preventScroll: true });
264
+ }
265
+ else {
266
+ this.#inputEl?.blur();
267
+ this.#toggleEl.focus({ preventScroll: true });
268
+ }
269
+ }
270
+ #watchViewport(on) {
271
+ const viewport = window.visualViewport;
272
+ if (!viewport)
273
+ return;
274
+ if (on) {
275
+ viewport.addEventListener("resize", this.#onViewportChange);
276
+ viewport.addEventListener("scroll", this.#onViewportChange);
277
+ this.#syncViewport();
278
+ }
279
+ else {
280
+ viewport.removeEventListener("resize", this.#onViewportChange);
281
+ viewport.removeEventListener("scroll", this.#onViewportChange);
282
+ }
283
+ }
284
+ #syncViewport() {
285
+ const viewport = window.visualViewport;
286
+ if (!viewport || !this.#panelEl)
287
+ return;
288
+ this.#panelEl.style.setProperty("--vv-height", `${viewport.height}px`);
289
+ this.#panelEl.style.setProperty("--vv-top", `${viewport.offsetTop}px`);
290
+ this.#scrollToEnd();
291
+ }
292
+ // 全画面表示中に背後のページがスクロールすると、iOS でパネルごと画面外へずれる
293
+ #lockScroll(lock) {
294
+ if (lock === this.#scrollLocked)
295
+ return;
296
+ const rootStyle = document.documentElement.style;
297
+ if (lock) {
298
+ this.#previousOverflow = rootStyle.overflow;
299
+ rootStyle.overflow = "hidden";
300
+ }
301
+ else {
302
+ rootStyle.overflow = this.#previousOverflow;
303
+ }
304
+ this.#scrollLocked = lock;
305
+ }
306
+ #syncInput() {
307
+ const input = this.#inputEl;
308
+ if (!input || !this.#sendEl)
309
+ return;
310
+ input.style.height = "auto";
311
+ input.style.height = `${input.scrollHeight}px`;
312
+ this.#sendEl.disabled = this.#pending || input.value.trim().length === 0;
313
+ }
314
+ #setPending(pending) {
315
+ this.#pending = pending;
316
+ this.#syncInput();
317
+ if (pending && this.#messagesEl) {
318
+ const typing = element("div", "typing");
319
+ typing.setAttribute("role", "status");
320
+ typing.setAttribute("aria-label", this.#text.typingLabel);
321
+ typing.append(element("span"), element("span"), element("span"));
322
+ this.#messagesEl.appendChild(typing);
323
+ this.#typingEl = typing;
324
+ this.#scrollToEnd();
325
+ }
326
+ else {
327
+ this.#typingEl?.remove();
328
+ this.#typingEl = undefined;
329
+ }
106
330
  }
107
331
  async #sendMessage(rawText) {
108
332
  const text = rawText.trim();
109
- if (!text)
333
+ if (!text || this.#pending)
110
334
  return;
335
+ this.#suggestionsEl?.remove();
336
+ this.#suggestionsEl = undefined;
111
337
  this.#appendMessage({ role: "user", text });
112
338
  const endpoint = this.getAttribute("endpoint");
113
339
  if (!endpoint) {
114
340
  this.#appendMessage({ role: "assistant", text: this.#text.configErrorText });
115
341
  return;
116
342
  }
343
+ const conversation = this.#conversation;
344
+ this.#setPending(true);
345
+ let answer;
117
346
  try {
118
347
  const response = await fetch(endpoint, {
119
348
  method: "POST",
@@ -121,27 +350,30 @@ export class FolioAgentWidgetElement extends HTMLElement {
121
350
  body: JSON.stringify({ message: text }),
122
351
  });
123
352
  const body = (await response.json());
124
- this.#appendMessage({
125
- role: "assistant",
126
- text: body.answer ?? this.#text.answerFallbackText,
127
- });
353
+ answer = body.answer ?? this.#text.answerFallbackText;
128
354
  }
129
355
  catch {
130
- this.#appendMessage({
131
- role: "assistant",
132
- text: this.#text.networkErrorText,
133
- });
356
+ answer = this.#text.networkErrorText;
134
357
  }
358
+ if (conversation !== this.#conversation)
359
+ return;
360
+ this.#setPending(false);
361
+ this.#appendMessage({ role: "assistant", text: answer });
135
362
  }
136
363
  #appendMessage(message) {
137
364
  this.#messages.push(message);
365
+ if (this.#resetEl)
366
+ this.#resetEl.hidden = false;
138
367
  if (!this.#messagesEl)
139
368
  return;
140
- const el = document.createElement("div");
141
- el.className = `message ${message.role}`;
369
+ const el = element("div", `message ${message.role}`);
142
370
  el.textContent = message.text;
143
371
  this.#messagesEl.appendChild(el);
144
- this.#messagesEl.scrollTop = this.#messagesEl.scrollHeight;
372
+ this.#scrollToEnd();
373
+ }
374
+ #scrollToEnd() {
375
+ if (this.#messagesEl)
376
+ this.#messagesEl.scrollTop = this.#messagesEl.scrollHeight;
145
377
  }
146
378
  }
147
379
  export function defineFolioAgentWidget(tagName = FolioAgentWidgetElement.tagName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@folio-agent/widget",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-agnostic reception chat widget (silent until clicked)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,6 +32,6 @@
32
32
  "prepublishOnly": "tsc -b --force"
33
33
  },
34
34
  "devDependencies": {
35
- "happy-dom": "^20.11.1"
35
+ "happy-dom": "^20.14.5"
36
36
  }
37
37
  }