thebird 1.2.8 → 1.2.10
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/docs/app.js +73 -73
- package/docs/index.html +0 -1
- package/package.json +1 -1
package/docs/app.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { createElement, applyDiff } from 'webjsx';
|
|
1
|
+
import { createElement, applyDiff } from 'https://esm.sh/webjsx@0.0.73';
|
|
2
|
+
import htm from 'https://esm.sh/htm@3';
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
const html = htm.bind(createElement);
|
|
5
|
+
|
|
6
|
+
const MODELS_API = key => `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
|
|
7
|
+
|
|
8
|
+
async function fetchModels(apiKey) {
|
|
9
|
+
const res = await fetch(MODELS_API(apiKey));
|
|
10
|
+
if (!res.ok) throw new Error(`Models API ${res.status}: ${await res.text()}`);
|
|
11
|
+
const { models = [] } = await res.json();
|
|
12
|
+
return models
|
|
13
|
+
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
14
|
+
.map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
function convertMessages(messages) {
|
|
11
18
|
const contents = [];
|
|
@@ -43,85 +50,84 @@ class BirdChat extends HTMLElement {
|
|
|
43
50
|
constructor() {
|
|
44
51
|
super();
|
|
45
52
|
this.state = {
|
|
46
|
-
messages: [],
|
|
47
|
-
streaming: false,
|
|
48
|
-
model: MODELS[0],
|
|
53
|
+
messages: [], streaming: false, model: 'gemini-2.5-flash',
|
|
49
54
|
apiKey: localStorage.getItem('gemini_api_key') || '',
|
|
50
|
-
status: '',
|
|
51
|
-
|
|
55
|
+
models: [], modelsLoading: false, status: '', streamingText: '',
|
|
56
|
+
};
|
|
57
|
+
window.__debug = {
|
|
58
|
+
get state() { return this.state; }.bind(this),
|
|
59
|
+
get messages() { return this.state.messages; }.bind(this),
|
|
60
|
+
get models() { return this.state.models; }.bind(this),
|
|
52
61
|
};
|
|
53
|
-
window.__debug = { get state() { return this.state; }.bind(this), get messages() { return this.state.messages; }.bind(this) };
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
connectedCallback() {
|
|
64
|
+
connectedCallback() {
|
|
65
|
+
this.render();
|
|
66
|
+
if (this.state.apiKey) this.loadModels(this.state.apiKey);
|
|
67
|
+
}
|
|
57
68
|
|
|
58
69
|
setState(patch) { Object.assign(this.state, patch); this.render(); }
|
|
59
70
|
|
|
71
|
+
async loadModels(apiKey) {
|
|
72
|
+
this.setState({ modelsLoading: true, status: '' });
|
|
73
|
+
try {
|
|
74
|
+
const models = await fetchModels(apiKey);
|
|
75
|
+
const current = this.state.model;
|
|
76
|
+
const model = models.find(m => m.id === current) ? current : (models[0]?.id || current);
|
|
77
|
+
this.setState({ models, model, modelsLoading: false });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.setState({ modelsLoading: false, status: 'Failed to load models: ' + (err?.message || String(err)) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
render() {
|
|
61
|
-
const { messages, streaming, model, apiKey, status, streamingText } = this.state;
|
|
62
|
-
|
|
84
|
+
const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
|
|
85
|
+
const modelOptions = (models.length === 0 ? [{ id: model, label: model }] : models)
|
|
86
|
+
.map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
|
|
87
|
+
|
|
88
|
+
applyDiff(this, html`
|
|
63
89
|
<div class="flex flex-col h-full">
|
|
64
90
|
<header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
|
|
65
91
|
<span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
|
|
66
92
|
<span class="text-base-content/50 text-xs hidden sm:inline">Anthropic SDK format → Gemini API</span>
|
|
67
93
|
<div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
|
|
68
|
-
<input
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
value={model}
|
|
79
|
-
onchange={e => this.setState({ model: e.target.value })}
|
|
80
|
-
>
|
|
81
|
-
{MODELS.map(m => <option value={m} selected={m === model}>{m}</option>)}
|
|
82
|
-
</select>
|
|
83
|
-
<button class="btn btn-sm btn-ghost" onclick={() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
94
|
+
<input id="api-key-input" type="password" class="input input-sm input-bordered flex-1 min-w-[160px]"
|
|
95
|
+
placeholder="GEMINI_API_KEY" value=${apiKey}
|
|
96
|
+
onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('gemini_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(v); }} />
|
|
97
|
+
<div class="relative">
|
|
98
|
+
${modelsLoading
|
|
99
|
+
? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
|
|
100
|
+
: html`<select class="select select-sm select-bordered" value=${model} disabled=${models.length === 0}
|
|
101
|
+
onchange=${e => this.setState({ model: e.target.value })}>${modelOptions}</select>`}
|
|
102
|
+
</div>
|
|
103
|
+
<button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
84
104
|
</div>
|
|
85
105
|
</header>
|
|
86
106
|
|
|
87
107
|
<div id="msg-list" class="flex-1 overflow-y-auto flex flex-col gap-3 p-4">
|
|
88
|
-
{messages.map((m, i) =>
|
|
89
|
-
<div key
|
|
90
|
-
<div class
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</div>
|
|
94
|
-
))}
|
|
95
|
-
{streamingText && (
|
|
108
|
+
${messages.map((m, i) => html`
|
|
109
|
+
<div key=${i} class=${'flex ' + (m.role === 'user' ? 'justify-end' : 'justify-start')}>
|
|
110
|
+
<div class=${'msg-bubble card px-4 py-3 text-sm leading-relaxed ' + (m.role === 'user' ? 'bg-primary text-primary-content' : 'bg-base-200 text-base-content')}>${m.content}</div>
|
|
111
|
+
</div>`)}
|
|
112
|
+
${streamingText && html`
|
|
96
113
|
<div class="flex justify-start">
|
|
97
|
-
<div class="msg-bubble card bg-base-200 text-base-content px-4 py-3 text-sm leading-relaxed"
|
|
98
|
-
</div
|
|
99
|
-
|
|
100
|
-
{!streamingText && streaming && (
|
|
101
|
-
<div class="flex justify-start">
|
|
102
|
-
<div class="card bg-base-200 px-4 py-3"><span class="loading loading-dots loading-sm"></span></div>
|
|
103
|
-
</div>
|
|
104
|
-
)}
|
|
114
|
+
<div class="msg-bubble card bg-base-200 text-base-content px-4 py-3 text-sm leading-relaxed">${streamingText}<span class="animate-pulse ml-1">▋</span></div>
|
|
115
|
+
</div>`}
|
|
116
|
+
${!streamingText && streaming && html`<div class="flex justify-start"><div class="card bg-base-200 px-4 py-3"><span class="loading loading-dots loading-sm"></span></div></div>`}
|
|
105
117
|
</div>
|
|
106
118
|
|
|
107
|
-
{status &&
|
|
108
|
-
|
|
109
|
-
<form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit
|
|
110
|
-
<textarea
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onkeydown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }}
|
|
117
|
-
oninput={e => { e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}
|
|
118
|
-
></textarea>
|
|
119
|
-
<button type="submit" class="btn btn-primary self-end" disabled={streaming}>
|
|
120
|
-
{streaming ? <span class="loading loading-spinner loading-sm"></span> : 'Send'}
|
|
119
|
+
${status && html`<div class="text-xs text-error px-4 pb-1">${status}</div>`}
|
|
120
|
+
|
|
121
|
+
<form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit=${e => { e.preventDefault(); this.send(); }}>
|
|
122
|
+
<textarea id="chat-input" class="textarea textarea-bordered flex-1 resize-none min-h-[42px] max-h-[120px] text-sm"
|
|
123
|
+
placeholder="Message… (Shift+Enter for newline)" rows="1" disabled=${streaming}
|
|
124
|
+
onkeydown=${e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }}
|
|
125
|
+
oninput=${e => { e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}></textarea>
|
|
126
|
+
<button type="submit" class="btn btn-primary self-end" disabled=${streaming}>
|
|
127
|
+
${streaming ? html`<span class="loading loading-spinner loading-sm"></span>` : 'Send'}
|
|
121
128
|
</button>
|
|
122
129
|
</form>
|
|
123
|
-
</div
|
|
124
|
-
));
|
|
130
|
+
</div>`);
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
async send() {
|
|
@@ -137,18 +143,12 @@ class BirdChat extends HTMLElement {
|
|
|
137
143
|
try {
|
|
138
144
|
const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
|
|
139
145
|
const ai = new GoogleGenAI({ apiKey });
|
|
140
|
-
const stream = await ai.models.generateContentStream({
|
|
141
|
-
model,
|
|
142
|
-
contents: convertMessages(messages),
|
|
143
|
-
config: buildConfig(),
|
|
144
|
-
});
|
|
146
|
+
const stream = await ai.models.generateContentStream({ model, contents: convertMessages(messages), config: buildConfig() });
|
|
145
147
|
let full = '';
|
|
146
148
|
for await (const chunk of stream) {
|
|
147
|
-
for (const candidate of (chunk.candidates || []))
|
|
148
|
-
for (const part of (candidate.content?.parts || []))
|
|
149
|
+
for (const candidate of (chunk.candidates || []))
|
|
150
|
+
for (const part of (candidate.content?.parts || []))
|
|
149
151
|
if (part.text && !part.thought) { full += part.text; this.setState({ streamingText: full }); }
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
152
|
}
|
|
153
153
|
const list = document.getElementById('msg-list');
|
|
154
154
|
if (list) list.scrollTop = list.scrollHeight;
|
package/docs/index.html
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
<title>thebird — Gemini chat</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rippleui@1.12.1/dist/css/styles.css" />
|
|
9
|
-
<script type="importmap">{"imports":{"webjsx":"https://unpkg.com/webjsx/dist/index.js"}}</script>
|
|
10
9
|
<style>
|
|
11
10
|
:root { --bg: #0f1117; }
|
|
12
11
|
html, body { height: 100%; background: var(--bg); }
|
package/package.json
CHANGED