@hidemikimura/chit-ui 0.3.1 → 0.3.2
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/CHANGELOG.md +13 -0
- package/README.md +13 -2
- package/package.json +2 -1
- package/skills/README.md +34 -0
- package/skills/chit-ui/SKILL.md +148 -0
- package/skills/chit-ui/references/api.md +189 -0
- package/skills/chit-ui/references/recipes.md +204 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
バージョンは [Semantic Versioning](https://semver.org/lang/ja/) に従いますが、0.x の間は
|
|
5
5
|
マイナー更新に破壊的変更が入ることがあります。
|
|
6
6
|
|
|
7
|
+
## 0.3.2 — 2026-09-20
|
|
8
|
+
|
|
9
|
+
### 追加
|
|
10
|
+
|
|
11
|
+
- **AI コーディング支援向けの説明書を同梱しました**(`skills/chit-ui/`)。Claude Code などに
|
|
12
|
+
読ませると、`messages` の所有者が利用者側であること、`push` では描画されないこと、
|
|
13
|
+
`text` と `html` の使い分けといった、このライブラリの癖を踏まえたコードになります。
|
|
14
|
+
npm パッケージにも入っているので、`node_modules/@hidemikimura/chit-ui/skills/chit-ui/` から
|
|
15
|
+
そのまま参照できます。
|
|
16
|
+
- **ドキュメントサイトを用意しました**(`site/`)。ビルド工程のない素の HTML/CSS/JS で、
|
|
17
|
+
ページの上で本物のウィジェットが動きます。GitHub Pages へ上げるワークフローも
|
|
18
|
+
同梱しています(npm パッケージには含めていません)。
|
|
19
|
+
|
|
7
20
|
## 0.3.1 — 2026-09-20
|
|
8
21
|
|
|
9
22
|
### 追加
|
package/README.md
CHANGED
|
@@ -8,7 +8,11 @@ JavaScript ライブラリです。通信も会話ロジックも、発言の中
|
|
|
8
8
|
- 閉じた状態(ランチャー)・開いた状態(パネル)・非表示の 3 状態
|
|
9
9
|
- 状態ごとのテーマ。閉じた状態は PC とスマホで別々に指定できます
|
|
10
10
|
- 発言の中身は HTML 文字列でも Lit テンプレートでも `LitElement` を継承したコンポーネントでも可
|
|
11
|
-
- 依存は Lit のみ。単一バンドルは gzip 約
|
|
11
|
+
- 依存は Lit のみ。単一バンドルは gzip 約 26 KB
|
|
12
|
+
|
|
13
|
+
ドキュメントサイト(人間向け)は [`site/`](site/) にあります。`npm run dev` のあと
|
|
14
|
+
`/site/` を開くとその場で読めます。AI コーディング支援向けの説明書は
|
|
15
|
+
[`skills/chit-ui/`](skills/chit-ui/) にあり、npm パッケージにも同梱しています。
|
|
12
16
|
|
|
13
17
|
## はじめに
|
|
14
18
|
|
|
@@ -731,7 +735,7 @@ chat.addEventListener('chat-message-click', (event) => {
|
|
|
731
735
|
- ストリーミング中の発言は `aria-busy="true"` なので、1 文字ずつ読み上げられません
|
|
732
736
|
- `prefers-reduced-motion` を尊重します
|
|
733
737
|
- 既定テーマの文字色と背景色の組み合わせはすべて WCAG AA(4.5:1)以上です
|
|
734
|
-
- `npm run audit:a11y` で axe-core による監査を実行できます(
|
|
738
|
+
- `npm run audit:a11y` で axe-core による監査を実行できます(10 状態、違反 0 を維持)
|
|
735
739
|
|
|
736
740
|
## ブラウザ
|
|
737
741
|
|
|
@@ -752,6 +756,13 @@ npm run build # 型定義と単一バンドルを dist/ に出力
|
|
|
752
756
|
npm run check # 上記をまとめて
|
|
753
757
|
```
|
|
754
758
|
|
|
759
|
+
### ドキュメントと skill
|
|
760
|
+
|
|
761
|
+
API を変えたら、`README.md`・`site/`(ドキュメントサイト)・`skills/`(AI 向け)の 3 つを
|
|
762
|
+
一緒に更新します。サイトはビルド工程を持たないので、`site/` の HTML を直せばそれで終わりです。
|
|
763
|
+
公開は `.github/workflows/pages.yml` が `site/` を GitHub Pages へ上げます
|
|
764
|
+
(Settings → Pages → Source を「GitHub Actions」にしておく)。
|
|
765
|
+
|
|
755
766
|
### リリース前の手動チェック
|
|
756
767
|
|
|
757
768
|
IME の実挙動は合成イベントでは再現しきれないため、リリース前に実機で確認します。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hidemikimura/chit-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Lit-based chat widget UI: launcher, panel, themable, any HTML or Lit component as a message",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hidemi Kimura",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"src",
|
|
28
28
|
"dist",
|
|
29
|
+
"skills",
|
|
29
30
|
"README.md",
|
|
30
31
|
"CHANGELOG.md",
|
|
31
32
|
"LICENSE"
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# AI 向けの skill
|
|
2
|
+
|
|
3
|
+
`chit-ui/` は、Chit UI を組み込む人の AI コーディング支援(Claude Code、Cursor、
|
|
4
|
+
その他 skill を読めるツール)向けの説明書です。npm パッケージにも同梱しているので、
|
|
5
|
+
`node_modules/@hidemikimura/chit-ui/skills/chit-ui/` からそのまま参照できます。
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
chit-ui/
|
|
9
|
+
├── SKILL.md 境界・最小の組み込み・間違えやすいところ
|
|
10
|
+
└── references/
|
|
11
|
+
├── api.md 全プロパティ・イベント・スロット・パーツ・CSS 変数・テーマ
|
|
12
|
+
└── recipes.md 用途別の書き方(ストリーミング、シナリオ型、添付、LIFF ほか)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 使い方
|
|
16
|
+
|
|
17
|
+
Claude Code なら、プロジェクトの `.claude/skills/` に置くか、シンボリックリンクを張ります。
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
mkdir -p .claude/skills
|
|
21
|
+
ln -s ../../node_modules/@hidemikimura/chit-ui/skills/chit-ui .claude/skills/chit-ui
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
skill を読み込まないツールでも、`SKILL.md` と `references/` を渡せばそのまま使えます。
|
|
25
|
+
|
|
26
|
+
## 書くときの約束
|
|
27
|
+
|
|
28
|
+
人間向けの説明は README.md とドキュメントサイト(`site/`)が正本で、ここはその要約では
|
|
29
|
+
ありません。**AI が間違えやすいところ**を優先して書きます。`messages` の所有者が利用者側で
|
|
30
|
+
あること、`push` では描画されないこと、`text` と `html` の使い分け、`typing` と `loading`
|
|
31
|
+
が排他であること、といった「知らないと自然に踏む地雷」が中心です。
|
|
32
|
+
|
|
33
|
+
API を変えたら、README.md・`site/`・`skills/` の 3 つを一緒に更新してください。
|
|
34
|
+
`references/api.md` の冒頭には対象バージョンを書いています。
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chit-ui
|
|
3
|
+
description: Chit UI(@hidemikimura/chit-ui)でチャットウィジェットを組み込む・直すときに必ず使う。Lit 製のチャット UI を Web ページやアプリに載せる、吹き出しやランチャーの見た目を変える、発言にカスタムコンポーネントを差し込む、送信イベントをバックエンドにつなぐ、`<chit-ui>` というタグや chat-submit / chat-attach / chat-move といったイベントがコードに出てきた、といった場面で参照する。チャットウィジェット・チャット UI・問い合わせウィジェットを新しく作る相談にも使う。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Chit UI
|
|
7
|
+
|
|
8
|
+
Lit で書かれたチャットウィジェットのカスタム要素。ページの隅に丸いランチャーを置き、
|
|
9
|
+
押すとパネルが開く。スマホ幅では全画面になる。
|
|
10
|
+
|
|
11
|
+
**このライブラリが持たないもの**: 通信、状態管理、発言の保存。`messages` 配列の所有者は
|
|
12
|
+
利用者側で、ライブラリはそれを読んで描くだけ。書き換えることは一度もない。送信も
|
|
13
|
+
「送信された」というイベントを出すところまでで、そこから先は利用者のコードが決める。
|
|
14
|
+
この境界を踏み外した設計(ライブラリが勝手に発言を足す、通信を始める)にしないこと。
|
|
15
|
+
|
|
16
|
+
## 最小の組み込み
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@hidemikimura/chit-ui/dist/chit-ui.min.js"></script>
|
|
20
|
+
<chit-ui id="chat"></chit-ui>
|
|
21
|
+
|
|
22
|
+
<script type="module">
|
|
23
|
+
const chat = document.getElementById('chat');
|
|
24
|
+
|
|
25
|
+
chat.addEventListener('chat-submit', async (event) => {
|
|
26
|
+
const text = event.detail.text;
|
|
27
|
+
|
|
28
|
+
chat.messages = [...chat.messages, { id: crypto.randomUUID(), role: 'user', text }];
|
|
29
|
+
chat.busy = true;
|
|
30
|
+
chat.typing = true;
|
|
31
|
+
|
|
32
|
+
const reply = await yourBackend(text);
|
|
33
|
+
|
|
34
|
+
chat.typing = false;
|
|
35
|
+
chat.busy = false;
|
|
36
|
+
chat.messages = [...chat.messages,
|
|
37
|
+
{ id: crypto.randomUUID(), role: 'assistant', text: reply }];
|
|
38
|
+
});
|
|
39
|
+
</script>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
npm から使うときは `npm install @hidemikimura/chit-ui lit` して `import '@hidemikimura/chit-ui';`。
|
|
43
|
+
タグ名を自分で決めるなら `import { ChitUI } from '@hidemikimura/chit-ui/element.js';` を
|
|
44
|
+
使って `customElements.define()` する。型は
|
|
45
|
+
`import type { Message, Theme } from '@hidemikimura/chit-ui';`。
|
|
46
|
+
|
|
47
|
+
**npm 版と単一バンドル(dist/chit-ui.min.js)を同じページで混ぜない。** Lit が二重に
|
|
48
|
+
読み込まれる。`window.ChitUI` を作る IIFE 版(`dist/chit-ui.iife.min.js`)もある。
|
|
49
|
+
|
|
50
|
+
## 間違えやすいところ
|
|
51
|
+
|
|
52
|
+
**`messages` は必ず新しい配列を代入する。** `chat.messages.push(...)` は描画されない
|
|
53
|
+
(Lit が同一参照を変更なしとみなす)。`[...chat.messages, next]` の形で代入する。
|
|
54
|
+
|
|
55
|
+
**`id` は必須で、安定していること。** 差分描画のキーになる。毎回採番し直すと全部の
|
|
56
|
+
吹き出しが作り直され、コンポーネント発言のインスタンスも捨てられる。
|
|
57
|
+
|
|
58
|
+
**人が書いた文章は `text`、自分で組み立てた HTML だけ `html`。** `text` は Lit が
|
|
59
|
+
テキストノードとして描くのでエスケープが構造的に保証され、改行もそのまま出る。
|
|
60
|
+
`html` を使うなら `chat.sanitize = (html) => DOMPurify.sanitize(html)` を必ず設定する。
|
|
61
|
+
バックエンドや LLM の出力をそのまま `html` に入れるのは事故のもと。
|
|
62
|
+
|
|
63
|
+
**`typing` と `loading` は排他。** 一方を `true` にすると他方が `false` になる。
|
|
64
|
+
相手が機械なら「入力中」より `loading`(スピナーや文言)が正しい。
|
|
65
|
+
|
|
66
|
+
**`chat-submit` は中止できる。** `event.preventDefault()` すると入力欄は空にならず、
|
|
67
|
+
`open.loading.auto` の自動ローディングも始まらない。
|
|
68
|
+
|
|
69
|
+
**スマホ幅では `open.width` / `open.height` は無視される**(常に全画面)。
|
|
70
|
+
`open.draggable` もスマホでは効かない。
|
|
71
|
+
|
|
72
|
+
**`open.header.visible: false` にすると閉じるボタンも消える。** LINE LIFF のように
|
|
73
|
+
外側がタイトルを持つ画面向け。Esc とランチャーでは閉じられる。
|
|
74
|
+
|
|
75
|
+
## 発言の 5 形式
|
|
76
|
+
|
|
77
|
+
`text` / `html` / `template`(Lit の `TemplateResult`)/ `element`(`HTMLElement`)/
|
|
78
|
+
`component`(コンストラクタかタグ名 + `props`)のいずれか 1 つを持たせる。
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
{ id: 'x', role: 'assistant', component: 'order-card', props: { orderId: '1001' } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`component` は同じ `id` である限りインスタンスが再利用され、変わった `props` だけが
|
|
85
|
+
再代入される。発言の中のボタンは個別にリスナーを付けるより
|
|
86
|
+
`chat-message-click`(`detail: { message, target, originalEvent }`)で一括して受けるほうが楽。
|
|
87
|
+
`target` はコンポーネントの Shadow DOM 内まで辿れている。
|
|
88
|
+
|
|
89
|
+
`role` は `'user'` / `'assistant'` / `'system'`。`system` は中央寄せの連絡行で、
|
|
90
|
+
名前もアイコンも出ない。
|
|
91
|
+
|
|
92
|
+
## テーマ
|
|
93
|
+
|
|
94
|
+
部分指定でよく、指定しなかった項目は既定値で埋まる。`null` を渡すと既定値を消せる。
|
|
95
|
+
実行中に差し替えれば即反映される(ダークモードの切り替えなど)。
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
chat.theme = {
|
|
99
|
+
breakpoint: 768,
|
|
100
|
+
closed: { pc: { size: 64, image: '/icon.png' }, mobile: { size: 56 }, draggable: true },
|
|
101
|
+
open: {
|
|
102
|
+
width: 380, height: 600,
|
|
103
|
+
colors: { accent: '#2563eb' },
|
|
104
|
+
bubble: { radius: 18, tail: 'top' },
|
|
105
|
+
header: { title: 'サポート', logo: '/logo.png', home: true },
|
|
106
|
+
loading: { auto: true, style: 'spinner' },
|
|
107
|
+
input: { attach: true, placeholder: 'ご質問をどうぞ' },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
付属テーマは `import { greenTheme } from '@hidemikimura/chit-ui/themes.js'`
|
|
113
|
+
(単一バンドルなら `window.ChitUI.greenTheme`)。重ねて調整できる。
|
|
114
|
+
|
|
115
|
+
細かい色だけ変えたいなら CSS のほうが早い。テーマの値は CSS カスタムプロパティになり、
|
|
116
|
+
ページ側の指定が勝つ。
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
chit-ui { --chit-color-accent: #d81b60; --chit-launcher-size: 72px; }
|
|
120
|
+
chit-ui::part(bubble) { font-size: 15px; }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--_chit-` で始まる変数は内部用。触らない。
|
|
124
|
+
|
|
125
|
+
## よく使う API
|
|
126
|
+
|
|
127
|
+
| 種類 | もの |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| プロパティ | `messages` `state` `theme` `typing` `loading` `busy` `value` `inputDisabled` `inputHidden` `maxLength` `sanitize` `locale` `labels` `dragOffset` |
|
|
130
|
+
| メソッド | `open()` `close()` `toggle()` `submit(text?)` `focusInput()` `scrollToBottom()` `getMessageElement(id)` `home()` `openAttach()` `resetPosition()` |
|
|
131
|
+
| イベント | `chat-submit` `chat-open` `chat-close` `chat-state-change` `chat-input` `chat-message-click` `chat-message-render` `chat-scroll-top` `chat-home` `chat-attach` `chat-move` `chat-breakpoint-change` |
|
|
132
|
+
|
|
133
|
+
状態遷移のメソッドはアニメーション完了で resolve する Promise を返す(中止されたら `false`)。
|
|
134
|
+
すべてのイベントは `bubbles` と `composed` が立っているので `document` でも拾える。
|
|
135
|
+
|
|
136
|
+
網羅した表(全プロパティ・全イベントの `detail`・スロット・CSS パーツ・CSS 変数・
|
|
137
|
+
テーマの全項目)は `references/api.md`。用途別の書き方(ストリーミング、シナリオ型、
|
|
138
|
+
未読バッジ、LIFF、添付の送信)は `references/recipes.md`。
|
|
139
|
+
|
|
140
|
+
## 作るときの順序
|
|
141
|
+
|
|
142
|
+
1. `chat-submit` を受けて自分の発言を積み、返事を積むところまで作る。ここが本体。
|
|
143
|
+
2. 待っている間の表示を決める(`busy` + `loading`、または `open.loading.auto`)。
|
|
144
|
+
3. 見た目はテーマ、細部は CSS カスタムプロパティと `::part()`。
|
|
145
|
+
4. 必要なら発言の中身を `component` にして、操作は `chat-message-click` で受ける。
|
|
146
|
+
|
|
147
|
+
会話の保存・復元、未読管理、ログインの有無といった判断はすべて利用者側のコードに置く。
|
|
148
|
+
ライブラリに寄せようとしないこと。
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Chit UI API リファレンス
|
|
2
|
+
|
|
3
|
+
`@hidemikimura/chit-ui` 0.3.2 時点。パーツ名・プロパティ名・イベント名・スロット名・
|
|
4
|
+
CSS カスタムプロパティ名は公開 API で、変更はメジャーバージョンでのみ行う。
|
|
5
|
+
|
|
6
|
+
## プロパティ
|
|
7
|
+
|
|
8
|
+
| プロパティ | 型 | 既定 | 内容 |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| `state` | `'closed' \| 'open' \| 'hidden'` | `'closed'` | 現在の状態 |
|
|
11
|
+
| `theme` | `Theme` | `{}` | テーマ(部分指定可) |
|
|
12
|
+
| `messages` | `Message[]` | `[]` | 描画する発言。必ず新しい配列を代入する |
|
|
13
|
+
| `typing` | `boolean \| { html }` | `false` | 相手が入力中の表示。`loading` とは排他 |
|
|
14
|
+
| `loading` | `boolean` | `false` | 応答待ちの表示。`typing` とは排他。属性にも反映される |
|
|
15
|
+
| `busy` | `boolean` | `false` | 送信中。入力をロックする |
|
|
16
|
+
| `inputDisabled` | `boolean` | `false` | 入力欄を見せたままロック |
|
|
17
|
+
| `inputHidden` | `boolean` | `false` | 入力欄を消す(`submit(text)` は使える) |
|
|
18
|
+
| `value` | `string` | `''` | 入力欄の内容 |
|
|
19
|
+
| `placeholder` | `string` | テーマの値 | 入力欄のプレースホルダー |
|
|
20
|
+
| `sendOnEnter` | `boolean` | `true` | Enter で送信するか |
|
|
21
|
+
| `maxLength` | `number` | なし | 入力の上限(コードポイント単位) |
|
|
22
|
+
| `focusOnOpen` | `'auto' \| 'always' \| 'never'` | `'auto'` | 開いたときの自動フォーカス。`auto` は PC のみ |
|
|
23
|
+
| `sanitize` | `(html: string) => string` | なし | `html` 発言のサニタイズ |
|
|
24
|
+
| `formatTime` | `(time: Date) => string` | `HH:mm` | 時刻表示 |
|
|
25
|
+
| `messageStyles` | `string` | `''` | 発言の中に適用する追加 CSS |
|
|
26
|
+
| `locale` | `string` | `<html lang>` | ラベルと時刻の言語(`ja` / `en`) |
|
|
27
|
+
| `labels` | `Partial<Labels>` | なし | UI 文字列の上書き |
|
|
28
|
+
| `dragOffset` | `{ x, y } \| null` | `null` | ドラッグで生じたずれ。読み書きできる |
|
|
29
|
+
|
|
30
|
+
読み取り専用: `renderedState` `device`(`'pc' \| 'mobile'`)`hasUnseen` `canSend`
|
|
31
|
+
`currentTheme` `currentLabels` `resolvedLocale`。
|
|
32
|
+
|
|
33
|
+
## メソッド
|
|
34
|
+
|
|
35
|
+
| メソッド | 内容 |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `open()` `close()` `hide()` `show()` `toggle()` | 状態遷移。アニメーション完了で resolve する `Promise<boolean>`(中止なら `false`) |
|
|
38
|
+
| `submit(text?)` | `chat-submit` を発火。省略時は入力欄の内容 |
|
|
39
|
+
| `focusInput()` `clearInput()` | 入力欄の操作 |
|
|
40
|
+
| `scrollToBottom({ smooth })` | 最新へスクロール。省略時はテーマの設定に従う |
|
|
41
|
+
| `getMessageElement(id)` | その発言のコンテナ DOM(未描画なら `null`) |
|
|
42
|
+
| `home()` | `chat-home` を発火(ホームボタンと同じ) |
|
|
43
|
+
| `openAttach()` | 添付のファイル選択を開く |
|
|
44
|
+
| `resetPosition()` | ドラッグで動かした位置を忘れる |
|
|
45
|
+
|
|
46
|
+
## イベント
|
|
47
|
+
|
|
48
|
+
すべて `CustomEvent`。`bubbles` と `composed` が立っているので `document` でも拾える。
|
|
49
|
+
|
|
50
|
+
| イベント | タイミング | `detail` | 中止 |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `chat-submit` | 送信された | `{ text }` | ○ |
|
|
53
|
+
| `chat-before-open` / `chat-before-close` | 遷移の直前 | `{ from, trigger }` | ○ |
|
|
54
|
+
| `chat-open` / `chat-close` | 遷移アニメーション完了 | `{ from, trigger }` | |
|
|
55
|
+
| `chat-hide` / `chat-show` | 非表示になった / 戻った | `{ from, to }` | |
|
|
56
|
+
| `chat-state-change` | `state` が変わった直後 | `{ from, to, trigger }` | |
|
|
57
|
+
| `chat-input` | 入力欄が編集された | `{ value }` | |
|
|
58
|
+
| `chat-message-render` | 発言が描画・更新された | `{ message, element, instance }` | |
|
|
59
|
+
| `chat-message-click` | 発言の中がクリックされた | `{ message, target, originalEvent }` | ○ |
|
|
60
|
+
| `chat-scroll-top` | 一覧が最上部に達した | `{}` | |
|
|
61
|
+
| `chat-breakpoint-change` | PC / スマホの判定が変わった | `{ device }` | |
|
|
62
|
+
| `chat-home` | ホームボタン、または `home()` | `{ trigger }` | |
|
|
63
|
+
| `chat-attach` | 添付ファイルが選ばれた | `{ files: File[] }` | |
|
|
64
|
+
| `chat-move` | ドラッグまたは矢印キー | `{ target, position, offset, displacement }` | |
|
|
65
|
+
|
|
66
|
+
`trigger` は `'user'`(クリックや Esc)か `'api'`(メソッドやプロパティ代入)。
|
|
67
|
+
|
|
68
|
+
## 発言オブジェクト
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
{
|
|
72
|
+
id: string, // 必須・安定していること
|
|
73
|
+
role: 'user' | 'assistant' | 'system',
|
|
74
|
+
name?: string | null, // テーマの既定を打ち消すなら null
|
|
75
|
+
avatar?: string | null,
|
|
76
|
+
time?: Date | string | number,
|
|
77
|
+
status?: 'sending' | 'sent' | 'error',
|
|
78
|
+
streaming?: boolean, // 末尾にカーソル、aria-busy
|
|
79
|
+
meta?: unknown, // 利用者の自由欄。イベントでそのまま返る
|
|
80
|
+
// 以下のいずれか 1 つ
|
|
81
|
+
text?: string,
|
|
82
|
+
html?: string,
|
|
83
|
+
template?: TemplateResult,
|
|
84
|
+
element?: HTMLElement,
|
|
85
|
+
component?: (new () => HTMLElement) | string,
|
|
86
|
+
props?: Record<string, unknown>, // component のとき
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## テーマ
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
{
|
|
94
|
+
breakpoint?: number, // 既定 768。これ未満をスマホとみなす
|
|
95
|
+
closed?: {
|
|
96
|
+
// pc / mobile に分けて書ける。分けなければ両方に同じ設定
|
|
97
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left',
|
|
98
|
+
offset?: { x: number, y: number },
|
|
99
|
+
size?: number | 'auto',
|
|
100
|
+
radius?: number,
|
|
101
|
+
image?: string | null,
|
|
102
|
+
label?: string | null,
|
|
103
|
+
component?: (new () => HTMLElement) | string,
|
|
104
|
+
props?: Record<string, unknown>,
|
|
105
|
+
colors?: { bg?, text?, shadow? },
|
|
106
|
+
draggable?: boolean, // 既定 false
|
|
107
|
+
animation?: { enter?, exit?, duration? },
|
|
108
|
+
},
|
|
109
|
+
open?: {
|
|
110
|
+
width?: number, height?: number, // スマホでは無視(常に全画面)
|
|
111
|
+
position?: Position, offset?: { x, y },
|
|
112
|
+
radius?: number,
|
|
113
|
+
launcher?: 'hidden' | 'visible',
|
|
114
|
+
draggable?: boolean, // 取っ手はタイトルバー。スマホでは無効
|
|
115
|
+
header?: { visible?: boolean, title?: string | null, logo?: string | null, home?: boolean },
|
|
116
|
+
bubble?: { radius?: number, tail?: 'none' | 'top' | 'bottom' },
|
|
117
|
+
speaker?: { assistant?: { name?, avatar? }, user?: { name?, avatar? } },
|
|
118
|
+
loading?: { auto?: boolean, style?: 'dots' | 'spinner' | 'text', text?: string | null, timeout?: number },
|
|
119
|
+
input?: { maxRows?: number, placeholder?: string | null, attach?: boolean, accept?: string, multiple?: boolean },
|
|
120
|
+
colors?: { bg?, text?, accent?, border?, userBubble?, userText?, assistantBubble?,
|
|
121
|
+
assistantText?, systemText?, headerBg?, headerText?, inputBg?, inputText?,
|
|
122
|
+
inputPlaceholder? },
|
|
123
|
+
bgImage?: string | null,
|
|
124
|
+
animation?: { enter?, exit?, duration?, scroll?: 'smooth' | 'instant' },
|
|
125
|
+
},
|
|
126
|
+
hidden?: { animation?: { exit?, duration? } },
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`animation` の `enter` / `exit` は `'fade' | 'scale' | 'slide' | 'none'`。
|
|
131
|
+
|
|
132
|
+
## スロット
|
|
133
|
+
|
|
134
|
+
| スロット | 差し替え対象 |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `launcher` | 閉じた状態のアイコンの中身 |
|
|
137
|
+
| `header` | パネルヘッダー全体(`header-title` / `header-actions` で部分差し替えも可) |
|
|
138
|
+
| `empty` | 発言が 1 件もないときの表示 |
|
|
139
|
+
| `input-before` / `input-after` | 入力欄の前後 |
|
|
140
|
+
| `composer` | 入力欄全体。差し替えたら `submit(text)` を自分で呼ぶ |
|
|
141
|
+
| `footer` | 入力欄の下(免責事項など) |
|
|
142
|
+
|
|
143
|
+
## CSS パーツ
|
|
144
|
+
|
|
145
|
+
`launcher` `launcher-icon` `launcher-label` `panel` `header` `header-title`
|
|
146
|
+
`header-heading` `header-logo` `header-actions` `home-button` `close-button`
|
|
147
|
+
`messages` `messages-inner` `message` `message-user` `message-assistant`
|
|
148
|
+
`message-system` `bubble` `message-content` `avatar` `name` `meta` `time` `status`
|
|
149
|
+
`cursor` `typing` `loading` `loading-text` `to-latest` `composer` `input`
|
|
150
|
+
`attach-button` `attach-input` `counter` `send-button` `spinner`
|
|
151
|
+
|
|
152
|
+
## CSS カスタムプロパティ(33)
|
|
153
|
+
|
|
154
|
+
色: `--chit-color-bg` `--chit-color-text` `--chit-color-accent` `--chit-color-border`
|
|
155
|
+
`--chit-color-user-bg` `--chit-color-user-text` `--chit-color-assistant-bg`
|
|
156
|
+
`--chit-color-assistant-text` `--chit-color-system-text` `--chit-color-header-bg`
|
|
157
|
+
`--chit-color-header-text` `--chit-color-input-bg` `--chit-color-input-text`
|
|
158
|
+
`--chit-color-input-placeholder`
|
|
159
|
+
|
|
160
|
+
ランチャー: `--chit-launcher-size` `--chit-launcher-radius` `--chit-launcher-offset-x`
|
|
161
|
+
`--chit-launcher-offset-y` `--chit-launcher-bg` `--chit-launcher-text`
|
|
162
|
+
`--chit-launcher-shadow` `--chit-launcher-image`
|
|
163
|
+
|
|
164
|
+
吹き出し: `--chit-bubble-radius`
|
|
165
|
+
|
|
166
|
+
パネル: `--chit-panel-width` `--chit-panel-height` `--chit-panel-radius`
|
|
167
|
+
`--chit-panel-offset-x` `--chit-panel-offset-y` `--chit-panel-shadow`
|
|
168
|
+
`--chit-panel-bg-image`
|
|
169
|
+
|
|
170
|
+
全体: `--chit-font-family` `--chit-font-size` `--chit-z-index`
|
|
171
|
+
|
|
172
|
+
アニメーションの長さは CSS 変数にしていない(遷移の完了を JavaScript 側が知る必要が
|
|
173
|
+
あるため、テーマの `animation.duration` が唯一の設定箇所)。
|
|
174
|
+
|
|
175
|
+
## エントリポイント
|
|
176
|
+
|
|
177
|
+
| 指定 | 中身 |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `@hidemikimura/chit-ui` | `<chit-ui>` を登録する。型もここから読める |
|
|
180
|
+
| `@hidemikimura/chit-ui/element.js` | 登録しない。`ChitUI` クラスだけ |
|
|
181
|
+
| `@hidemikimura/chit-ui/themes.js` | 付属テーマ(`greenTheme`) |
|
|
182
|
+
| `dist/chit-ui.min.js` | Lit 同梱の ESM 単一バンドル |
|
|
183
|
+
| `dist/chit-ui.iife.min.js` | 同上の IIFE 版。`window.ChitUI` |
|
|
184
|
+
|
|
185
|
+
## 対応ブラウザ
|
|
186
|
+
|
|
187
|
+
Chrome / Edge / Firefox / Safari の最新 2 バージョンと iOS Safari 16 以降。
|
|
188
|
+
吹き出しのしっぽだけ `clip-path: path()` を使っていて、非対応の環境ではしっぽが出ない
|
|
189
|
+
(他の機能には影響しない)。
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Chit UI レシピ
|
|
2
|
+
|
|
3
|
+
よくある組み方。どれも「発言を持っているのは利用者側」という前提の上に乗っている。
|
|
4
|
+
|
|
5
|
+
## 送信から返事まで(基本形)
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
chat.addEventListener('chat-submit', async (event) => {
|
|
9
|
+
const text = event.detail.text.trim();
|
|
10
|
+
if (!text) { event.preventDefault(); return; }
|
|
11
|
+
|
|
12
|
+
const mine = { id: crypto.randomUUID(), role: 'user', text, status: 'sending' };
|
|
13
|
+
chat.messages = [...chat.messages, mine];
|
|
14
|
+
chat.busy = true;
|
|
15
|
+
chat.loading = true;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const reply = await api.send(text);
|
|
19
|
+
chat.messages = chat.messages
|
|
20
|
+
.map((m) => (m.id === mine.id ? { ...m, status: 'sent' } : m))
|
|
21
|
+
.concat({ id: crypto.randomUUID(), role: 'assistant', text: reply });
|
|
22
|
+
} catch {
|
|
23
|
+
chat.messages = chat.messages.map((m) =>
|
|
24
|
+
m.id === mine.id ? { ...m, status: 'error' } : m,
|
|
25
|
+
);
|
|
26
|
+
chat.messages = [...chat.messages,
|
|
27
|
+
{ id: crypto.randomUUID(), role: 'system', text: '送信できませんでした' }];
|
|
28
|
+
} finally {
|
|
29
|
+
chat.loading = false;
|
|
30
|
+
chat.busy = false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`open.loading.auto: true` にしておけば `chat.loading` の上げ下げは要らない。送信から
|
|
36
|
+
相手側の発言が増えるまで自動で出て、`timeout`(ms)を過ぎれば自分で引っ込む。
|
|
37
|
+
|
|
38
|
+
## ストリーミング
|
|
39
|
+
|
|
40
|
+
同じ `id` の発言を書き換えていく。`streaming: true` の間は末尾にカーソルが出て、
|
|
41
|
+
`aria-busy` が立つので 1 文字ずつ読み上げられない。
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const id = crypto.randomUUID();
|
|
45
|
+
chat.messages = [...chat.messages, { id, role: 'assistant', text: '', streaming: true }];
|
|
46
|
+
|
|
47
|
+
for await (const chunk of stream) {
|
|
48
|
+
chat.messages = chat.messages.map((m) =>
|
|
49
|
+
m.id === id ? { ...m, text: m.text + chunk } : m,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
chat.messages = chat.messages.map((m) =>
|
|
54
|
+
m.id === id ? { ...m, streaming: false } : m,
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
最下部にいる読み手は自動で追従する。上にさかのぼっている読み手は動かされず、
|
|
59
|
+
「最新へ」のボタンが出る。
|
|
60
|
+
|
|
61
|
+
## 履歴の追い読み
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
chat.addEventListener('chat-scroll-top', async () => {
|
|
65
|
+
if (loadingOlder || !cursor) return;
|
|
66
|
+
loadingOlder = true;
|
|
67
|
+
const older = await api.history(cursor);
|
|
68
|
+
chat.messages = [...older, ...chat.messages]; // 前に足す
|
|
69
|
+
cursor = older.at(0)?.cursor ?? null;
|
|
70
|
+
loadingOlder = false;
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
このイベントは最上部に着くたびに 1 回だけ出る(居座っても繰り返さない)。
|
|
75
|
+
|
|
76
|
+
## シナリオ型(ホームボタンで最初に戻す)
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
chat.theme = { open: { header: { home: true } } };
|
|
80
|
+
|
|
81
|
+
chat.addEventListener('chat-home', async (event) => {
|
|
82
|
+
if (event.detail.trigger === 'user' && !confirm('最初からやり直しますか?')) return;
|
|
83
|
+
chat.messages = [firstMessage()];
|
|
84
|
+
scenario.reset();
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
ボタンを押してもウィジェットは何も消さない。どこまで戻すかは利用者が決める。
|
|
89
|
+
|
|
90
|
+
## 発言の中にコンポーネントを置く
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
class OrderCard extends HTMLElement { /* props を受け取って描く */ }
|
|
94
|
+
customElements.define('order-card', OrderCard);
|
|
95
|
+
|
|
96
|
+
chat.messages = [...chat.messages, {
|
|
97
|
+
id: 'order-1001',
|
|
98
|
+
role: 'assistant',
|
|
99
|
+
component: 'order-card',
|
|
100
|
+
props: { orderId: '1001', total: 4800 },
|
|
101
|
+
meta: { orderId: '1001' },
|
|
102
|
+
}];
|
|
103
|
+
|
|
104
|
+
chat.addEventListener('chat-message-click', (event) => {
|
|
105
|
+
const action = event.detail.target.closest('[data-action]')?.dataset.action;
|
|
106
|
+
if (action === 'detail') showOrder(event.detail.message.meta.orderId);
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
同じ `id` の間はインスタンスが再利用され、変わった `props` だけ再代入される。
|
|
111
|
+
`chat-message-render` の `detail.instance` でインスタンスを直接触ることもできる。
|
|
112
|
+
|
|
113
|
+
## 未読バッジ
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<chit-ui id="chat">
|
|
117
|
+
<span slot="launcher" class="badge" hidden></span>
|
|
118
|
+
</chit-ui>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
function pushFromServer(message) {
|
|
123
|
+
chat.messages = [...chat.messages, message];
|
|
124
|
+
if (chat.state !== 'open') {
|
|
125
|
+
unread += 1;
|
|
126
|
+
badge.textContent = String(unread);
|
|
127
|
+
badge.hidden = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
chat.addEventListener('chat-open', () => { unread = 0; badge.hidden = true; });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
パネルの中で上にさかのぼっている読み手に新着が届いたかどうかは `chat.hasUnseen` でわかる。
|
|
135
|
+
|
|
136
|
+
## 添付ファイル
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
chat.theme = { open: { input: { attach: true, accept: 'image/*', multiple: false } } };
|
|
140
|
+
|
|
141
|
+
chat.addEventListener('chat-attach', async (event) => {
|
|
142
|
+
const [file] = event.detail.files;
|
|
143
|
+
const url = await api.upload(file);
|
|
144
|
+
chat.messages = [...chat.messages, {
|
|
145
|
+
id: crypto.randomUUID(), role: 'user',
|
|
146
|
+
html: `<img src="${url}" alt="添付した画像">`,
|
|
147
|
+
}];
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
ライブラリはアップロードもプレビューもしない。同じファイルを続けて選んでも毎回発火する。
|
|
152
|
+
|
|
153
|
+
## LINE LIFF など、外側がタイトルを持つ画面
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
chat.theme = { open: { header: { visible: false } } };
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
タイトルバーごと消えるので閉じるボタンも消える。Esc とランチャーでは閉じられる。
|
|
160
|
+
`title` は描かれなくても `role="dialog"` の読み上げ名には使われる。
|
|
161
|
+
|
|
162
|
+
## ドラッグで動かす
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
chat.theme = { closed: { draggable: true }, open: { draggable: true } };
|
|
166
|
+
|
|
167
|
+
chat.addEventListener('chat-move', (event) => {
|
|
168
|
+
localStorage.setItem('chit-offset', JSON.stringify(event.detail.displacement));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const saved = localStorage.getItem('chit-offset');
|
|
172
|
+
if (saved) chat.dragOffset = JSON.parse(saved);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
閉じた状態と開いた状態は同じずれを共有するので、片方を動かせばもう片方も動く。
|
|
176
|
+
`resetPosition()` でテーマの位置に戻る。保存するかどうかは利用者の判断。
|
|
177
|
+
|
|
178
|
+
## ダークモード
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
const dark = matchMedia('(prefers-color-scheme: dark)');
|
|
182
|
+
const apply = () => { chat.theme = dark.matches ? darkTheme : lightTheme; };
|
|
183
|
+
dark.addEventListener('change', apply);
|
|
184
|
+
apply();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
テーマは実行中に差し替えても即座に反映される。
|
|
188
|
+
|
|
189
|
+
## 入力欄を消して選択肢だけで進める
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
chat.inputHidden = true;
|
|
193
|
+
chat.messages = [...chat.messages, {
|
|
194
|
+
id: 'q1', role: 'assistant',
|
|
195
|
+
html: '<p>どちらにしますか?</p><button data-choice="a">A</button><button data-choice="b">B</button>',
|
|
196
|
+
}];
|
|
197
|
+
|
|
198
|
+
chat.addEventListener('chat-message-click', (event) => {
|
|
199
|
+
const choice = event.detail.target.closest('[data-choice]')?.dataset.choice;
|
|
200
|
+
if (choice) chat.submit(choice === 'a' ? 'A' : 'B');
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
入力欄を消しても `submit(text)` は使えるので、選択肢からの送信で会話を進められる。
|