@folio-agent/widget 0.4.1 → 0.5.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 +2 -2
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +303 -57
- package/dist/widget-element.d.ts +1 -0
- package/dist/widget-element.js +254 -65
- package/package.json +2 -2
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
|
|
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\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: 12px;\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 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 .close:hover {\n background: color-mix(in srgb, var(--_text) 12%, transparent);\n }\n .close 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 /* 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 .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,350 @@
|
|
|
1
1
|
// テーマ注入: 利用側サイトのグローバルCSSで
|
|
2
2
|
// folio-agent-widget { --folio-agent-accent: #...; }
|
|
3
3
|
// のようにカスタムプロパティを指定するとShadow DOM内に継承されテーマが反映される。
|
|
4
|
-
//
|
|
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
|
+
|
|
13
37
|
.toggle {
|
|
14
38
|
position: fixed;
|
|
15
39
|
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);
|
|
40
|
+
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
|
26
41
|
z-index: 2147483000;
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
height: 46px;
|
|
46
|
+
padding: 0 18px 0 14px;
|
|
47
|
+
border-radius: 999px;
|
|
48
|
+
border: 1px solid color-mix(in srgb, var(--_accent-contrast) 14%, transparent);
|
|
49
|
+
background: color-mix(in srgb, var(--_accent) 88%, transparent);
|
|
50
|
+
-webkit-backdrop-filter: blur(16px) saturate(1.6);
|
|
51
|
+
backdrop-filter: blur(16px) saturate(1.6);
|
|
52
|
+
color: var(--_accent-contrast);
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
letter-spacing: 0.02em;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
box-shadow: 0 10px 30px rgba(16, 20, 28, 0.22);
|
|
58
|
+
transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.2s ease;
|
|
27
59
|
}
|
|
60
|
+
.toggle:hover {
|
|
61
|
+
transform: translateY(-1px);
|
|
62
|
+
}
|
|
63
|
+
.toggle:active {
|
|
64
|
+
transform: scale(0.97);
|
|
65
|
+
}
|
|
66
|
+
.toggle svg {
|
|
67
|
+
width: 20px;
|
|
68
|
+
height: 20px;
|
|
69
|
+
flex: none;
|
|
70
|
+
}
|
|
71
|
+
.root.open .toggle {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
transform: scale(0.85);
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
28
77
|
.panel {
|
|
29
78
|
position: fixed;
|
|
30
79
|
right: 20px;
|
|
31
|
-
bottom:
|
|
32
|
-
|
|
33
|
-
|
|
80
|
+
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
|
81
|
+
z-index: 2147483001;
|
|
82
|
+
width: min(400px, calc(100vw - 40px));
|
|
83
|
+
height: min(620px, calc(100dvh - 40px));
|
|
34
84
|
display: flex;
|
|
35
85
|
flex-direction: column;
|
|
36
|
-
|
|
37
|
-
border
|
|
38
|
-
|
|
86
|
+
border-radius: 22px;
|
|
87
|
+
border: 1px solid var(--_hair);
|
|
88
|
+
background: var(--_glass);
|
|
89
|
+
-webkit-backdrop-filter: blur(28px) saturate(1.8);
|
|
90
|
+
backdrop-filter: blur(28px) saturate(1.8);
|
|
91
|
+
box-shadow: 0 24px 64px rgba(16, 20, 28, 0.2), 0 2px 6px rgba(16, 20, 28, 0.08);
|
|
92
|
+
color: var(--_text);
|
|
39
93
|
overflow: hidden;
|
|
40
|
-
|
|
94
|
+
transform-origin: calc(100% - 40px) calc(100% - 20px);
|
|
95
|
+
opacity: 0;
|
|
96
|
+
visibility: hidden;
|
|
97
|
+
transform: translateY(10px) scale(0.9);
|
|
98
|
+
transition:
|
|
99
|
+
opacity 0.2s ease,
|
|
100
|
+
transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
101
|
+
visibility 0s linear 0.32s;
|
|
102
|
+
}
|
|
103
|
+
.root.open .panel {
|
|
104
|
+
opacity: 1;
|
|
105
|
+
visibility: visible;
|
|
106
|
+
transform: none;
|
|
107
|
+
transition:
|
|
108
|
+
opacity 0.22s ease,
|
|
109
|
+
transform 0.38s cubic-bezier(0.2, 0.9, 0.25, 1.05),
|
|
110
|
+
visibility 0s;
|
|
41
111
|
}
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
|
|
113
|
+
.header {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 12px;
|
|
117
|
+
padding: 14px 12px 12px 18px;
|
|
118
|
+
border-bottom: 1px solid var(--_hair);
|
|
44
119
|
}
|
|
120
|
+
.titles {
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
}
|
|
124
|
+
.heading {
|
|
125
|
+
font-size: 15px;
|
|
126
|
+
font-weight: 700;
|
|
127
|
+
line-height: 1.3;
|
|
128
|
+
}
|
|
129
|
+
.subheading {
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
line-height: 1.4;
|
|
132
|
+
color: var(--_muted);
|
|
133
|
+
}
|
|
134
|
+
.close {
|
|
135
|
+
flex: none;
|
|
136
|
+
display: grid;
|
|
137
|
+
place-items: center;
|
|
138
|
+
width: 34px;
|
|
139
|
+
height: 34px;
|
|
140
|
+
border: 0;
|
|
141
|
+
border-radius: 50%;
|
|
142
|
+
background: color-mix(in srgb, var(--_text) 7%, transparent);
|
|
143
|
+
color: var(--_text);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
}
|
|
146
|
+
.close:hover {
|
|
147
|
+
background: color-mix(in srgb, var(--_text) 12%, transparent);
|
|
148
|
+
}
|
|
149
|
+
.close svg {
|
|
150
|
+
width: 16px;
|
|
151
|
+
height: 16px;
|
|
152
|
+
}
|
|
153
|
+
|
|
45
154
|
.messages {
|
|
46
155
|
flex: 1;
|
|
47
156
|
overflow-y: auto;
|
|
48
|
-
|
|
157
|
+
overscroll-behavior: contain;
|
|
158
|
+
padding: 18px 18px 8px;
|
|
49
159
|
display: flex;
|
|
50
160
|
flex-direction: column;
|
|
51
|
-
gap:
|
|
52
|
-
font-size:
|
|
53
|
-
line-height: 1.
|
|
54
|
-
color: var(--folio-agent-text, CanvasText);
|
|
161
|
+
gap: 14px;
|
|
162
|
+
font-size: 15px;
|
|
163
|
+
line-height: 1.7;
|
|
55
164
|
}
|
|
56
|
-
.message
|
|
165
|
+
.message,
|
|
166
|
+
.greeting {
|
|
57
167
|
white-space: pre-wrap;
|
|
168
|
+
overflow-wrap: anywhere;
|
|
169
|
+
animation: fade-in 0.28s ease both;
|
|
58
170
|
}
|
|
59
|
-
.message.assistant
|
|
60
|
-
|
|
61
|
-
|
|
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;
|
|
171
|
+
.message.assistant,
|
|
172
|
+
.greeting {
|
|
173
|
+
align-self: stretch;
|
|
65
174
|
}
|
|
66
175
|
.message.user {
|
|
67
176
|
align-self: flex-end;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
border-radius:
|
|
71
|
-
|
|
177
|
+
max-width: 85%;
|
|
178
|
+
padding: 8px 14px;
|
|
179
|
+
border-radius: 18px 18px 6px 18px;
|
|
180
|
+
background: color-mix(in srgb, var(--_accent) 14%, var(--_surface));
|
|
181
|
+
border: 1px solid color-mix(in srgb, var(--_accent) 28%, var(--_surface));
|
|
72
182
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
padding: 0 0 4px;
|
|
183
|
+
@keyframes fade-in {
|
|
184
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
185
|
+
to { opacity: 1; transform: none; }
|
|
77
186
|
}
|
|
78
|
-
|
|
79
|
-
|
|
187
|
+
|
|
188
|
+
.suggestions {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
align-items: flex-start;
|
|
192
|
+
gap: 8px;
|
|
193
|
+
}
|
|
194
|
+
.suggestions button {
|
|
195
|
+
padding: 7px 14px;
|
|
196
|
+
border: 1px solid var(--_hair);
|
|
197
|
+
border-radius: 999px;
|
|
198
|
+
background: var(--_chip);
|
|
199
|
+
color: var(--_text);
|
|
200
|
+
font-size: 14px;
|
|
201
|
+
text-align: left;
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
transition: background 0.15s ease;
|
|
204
|
+
}
|
|
205
|
+
.suggestions button:hover {
|
|
206
|
+
background: color-mix(in srgb, var(--_text) 8%, transparent);
|
|
80
207
|
}
|
|
208
|
+
|
|
209
|
+
.typing {
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
gap: 5px;
|
|
212
|
+
padding: 8px 0;
|
|
213
|
+
}
|
|
214
|
+
.typing span {
|
|
215
|
+
width: 7px;
|
|
216
|
+
height: 7px;
|
|
217
|
+
border-radius: 50%;
|
|
218
|
+
background: var(--_muted);
|
|
219
|
+
animation: typing-dot 1.1s ease-in-out infinite;
|
|
220
|
+
}
|
|
221
|
+
.typing span:nth-child(2) {
|
|
222
|
+
animation-delay: 0.15s;
|
|
223
|
+
}
|
|
224
|
+
.typing span:nth-child(3) {
|
|
225
|
+
animation-delay: 0.3s;
|
|
226
|
+
}
|
|
227
|
+
@keyframes typing-dot {
|
|
228
|
+
0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
|
|
229
|
+
40% { opacity: 1; transform: translateY(-3px); }
|
|
230
|
+
}
|
|
231
|
+
|
|
81
232
|
form {
|
|
233
|
+
padding: 8px 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
|
234
|
+
}
|
|
235
|
+
.field {
|
|
82
236
|
display: flex;
|
|
83
|
-
|
|
237
|
+
align-items: flex-end;
|
|
238
|
+
gap: 8px;
|
|
239
|
+
padding: 6px 6px 6px 14px;
|
|
240
|
+
border: 1px solid var(--_hair);
|
|
241
|
+
border-radius: 22px;
|
|
242
|
+
background: var(--_field);
|
|
243
|
+
transition: border-color 0.15s ease;
|
|
244
|
+
}
|
|
245
|
+
.field:focus-within {
|
|
246
|
+
border-color: color-mix(in srgb, var(--_text) 32%, transparent);
|
|
84
247
|
}
|
|
85
|
-
|
|
248
|
+
textarea {
|
|
86
249
|
flex: 1;
|
|
87
|
-
|
|
250
|
+
min-width: 0;
|
|
251
|
+
max-height: 144px;
|
|
252
|
+
padding: 6px 0;
|
|
253
|
+
border: 0;
|
|
254
|
+
outline: 0;
|
|
255
|
+
resize: none;
|
|
88
256
|
background: transparent;
|
|
89
|
-
color:
|
|
90
|
-
|
|
91
|
-
font-size:
|
|
257
|
+
color: var(--_text);
|
|
258
|
+
/* 16px 未満だと iOS Safari がフォーカス時にページを拡大する */
|
|
259
|
+
font-size: 16px;
|
|
260
|
+
line-height: 1.5;
|
|
92
261
|
}
|
|
93
|
-
|
|
94
|
-
|
|
262
|
+
textarea::placeholder {
|
|
263
|
+
color: var(--_muted);
|
|
95
264
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
265
|
+
.send {
|
|
266
|
+
flex: none;
|
|
267
|
+
display: grid;
|
|
268
|
+
place-items: center;
|
|
269
|
+
width: 34px;
|
|
270
|
+
height: 34px;
|
|
271
|
+
border: 0;
|
|
272
|
+
border-radius: 50%;
|
|
273
|
+
background: var(--_accent);
|
|
274
|
+
color: var(--_accent-contrast);
|
|
100
275
|
cursor: pointer;
|
|
101
|
-
|
|
102
|
-
|
|
276
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
277
|
+
}
|
|
278
|
+
.send:disabled {
|
|
279
|
+
opacity: 0.25;
|
|
280
|
+
cursor: default;
|
|
281
|
+
}
|
|
282
|
+
.send:not(:disabled):active {
|
|
283
|
+
transform: scale(0.92);
|
|
284
|
+
}
|
|
285
|
+
.send svg {
|
|
286
|
+
width: 18px;
|
|
287
|
+
height: 18px;
|
|
288
|
+
}
|
|
289
|
+
.disclosure {
|
|
290
|
+
margin: 8px 0 0;
|
|
291
|
+
font-size: 11px;
|
|
292
|
+
line-height: 1.5;
|
|
293
|
+
text-align: center;
|
|
294
|
+
color: var(--_muted);
|
|
295
|
+
}
|
|
296
|
+
.disclosure a {
|
|
297
|
+
color: inherit;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.toggle:focus-visible,
|
|
301
|
+
.close:focus-visible,
|
|
302
|
+
.send:focus-visible,
|
|
303
|
+
.suggestions button:focus-visible {
|
|
304
|
+
outline: 2px solid var(--_focus);
|
|
305
|
+
outline-offset: 2px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@media (max-width: 640px) {
|
|
309
|
+
.toggle {
|
|
310
|
+
right: 16px;
|
|
311
|
+
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
|
312
|
+
}
|
|
313
|
+
/* 高さと上端は visualViewport から JS が流し込む。キーボード表示中も入力欄を見える位置に保つため */
|
|
314
|
+
.panel {
|
|
315
|
+
top: var(--vv-top, 0px);
|
|
316
|
+
right: 0;
|
|
317
|
+
bottom: auto;
|
|
318
|
+
left: 0;
|
|
319
|
+
width: 100%;
|
|
320
|
+
height: var(--vv-height, 100dvh);
|
|
321
|
+
border: 0;
|
|
322
|
+
border-radius: 0;
|
|
323
|
+
background: var(--_glass-strong);
|
|
324
|
+
transform-origin: 50% 100%;
|
|
325
|
+
transform: translateY(32px);
|
|
326
|
+
}
|
|
327
|
+
.header {
|
|
328
|
+
padding-top: calc(12px + env(safe-area-inset-top, 0px));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@media (prefers-reduced-motion: reduce) {
|
|
333
|
+
.toggle,
|
|
334
|
+
.panel,
|
|
335
|
+
.root.open .panel {
|
|
336
|
+
transform: none;
|
|
337
|
+
}
|
|
338
|
+
.panel {
|
|
339
|
+
transition: opacity 0.15s linear, visibility 0s linear 0.15s;
|
|
340
|
+
}
|
|
341
|
+
.root.open .panel {
|
|
342
|
+
transition: opacity 0.15s linear;
|
|
343
|
+
}
|
|
344
|
+
.message,
|
|
345
|
+
.greeting,
|
|
346
|
+
.typing span {
|
|
347
|
+
animation: none;
|
|
348
|
+
}
|
|
103
349
|
}
|
|
104
350
|
`;
|
package/dist/widget-element.d.ts
CHANGED
package/dist/widget-element.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { WIDGET_STYLES } from "./styles.js";
|
|
2
2
|
const WIDGET_TEXT = {
|
|
3
3
|
ja: {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
toggleLabel: "質問する",
|
|
5
|
+
headingText: "このサイトについて質問",
|
|
6
|
+
subheadingText: "公開している情報をもとに AI が答えます",
|
|
7
|
+
closeLabel: "閉じる",
|
|
8
|
+
greetingText: "制作実績・記事の内容・仕事の依頼について答えます。",
|
|
9
|
+
suggestions: ["どんな制作実績がありますか?", "どんな考え方で仕事をしていますか?", "仕事を依頼するには?"],
|
|
10
|
+
inputPlaceholder: "質問を入力",
|
|
6
11
|
submitLabel: "送信",
|
|
12
|
+
typingLabel: "回答を作成中",
|
|
7
13
|
disclosureText: "入力内容は品質改善のため記録されます。",
|
|
8
|
-
policyLinkText: "
|
|
14
|
+
policyLinkText: "利用データの扱い",
|
|
9
15
|
configErrorText: "設定エラー: endpoint属性が指定されていません。",
|
|
10
16
|
answerFallbackText: "回答を取得できませんでした。しばらくしてから再度お試しください。",
|
|
11
17
|
networkErrorText: "通信エラーが発生しました。しばらくしてから再度お試しください。",
|
|
12
18
|
},
|
|
13
19
|
en: {
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
toggleLabel: "Ask",
|
|
21
|
+
headingText: "Ask about this site",
|
|
22
|
+
subheadingText: "AI answers from what this site publishes",
|
|
23
|
+
closeLabel: "Close",
|
|
24
|
+
greetingText: "Ask about past work, articles, or how to request a project.",
|
|
25
|
+
suggestions: ["What have you built?", "How do you approach your work?", "How can I request a project?"],
|
|
26
|
+
inputPlaceholder: "Type a question",
|
|
16
27
|
submitLabel: "Send",
|
|
28
|
+
typingLabel: "Writing an answer",
|
|
17
29
|
disclosureText: "Your input is logged for quality improvement.",
|
|
18
30
|
policyLinkText: "About data usage",
|
|
19
31
|
configErrorText: "Configuration error: the endpoint attribute is not set.",
|
|
@@ -21,99 +33,277 @@ const WIDGET_TEXT = {
|
|
|
21
33
|
networkErrorText: "A network error occurred. Please try again later.",
|
|
22
34
|
},
|
|
23
35
|
};
|
|
36
|
+
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>`;
|
|
37
|
+
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>`;
|
|
38
|
+
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>`;
|
|
39
|
+
const COMPACT_QUERY = "(max-width: 640px)";
|
|
40
|
+
const FINE_POINTER_QUERY = "(pointer: fine)";
|
|
24
41
|
function resolveLanguage(value) {
|
|
25
42
|
return value === "en" ? "en" : "ja";
|
|
26
43
|
}
|
|
44
|
+
function matches(query) {
|
|
45
|
+
return typeof window.matchMedia === "function" && window.matchMedia(query).matches;
|
|
46
|
+
}
|
|
47
|
+
function element(tag, className) {
|
|
48
|
+
const el = document.createElement(tag);
|
|
49
|
+
if (className)
|
|
50
|
+
el.className = className;
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
27
53
|
export class FolioAgentWidgetElement extends HTMLElement {
|
|
28
54
|
static tagName = "folio-agent-widget";
|
|
29
55
|
#open = false;
|
|
30
|
-
#
|
|
56
|
+
#pending = false;
|
|
31
57
|
#messages = [];
|
|
58
|
+
#text = WIDGET_TEXT.ja;
|
|
59
|
+
#rootEl;
|
|
60
|
+
#toggleEl;
|
|
32
61
|
#panelEl;
|
|
62
|
+
#closeEl;
|
|
33
63
|
#messagesEl;
|
|
64
|
+
#suggestionsEl;
|
|
65
|
+
#typingEl;
|
|
34
66
|
#inputEl;
|
|
35
|
-
#
|
|
67
|
+
#sendEl;
|
|
68
|
+
#scrollLocked = false;
|
|
69
|
+
#previousOverflow = "";
|
|
70
|
+
#onViewportChange = () => this.#syncViewport();
|
|
36
71
|
connectedCallback() {
|
|
72
|
+
if (this.shadowRoot)
|
|
73
|
+
return;
|
|
37
74
|
this.#text = WIDGET_TEXT[resolveLanguage(this.getAttribute("lang"))];
|
|
38
75
|
const root = this.attachShadow({ mode: "open" });
|
|
39
76
|
const style = document.createElement("style");
|
|
40
77
|
style.textContent = WIDGET_STYLES;
|
|
41
78
|
root.appendChild(style);
|
|
42
|
-
const
|
|
43
|
-
|
|
79
|
+
const wrapper = element("div", "root");
|
|
80
|
+
wrapper.append(this.#buildToggle(), this.#buildPanel());
|
|
81
|
+
root.appendChild(wrapper);
|
|
82
|
+
this.#rootEl = wrapper;
|
|
83
|
+
}
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
this.#watchViewport(false);
|
|
86
|
+
this.#lockScroll(false);
|
|
87
|
+
}
|
|
88
|
+
#buildToggle() {
|
|
89
|
+
const toggle = element("button", "toggle");
|
|
44
90
|
toggle.type = "button";
|
|
45
|
-
toggle.setAttribute("aria-
|
|
46
|
-
toggle.
|
|
47
|
-
toggle.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
91
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
92
|
+
toggle.setAttribute("aria-controls", "panel");
|
|
93
|
+
toggle.innerHTML = CHAT_ICON;
|
|
94
|
+
const label = element("span");
|
|
95
|
+
label.textContent = this.#text.toggleLabel;
|
|
96
|
+
toggle.appendChild(label);
|
|
97
|
+
toggle.addEventListener("click", () => this.#setOpen(true));
|
|
98
|
+
this.#toggleEl = toggle;
|
|
99
|
+
return toggle;
|
|
100
|
+
}
|
|
101
|
+
#buildPanel() {
|
|
102
|
+
const heading = this.getAttribute("heading") ?? this.#text.headingText;
|
|
103
|
+
const panel = element("section", "panel");
|
|
104
|
+
panel.id = "panel";
|
|
105
|
+
panel.setAttribute("role", "dialog");
|
|
106
|
+
panel.setAttribute("aria-label", heading);
|
|
107
|
+
panel.setAttribute("aria-hidden", "true");
|
|
108
|
+
panel.inert = true;
|
|
109
|
+
panel.addEventListener("keydown", (event) => {
|
|
110
|
+
if (event.key === "Escape")
|
|
111
|
+
this.#setOpen(false);
|
|
112
|
+
});
|
|
113
|
+
const header = element("header", "header");
|
|
114
|
+
const titles = element("div", "titles");
|
|
115
|
+
const headingEl = element("div", "heading");
|
|
116
|
+
headingEl.textContent = heading;
|
|
117
|
+
const subheading = element("div", "subheading");
|
|
118
|
+
subheading.textContent = this.#text.subheadingText;
|
|
119
|
+
titles.append(headingEl, subheading);
|
|
120
|
+
const close = element("button", "close");
|
|
121
|
+
close.type = "button";
|
|
122
|
+
close.setAttribute("aria-label", this.#text.closeLabel);
|
|
123
|
+
close.innerHTML = CLOSE_ICON;
|
|
124
|
+
close.addEventListener("click", () => this.#setOpen(false));
|
|
125
|
+
header.append(titles, close);
|
|
126
|
+
const messages = element("div", "messages");
|
|
127
|
+
messages.setAttribute("aria-live", "polite");
|
|
128
|
+
const greeting = this.getAttribute("greeting") ?? this.#text.greetingText;
|
|
129
|
+
if (greeting.trim()) {
|
|
130
|
+
const greetingEl = element("div", "greeting");
|
|
131
|
+
greetingEl.textContent = greeting;
|
|
132
|
+
messages.appendChild(greetingEl);
|
|
133
|
+
}
|
|
134
|
+
const suggestions = this.#buildSuggestions();
|
|
135
|
+
if (suggestions)
|
|
136
|
+
messages.appendChild(suggestions);
|
|
137
|
+
panel.append(header, messages, this.#buildForm());
|
|
138
|
+
this.#panelEl = panel;
|
|
139
|
+
this.#closeEl = close;
|
|
140
|
+
this.#messagesEl = messages;
|
|
141
|
+
return panel;
|
|
142
|
+
}
|
|
143
|
+
#buildSuggestions() {
|
|
144
|
+
const attr = this.getAttribute("suggestions");
|
|
145
|
+
const items = (attr === null ? this.#text.suggestions : attr.split("|"))
|
|
146
|
+
.map((item) => item.trim())
|
|
147
|
+
.filter((item) => item.length > 0);
|
|
148
|
+
if (items.length === 0)
|
|
149
|
+
return undefined;
|
|
150
|
+
const list = element("div", "suggestions");
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
const button = element("button");
|
|
153
|
+
button.type = "button";
|
|
154
|
+
button.textContent = item;
|
|
155
|
+
button.addEventListener("click", () => void this.#sendMessage(item));
|
|
156
|
+
list.appendChild(button);
|
|
157
|
+
}
|
|
158
|
+
this.#suggestionsEl = list;
|
|
159
|
+
return list;
|
|
160
|
+
}
|
|
161
|
+
#buildForm() {
|
|
162
|
+
const form = element("form");
|
|
163
|
+
const field = element("div", "field");
|
|
164
|
+
const input = element("textarea");
|
|
165
|
+
input.rows = 1;
|
|
58
166
|
input.placeholder = this.#text.inputPlaceholder;
|
|
59
|
-
input.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
167
|
+
input.setAttribute("aria-label", this.#text.inputPlaceholder);
|
|
168
|
+
input.setAttribute("enterkeyhint", "send");
|
|
169
|
+
input.addEventListener("input", () => this.#syncInput());
|
|
170
|
+
input.addEventListener("keydown", (event) => {
|
|
171
|
+
if (event.key !== "Enter" || event.shiftKey || event.isComposing)
|
|
172
|
+
return;
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
form.requestSubmit();
|
|
175
|
+
});
|
|
176
|
+
const send = element("button", "send");
|
|
177
|
+
send.type = "submit";
|
|
178
|
+
send.disabled = true;
|
|
179
|
+
send.setAttribute("aria-label", this.#text.submitLabel);
|
|
180
|
+
send.innerHTML = SEND_ICON;
|
|
181
|
+
field.append(input, send);
|
|
182
|
+
form.append(field, this.#buildDisclosure());
|
|
65
183
|
form.addEventListener("submit", (event) => {
|
|
66
184
|
event.preventDefault();
|
|
67
185
|
const text = input.value;
|
|
68
186
|
input.value = "";
|
|
187
|
+
this.#syncInput();
|
|
69
188
|
void this.#sendMessage(text);
|
|
70
189
|
});
|
|
71
|
-
panel.appendChild(form);
|
|
72
|
-
root.appendChild(panel);
|
|
73
|
-
this.#panelEl = panel;
|
|
74
|
-
this.#messagesEl = messages;
|
|
75
190
|
this.#inputEl = input;
|
|
191
|
+
this.#sendEl = send;
|
|
192
|
+
return form;
|
|
76
193
|
}
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
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();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
#renderDisclosure() {
|
|
91
|
-
if (!this.#messagesEl)
|
|
92
|
-
return;
|
|
93
|
-
const disclosure = document.createElement("div");
|
|
94
|
-
disclosure.className = "disclosure";
|
|
95
|
-
disclosure.append(`${this.#text.disclosureText} `);
|
|
194
|
+
#buildDisclosure() {
|
|
195
|
+
const disclosure = element("p", "disclosure");
|
|
196
|
+
disclosure.append(this.#text.disclosureText);
|
|
96
197
|
const policyHref = this.getAttribute("policy-href");
|
|
97
198
|
if (policyHref) {
|
|
98
|
-
|
|
199
|
+
disclosure.append(" ");
|
|
200
|
+
const link = element("a");
|
|
99
201
|
link.href = policyHref;
|
|
100
202
|
link.target = "_blank";
|
|
101
203
|
link.rel = "noopener noreferrer";
|
|
102
204
|
link.textContent = this.#text.policyLinkText;
|
|
103
205
|
disclosure.appendChild(link);
|
|
104
206
|
}
|
|
105
|
-
|
|
207
|
+
return disclosure;
|
|
208
|
+
}
|
|
209
|
+
#setOpen(open) {
|
|
210
|
+
if (this.#open === open || !this.#rootEl || !this.#panelEl || !this.#toggleEl)
|
|
211
|
+
return;
|
|
212
|
+
this.#open = open;
|
|
213
|
+
this.#rootEl.classList.toggle("open", open);
|
|
214
|
+
this.#toggleEl.setAttribute("aria-expanded", String(open));
|
|
215
|
+
this.#toggleEl.inert = open;
|
|
216
|
+
this.#panelEl.setAttribute("aria-hidden", String(!open));
|
|
217
|
+
this.#panelEl.inert = !open;
|
|
218
|
+
this.#watchViewport(open);
|
|
219
|
+
this.#lockScroll(open && matches(COMPACT_QUERY));
|
|
220
|
+
if (open) {
|
|
221
|
+
// タッチ端末で開いた瞬間にキーボードを出すと、パネルの登場とキーボードの動きが重なる
|
|
222
|
+
if (matches(FINE_POINTER_QUERY))
|
|
223
|
+
this.#inputEl?.focus();
|
|
224
|
+
else
|
|
225
|
+
this.#closeEl?.focus({ preventScroll: true });
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.#inputEl?.blur();
|
|
229
|
+
this.#toggleEl.focus({ preventScroll: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
#watchViewport(on) {
|
|
233
|
+
const viewport = window.visualViewport;
|
|
234
|
+
if (!viewport)
|
|
235
|
+
return;
|
|
236
|
+
if (on) {
|
|
237
|
+
viewport.addEventListener("resize", this.#onViewportChange);
|
|
238
|
+
viewport.addEventListener("scroll", this.#onViewportChange);
|
|
239
|
+
this.#syncViewport();
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
viewport.removeEventListener("resize", this.#onViewportChange);
|
|
243
|
+
viewport.removeEventListener("scroll", this.#onViewportChange);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
#syncViewport() {
|
|
247
|
+
const viewport = window.visualViewport;
|
|
248
|
+
if (!viewport || !this.#panelEl)
|
|
249
|
+
return;
|
|
250
|
+
this.#panelEl.style.setProperty("--vv-height", `${viewport.height}px`);
|
|
251
|
+
this.#panelEl.style.setProperty("--vv-top", `${viewport.offsetTop}px`);
|
|
252
|
+
this.#scrollToEnd();
|
|
253
|
+
}
|
|
254
|
+
// 全画面表示中に背後のページがスクロールすると、iOS でパネルごと画面外へずれる
|
|
255
|
+
#lockScroll(lock) {
|
|
256
|
+
if (lock === this.#scrollLocked)
|
|
257
|
+
return;
|
|
258
|
+
const rootStyle = document.documentElement.style;
|
|
259
|
+
if (lock) {
|
|
260
|
+
this.#previousOverflow = rootStyle.overflow;
|
|
261
|
+
rootStyle.overflow = "hidden";
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
rootStyle.overflow = this.#previousOverflow;
|
|
265
|
+
}
|
|
266
|
+
this.#scrollLocked = lock;
|
|
267
|
+
}
|
|
268
|
+
#syncInput() {
|
|
269
|
+
const input = this.#inputEl;
|
|
270
|
+
if (!input || !this.#sendEl)
|
|
271
|
+
return;
|
|
272
|
+
input.style.height = "auto";
|
|
273
|
+
input.style.height = `${input.scrollHeight}px`;
|
|
274
|
+
this.#sendEl.disabled = this.#pending || input.value.trim().length === 0;
|
|
275
|
+
}
|
|
276
|
+
#setPending(pending) {
|
|
277
|
+
this.#pending = pending;
|
|
278
|
+
this.#syncInput();
|
|
279
|
+
if (pending && this.#messagesEl) {
|
|
280
|
+
const typing = element("div", "typing");
|
|
281
|
+
typing.setAttribute("role", "status");
|
|
282
|
+
typing.setAttribute("aria-label", this.#text.typingLabel);
|
|
283
|
+
typing.append(element("span"), element("span"), element("span"));
|
|
284
|
+
this.#messagesEl.appendChild(typing);
|
|
285
|
+
this.#typingEl = typing;
|
|
286
|
+
this.#scrollToEnd();
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.#typingEl?.remove();
|
|
290
|
+
this.#typingEl = undefined;
|
|
291
|
+
}
|
|
106
292
|
}
|
|
107
293
|
async #sendMessage(rawText) {
|
|
108
294
|
const text = rawText.trim();
|
|
109
|
-
if (!text)
|
|
295
|
+
if (!text || this.#pending)
|
|
110
296
|
return;
|
|
297
|
+
this.#suggestionsEl?.remove();
|
|
298
|
+
this.#suggestionsEl = undefined;
|
|
111
299
|
this.#appendMessage({ role: "user", text });
|
|
112
300
|
const endpoint = this.getAttribute("endpoint");
|
|
113
301
|
if (!endpoint) {
|
|
114
302
|
this.#appendMessage({ role: "assistant", text: this.#text.configErrorText });
|
|
115
303
|
return;
|
|
116
304
|
}
|
|
305
|
+
this.#setPending(true);
|
|
306
|
+
let answer;
|
|
117
307
|
try {
|
|
118
308
|
const response = await fetch(endpoint, {
|
|
119
309
|
method: "POST",
|
|
@@ -121,27 +311,26 @@ export class FolioAgentWidgetElement extends HTMLElement {
|
|
|
121
311
|
body: JSON.stringify({ message: text }),
|
|
122
312
|
});
|
|
123
313
|
const body = (await response.json());
|
|
124
|
-
this.#
|
|
125
|
-
role: "assistant",
|
|
126
|
-
text: body.answer ?? this.#text.answerFallbackText,
|
|
127
|
-
});
|
|
314
|
+
answer = body.answer ?? this.#text.answerFallbackText;
|
|
128
315
|
}
|
|
129
316
|
catch {
|
|
130
|
-
this.#
|
|
131
|
-
role: "assistant",
|
|
132
|
-
text: this.#text.networkErrorText,
|
|
133
|
-
});
|
|
317
|
+
answer = this.#text.networkErrorText;
|
|
134
318
|
}
|
|
319
|
+
this.#setPending(false);
|
|
320
|
+
this.#appendMessage({ role: "assistant", text: answer });
|
|
135
321
|
}
|
|
136
322
|
#appendMessage(message) {
|
|
137
323
|
this.#messages.push(message);
|
|
138
324
|
if (!this.#messagesEl)
|
|
139
325
|
return;
|
|
140
|
-
const el =
|
|
141
|
-
el.className = `message ${message.role}`;
|
|
326
|
+
const el = element("div", `message ${message.role}`);
|
|
142
327
|
el.textContent = message.text;
|
|
143
328
|
this.#messagesEl.appendChild(el);
|
|
144
|
-
this.#
|
|
329
|
+
this.#scrollToEnd();
|
|
330
|
+
}
|
|
331
|
+
#scrollToEnd() {
|
|
332
|
+
if (this.#messagesEl)
|
|
333
|
+
this.#messagesEl.scrollTop = this.#messagesEl.scrollHeight;
|
|
145
334
|
}
|
|
146
335
|
}
|
|
147
336
|
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.
|
|
3
|
+
"version": "0.5.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.
|
|
35
|
+
"happy-dom": "^20.14.0"
|
|
36
36
|
}
|
|
37
37
|
}
|