thebird 1.2.6 → 1.2.8
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/.github/workflows/pages.yml +31 -0
- package/docs/app.js +162 -0
- package/docs/index.html +22 -0
- package/package.json +1 -1
- package/wasi/component.js +70 -0
- package/wasi/demo.js +23 -0
- package/wasi/package.json +18 -0
- package/wasi/wit/world.wit +45 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Deploy GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: [docs/**]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: pages
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
environment:
|
|
22
|
+
name: github-pages
|
|
23
|
+
url: ${{ steps.deploy.outputs.page_url }}
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/configure-pages@v4
|
|
27
|
+
- uses: actions/upload-pages-artifact@v3
|
|
28
|
+
with:
|
|
29
|
+
path: docs
|
|
30
|
+
- id: deploy
|
|
31
|
+
uses: actions/deploy-pages@v4
|
package/docs/app.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createElement, applyDiff } from 'webjsx';
|
|
2
|
+
|
|
3
|
+
const MODELS = [
|
|
4
|
+
'gemini-2.0-flash',
|
|
5
|
+
'gemini-2.0-flash-thinking-exp',
|
|
6
|
+
'gemini-1.5-pro',
|
|
7
|
+
'gemini-1.5-flash',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function convertMessages(messages) {
|
|
11
|
+
const contents = [];
|
|
12
|
+
for (const m of messages) {
|
|
13
|
+
const role = m.role === 'assistant' ? 'model' : 'user';
|
|
14
|
+
if (typeof m.content === 'string') {
|
|
15
|
+
if (m.content) contents.push({ role, parts: [{ text: m.content }] });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(m.content)) {
|
|
19
|
+
const parts = m.content.map(b => {
|
|
20
|
+
if (b.type === 'text' && b.text) return { text: b.text };
|
|
21
|
+
if (b.type === 'tool_use') return { functionCall: { name: b.name, args: b.input || {} } };
|
|
22
|
+
if (b.type === 'tool_result') {
|
|
23
|
+
let resp;
|
|
24
|
+
try { resp = typeof b.content === 'string' ? JSON.parse(b.content) : (b.content || {}); }
|
|
25
|
+
catch { resp = { result: b.content }; }
|
|
26
|
+
return { functionResponse: { name: b.name || 'unknown', response: resp } };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}).filter(Boolean);
|
|
30
|
+
if (parts.length) contents.push({ role, parts });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return contents;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildConfig({ system, temperature } = {}) {
|
|
37
|
+
const config = { maxOutputTokens: 8192, temperature: temperature ?? 0.7 };
|
|
38
|
+
if (system) config.systemInstruction = system;
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class BirdChat extends HTMLElement {
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
this.state = {
|
|
46
|
+
messages: [],
|
|
47
|
+
streaming: false,
|
|
48
|
+
model: MODELS[0],
|
|
49
|
+
apiKey: localStorage.getItem('gemini_api_key') || '',
|
|
50
|
+
status: '',
|
|
51
|
+
streamingText: '',
|
|
52
|
+
};
|
|
53
|
+
window.__debug = { get state() { return this.state; }.bind(this), get messages() { return this.state.messages; }.bind(this) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
connectedCallback() { this.render(); }
|
|
57
|
+
|
|
58
|
+
setState(patch) { Object.assign(this.state, patch); this.render(); }
|
|
59
|
+
|
|
60
|
+
render() {
|
|
61
|
+
const { messages, streaming, model, apiKey, status, streamingText } = this.state;
|
|
62
|
+
applyDiff(this, (
|
|
63
|
+
<div class="flex flex-col h-full">
|
|
64
|
+
<header class="navbar bg-base-200 border-b border-base-300 gap-2 flex-wrap px-4 py-2">
|
|
65
|
+
<span class="text-primary font-bold text-lg mr-2">🐦 thebird</span>
|
|
66
|
+
<span class="text-base-content/50 text-xs hidden sm:inline">Anthropic SDK format → Gemini API</span>
|
|
67
|
+
<div class="flex gap-2 flex-1 min-w-0 items-center flex-wrap">
|
|
68
|
+
<input
|
|
69
|
+
id="api-key-input"
|
|
70
|
+
type="password"
|
|
71
|
+
class="input input-sm input-bordered flex-1 min-w-[160px]"
|
|
72
|
+
placeholder="GEMINI_API_KEY"
|
|
73
|
+
value={apiKey}
|
|
74
|
+
onchange={e => { const v = e.target.value.trim(); localStorage.setItem('gemini_api_key', v); this.setState({ apiKey: v }); }}
|
|
75
|
+
/>
|
|
76
|
+
<select
|
|
77
|
+
class="select select-sm select-bordered"
|
|
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>
|
|
84
|
+
</div>
|
|
85
|
+
</header>
|
|
86
|
+
|
|
87
|
+
<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={i} class={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
90
|
+
<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'}`}>
|
|
91
|
+
{m.content}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
{streamingText && (
|
|
96
|
+
<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">{streamingText}<span class="animate-pulse ml-1">▋</span></div>
|
|
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
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{status && <div class="text-xs text-error px-4 pb-1">{status}</div>}
|
|
108
|
+
|
|
109
|
+
<form class="flex gap-2 p-3 border-t border-base-300 bg-base-200" onsubmit={e => { e.preventDefault(); this.send(); }}>
|
|
110
|
+
<textarea
|
|
111
|
+
id="chat-input"
|
|
112
|
+
class="textarea textarea-bordered flex-1 resize-none min-h-[42px] max-h-[120px] text-sm"
|
|
113
|
+
placeholder="Message… (Shift+Enter for newline)"
|
|
114
|
+
rows="1"
|
|
115
|
+
disabled={streaming}
|
|
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'}
|
|
121
|
+
</button>
|
|
122
|
+
</form>
|
|
123
|
+
</div>
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async send() {
|
|
128
|
+
const input = this.querySelector('#chat-input');
|
|
129
|
+
const text = input?.value.trim();
|
|
130
|
+
if (!text || this.state.streaming) return;
|
|
131
|
+
const { apiKey, model } = this.state;
|
|
132
|
+
if (!apiKey) { this.setState({ status: 'Enter a Gemini API key above.' }); return; }
|
|
133
|
+
input.value = '';
|
|
134
|
+
input.style.height = 'auto';
|
|
135
|
+
const messages = [...this.state.messages, { role: 'user', content: text }];
|
|
136
|
+
this.setState({ messages, streaming: true, status: '', streamingText: '' });
|
|
137
|
+
try {
|
|
138
|
+
const { GoogleGenAI } = await import('https://esm.sh/@google/genai@1');
|
|
139
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
140
|
+
const stream = await ai.models.generateContentStream({
|
|
141
|
+
model,
|
|
142
|
+
contents: convertMessages(messages),
|
|
143
|
+
config: buildConfig(),
|
|
144
|
+
});
|
|
145
|
+
let full = '';
|
|
146
|
+
for await (const chunk of stream) {
|
|
147
|
+
for (const candidate of (chunk.candidates || [])) {
|
|
148
|
+
for (const part of (candidate.content?.parts || [])) {
|
|
149
|
+
if (part.text && !part.thought) { full += part.text; this.setState({ streamingText: full }); }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const list = document.getElementById('msg-list');
|
|
154
|
+
if (list) list.scrollTop = list.scrollHeight;
|
|
155
|
+
this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
this.setState({ streaming: false, streamingText: '', status: 'Error: ' + (err?.message || String(err)) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
customElements.define('bird-chat', BirdChat);
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
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>
|
|
10
|
+
<style>
|
|
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; }
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body class="bg-base-100 text-base-content">
|
|
19
|
+
<bird-chat></bird-chat>
|
|
20
|
+
<script type="module" src="app.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
|
|
3
|
+
function cleanSchema(s) {
|
|
4
|
+
if (!s || typeof s !== 'object') return s;
|
|
5
|
+
if (Array.isArray(s)) return s.map(cleanSchema);
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const [k, v] of Object.entries(s)) {
|
|
8
|
+
if (k === 'additionalProperties' || k === '$schema') continue;
|
|
9
|
+
out[k] = cleanSchema(v);
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function partToGemini(b) {
|
|
15
|
+
if (b.kind === 'text' && b.text) return { text: b.text };
|
|
16
|
+
if (b.kind === 'tool_use') return { functionCall: { name: b.toolName || '', args: b.toolInput ? JSON.parse(b.toolInput) : {} } };
|
|
17
|
+
if (b.kind === 'tool_result') {
|
|
18
|
+
let resp;
|
|
19
|
+
try { resp = JSON.parse(b.text || '{}'); } catch { resp = { result: b.text }; }
|
|
20
|
+
return { functionResponse: { name: b.toolName || 'unknown', response: resp } };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function convertMessages(messages) {
|
|
26
|
+
const contents = [];
|
|
27
|
+
for (const m of messages) {
|
|
28
|
+
const role = m.role === 'assistant' ? 'model' : 'user';
|
|
29
|
+
const parts = m.content.map(partToGemini).filter(Boolean);
|
|
30
|
+
if (parts.length) contents.push({ role, parts });
|
|
31
|
+
}
|
|
32
|
+
return contents;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildConfig({ system, temperature, maxOutputTokens } = {}) {
|
|
36
|
+
const config = {
|
|
37
|
+
maxOutputTokens: maxOutputTokens ?? 8192,
|
|
38
|
+
temperature: temperature ?? 0.7,
|
|
39
|
+
};
|
|
40
|
+
if (system) config.systemInstruction = system;
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function generate(messages, config) {
|
|
45
|
+
const { apiKey, model, system, temperature, maxOutputTokens } = config;
|
|
46
|
+
if (!apiKey) return { text: '', error: 'apiKey required' };
|
|
47
|
+
try {
|
|
48
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
49
|
+
const contents = convertMessages(messages);
|
|
50
|
+
const geminiConfig = buildConfig({ system, temperature, maxOutputTokens });
|
|
51
|
+
const response = await ai.models.generateContent({
|
|
52
|
+
model: model || 'gemini-2.0-flash',
|
|
53
|
+
contents,
|
|
54
|
+
config: geminiConfig,
|
|
55
|
+
});
|
|
56
|
+
const candidate = response.candidates?.[0];
|
|
57
|
+
if (!candidate) return { text: '', error: 'no candidates returned' };
|
|
58
|
+
const text = (candidate.content?.parts || [])
|
|
59
|
+
.filter(p => p.text && !p.thought)
|
|
60
|
+
.map(p => p.text)
|
|
61
|
+
.join('');
|
|
62
|
+
return { text, error: null };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return { text: '', error: err?.message || String(err) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function convertMessagesExport(messages) {
|
|
69
|
+
return JSON.stringify(convertMessages(messages));
|
|
70
|
+
}
|
package/wasi/demo.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { generate } from './dist/js/component.js';
|
|
2
|
+
|
|
3
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
4
|
+
if (!apiKey) throw new Error('GEMINI_API_KEY env var required');
|
|
5
|
+
|
|
6
|
+
const messages = [
|
|
7
|
+
{
|
|
8
|
+
role: 'user',
|
|
9
|
+
content: [{ kind: 'text', text: 'Say hello in exactly 5 words.' }],
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const config = {
|
|
14
|
+
model: 'gemini-2.0-flash',
|
|
15
|
+
apiKey,
|
|
16
|
+
system: null,
|
|
17
|
+
temperature: 0.7,
|
|
18
|
+
maxOutputTokens: 256,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const result = await generate(messages, config);
|
|
22
|
+
if (result.error) throw new Error('generate failed: ' + result.error);
|
|
23
|
+
console.log(result.text);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thebird-wasi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WASI component wrapper for thebird — runs thebird in any WASI runtime or Node.js via jco transpile",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "jco componentize component.js --wit wit/world.wit --world-name thebird --out dist/component.wasm",
|
|
8
|
+
"transpile": "jco transpile dist/component.wasm --out-dir dist/js",
|
|
9
|
+
"run": "jco run dist/component.wasm",
|
|
10
|
+
"demo": "node demo.js"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@bytecodealliance/jco": "^1.0.0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@google/genai": "^1.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package thebird:core@0.1.0;
|
|
2
|
+
|
|
3
|
+
interface types {
|
|
4
|
+
record content-part {
|
|
5
|
+
kind: string,
|
|
6
|
+
text: option<string>,
|
|
7
|
+
tool-name: option<string>,
|
|
8
|
+
tool-input: option<string>,
|
|
9
|
+
tool-id: option<string>,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
record message {
|
|
13
|
+
role: string,
|
|
14
|
+
content: list<content-part>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
record generate-config {
|
|
18
|
+
model: string,
|
|
19
|
+
api-key: string,
|
|
20
|
+
system: option<string>,
|
|
21
|
+
temperature: option<f32>,
|
|
22
|
+
max-output-tokens: option<u32>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
record generate-result {
|
|
26
|
+
text: string,
|
|
27
|
+
error: option<string>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
record stream-chunk {
|
|
31
|
+
chunk-type: string,
|
|
32
|
+
text-delta: option<string>,
|
|
33
|
+
tool-call-id: option<string>,
|
|
34
|
+
tool-name: option<string>,
|
|
35
|
+
finish-reason: option<string>,
|
|
36
|
+
error: option<string>,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
world thebird {
|
|
41
|
+
use types.{message, generate-config, generate-result, stream-chunk};
|
|
42
|
+
|
|
43
|
+
export generate: func(messages: list<message>, config: generate-config) -> generate-result;
|
|
44
|
+
export convert-messages: func(messages: list<message>) -> string;
|
|
45
|
+
}
|