@folio-agent/widget 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ # @folio-agent/widget
2
+
3
+ ポートフォリオ受付チャットボットのフロントエンド Web Component。クリックされるまで沈黙し、フレームワーク非依存(Shadow DOM)で導入先の CSS と衝突しない。
4
+
5
+ `@folio-agent/handler` とセットで使う。詳細(アーキテクチャ・設計判断・利用手順全体)はリポジトリの [README](https://github.com/yktsnet/folio-agent#readme) を参照。
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npm install @folio-agent/widget
11
+ ```
12
+
13
+ ```html
14
+ <folio-agent-widget endpoint="/api/chat" policy-href="/data-policy"></folio-agent-widget>
15
+ <script type="module">
16
+ import { defineFolioAgentWidget } from "@folio-agent/widget";
17
+ defineFolioAgentWidget();
18
+ </script>
19
+ ```
20
+
21
+ 配色・フォントは CSS カスタムプロパティ6トークン(`--folio-agent-surface` / `text` / `muted` / `accent` / `accent-contrast` / `font`)で上書きできる。未指定時は既定デザインのまま動く。
@@ -0,0 +1,2 @@
1
+ export { FolioAgentWidgetElement, defineFolioAgentWidget } from "./widget-element.js";
2
+ export type { ChatMessage, ChatResponseBody } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { FolioAgentWidgetElement, defineFolioAgentWidget } from "./widget-element.js";
@@ -0,0 +1 @@
1
+ export declare const WIDGET_STYLES = "\n :host {\n all: initial;\n font-family: var(--folio-agent-font, system-ui, sans-serif);\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, #1f2937);\n color: var(--folio-agent-accent-contrast, #fff);\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, #fff);\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: 12px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n font-size: 14px;\n line-height: 1.5;\n color: var(--folio-agent-text, #111827);\n }\n .message.assistant {\n align-self: flex-start;\n background: var(--folio-agent-muted, #f3f4f6);\n border-radius: 8px;\n padding: 6px 10px;\n }\n .message.user {\n align-self: flex-end;\n background: var(--folio-agent-accent, #1f2937);\n color: var(--folio-agent-accent-contrast, #fff);\n border-radius: 8px;\n padding: 6px 10px;\n }\n .disclosure {\n font-size: 12px;\n color: var(--folio-agent-muted, #6b7280);\n padding: 0 0 4px;\n }\n .disclosure a {\n color: #2563eb;\n }\n form {\n display: flex;\n border-top: 1px solid #e5e7eb;\n }\n input {\n flex: 1;\n border: none;\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, #1f2937);\n font-weight: 600;\n }\n";
package/dist/styles.js ADDED
@@ -0,0 +1,95 @@
1
+ // テーマ注入: 利用側サイトのグローバルCSSで
2
+ // folio-agent-widget { --folio-agent-accent: #...; }
3
+ // のようにカスタムプロパティを指定するとShadow DOM内に継承されテーマが反映される。
4
+ // 未指定の場合は各プロパティのフォールバック値(現行デザイン)がそのまま使われる。
5
+ export const WIDGET_STYLES = `
6
+ :host {
7
+ all: initial;
8
+ font-family: var(--folio-agent-font, system-ui, sans-serif);
9
+ }
10
+ .toggle {
11
+ position: fixed;
12
+ right: 20px;
13
+ bottom: 20px;
14
+ width: 56px;
15
+ height: 56px;
16
+ border-radius: 50%;
17
+ border: none;
18
+ background: var(--folio-agent-accent, #1f2937);
19
+ color: var(--folio-agent-accent-contrast, #fff);
20
+ font-size: 24px;
21
+ cursor: pointer;
22
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
23
+ z-index: 2147483000;
24
+ }
25
+ .panel {
26
+ position: fixed;
27
+ right: 20px;
28
+ bottom: 88px;
29
+ width: 320px;
30
+ max-height: 420px;
31
+ display: flex;
32
+ flex-direction: column;
33
+ background: var(--folio-agent-surface, #fff);
34
+ border-radius: 12px;
35
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
36
+ overflow: hidden;
37
+ z-index: 2147483000;
38
+ }
39
+ .panel[hidden] {
40
+ display: none;
41
+ }
42
+ .messages {
43
+ flex: 1;
44
+ overflow-y: auto;
45
+ padding: 12px;
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: 8px;
49
+ font-size: 14px;
50
+ line-height: 1.5;
51
+ color: var(--folio-agent-text, #111827);
52
+ }
53
+ .message.assistant {
54
+ align-self: flex-start;
55
+ background: var(--folio-agent-muted, #f3f4f6);
56
+ border-radius: 8px;
57
+ padding: 6px 10px;
58
+ }
59
+ .message.user {
60
+ align-self: flex-end;
61
+ background: var(--folio-agent-accent, #1f2937);
62
+ color: var(--folio-agent-accent-contrast, #fff);
63
+ border-radius: 8px;
64
+ padding: 6px 10px;
65
+ }
66
+ .disclosure {
67
+ font-size: 12px;
68
+ color: var(--folio-agent-muted, #6b7280);
69
+ padding: 0 0 4px;
70
+ }
71
+ .disclosure a {
72
+ color: #2563eb;
73
+ }
74
+ form {
75
+ display: flex;
76
+ border-top: 1px solid #e5e7eb;
77
+ }
78
+ input {
79
+ flex: 1;
80
+ border: none;
81
+ padding: 10px;
82
+ font-size: 14px;
83
+ }
84
+ input:focus {
85
+ outline: none;
86
+ }
87
+ button[type="submit"] {
88
+ border: none;
89
+ background: none;
90
+ padding: 0 14px;
91
+ cursor: pointer;
92
+ color: var(--folio-agent-accent, #1f2937);
93
+ font-weight: 600;
94
+ }
95
+ `;
@@ -0,0 +1,8 @@
1
+ export interface ChatMessage {
2
+ role: "user" | "assistant";
3
+ text: string;
4
+ }
5
+ export interface ChatResponseBody {
6
+ answer?: string;
7
+ route?: string;
8
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare class FolioAgentWidgetElement extends HTMLElement {
2
+ #private;
3
+ static readonly tagName = "folio-agent-widget";
4
+ connectedCallback(): void;
5
+ }
6
+ export declare function defineFolioAgentWidget(tagName?: string): void;
@@ -0,0 +1,125 @@
1
+ import { WIDGET_STYLES } from "./styles.js";
2
+ const DISCLOSURE_TEXT = "入力内容は品質改善のため記録されます。";
3
+ export class FolioAgentWidgetElement extends HTMLElement {
4
+ static tagName = "folio-agent-widget";
5
+ #open = false;
6
+ #hasShownDisclosure = false;
7
+ #messages = [];
8
+ #panelEl;
9
+ #messagesEl;
10
+ #inputEl;
11
+ connectedCallback() {
12
+ const root = this.attachShadow({ mode: "open" });
13
+ const style = document.createElement("style");
14
+ style.textContent = WIDGET_STYLES;
15
+ root.appendChild(style);
16
+ const toggle = document.createElement("button");
17
+ toggle.className = "toggle";
18
+ toggle.type = "button";
19
+ toggle.setAttribute("aria-label", "チャットを開く");
20
+ toggle.textContent = "💬";
21
+ toggle.addEventListener("click", () => this.#togglePanel());
22
+ root.appendChild(toggle);
23
+ const panel = document.createElement("div");
24
+ panel.className = "panel";
25
+ panel.hidden = true;
26
+ const messages = document.createElement("div");
27
+ messages.className = "messages";
28
+ panel.appendChild(messages);
29
+ const form = document.createElement("form");
30
+ const input = document.createElement("input");
31
+ input.type = "text";
32
+ input.placeholder = "メッセージを入力";
33
+ input.autocomplete = "off";
34
+ const submit = document.createElement("button");
35
+ submit.type = "submit";
36
+ submit.textContent = "送信";
37
+ form.appendChild(input);
38
+ form.appendChild(submit);
39
+ form.addEventListener("submit", (event) => {
40
+ event.preventDefault();
41
+ const text = input.value;
42
+ input.value = "";
43
+ void this.#sendMessage(text);
44
+ });
45
+ panel.appendChild(form);
46
+ root.appendChild(panel);
47
+ this.#panelEl = panel;
48
+ this.#messagesEl = messages;
49
+ this.#inputEl = input;
50
+ }
51
+ #togglePanel() {
52
+ this.#open = !this.#open;
53
+ if (!this.#panelEl)
54
+ return;
55
+ this.#panelEl.hidden = !this.#open;
56
+ if (this.#open && !this.#hasShownDisclosure) {
57
+ this.#hasShownDisclosure = true;
58
+ this.#renderDisclosure();
59
+ }
60
+ if (this.#open) {
61
+ this.#inputEl?.focus();
62
+ }
63
+ }
64
+ #renderDisclosure() {
65
+ if (!this.#messagesEl)
66
+ return;
67
+ const disclosure = document.createElement("div");
68
+ disclosure.className = "disclosure";
69
+ disclosure.append(`${DISCLOSURE_TEXT} `);
70
+ const policyHref = this.getAttribute("policy-href");
71
+ if (policyHref) {
72
+ const link = document.createElement("a");
73
+ link.href = policyHref;
74
+ link.target = "_blank";
75
+ link.rel = "noopener noreferrer";
76
+ link.textContent = "利用データの扱いについて";
77
+ disclosure.appendChild(link);
78
+ }
79
+ this.#messagesEl.appendChild(disclosure);
80
+ }
81
+ async #sendMessage(rawText) {
82
+ const text = rawText.trim();
83
+ if (!text)
84
+ return;
85
+ this.#appendMessage({ role: "user", text });
86
+ const endpoint = this.getAttribute("endpoint");
87
+ if (!endpoint) {
88
+ this.#appendMessage({ role: "assistant", text: "設定エラー: endpoint属性が指定されていません。" });
89
+ return;
90
+ }
91
+ try {
92
+ const response = await fetch(endpoint, {
93
+ method: "POST",
94
+ headers: { "content-type": "application/json" },
95
+ body: JSON.stringify({ message: text }),
96
+ });
97
+ const body = (await response.json());
98
+ this.#appendMessage({
99
+ role: "assistant",
100
+ text: body.answer ?? "回答を取得できませんでした。しばらくしてから再度お試しください。",
101
+ });
102
+ }
103
+ catch {
104
+ this.#appendMessage({
105
+ role: "assistant",
106
+ text: "通信エラーが発生しました。しばらくしてから再度お試しください。",
107
+ });
108
+ }
109
+ }
110
+ #appendMessage(message) {
111
+ this.#messages.push(message);
112
+ if (!this.#messagesEl)
113
+ return;
114
+ const el = document.createElement("div");
115
+ el.className = `message ${message.role}`;
116
+ el.textContent = message.text;
117
+ this.#messagesEl.appendChild(el);
118
+ this.#messagesEl.scrollTop = this.#messagesEl.scrollHeight;
119
+ }
120
+ }
121
+ export function defineFolioAgentWidget(tagName = FolioAgentWidgetElement.tagName) {
122
+ if (!customElements.get(tagName)) {
123
+ customElements.define(tagName, FolioAgentWidgetElement);
124
+ }
125
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@folio-agent/widget",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic reception chat widget (silent until clicked)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/yktsnet/folio-agent.git",
15
+ "directory": "packages/widget"
16
+ },
17
+ "homepage": "https://github.com/yktsnet/folio-agent#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/yktsnet/folio-agent/issues"
20
+ },
21
+ "keywords": [
22
+ "chatbot",
23
+ "web-components",
24
+ "chat-widget",
25
+ "shadow-dom"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -b"
32
+ },
33
+ "devDependencies": {
34
+ "happy-dom": "^20.10.6"
35
+ }
36
+ }