thebird 1.2.7 β†’ 1.2.9

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.
Files changed (3) hide show
  1. package/docs/app.js +152 -84
  2. package/docs/index.html +12 -49
  3. package/package.json +1 -1
package/docs/app.js CHANGED
@@ -1,4 +1,15 @@
1
- import { GoogleGenAI } from 'https://esm.sh/@google/genai@1';
1
+ import { createElement, applyDiff } from 'webjsx';
2
+
3
+ const MODELS_API = key => `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
4
+
5
+ async function fetchModels(apiKey) {
6
+ const res = await fetch(MODELS_API(apiKey));
7
+ if (!res.ok) throw new Error(`Models API ${res.status}: ${await res.text()}`);
8
+ const { models = [] } = await res.json();
9
+ return models
10
+ .filter(m => m.supportedGenerationMethods?.includes('generateContent'))
11
+ .map(m => ({ id: m.name.replace('models/', ''), label: m.displayName || m.name }));
12
+ }
2
13
 
3
14
  function convertMessages(messages) {
4
15
  const contents = [];
@@ -26,102 +37,159 @@ function convertMessages(messages) {
26
37
  return contents;
27
38
  }
28
39
 
29
- function buildConfig({ system, temperature, maxOutputTokens } = {}) {
30
- const config = { maxOutputTokens: maxOutputTokens ?? 8192, temperature: temperature ?? 0.7 };
40
+ function buildConfig({ system, temperature } = {}) {
41
+ const config = { maxOutputTokens: 8192, temperature: temperature ?? 0.7 };
31
42
  if (system) config.systemInstruction = system;
32
43
  return config;
33
44
  }
34
45
 
35
- const state = { messages: [], streaming: false, model: 'gemini-2.0-flash' };
46
+ class BirdChat extends HTMLElement {
47
+ constructor() {
48
+ super();
49
+ this.state = {
50
+ messages: [],
51
+ streaming: false,
52
+ model: 'gemini-2.5-flash',
53
+ apiKey: localStorage.getItem('gemini_api_key') || '',
54
+ models: [],
55
+ modelsLoading: false,
56
+ status: '',
57
+ streamingText: '',
58
+ };
59
+ window.__debug = {
60
+ get state() { return this.state; }.bind(this),
61
+ get messages() { return this.state.messages; }.bind(this),
62
+ get models() { return this.state.models; }.bind(this),
63
+ };
64
+ }
65
+
66
+ connectedCallback() {
67
+ this.render();
68
+ if (this.state.apiKey) this.loadModels(this.state.apiKey);
69
+ }
70
+
71
+ setState(patch) { Object.assign(this.state, patch); this.render(); }
36
72
 
37
- window.__debug = {
38
- get state() { return state; },
39
- get messages() { return state.messages; },
40
- };
73
+ async loadModels(apiKey) {
74
+ this.setState({ modelsLoading: true, status: '' });
75
+ try {
76
+ const models = await fetchModels(apiKey);
77
+ const current = this.state.model;
78
+ const first = models[0]?.id || 'gemini-2.5-flash';
79
+ const model = models.find(m => m.id === current) ? current : first;
80
+ this.setState({ models, model, modelsLoading: false });
81
+ } catch (err) {
82
+ this.setState({ modelsLoading: false, status: 'Failed to load models: ' + (err?.message || String(err)) });
83
+ }
84
+ }
41
85
 
42
- const $messages = document.getElementById('messages');
43
- const $input = document.getElementById('input');
44
- const $form = document.getElementById('chat-form');
45
- const $send = document.getElementById('send-btn');
46
- const $apiKey = document.getElementById('api-key');
47
- const $model = document.getElementById('model-select');
48
- const $status = document.getElementById('status');
49
- const $clear = document.getElementById('clear-btn');
86
+ render() {
87
+ const { messages, streaming, model, apiKey, models, modelsLoading, status, streamingText } = this.state;
88
+ applyDiff(this, (
89
+ <div class="flex flex-col h-full">
90
+ <header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
91
+ <span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
92
+ <span class="text-base-content/50 text-xs hidden sm:inline">Anthropic SDK format β†’ Gemini API</span>
93
+ <div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
94
+ <input
95
+ id="api-key-input"
96
+ type="password"
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
+ />
107
+ <div class="relative">
108
+ {modelsLoading
109
+ ? <span class="loading loading-spinner loading-sm text-primary"></span>
110
+ : <select
111
+ class="select select-sm select-bordered"
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
+ }
119
+ </div>
120
+ <button class="btn btn-sm btn-ghost" onclick={() => this.setState({ messages: [], status: '' })}>Clear</button>
121
+ </div>
122
+ </header>
50
123
 
51
- const savedKey = localStorage.getItem('gemini_api_key') || '';
52
- if (savedKey) $apiKey.value = savedKey;
53
- $apiKey.addEventListener('change', () => localStorage.setItem('gemini_api_key', $apiKey.value.trim()));
54
- $model.addEventListener('change', () => { state.model = $model.value; });
124
+ <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={i} class={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
127
+ <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'}`}>
128
+ {m.content}
129
+ </div>
130
+ </div>
131
+ ))}
132
+ {streamingText && (
133
+ <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">{streamingText}<span class="animate-pulse ml-1">β–‹</span></div>
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>}
138
+ </div>
55
139
 
56
- function addMsg(role, text) {
57
- const el = document.createElement('div');
58
- el.className = 'msg ' + role;
59
- el.textContent = text;
60
- $messages.appendChild(el);
61
- $messages.scrollTop = $messages.scrollHeight;
62
- return el;
63
- }
140
+ {status && <div class="text-xs text-error px-4 pb-1">{status}</div>}
64
141
 
65
- function setStatus(t) { $status.textContent = t; }
142
+ <form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit={e => { e.preventDefault(); this.send(); }}>
143
+ <textarea
144
+ id="chat-input"
145
+ class="textarea textarea-bordered flex-1 resize-none min-h-[42px] max-h-[120px] text-sm"
146
+ placeholder="Message… (Shift+Enter for newline)"
147
+ rows="1"
148
+ disabled={streaming}
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'}
154
+ </button>
155
+ </form>
156
+ </div>
157
+ ));
158
+ }
66
159
 
67
- async function sendMessage(userText) {
68
- const apiKey = $apiKey.value.trim();
69
- if (!apiKey) { addMsg('error', 'Enter a Gemini API key above.'); return; }
70
- state.messages.push({ role: 'user', content: userText });
71
- addMsg('user', userText);
72
- state.streaming = true;
73
- $send.disabled = true;
74
- setStatus('Streaming…');
75
- const modelEl = addMsg('model', '');
76
- let full = '';
77
- try {
78
- const ai = new GoogleGenAI({ apiKey });
79
- const stream = await ai.models.generateContentStream({
80
- model: state.model,
81
- contents: convertMessages(state.messages),
82
- config: buildConfig({ temperature: 0.7 }),
83
- });
84
- for await (const chunk of stream) {
85
- for (const candidate of (chunk.candidates || [])) {
86
- for (const part of (candidate.content?.parts || [])) {
87
- if (part.text && !part.thought) { full += part.text; modelEl.textContent = full; $messages.scrollTop = $messages.scrollHeight; }
160
+ async send() {
161
+ const input = this.querySelector('#chat-input');
162
+ const text = input?.value.trim();
163
+ if (!text || this.state.streaming) return;
164
+ const { apiKey, model } = this.state;
165
+ if (!apiKey) { this.setState({ status: 'Enter a Gemini API key above.' }); return; }
166
+ input.value = '';
167
+ input.style.height = 'auto';
168
+ const messages = [...this.state.messages, { role: 'user', content: text }];
169
+ this.setState({ messages, streaming: true, status: '', streamingText: '' });
170
+ try {
171
+ const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
172
+ const ai = new GoogleGenAI({ apiKey });
173
+ const stream = await ai.models.generateContentStream({
174
+ model,
175
+ contents: convertMessages(messages),
176
+ config: buildConfig(),
177
+ });
178
+ let full = '';
179
+ for await (const chunk of stream) {
180
+ for (const candidate of (chunk.candidates || [])) {
181
+ for (const part of (candidate.content?.parts || [])) {
182
+ if (part.text && !part.thought) { full += part.text; this.setState({ streamingText: full }); }
183
+ }
88
184
  }
89
185
  }
186
+ const list = document.getElementById('msg-list');
187
+ if (list) list.scrollTop = list.scrollHeight;
188
+ this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
189
+ } catch (err) {
190
+ this.setState({ streaming: false, streamingText: '', status: 'Error: ' + (err?.message || String(err)) });
90
191
  }
91
- if (!full) full = '(empty response)';
92
- modelEl.textContent = full;
93
- state.messages.push({ role: 'assistant', content: full });
94
- setStatus('');
95
- } catch (err) {
96
- modelEl.remove();
97
- addMsg('error', 'Error: ' + (err?.message || String(err)));
98
- state.messages.pop();
99
- setStatus('');
100
- } finally {
101
- state.streaming = false;
102
- $send.disabled = false;
103
- $input.focus();
104
192
  }
105
193
  }
106
194
 
107
- $input.addEventListener('input', () => { $input.style.height = 'auto'; $input.style.height = Math.min($input.scrollHeight, 160) + 'px'; });
108
-
109
- $form.addEventListener('submit', e => {
110
- e.preventDefault();
111
- const text = $input.value.trim();
112
- if (!text || state.streaming) return;
113
- $input.value = '';
114
- $input.style.height = 'auto';
115
- sendMessage(text);
116
- });
117
-
118
- $input.addEventListener('keydown', e => {
119
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); $form.requestSubmit(); }
120
- });
121
-
122
- $clear.addEventListener('click', () => {
123
- state.messages = [];
124
- $messages.innerHTML = '';
125
- setStatus('Cleared.');
126
- setTimeout(() => setStatus(''), 1500);
127
- });
195
+ customElements.define('bird-chat', BirdChat);
package/docs/index.html CHANGED
@@ -1,59 +1,22 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-theme="dark">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>thebird β€” Anthropic SDK β†’ Gemini demo</title>
6
+ <title>thebird β€” Gemini chat</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
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>
7
10
  <style>
8
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
- :root {
10
- --bg: #0f1117; --surface: #1a1d27; --border: #2a2d3e;
11
- --text: #e2e8f0; --muted: #64748b; --accent: #6366f1;
12
- --user: #1e293b; --model: #0f2027;
13
- }
14
- body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; height: 100dvh; display: flex; flex-direction: column; }
15
- header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
16
- header h1 { font-size: 1.1rem; font-weight: 700; color: var(--accent); }
17
- header span { font-size: 0.8rem; color: var(--muted); }
18
- #key-row { display: flex; gap: 0.5rem; flex: 1; min-width: 240px; }
19
- #api-key { flex: 1; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 0.4rem 0.75rem; border-radius: 6px; font-size: 0.85rem; outline: none; }
20
- #api-key:focus { border-color: var(--accent); }
21
- #model-select { background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 0.4rem 0.5rem; border-radius: 6px; font-size: 0.85rem; outline: none; }
22
- #messages { flex: 1; overflow-y: auto; padding: 1rem 1.5rem; display: flex; flex-direction: column; gap: 0.75rem; }
23
- .msg { padding: 0.75rem 1rem; border-radius: 8px; font-size: 0.9rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-width: 800px; }
24
- .msg.user { background: var(--user); align-self: flex-end; border: 1px solid var(--border); }
25
- .msg.model { background: var(--model); align-self: flex-start; border: 1px solid #1e3a4a; }
26
- .msg.error { background: #2d1515; border: 1px solid #5a2020; color: #fca5a5; align-self: center; font-size: 0.8rem; }
27
- form { padding: 1rem 1.5rem; border-top: 1px solid var(--border); display: flex; gap: 0.5rem; }
28
- textarea { flex: 1; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 0.6rem 0.75rem; border-radius: 8px; font-size: 0.9rem; resize: none; outline: none; font-family: inherit; min-height: 42px; max-height: 160px; line-height: 1.5; }
29
- textarea:focus { border-color: var(--accent); }
30
- button { background: var(--accent); color: #fff; border: none; padding: 0.6rem 1.25rem; border-radius: 8px; font-size: 0.9rem; cursor: pointer; white-space: nowrap; }
31
- button:disabled { opacity: 0.4; cursor: not-allowed; }
32
- button#clear-btn { background: transparent; border: 1px solid var(--border); color: var(--muted); padding: 0.4rem 0.75rem; font-size: 0.8rem; }
33
- #status { font-size: 0.75rem; color: var(--muted); padding: 0 1.5rem 0.5rem; }
11
+ :root { --bg: #0f1117; }
12
+ html, body { height: 100%; background: var(--bg); }
13
+ bird-chat { display: flex; flex-direction: column; height: 100dvh; }
14
+ .msg-bubble { max-width: 680px; white-space: pre-wrap; word-break: break-word; }
15
+ #msg-list { scroll-behavior: smooth; }
34
16
  </style>
35
17
  </head>
36
- <body>
37
- <header>
38
- <h1>thebird</h1>
39
- <span>Anthropic SDK format β†’ Gemini API</span>
40
- <div id="key-row">
41
- <input id="api-key" type="password" placeholder="GEMINI_API_KEY" autocomplete="off" />
42
- <select id="model-select">
43
- <option value="gemini-2.0-flash">gemini-2.0-flash</option>
44
- <option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking</option>
45
- <option value="gemini-1.5-pro">gemini-1.5-pro</option>
46
- <option value="gemini-1.5-flash">gemini-1.5-flash</option>
47
- </select>
48
- <button id="clear-btn" type="button">Clear</button>
49
- </div>
50
- </header>
51
- <div id="messages"></div>
52
- <div id="status"></div>
53
- <form id="chat-form">
54
- <textarea id="input" placeholder="Message… (Shift+Enter for newline)" rows="1"></textarea>
55
- <button type="submit" id="send-btn">Send</button>
56
- </form>
18
+ <body class="bg-base-100 text-base-content">
19
+ <bird-chat></bird-chat>
57
20
  <script type="module" src="app.js"></script>
58
21
  </body>
59
22
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thebird",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Anthropic SDK to Gemini streaming bridge β€” drop-in proxy that translates Anthropic message format and tool calls to Google Gemini",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",