thebird 1.2.11 → 1.2.13
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 +45 -26
- package/package.json +1 -1
package/docs/app.js
CHANGED
|
@@ -3,17 +3,47 @@ import htm from 'https://esm.sh/htm@3';
|
|
|
3
3
|
|
|
4
4
|
const html = htm.bind(createElement);
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const BASE = 'https://generativelanguage.googleapis.com/v1beta';
|
|
7
7
|
|
|
8
8
|
async function fetchModels(apiKey) {
|
|
9
|
-
const res = await fetch(
|
|
10
|
-
if (!res.ok) throw new Error(`Models API ${res.status}
|
|
9
|
+
const res = await fetch(`${BASE}/models?key=${apiKey}`);
|
|
10
|
+
if (!res.ok) throw new Error(`Models API ${res.status}`);
|
|
11
11
|
const { models = [] } = await res.json();
|
|
12
12
|
return models
|
|
13
13
|
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
14
14
|
.map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
async function* streamGenerate(apiKey, model, contents) {
|
|
18
|
+
const res = await fetch(`${BASE}/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({ contents, generationConfig: { maxOutputTokens: 8192, temperature: 0.7 } }),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) throw new Error(`Generate API ${res.status}: ${await res.text()}`);
|
|
24
|
+
const reader = res.body.getReader();
|
|
25
|
+
const dec = new TextDecoder();
|
|
26
|
+
let buf = '';
|
|
27
|
+
while (true) {
|
|
28
|
+
const { done, value } = await reader.read();
|
|
29
|
+
if (done) break;
|
|
30
|
+
buf += dec.decode(value, { stream: true });
|
|
31
|
+
const lines = buf.split('\n');
|
|
32
|
+
buf = lines.pop();
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line.startsWith('data: ')) continue;
|
|
35
|
+
const json = line.slice(6).trim();
|
|
36
|
+
if (!json || json === '[DONE]') continue;
|
|
37
|
+
try {
|
|
38
|
+
const chunk = JSON.parse(json);
|
|
39
|
+
for (const c of (chunk.candidates || []))
|
|
40
|
+
for (const p of (c.content?.parts || []))
|
|
41
|
+
if (p.text && !p.thought) yield p.text;
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
function convertMessages(messages) {
|
|
18
48
|
const contents = [];
|
|
19
49
|
for (const m of messages) {
|
|
@@ -25,13 +55,6 @@ function convertMessages(messages) {
|
|
|
25
55
|
if (Array.isArray(m.content)) {
|
|
26
56
|
const parts = m.content.map(b => {
|
|
27
57
|
if (b.type === 'text' && b.text) return { text: b.text };
|
|
28
|
-
if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };
|
|
29
|
-
if (b.type === 'tool_result') {
|
|
30
|
-
let resp;
|
|
31
|
-
try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }
|
|
32
|
-
catch { resp = { result: b.content }; }
|
|
33
|
-
return { functionResponse: { name: b.name || 'unknown', response: resp } };
|
|
34
|
-
}
|
|
35
58
|
return null;
|
|
36
59
|
}).filter(Boolean);
|
|
37
60
|
if (parts.length) contents.push({ role, parts });
|
|
@@ -40,12 +63,6 @@ function convertMessages(messages) {
|
|
|
40
63
|
return contents;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
|
-
function buildConfig({ system, temperature } = {}) {
|
|
44
|
-
const config = { maxOutputTokens: 8192, temperature: temperature ?? 0.7 };
|
|
45
|
-
if (system) config.systemInstruction = system;
|
|
46
|
-
return config;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
66
|
class BirdChat extends HTMLElement {
|
|
50
67
|
constructor() {
|
|
51
68
|
super();
|
|
@@ -83,7 +100,7 @@ class BirdChat extends HTMLElement {
|
|
|
83
100
|
|
|
84
101
|
render() {
|
|
85
102
|
const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
|
|
86
|
-
const
|
|
103
|
+
const opts = (models.length === 0 ? [{ id: model, label: model }] : models)
|
|
87
104
|
.map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
|
|
88
105
|
|
|
89
106
|
applyDiff(this, html`
|
|
@@ -99,7 +116,7 @@ class BirdChat extends HTMLElement {
|
|
|
99
116
|
${modelsLoading
|
|
100
117
|
? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
|
|
101
118
|
: html`<select class="select select-sm select-bordered" value=${model} disabled=${models.length === 0}
|
|
102
|
-
onchange=${e => this.setState({ model: e.target.value })}>${
|
|
119
|
+
onchange=${e => this.setState({ model: e.target.value })}>${opts}</select>`}
|
|
103
120
|
</div>
|
|
104
121
|
<button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
105
122
|
</div>
|
|
@@ -142,18 +159,20 @@ class BirdChat extends HTMLElement {
|
|
|
142
159
|
const messages = [...this.state.messages, { role: 'user', content: text }];
|
|
143
160
|
this.setState({ messages, streaming: true, status: '', streamingText: '' });
|
|
144
161
|
try {
|
|
145
|
-
const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
|
|
146
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
147
|
-
const stream = await ai.models.generateContentStream({ model, contents: convertMessages(messages), config: buildConfig() });
|
|
148
162
|
let full = '';
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
let rafPending = false;
|
|
164
|
+
const gen = streamGenerate(apiKey, model, convertMessages(messages));
|
|
165
|
+
for await (const chunk of gen) {
|
|
166
|
+
full += chunk;
|
|
167
|
+
if (!rafPending) {
|
|
168
|
+
rafPending = true;
|
|
169
|
+
const snap = full;
|
|
170
|
+
requestAnimationFrame(() => { this.setState({ streamingText: snap }); rafPending = false; });
|
|
171
|
+
}
|
|
153
172
|
}
|
|
173
|
+
this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
|
|
154
174
|
const list = document.getElementById('msg-list');
|
|
155
175
|
if (list) list.scrollTop = list.scrollHeight;
|
|
156
|
-
this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
|
|
157
176
|
} catch (err) {
|
|
158
177
|
this.setState({ streaming: false, streamingText: '', status: 'Error: ' + (err?.message || String(err)) });
|
|
159
178
|
}
|
package/package.json
CHANGED