@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 +2 -2
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +320 -56
- package/dist/widget-element.d.ts +1 -0
- package/dist/widget-element.js +297 -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 /* \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
|
-
//
|
|
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:
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
border
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
173
|
+
overscroll-behavior: contain;
|
|
174
|
+
padding: 18px 18px 8px;
|
|
49
175
|
display: flex;
|
|
50
176
|
flex-direction: column;
|
|
51
|
-
gap:
|
|
52
|
-
font-size:
|
|
53
|
-
line-height: 1.
|
|
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
|
-
|
|
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;
|
|
187
|
+
.message.assistant,
|
|
188
|
+
.greeting {
|
|
189
|
+
align-self: stretch;
|
|
65
190
|
}
|
|
66
191
|
.message.user {
|
|
67
192
|
align-self: flex-end;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
border-radius:
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
+
textarea {
|
|
86
265
|
flex: 1;
|
|
87
|
-
|
|
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:
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
279
|
+
textarea::placeholder {
|
|
280
|
+
color: var(--_muted);
|
|
95
281
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
`;
|
package/dist/widget-element.d.ts
CHANGED
package/dist/widget-element.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
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
|
+
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
|
-
|
|
15
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
43
|
-
|
|
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-
|
|
46
|
-
toggle.
|
|
47
|
-
toggle.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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.#
|
|
146
|
+
this.#renderIntro();
|
|
147
|
+
return panel;
|
|
76
148
|
}
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
if (!this.#panelEl)
|
|
149
|
+
#renderIntro() {
|
|
150
|
+
if (!this.#messagesEl)
|
|
80
151
|
return;
|
|
81
|
-
this
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
#
|
|
162
|
+
#resetConversation() {
|
|
91
163
|
if (!this.#messagesEl)
|
|
92
164
|
return;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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 =
|
|
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.#
|
|
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.
|
|
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.
|
|
35
|
+
"happy-dom": "^20.14.5"
|
|
36
36
|
}
|
|
37
37
|
}
|