@nevescloud/pip 2.11.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/LICENSE +21 -0
- package/README.md +166 -0
- package/package.json +44 -0
- package/pip-core.esm.js +2210 -0
- package/providers/anthropic.esm.js +120 -0
- package/providers/openai.esm.js +164 -0
- package/providers/transformers.esm.js +273 -0
- package/runtime.esm.js +386 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonas Neves
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Pip
|
|
2
|
+
|
|
3
|
+
Floating assistant bubble + panel. ESM, no build, no dependencies. The module owns DOM, CSS, open/close, turn rendering. You provide the brains.
|
|
4
|
+
|
|
5
|
+
## Use
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createPip } from 'https://cdn.jsdelivr.net/npm/@nevescloud/pip@latest/pip-core.esm.js';
|
|
9
|
+
|
|
10
|
+
const pip = createPip({
|
|
11
|
+
introText: 'Ask me anything.',
|
|
12
|
+
placeholder: 'Type here…',
|
|
13
|
+
async onSubmit(text, ctx) {
|
|
14
|
+
return await yourAssistant(text);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`@latest` (above) auto-tracks new releases on next jsdelivr/SW revalidation (~7d browser cache). Pin a specific version (`@2.11.0`) for production sites where a regression is costly — npm distributions are immutable per version, safe for forever-cached CDN delivery. `@2` semver-locks the major (auto-updates minor/patch, catches breaking API changes) if you want a middle ground. See [CONSUMERS.md](../../CONSUMERS.md) for the trade-off.
|
|
20
|
+
|
|
21
|
+
Or via npm: `npm install @nevescloud/pip`.
|
|
22
|
+
|
|
23
|
+
## Options
|
|
24
|
+
|
|
25
|
+
| Key | Default | Notes |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `onSubmit(text, ctx)` | — | Required. Returns the reply string (or a promise of one). |
|
|
28
|
+
| `onSlash(text)` | `null` | Legacy fallback intercept for `/`-prefixed input. Runs *after* registered commands and built-ins miss. New code should call `pip.registerSlash()` instead. |
|
|
29
|
+
| `slashSource()` | built-in | Override for the autocomplete dropdown's source. By default, pip enumerates `pip.registerSlash()` registrations + built-in `/help` and `/clear`. ↑/↓ to cycle, Tab/Enter to accept, Esc to close; `complete(partial)` per-keystroke after the space supplies arg suggestions. |
|
|
30
|
+
| `introText` | `""` | Muted message shown before first turn; auto-dismisses on submit. |
|
|
31
|
+
| `introDismissMs` | `7000` | How long the intro stays before fading. `0` to keep until first message. |
|
|
32
|
+
| `placeholder` | `"Ask Pip…"` | Input placeholder. |
|
|
33
|
+
| `autoOpen` | `false` | Open the panel on mount. |
|
|
34
|
+
| `autoOpenDelayMs` | `700` | Delay before auto-open. |
|
|
35
|
+
| `maxLength` | `4000` | Input character cap. |
|
|
36
|
+
| `openHotkey` | `"/"` | Global key that opens pip and prefills the input. `""` or `false` to disable. Skipped while typing in another field. |
|
|
37
|
+
| `historyLimit` | `10` | Rolling history window passed to `onSubmit` via `ctx.history`. |
|
|
38
|
+
| `fallbackReply` | `"Can't think right now — try again?"` | Shown when `onSubmit` returns `null` / `undefined`. Override per host to point at recovery commands you actually expose, e.g. `"Can't think right now — try \`/model tiny\`."`. |
|
|
39
|
+
| `emptyReply` | `"I don't have a good answer for that — tell me more?"` | Shown when `onSubmit` returns `""`. |
|
|
40
|
+
| `modelLabel` | `""` | Small pill rendered in the header strip (active backend / model name). Update via `pip.setModelLabel(label)`. |
|
|
41
|
+
| `slashHint` | `true` | Render a `/` key-cap button that seeds the input + opens the slash autocomplete. Discoverable affordance; set `false` to hide. |
|
|
42
|
+
| `onAbort` | `null` | Called when the user clicks the stop button while pip is responding. Wire to your "abort the in-flight LLM call" path. If omitted, the stop button still renders (visual consistency with the responding state) but click is a no-op. |
|
|
43
|
+
| `mic` | `false` | See [Mic input](#mic-input) below. `true` mounts the Web Speech button with default behavior; pass `{ onChunk, onFinal }` for advanced hooks. |
|
|
44
|
+
| `container` | `document.body` | Mount point. |
|
|
45
|
+
| `onOpen`, `onClose` | `null` | Lifecycle hooks. |
|
|
46
|
+
|
|
47
|
+
## Slash commands
|
|
48
|
+
|
|
49
|
+
`pip.registerSlash({ name, handler, description?, complete? })` adds a command. Registry-first dispatch: registered commands win over built-ins so a host can override `/help` or `/clear` by registering with the same name.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
pip.registerSlash({
|
|
53
|
+
name: "model",
|
|
54
|
+
description: "switch backend",
|
|
55
|
+
complete: (partial) => ["claude", "gpt-4o"].filter(s => s.startsWith(partial)),
|
|
56
|
+
handler: (args) => {
|
|
57
|
+
setBackend(args.trim());
|
|
58
|
+
return { reply: `Switched to ${args.trim()}.` };
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pip.unregisterSlash("model");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Handler returns `{ reply?, clearedUI?, passThrough? } | null`. `null` falls through to `onSlash` (if provided) and then to the LLM. Built-ins ship `/help` (auto-lists registered + built-ins) and `/clear` (wipes pip's local history + DOM).
|
|
66
|
+
|
|
67
|
+
## Tool-using turns
|
|
68
|
+
|
|
69
|
+
For hosts running multi-step LLM loops (computer-use, tool dispatch), pip's default single-reply model collapses the turn into one block and hides the step-by-step reasoning users want to see. The interleave primitives let a turn render as `[text 1] [tool pill 1] [text 2] [image] [pill 2] [final text]` in arrival order — same shape Anthropic computer-use / hatch / Codex-style UIs converge on.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const pip = createPip({
|
|
73
|
+
onSubmit: async (text, { turnEl }) => {
|
|
74
|
+
let reply = null;
|
|
75
|
+
for await (const ev of llmStream(text)) {
|
|
76
|
+
if (ev.type === 'text_delta') {
|
|
77
|
+
if (!reply) reply = pip.appendReplyBubble(turnEl);
|
|
78
|
+
reply.setText(ev.fullText);
|
|
79
|
+
} else if (ev.type === 'tool_start') {
|
|
80
|
+
reply = null; // next deltas land below the pill
|
|
81
|
+
const pill = pip.appendToolPill(turnEl, ev.name, { label: `${ev.name} …` });
|
|
82
|
+
ev.onFinish = ({ input, result, error, durationMs }) => {
|
|
83
|
+
pill.finish({ label: summarize(ev.name, result), input, result, error, durationMs });
|
|
84
|
+
};
|
|
85
|
+
} else if (ev.type === 'image') {
|
|
86
|
+
pip.appendTurnImage(turnEl, { src: ev.dataUrl, alt: ev.caption });
|
|
87
|
+
reply = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return ''; // we owned the rendering; let pip's default-reply stay hidden
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Method | Returns | Notes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `appendToolPill(turnEl, name, { label? })` | `{ el, finish({ label?, input?, result?, error?, durationMs? }) }` | `.running` → completed/`.error` on finish. Disclosure button expands a pre with args + result (truncated at 240 chars per string). Hold the returned object across the await; pass it back to `finish()` without tracking the DOM separately. |
|
|
98
|
+
| `appendReplyBubble(turnEl)` | `{ el, setText(md), setHtml(html) }` | Multi-bubble per turn. `setText` renders markdown via pip's built-in `renderMd`. `setHtml` skips escaping — sanitize first. |
|
|
99
|
+
| `appendTurnImage(turnEl, { src, alt? })` | `HTMLImageElement` | Inline camera frame / screenshot. Capped at 220px tall. Lazy-loaded. |
|
|
100
|
+
| `scrollToBottom()` | — | Exposed for hosts interleaving their own DOM between primitives. |
|
|
101
|
+
|
|
102
|
+
## Mic input
|
|
103
|
+
|
|
104
|
+
`createPip({ mic: true })` mounts a Web Speech-backed mic button next to the send button. Click toggles sticky-mode dictation: final transcripts flow through `onSubmit` exactly as if the user had typed and sent them. Escape cancels. No-speech retry with sticky-disarm after 2 consecutive flakes. No-op when Web Speech isn't supported in the current browser (no broken affordance).
|
|
105
|
+
|
|
106
|
+
For advanced hosts — safety-verb instant-fire that needs sub-second response, mid-turn injection that bypasses the input field — pass an object:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const pip = createPip({
|
|
110
|
+
mic: {
|
|
111
|
+
onChunk: async (text) => {
|
|
112
|
+
// Fires on every Web Speech "final chunk" before the silence-commit
|
|
113
|
+
// window. Return true to consume — pip stops the mic + clears the
|
|
114
|
+
// input. Use for instant-fire safety verbs ("stop", "halt").
|
|
115
|
+
if (isSafetyVerb(text)) { handleSafetyVerb(text); return true; }
|
|
116
|
+
return false;
|
|
117
|
+
},
|
|
118
|
+
onFinal: async (text) => {
|
|
119
|
+
// Fires on final + silence-commit. Return true if you handled the
|
|
120
|
+
// transcript yourself (e.g. injected into an already-in-flight turn);
|
|
121
|
+
// pip skips its default submit.
|
|
122
|
+
if (isMidTurn()) { injectIntoActiveTurn(text); return true; }
|
|
123
|
+
return false;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Suspend/resume drive the muted visual state for events the gate can't
|
|
129
|
+
// see (your TTS playback, anything else).
|
|
130
|
+
pip.suspendMic(); // drops active session + lights mute icon
|
|
131
|
+
pip.resumeMic(); // re-arms sticky if it was on
|
|
132
|
+
|
|
133
|
+
// Programmatic toggle — useful for /voice slash:
|
|
134
|
+
pip.toggleMic();
|
|
135
|
+
|
|
136
|
+
// Render an inline cyan notice for mic-related status (permission, retry).
|
|
137
|
+
pip.surfaceMicNotice("Didn't catch that — try again.");
|
|
138
|
+
|
|
139
|
+
// Check support before showing affordances:
|
|
140
|
+
if (pip.micSupported) { /* render Voice button */ }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Styling
|
|
144
|
+
|
|
145
|
+
CSS is auto-injected. Hosts customize via CSS variables on `:root` or any ancestor:
|
|
146
|
+
|
|
147
|
+
**Colors / surfaces**
|
|
148
|
+
- `--pip-surface` — panel background
|
|
149
|
+
- `--pip-ink`, `--pip-ink-muted` — text colors
|
|
150
|
+
- `--pip-border` — borders
|
|
151
|
+
- `--pip-accent` — focus / AI-generated highlight color
|
|
152
|
+
|
|
153
|
+
**Type scale** (defaults shown)
|
|
154
|
+
- `--pip-t-input` — input field. Default `14px` on desktop (`pointer: fine`) and `16px` on touch (mobile floor — going below 16 makes iOS Safari zoom on focus).
|
|
155
|
+
- `--pip-t-body: 14px` — assistant replies (the primary content layer)
|
|
156
|
+
- `--pip-t-caption: 12px` — intro, echoed user question, code blocks
|
|
157
|
+
|
|
158
|
+
Class overrides (`.pip-panel`, `.pip-input`, `.pip-bubble`, `.pip-notify`, …) work too.
|
|
159
|
+
|
|
160
|
+
## Runtime + providers
|
|
161
|
+
|
|
162
|
+
For a turn loop, tool dispatch, history, and an Anthropic provider, pair pip-core with `runtime.esm.js`. See [`docs/RUNTIME.md`](./docs/RUNTIME.md).
|
|
163
|
+
|
|
164
|
+
## Demo
|
|
165
|
+
|
|
166
|
+
Live: [jonasneves.github.io/pip/](https://jonasneves.github.io/pip/). Source: `docs/index.html`.
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nevescloud/pip",
|
|
3
|
+
"version": "2.11.0",
|
|
4
|
+
"description": "Floating assistant bubble + panel + chat runtime. ESM, no build.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "pip-core.esm.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./pip-core.esm.js",
|
|
9
|
+
"./pip-core.esm.js": "./pip-core.esm.js",
|
|
10
|
+
"./runtime.esm.js": "./runtime.esm.js",
|
|
11
|
+
"./providers/anthropic.esm.js": "./providers/anthropic.esm.js",
|
|
12
|
+
"./providers/openai.esm.js": "./providers/openai.esm.js",
|
|
13
|
+
"./providers/transformers.esm.js": "./providers/transformers.esm.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"pip-core.esm.js",
|
|
17
|
+
"runtime.esm.js",
|
|
18
|
+
"providers/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"chat",
|
|
24
|
+
"assistant",
|
|
25
|
+
"bubble",
|
|
26
|
+
"esm",
|
|
27
|
+
"agent",
|
|
28
|
+
"claude",
|
|
29
|
+
"anthropic"
|
|
30
|
+
],
|
|
31
|
+
"author": "Jonas Neves",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://github.com/jonasneves/pip#readme",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/jonasneves/pip.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/jonasneves/pip/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|