thebird 1.2.9 → 1.2.11
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 +44 -76
- package/docs/index.html +0 -1
- package/package.json +1 -1
package/docs/app.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(createElement);
|
|
2
5
|
|
|
3
6
|
const MODELS_API = key => `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
|
|
4
7
|
|
|
@@ -47,19 +50,15 @@ class BirdChat extends HTMLElement {
|
|
|
47
50
|
constructor() {
|
|
48
51
|
super();
|
|
49
52
|
this.state = {
|
|
50
|
-
messages: [],
|
|
51
|
-
streaming: false,
|
|
52
|
-
model: 'gemini-2.5-flash',
|
|
53
|
+
messages: [], streaming: false, model: 'gemini-2.5-flash',
|
|
53
54
|
apiKey: localStorage.getItem('gemini_api_key') || '',
|
|
54
|
-
models: [],
|
|
55
|
-
modelsLoading: false,
|
|
56
|
-
status: '',
|
|
57
|
-
streamingText: '',
|
|
55
|
+
models: [], modelsLoading: false, status: '', streamingText: '',
|
|
58
56
|
};
|
|
57
|
+
const self = this;
|
|
59
58
|
window.__debug = {
|
|
60
|
-
get state() { return
|
|
61
|
-
get messages() { return
|
|
62
|
-
get models() { return
|
|
59
|
+
get state() { return self.state; },
|
|
60
|
+
get messages() { return self.state.messages; },
|
|
61
|
+
get models() { return self.state.models; },
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
64
|
|
|
@@ -75,8 +74,7 @@ class BirdChat extends HTMLElement {
|
|
|
75
74
|
try {
|
|
76
75
|
const models = await fetchModels(apiKey);
|
|
77
76
|
const current = this.state.model;
|
|
78
|
-
const
|
|
79
|
-
const model = models.find(m => m.id === current) ? current : first;
|
|
77
|
+
const model = models.find(m => m.id === current) ? current : (models[0]?.id || current);
|
|
80
78
|
this.setState({ models, model, modelsLoading: false });
|
|
81
79
|
} catch (err) {
|
|
82
80
|
this.setState({ modelsLoading: false, status: 'Failed to load models: ' + (err?.message || String(err)) });
|
|
@@ -85,76 +83,52 @@ class BirdChat extends HTMLElement {
|
|
|
85
83
|
|
|
86
84
|
render() {
|
|
87
85
|
const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
|
|
88
|
-
|
|
86
|
+
const modelOptions = (models.length === 0 ? [{ id: model, label: model }] : models)
|
|
87
|
+
.map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
|
|
88
|
+
|
|
89
|
+
applyDiff(this, html`
|
|
89
90
|
<div class="flex flex-col h-full">
|
|
90
91
|
<header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
|
|
91
92
|
<span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
|
|
92
93
|
<span class="text-base-content/50 text-xs hidden sm:inline">Anthropic SDK format → Gemini API</span>
|
|
93
94
|
<div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
|
|
94
|
-
<input
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class="input input-sm input-bordered flex-1 min-w-[160px]"
|
|
98
|
-
placeholder="GEMINI_API_KEY"
|
|
99
|
-
value={apiKey}
|
|
100
|
-
onchange={e => {
|
|
101
|
-
const v = e.target.value.trim();
|
|
102
|
-
localStorage.setItem('gemini_api_key', v);
|
|
103
|
-
this.setState({ apiKey: v });
|
|
104
|
-
if (v) this.loadModels(v);
|
|
105
|
-
}}
|
|
106
|
-
/>
|
|
95
|
+
<input id="api-key-input" type="password" class="input input-sm input-bordered flex-1 min-w-[160px]"
|
|
96
|
+
placeholder="GEMINI_API_KEY" value=${apiKey}
|
|
97
|
+
onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('gemini_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(v); }} />
|
|
107
98
|
<div class="relative">
|
|
108
|
-
{modelsLoading
|
|
109
|
-
?
|
|
110
|
-
:
|
|
111
|
-
|
|
112
|
-
value={model}
|
|
113
|
-
disabled={models.length === 0}
|
|
114
|
-
onchange={e => this.setState({ model: e.target.value })}
|
|
115
|
-
>
|
|
116
|
-
{(models.length === 0 ? [{ id: model, label: model }] : models).map(m => <option value={m.id} selected={m.id === model}>{m.label}</option>)}
|
|
117
|
-
</select>
|
|
118
|
-
}
|
|
99
|
+
${modelsLoading
|
|
100
|
+
? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
|
|
101
|
+
: html`<select class="select select-sm select-bordered" value=${model} disabled=${models.length === 0}
|
|
102
|
+
onchange=${e => this.setState({ model: e.target.value })}>${modelOptions}</select>`}
|
|
119
103
|
</div>
|
|
120
|
-
<button class="btn btn-sm btn-ghost" onclick
|
|
104
|
+
<button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
121
105
|
</div>
|
|
122
106
|
</header>
|
|
123
107
|
|
|
124
108
|
<div id="msg-list" class="flex-1 overflow-y-auto flex flex-col gap-3 p-4">
|
|
125
|
-
{messages.map((m, i) =>
|
|
126
|
-
<div key
|
|
127
|
-
<div class
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
</div>
|
|
131
|
-
))}
|
|
132
|
-
{streamingText && (
|
|
109
|
+
${messages.map((m, i) => html`
|
|
110
|
+
<div key=${i} class=${'flex ' + (m.role === 'user' ? 'justify-end' : 'justify-start')}>
|
|
111
|
+
<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>
|
|
112
|
+
</div>`)}
|
|
113
|
+
${streamingText && html`
|
|
133
114
|
<div class="flex justify-start">
|
|
134
|
-
<div class="msg-bubble card bg-base-200 text-base-content px-4 py-3 text-sm leading-relaxed"
|
|
135
|
-
</div
|
|
136
|
-
|
|
137
|
-
{!streamingText && streaming && <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>}
|
|
115
|
+
<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>
|
|
116
|
+
</div>`}
|
|
117
|
+
${!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>`}
|
|
138
118
|
</div>
|
|
139
119
|
|
|
140
|
-
{status &&
|
|
141
|
-
|
|
142
|
-
<form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit
|
|
143
|
-
<textarea
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
onkeydown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }}
|
|
150
|
-
oninput={e => { e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}
|
|
151
|
-
></textarea>
|
|
152
|
-
<button type="submit" class="btn btn-primary self-end" disabled={streaming}>
|
|
153
|
-
{streaming ? <span class="loading loading-spinner loading-sm"></span> : 'Send'}
|
|
120
|
+
${status && html`<div class="text-xs text-error px-4 pb-1">${status}</div>`}
|
|
121
|
+
|
|
122
|
+
<form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit=${e => { e.preventDefault(); this.send(); }}>
|
|
123
|
+
<textarea id="chat-input" class="textarea textarea-bordered flex-1 resize-none min-h-[42px] max-h-[120px] text-sm"
|
|
124
|
+
placeholder="Message… (Shift+Enter for newline)" rows="1" disabled=${streaming}
|
|
125
|
+
onkeydown=${e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send(); } }}
|
|
126
|
+
oninput=${e => { e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }}></textarea>
|
|
127
|
+
<button type="submit" class="btn btn-primary self-end" disabled=${streaming}>
|
|
128
|
+
${streaming ? html`<span class="loading loading-spinner loading-sm"></span>` : 'Send'}
|
|
154
129
|
</button>
|
|
155
130
|
</form>
|
|
156
|
-
</div
|
|
157
|
-
));
|
|
131
|
+
</div>`);
|
|
158
132
|
}
|
|
159
133
|
|
|
160
134
|
async send() {
|
|
@@ -170,18 +144,12 @@ class BirdChat extends HTMLElement {
|
|
|
170
144
|
try {
|
|
171
145
|
const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
|
|
172
146
|
const ai = new GoogleGenAI({ apiKey });
|
|
173
|
-
const stream = await ai.models.generateContentStream({
|
|
174
|
-
model,
|
|
175
|
-
contents: convertMessages(messages),
|
|
176
|
-
config: buildConfig(),
|
|
177
|
-
});
|
|
147
|
+
const stream = await ai.models.generateContentStream({ model, contents: convertMessages(messages), config: buildConfig() });
|
|
178
148
|
let full = '';
|
|
179
149
|
for await (const chunk of stream) {
|
|
180
|
-
for (const candidate of (chunk.candidates || []))
|
|
181
|
-
for (const part of (candidate.content?.parts || []))
|
|
150
|
+
for (const candidate of (chunk.candidates || []))
|
|
151
|
+
for (const part of (candidate.content?.parts || []))
|
|
182
152
|
if (part.text && !part.thought) { full += part.text; this.setState({ streamingText: full }); }
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
153
|
}
|
|
186
154
|
const list = document.getElementById('msg-list');
|
|
187
155
|
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