thebird 1.2.10 → 1.2.12
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 +42 -28
- 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();
|
|
@@ -54,10 +71,11 @@ class BirdChat extends HTMLElement {
|
|
|
54
71
|
apiKey: localStorage.getItem('gemini_api_key') || '',
|
|
55
72
|
models: [], modelsLoading: false, status: '', streamingText: '',
|
|
56
73
|
};
|
|
74
|
+
const self = this;
|
|
57
75
|
window.__debug = {
|
|
58
|
-
get state() { return
|
|
59
|
-
get messages() { return
|
|
60
|
-
get models() { return
|
|
76
|
+
get state() { return self.state; },
|
|
77
|
+
get messages() { return self.state.messages; },
|
|
78
|
+
get models() { return self.state.models; },
|
|
61
79
|
};
|
|
62
80
|
}
|
|
63
81
|
|
|
@@ -82,7 +100,7 @@ class BirdChat extends HTMLElement {
|
|
|
82
100
|
|
|
83
101
|
render() {
|
|
84
102
|
const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
|
|
85
|
-
const
|
|
103
|
+
const opts = (models.length === 0 ? [{ id: model, label: model }] : models)
|
|
86
104
|
.map(m => html`<option value=${m.id} selected=${m.id === model}>${m.label}</option>`);
|
|
87
105
|
|
|
88
106
|
applyDiff(this, html`
|
|
@@ -98,7 +116,7 @@ class BirdChat extends HTMLElement {
|
|
|
98
116
|
${modelsLoading
|
|
99
117
|
? html`<span class="loading loading-spinner loading-sm text-primary"></span>`
|
|
100
118
|
: html`<select class="select select-sm select-bordered" value=${model} disabled=${models.length === 0}
|
|
101
|
-
onchange=${e => this.setState({ model: e.target.value })}>${
|
|
119
|
+
onchange=${e => this.setState({ model: e.target.value })}>${opts}</select>`}
|
|
102
120
|
</div>
|
|
103
121
|
<button class="btn btn-sm btn-ghost" onclick=${() => this.setState({ messages: [], status: '' })}>Clear</button>
|
|
104
122
|
</div>
|
|
@@ -141,14 +159,10 @@ class BirdChat extends HTMLElement {
|
|
|
141
159
|
const messages = [...this.state.messages, { role: 'user', content: text }];
|
|
142
160
|
this.setState({ messages, streaming: true, status: '', streamingText: '' });
|
|
143
161
|
try {
|
|
144
|
-
const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
|
|
145
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
146
|
-
const stream = await ai.models.generateContentStream({ model, contents: convertMessages(messages), config: buildConfig() });
|
|
147
162
|
let full = '';
|
|
148
|
-
for await (const chunk of
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (part.text && !part.thought) { full += part.text; this.setState({ streamingText: full }); }
|
|
163
|
+
for await (const chunk of streamGenerate(apiKey, model, convertMessages(messages))) {
|
|
164
|
+
full += chunk;
|
|
165
|
+
this.setState({ streamingText: full });
|
|
152
166
|
}
|
|
153
167
|
const list = document.getElementById('msg-list');
|
|
154
168
|
if (list) list.scrollTop = list.scrollHeight;
|
package/package.json
CHANGED