@haikit/client 0.9.0 → 0.9.1
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 +4 -0
- package/package.json +1 -1
- package/src/app.js +28 -4
- package/src/hai.css +12 -10
- package/src/icons.js +51 -2
package/README.md
CHANGED
|
@@ -20,6 +20,10 @@ property, so override what you like.
|
|
|
20
20
|
|
|
21
21
|
## The default UI
|
|
22
22
|
|
|
23
|
+
The [default UI guide](https://github.com/wfoxd/haikit/blob/main/docs/default-ui.md)
|
|
24
|
+
covers it in full: every option, what each part of the screen does, theming,
|
|
25
|
+
styling your surfaces, and building your own layout. In short:
|
|
26
|
+
|
|
23
27
|
`mountChat` gives you a header with **New chat** and a **Context** button that
|
|
24
28
|
shows how many tokens the model holds, a centred conversation, and a composer
|
|
25
29
|
that grows as you type.
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { createChat } from "./index.js";
|
|
19
19
|
import { renderTranscript, closeTranscript, h } from "./transcript.js";
|
|
20
|
-
import { icon } from "./icons.js";
|
|
20
|
+
import { icon, mark, FAVICON } from "./icons.js";
|
|
21
21
|
|
|
22
22
|
/** Below this width the drawer opens over the chat rather than beside it. */
|
|
23
23
|
const WIDE = "(min-width: 900px)";
|
|
@@ -60,6 +60,28 @@ function setPin(theme) {
|
|
|
60
60
|
pinning.ours = theme;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A page without an icon of its own gets the haikit favicon while a chat is
|
|
65
|
+
* open on it. Every chat shares the one link; the last to close takes it away.
|
|
66
|
+
*/
|
|
67
|
+
const favicon = { chats: 0, link: null };
|
|
68
|
+
|
|
69
|
+
function addFavicon() {
|
|
70
|
+
if (favicon.chats++ || document.querySelector('link[rel~="icon"]')) return;
|
|
71
|
+
const link = document.createElement("link");
|
|
72
|
+
link.rel = "icon";
|
|
73
|
+
link.type = "image/svg+xml";
|
|
74
|
+
link.href = FAVICON;
|
|
75
|
+
document.head.append(link);
|
|
76
|
+
favicon.link = link;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function dropFavicon() {
|
|
80
|
+
if (--favicon.chats) return;
|
|
81
|
+
favicon.link?.remove();
|
|
82
|
+
favicon.link = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
63
85
|
/**
|
|
64
86
|
* @param {{
|
|
65
87
|
* root: HTMLElement,
|
|
@@ -78,7 +100,7 @@ export function mountChat({
|
|
|
78
100
|
root,
|
|
79
101
|
registry,
|
|
80
102
|
endpoint = "/hai",
|
|
81
|
-
title = "
|
|
103
|
+
title = "HaiKIT",
|
|
82
104
|
subtitle = "",
|
|
83
105
|
suggestions = [],
|
|
84
106
|
inspector = true,
|
|
@@ -94,13 +116,14 @@ export function mountChat({
|
|
|
94
116
|
// so a pin on the shell alone would not reach them.
|
|
95
117
|
const pinned = theme === "light" || theme === "dark" ? { theme } : null;
|
|
96
118
|
if (pinned) pin(pinned);
|
|
119
|
+
addFavicon();
|
|
97
120
|
|
|
98
121
|
// ── shell ──────────────────────────────────────────────────────────
|
|
99
122
|
const shell = h("div", "hai-shell");
|
|
100
123
|
|
|
101
124
|
const top = h("header", "hai-top");
|
|
102
125
|
const brand = h("div", "hai-brand");
|
|
103
|
-
brand.append(
|
|
126
|
+
brand.append(mark("small", "hai-logo hai-mark"), h("span", "hai-title", title));
|
|
104
127
|
if (subtitle) brand.append(h("span", "hai-subtitle", subtitle));
|
|
105
128
|
|
|
106
129
|
const modelEl = h("span", "hai-model");
|
|
@@ -256,7 +279,7 @@ export function mountChat({
|
|
|
256
279
|
function emptyState() {
|
|
257
280
|
const empty = h("div", "hai-empty");
|
|
258
281
|
if (!emptyText && !suggestions.length) return empty;
|
|
259
|
-
empty.append(
|
|
282
|
+
empty.append(mark("full", "hai-logo hai-empty-mark"));
|
|
260
283
|
if (emptyText) empty.append(h("p", null, emptyText));
|
|
261
284
|
if (suggestions.length) {
|
|
262
285
|
const row = h("div", "hai-suggestions");
|
|
@@ -310,6 +333,7 @@ export function mountChat({
|
|
|
310
333
|
shell.remove();
|
|
311
334
|
releaseDrawer();
|
|
312
335
|
if (pinned) unpin(pinned);
|
|
336
|
+
dropFavicon();
|
|
313
337
|
return;
|
|
314
338
|
}
|
|
315
339
|
|
package/src/hai.css
CHANGED
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
--hai-user-bg: light-dark(#e7eefb, #1e2835);
|
|
42
42
|
--hai-code-bg: light-dark(#f3f5f8, #0a0d11);
|
|
43
43
|
--hai-shadow: 0 1px 2px light-dark(rgb(15 23 42 / .06), rgb(0 0 0 / .35));
|
|
44
|
+
--hai-logo-ink: light-dark(#0f1115, #f3f2ee);
|
|
45
|
+
--hai-logo-paper: light-dark(#f3f2ee, #0f1115);
|
|
46
|
+
--hai-logo-hot: #ff5a1f;
|
|
44
47
|
|
|
45
48
|
/* tints, derived — override the colour and these follow */
|
|
46
49
|
--hai-line-soft: color-mix(in srgb, var(--hai-line) 60%, transparent);
|
|
@@ -79,6 +82,8 @@
|
|
|
79
82
|
--hai-user-bg: #1e2835;
|
|
80
83
|
--hai-code-bg: #0a0d11;
|
|
81
84
|
--hai-shadow: 0 1px 2px rgb(0 0 0 / .35);
|
|
85
|
+
--hai-logo-ink: #f3f2ee;
|
|
86
|
+
--hai-logo-paper: #0f1115;
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -110,7 +115,12 @@
|
|
|
110
115
|
background: var(--hai-panel);
|
|
111
116
|
}
|
|
112
117
|
.hai-brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
113
|
-
.hai-
|
|
118
|
+
.hai-logo { flex: none; }
|
|
119
|
+
.hai-logo-ink { fill: var(--hai-logo-ink); }
|
|
120
|
+
.hai-logo-paper { fill: var(--hai-logo-paper); }
|
|
121
|
+
.hai-logo-paper-dim { fill: var(--hai-logo-paper); opacity: .5; }
|
|
122
|
+
.hai-logo-hot { fill: var(--hai-logo-hot); }
|
|
123
|
+
.hai-mark { width: 20px; height: 20px; }
|
|
114
124
|
.hai-title { font-size: 15px; font-weight: 650; letter-spacing: -.01em; white-space: nowrap; }
|
|
115
125
|
.hai-subtitle {
|
|
116
126
|
color: var(--hai-dim);
|
|
@@ -200,15 +210,7 @@
|
|
|
200
210
|
/* ── empty state ────────────────────────────────────────────────────── */
|
|
201
211
|
.hai-empty { color: var(--hai-dim); }
|
|
202
212
|
.hai-empty p { margin: 0; max-width: 54ch; font-size: 15px; line-height: 1.6; }
|
|
203
|
-
.hai-empty-mark {
|
|
204
|
-
width: 28px;
|
|
205
|
-
height: 28px;
|
|
206
|
-
padding: 12px;
|
|
207
|
-
margin-bottom: 18px;
|
|
208
|
-
border-radius: 16px;
|
|
209
|
-
background: var(--hai-accent-soft);
|
|
210
|
-
color: var(--hai-accent);
|
|
211
|
-
}
|
|
213
|
+
.hai-empty-mark { width: 44px; height: 44px; margin-bottom: 20px; }
|
|
212
214
|
/* alone, it greets; after what init showed, it stays out of the way */
|
|
213
215
|
.hai-empty:first-child {
|
|
214
216
|
display: flex;
|
package/src/icons.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const PATHS = {
|
|
10
|
-
// one line splitting in two: the dual channel
|
|
11
|
-
brand: "M2.5 12H8c3.5 0 4.5-6 9-6h4.5M8 12c3.5 0 4.5 6 9 6h4.5",
|
|
12
10
|
send: "M12 19V5M5.5 11.5 12 5l6.5 6.5",
|
|
13
11
|
plus: "M12 5v14M5 12h14",
|
|
14
12
|
panel: "M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM15 4v16",
|
|
@@ -22,6 +20,12 @@ const PATHS = {
|
|
|
22
20
|
|
|
23
21
|
const SVG = "http://www.w3.org/2000/svg";
|
|
24
22
|
|
|
23
|
+
/** The haikit favicon, docs/haikit-logo/favicon.svg, as a data URL. Its dark
|
|
24
|
+
* tile reads on a light tab bar and a dark one alike. */
|
|
25
|
+
export const FAVICON = "data:image/svg+xml," + encodeURIComponent(
|
|
26
|
+
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' role='img' aria-label='HaiKIT'><title>HaiKIT</title><rect width='64' height='64' rx='14' fill='#0F1115'/><g transform='translate(8 8) scale(0.75)'><rect x='6' y='6' width='28' height='52' rx='8' fill='#F3F2EE'/><rect x='33' y='26' width='12' height='12' fill='#F3F2EE'/><rect x='44' y='6' width='14' height='52' rx='4' fill='#FF5A1F'/></g></svg>",
|
|
27
|
+
);
|
|
28
|
+
|
|
25
29
|
/** An inline SVG icon, hidden from assistive technology: the text beside it, or
|
|
26
30
|
* the button's label, says what it means. */
|
|
27
31
|
export function icon(name, className = "hai-icon") {
|
|
@@ -39,3 +43,48 @@ export function icon(name, className = "hai-icon") {
|
|
|
39
43
|
svg.append(path);
|
|
40
44
|
return svg;
|
|
41
45
|
}
|
|
46
|
+
|
|
47
|
+
// The haikit mark, as filled rects: [x, y, width, height, rx, part]. The parts
|
|
48
|
+
// are coloured by hai.css, so the mark reverses in the dark scheme. Below about
|
|
49
|
+
// 32px the full mark's inner bars blur, so small sizes get the simpler one.
|
|
50
|
+
const MARKS = {
|
|
51
|
+
full: {
|
|
52
|
+
viewBox: "7 8 50 48",
|
|
53
|
+
rects: [
|
|
54
|
+
[7, 8, 26, 48, 7, "ink"],
|
|
55
|
+
[12.5, 14.5, 15, 3, 1.5, "paper"],
|
|
56
|
+
[12.5, 20.5, 9, 3, 1.5, "paper-dim"],
|
|
57
|
+
[12.5, 43.5, 15, 7, 3.5, "hot"],
|
|
58
|
+
[32, 28, 14, 8, 0, "ink"],
|
|
59
|
+
[45, 8, 12, 15, 3, "hot"],
|
|
60
|
+
[45, 25, 12, 14, 3, "hot"],
|
|
61
|
+
[45, 41, 12, 15, 3, "hot"],
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
small: {
|
|
65
|
+
viewBox: "6 6 52 52",
|
|
66
|
+
rects: [
|
|
67
|
+
[6, 6, 28, 52, 8, "ink"],
|
|
68
|
+
[33, 26, 12, 12, 0, "ink"],
|
|
69
|
+
[44, 6, 14, 52, 4, "hot"],
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The haikit logo mark, hidden from assistive technology like the icons. */
|
|
75
|
+
export function mark(size = "full", className = "hai-logo") {
|
|
76
|
+
const { viewBox, rects } = MARKS[size];
|
|
77
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
78
|
+
svg.setAttribute("class", className);
|
|
79
|
+
svg.setAttribute("viewBox", viewBox);
|
|
80
|
+
svg.setAttribute("aria-hidden", "true");
|
|
81
|
+
for (const [x, y, width, height, rx, part] of rects) {
|
|
82
|
+
const rect = document.createElementNS(SVG, "rect");
|
|
83
|
+
for (const [k, v] of Object.entries({ x, y, width, height, rx })) {
|
|
84
|
+
if (v) rect.setAttribute(k, String(v));
|
|
85
|
+
}
|
|
86
|
+
rect.setAttribute("class", `hai-logo-${part}`);
|
|
87
|
+
svg.append(rect);
|
|
88
|
+
}
|
|
89
|
+
return svg;
|
|
90
|
+
}
|