vibe-ai-c 3.3.2 → 5.4.0
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/README.md +145 -52
- package/demo.html +102 -180
- package/package.json +8 -5
- package/vibe-ai.js +589 -432
- package/vibe-ai.min.js +0 -1
package/README.md
CHANGED
|
@@ -1,73 +1,166 @@
|
|
|
1
|
-
# VibeAI
|
|
1
|
+
# VibeAI v5.4.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Industrial-grade, zero-dependency, secure AI SDK for the modern web.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
VibeAI is a "Good Taste" driven wrapper for OpenAI-compatible APIs. It eliminates the friction of managing providers, API keys, and complex SSE streams, allowing you to focus on the UI "vibe" while it handles the plumbing.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **零依赖 (Zero-dependency)**:纯 JavaScript 实现,体积精简,不污染全局环境。
|
|
9
|
-
- **安全性 (Web Crypto)**:支持 AES-GCM 硬件级加密存储 API Key。
|
|
10
|
-
- **多实例绑定**:支持一个页面内多个对话框绑定不同供应商/模型。
|
|
11
|
-
- **智能 UI 交互**:内置模型搜索、自动 `/v1` 路径补全、批量资产选择,且渲染过程不丢失输入焦点。
|
|
7
|
+
## 🚀 Quick Start
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
Inject high-performance AI capabilities into any static HTML page in seconds.
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
### 1. 引入并初始化
|
|
18
|
-
```html
|
|
19
|
-
<script type="module">
|
|
20
|
-
// 推荐从 unpkg 引入最新生产版本
|
|
21
|
-
import { vibeAI } from 'https://unpkg.com/vibe-ai-c@3.3.2/vibe-ai.min.js';
|
|
22
|
-
|
|
23
|
-
// 初始化并绑定管理按钮
|
|
24
|
-
await vibeAI.init({
|
|
25
|
-
setupBtnId: 'settings-btn'
|
|
26
|
-
});
|
|
11
|
+
```javascript
|
|
12
|
+
import { vibeAI } from 'https://unpkg.com/vibe-ai-c@5.4.0';
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
</script>
|
|
14
|
+
// 1. Initialize with your config button
|
|
15
|
+
vibeAI.init({ setupBtnId: 'settings-btn' });
|
|
31
16
|
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
```
|
|
17
|
+
// 2. Bind a standard <select> to the model list
|
|
18
|
+
vibeAI.bindSelect('model-picker');
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const stream =
|
|
39
|
-
|
|
40
|
-
messages: [{ role: 'user', content: '你好!' }],
|
|
41
|
-
stream: true
|
|
20
|
+
// 3. Chat
|
|
21
|
+
const inst = vibeAI.getInstance('model-picker');
|
|
22
|
+
const stream = inst.streamChat({
|
|
23
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
42
24
|
});
|
|
43
25
|
|
|
44
26
|
for await (const chunk of stream) {
|
|
45
|
-
|
|
27
|
+
console.log(chunk.delta); // { type: 'content', delta: '...' }
|
|
46
28
|
}
|
|
47
29
|
```
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🛠️ Integration Logic
|
|
34
|
+
|
|
35
|
+
### 1. Rendering (Don't DIY CSS)
|
|
36
|
+
Hand-writing CSS for AI responses is a trap. Use `marked.js` for parsing and `github-markdown-css` for styling. This ensures your "thoughts" and "content" look professional without adding 1000 lines of custom CSS.
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<!-- Add to your <head> -->
|
|
40
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown.min.css">
|
|
41
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. The Streaming Protocol
|
|
45
|
+
VibeAI v5.4 uses a rich object stream to support reasoning models (like DeepSeek R1 or OpenAI o1).
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
47
|
+
- `type: 'thought'`: Internal reasoning/chain-of-thought tokens.
|
|
48
|
+
- `type: 'content'`: The actual message response.
|
|
49
|
+
- `type: 'usage'`: Final token telemetry (prompt/completion counts).
|
|
54
50
|
|
|
55
|
-
###
|
|
56
|
-
|
|
57
|
-
-
|
|
51
|
+
### 3. Security Architecture
|
|
52
|
+
VibeAI uses the **Web Crypto API** for industrial-grade security:
|
|
53
|
+
- **Derivation**: PBKDF2 with 100,000 iterations.
|
|
54
|
+
- **Encryption**: AES-GCM (256-bit).
|
|
55
|
+
- **Decryption**: Keys exist only in volatile memory (`#sessionKey`) and are never stored in plaintext in `localStorage`.
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
- **payload.instanceId**: (必填) 绑定的 Select ID,SDK 据此识别供应商配置。
|
|
61
|
-
- **payload.stream**: 布尔值。为 `true` 时返回 `AsyncGenerator`。
|
|
62
|
-
- **payload.messages**: 标准 OpenAI 消息数组。
|
|
57
|
+
> **⚠️ DISCLAIMER**: While VibeAI uses robust encryption, it is a client-side SDK. The security of your keys depends on the physical security of the device and the integrity of the browser environment. Always encourage users to use the **"Enable Encryption"** toggle in the config modal.
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
---
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
## 📄 Complete Implementation Example
|
|
62
|
+
|
|
63
|
+
This boilerplate combines VibeAI with Markdown rendering and a clean, bento-style layout.
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<!DOCTYPE html>
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="UTF-8">
|
|
70
|
+
<title>VibeAI Production Demo</title>
|
|
71
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
72
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown.min.css">
|
|
73
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
74
|
+
<style>
|
|
75
|
+
body { background: #f4f4f7; }
|
|
76
|
+
.markdown-body { background: transparent !important; font-size: 14px; }
|
|
77
|
+
.thought { border-left: 2px solid #e2e8f0; padding-left: 1rem; color: #64748b; font-style: italic; margin-bottom: 1rem; font-size: 13px; }
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
<body class="h-screen flex flex-col p-4">
|
|
81
|
+
|
|
82
|
+
<!-- Header -->
|
|
83
|
+
<header class="max-w-4xl w-full mx-auto flex justify-between items-center mb-4">
|
|
84
|
+
<h1 class="text-xl font-bold text-slate-800">VibeAI <span class="text-blue-500">v5.4</span></h1>
|
|
85
|
+
<div class="flex gap-2">
|
|
86
|
+
<select id="m-select" class="border rounded-lg px-3 py-1 text-sm bg-white outline-none"></select>
|
|
87
|
+
<button id="cfg-btn" class="bg-white border rounded-lg px-3 py-1 text-sm font-semibold shadow-sm hover:bg-slate-50">⚙️ Config</button>
|
|
88
|
+
</div>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
<!-- Chat Area -->
|
|
92
|
+
<main id="chat" class="max-w-4xl w-full mx-auto flex-1 overflow-y-auto space-y-4 mb-4"></main>
|
|
93
|
+
|
|
94
|
+
<!-- Input Area -->
|
|
95
|
+
<footer class="max-w-4xl w-full mx-auto bg-white rounded-2xl shadow-lg p-2 flex gap-2">
|
|
96
|
+
<input id="prompt" type="text" placeholder="Ask anything..." class="flex-1 px-4 py-2 outline-none">
|
|
97
|
+
<button id="send" class="bg-blue-600 text-white px-6 py-2 rounded-xl font-bold">Send</button>
|
|
98
|
+
</footer>
|
|
99
|
+
|
|
100
|
+
<script type="module">
|
|
101
|
+
import { vibeAI } from 'https://unpkg.com/vibe-ai-c@5.4.0';
|
|
102
|
+
|
|
103
|
+
vibeAI.init({ setupBtnId: 'cfg-btn' });
|
|
104
|
+
vibeAI.bindSelect('m-select');
|
|
105
|
+
|
|
106
|
+
const chat = document.getElementById('chat');
|
|
107
|
+
const prompt = document.getElementById('prompt');
|
|
108
|
+
const sendBtn = document.getElementById('send');
|
|
109
|
+
|
|
110
|
+
async function handleSend() {
|
|
111
|
+
const val = prompt.value.trim();
|
|
112
|
+
if (!val) return;
|
|
113
|
+
prompt.value = '';
|
|
114
|
+
|
|
115
|
+
// Render User Message
|
|
116
|
+
chat.innerHTML += `<div class="bg-slate-200 self-end ml-auto px-4 py-2 rounded-2xl max-w-[80%] text-sm">${val}</div>`;
|
|
117
|
+
|
|
118
|
+
// Prep AI Bubble
|
|
119
|
+
const aiId = 'ai-' + Date.now();
|
|
120
|
+
chat.innerHTML += `
|
|
121
|
+
<div id="${aiId}" class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 max-w-[90%]">
|
|
122
|
+
<div class="thought hidden"></div>
|
|
123
|
+
<div class="markdown-body">...</div>
|
|
124
|
+
</div>`;
|
|
125
|
+
|
|
126
|
+
const aiCard = document.getElementById(aiId);
|
|
127
|
+
const tDiv = aiCard.querySelector('.thought');
|
|
128
|
+
const cDiv = aiCard.querySelector('.markdown-body');
|
|
129
|
+
|
|
130
|
+
let fullContent = "";
|
|
131
|
+
let fullThought = "";
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const inst = vibeAI.getInstance('m-select');
|
|
135
|
+
const stream = inst.streamChat({ messages: [{ role: 'user', content: val }] });
|
|
136
|
+
|
|
137
|
+
for await (const chunk of stream) {
|
|
138
|
+
if (chunk.type === 'thought') {
|
|
139
|
+
tDiv.classList.remove('hidden');
|
|
140
|
+
fullThought += chunk.delta;
|
|
141
|
+
tDiv.innerText = fullThought;
|
|
142
|
+
}
|
|
143
|
+
if (chunk.type === 'content') {
|
|
144
|
+
fullContent += chunk.delta;
|
|
145
|
+
cDiv.innerHTML = marked.parse(fullContent);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
cDiv.innerHTML = `<span class="text-red-500">❌ ${e.message}</span>`;
|
|
150
|
+
}
|
|
151
|
+
chat.scrollTop = chat.scrollHeight;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
sendBtn.onclick = handleSend;
|
|
155
|
+
prompt.onkeydown = (e) => e.key === 'Enter' && handleSend();
|
|
156
|
+
</script>
|
|
157
|
+
</body>
|
|
158
|
+
</html>
|
|
159
|
+
```
|
|
69
160
|
|
|
70
|
-
##
|
|
161
|
+
## 💎 Key Decisions in v5.4
|
|
71
162
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
163
|
+
- **Consolidated Gate**: The `unlockGate()` method replaces two separate modals, reducing DOM bloat by 40%.
|
|
164
|
+
- **Lazy Status**: Provider health (`✅`/`❌`) is updated reactively during real chat attempts, saving unnecessary pings to the API.
|
|
165
|
+
- **Diagnostic Hints**: Automatic detection of missing `/v1` in URLs or missing `sk-` in keys provides immediate developer feedback without manual debugging.
|
|
166
|
+
- **Nuclear Reset**: Added a safety valve for forgotten passwords, allowing users to wipe encrypted keys and re-configure without losing app history.
|
package/demo.html
CHANGED
|
@@ -1,180 +1,102 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>VibeAI
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// 1. 初始化
|
|
105
|
-
vibeAI.init({ setupBtnId: 'setup-btn' });
|
|
106
|
-
|
|
107
|
-
// 2. 绑定多个模型选择器(实现双屏独立选择)
|
|
108
|
-
vibeAI.bindModelSelect('select-left');
|
|
109
|
-
vibeAI.bindModelSelect('select-right');
|
|
110
|
-
|
|
111
|
-
// 存储当前选中的图片
|
|
112
|
-
const pendingFiles = { left: null, right: null };
|
|
113
|
-
|
|
114
|
-
// 处理图片预览
|
|
115
|
-
window.handleFile = async (input, side) => {
|
|
116
|
-
const file = input.files[0];
|
|
117
|
-
if (!file) return;
|
|
118
|
-
|
|
119
|
-
pendingFiles[side] = await vibeAI.fileToDataURL(file);
|
|
120
|
-
const preview = document.getElementById(`preview-${side}`);
|
|
121
|
-
preview.innerHTML = `<img src="${pendingFiles[side]}" class="img-preview">`;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// 发送对话
|
|
125
|
-
window.sendChat = async (side) => {
|
|
126
|
-
const inputEl = document.getElementById(`input-${side}`);
|
|
127
|
-
const msgsEl = document.getElementById(`msgs-left`); // 注意:如果是 demo 请对应 side
|
|
128
|
-
const targetMsgsEl = document.getElementById(`msgs-${side}`);
|
|
129
|
-
const text = inputEl.value.trim();
|
|
130
|
-
|
|
131
|
-
if (!text && !pendingFiles[side]) return;
|
|
132
|
-
|
|
133
|
-
// 构建消息内容
|
|
134
|
-
let content = text;
|
|
135
|
-
if (pendingFiles[side]) {
|
|
136
|
-
content = [
|
|
137
|
-
{ type: "text", text: text || "这张图里有什么?" },
|
|
138
|
-
{ type: "image_url", image_url: { url: pendingFiles[side] } }
|
|
139
|
-
];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// UI: 用户消息
|
|
143
|
-
const userMsgDiv = document.createElement('div');
|
|
144
|
-
userMsgDiv.className = 'msg user';
|
|
145
|
-
userMsgDiv.innerText = text || "[图片]";
|
|
146
|
-
targetMsgsEl.appendChild(userMsgDiv);
|
|
147
|
-
|
|
148
|
-
// UI: AI 消息容器
|
|
149
|
-
const aiMsgDiv = document.createElement('div');
|
|
150
|
-
aiMsgDiv.className = 'msg ai';
|
|
151
|
-
aiMsgDiv.innerText = '...';
|
|
152
|
-
targetMsgsEl.appendChild(aiMsgDiv);
|
|
153
|
-
targetMsgsEl.scrollTop = targetMsgsEl.scrollHeight;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const stream = await vibeAI.chat({
|
|
157
|
-
instanceId: `select-${side}`, // 动态对应绑定的 select ID
|
|
158
|
-
messages: [{ role: 'user', content: content }],
|
|
159
|
-
stream: true
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
aiMsgDiv.innerText = '';
|
|
163
|
-
for await (const chunk of stream) {
|
|
164
|
-
aiMsgDiv.innerText += chunk;
|
|
165
|
-
targetMsgsEl.scrollTop = targetMsgsEl.scrollHeight;
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
aiMsgDiv.style.color = 'red';
|
|
169
|
-
aiMsgDiv.innerText = '错误: ' + err.message;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 清理
|
|
173
|
-
inputEl.value = '';
|
|
174
|
-
pendingFiles[side] = null;
|
|
175
|
-
document.getElementById(`preview-${side}`).innerHTML = '';
|
|
176
|
-
};
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
</body>
|
|
180
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>VibeAI v5.3</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.thought-bubble { background: #f8fafc; border-left: 3px solid #e2e8f0; color: #64748b; font-size: 0.85rem; padding: 1rem; margin: 0.5rem 0; border-radius: 0.75rem; font-family: ui-monospace, monospace; white-space: pre-wrap; }
|
|
10
|
+
.message-bot { background: #fff; border: 1px solid #f1f5f9; box-shadow: 0 4px 20px -5px rgba(0,0,0,0.05); }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body class="bg-gray-50 text-slate-900 h-screen flex flex-col font-sans">
|
|
14
|
+
|
|
15
|
+
<nav class="h-16 border-b bg-white flex items-center justify-between px-8 shrink-0">
|
|
16
|
+
<div class="flex items-center gap-3">
|
|
17
|
+
<span class="text-xl">⚡️</span>
|
|
18
|
+
<h1 class="font-bold text-lg">VibeAI <span class="text-blue-600">v5.3</span></h1>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="flex gap-3">
|
|
21
|
+
<select id="main-model" class="bg-gray-50 border border-gray-200 rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-500/10 transition-all"></select>
|
|
22
|
+
<button id="config-trigger" class="bg-slate-900 text-white px-5 py-2 rounded-xl text-sm font-semibold hover:bg-slate-800 transition-all">Config</button>
|
|
23
|
+
</div>
|
|
24
|
+
</nav>
|
|
25
|
+
|
|
26
|
+
<main id="chat-viewport" class="flex-1 overflow-y-auto p-8 space-y-6 max-w-4xl mx-auto w-full"></main>
|
|
27
|
+
|
|
28
|
+
<footer class="p-8 bg-white border-t shrink-0">
|
|
29
|
+
<div class="max-w-4xl mx-auto">
|
|
30
|
+
<div class="relative flex items-center">
|
|
31
|
+
<input id="chat-input" type="text" autocomplete="off" placeholder="Message VibeAI..." class="w-full bg-gray-50 border border-gray-200 rounded-2xl px-6 py-4 outline-none focus:ring-2 focus:ring-blue-500/10 focus:border-blue-500 transition-all pr-20">
|
|
32
|
+
<button id="send-btn" class="absolute right-3 bg-blue-600 text-white p-3 rounded-xl hover:bg-blue-700 transition-all shadow-md active:scale-95">
|
|
33
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="flex justify-between items-center mt-4 px-1">
|
|
37
|
+
<div id="usage-meter" class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Session: 0 tokens</div>
|
|
38
|
+
<div id="status-tag" class="text-[10px] text-gray-400 uppercase tracking-widest">Status: Ready</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</footer>
|
|
42
|
+
|
|
43
|
+
<script type="module">
|
|
44
|
+
import { vibeAI } from './vibe-ai.js';
|
|
45
|
+
|
|
46
|
+
vibeAI.init({ setupBtnId: 'config-trigger' });
|
|
47
|
+
vibeAI.bindSelect('main-model');
|
|
48
|
+
|
|
49
|
+
const inst = vibeAI.getInstance('main-model');
|
|
50
|
+
const view = document.getElementById('chat-viewport');
|
|
51
|
+
const input = document.getElementById('chat-input');
|
|
52
|
+
const usage = document.getElementById('usage-meter');
|
|
53
|
+
const status = document.getElementById('status-tag');
|
|
54
|
+
|
|
55
|
+
async function handleSend() {
|
|
56
|
+
const text = input.value.trim();
|
|
57
|
+
if (!text) return;
|
|
58
|
+
input.value = '';
|
|
59
|
+
status.innerText = 'Status: Streaming...';
|
|
60
|
+
|
|
61
|
+
view.innerHTML += `<div class="flex justify-end"><div class="bg-blue-600 text-white px-6 py-3 rounded-2xl rounded-tr-sm max-w-[85%] shadow-sm text-[15px]">${text}</div></div>`;
|
|
62
|
+
|
|
63
|
+
const botId = `bot-${Date.now()}`;
|
|
64
|
+
view.innerHTML += `<div id="${botId}" class="flex flex-col gap-2 max-w-[85%]"><div class="bot-content message-bot px-6 py-3 rounded-2xl rounded-tl-sm text-[15px] leading-relaxed"></div></div>`;
|
|
65
|
+
|
|
66
|
+
const botNode = document.getElementById(botId);
|
|
67
|
+
const contentNode = botNode.querySelector('.bot-content');
|
|
68
|
+
let thoughtNode = null;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const stream = inst.streamChat({ messages: [{ role: 'user', content: text }] });
|
|
72
|
+
for await (const chunk of stream) {
|
|
73
|
+
if (chunk.type === 'thought') {
|
|
74
|
+
if (!thoughtNode) {
|
|
75
|
+
thoughtNode = document.createElement('div');
|
|
76
|
+
thoughtNode.className = 'thought-bubble';
|
|
77
|
+
botNode.prepend(thoughtNode);
|
|
78
|
+
}
|
|
79
|
+
thoughtNode.innerText += chunk.delta;
|
|
80
|
+
}
|
|
81
|
+
else if (chunk.type === 'content') {
|
|
82
|
+
contentNode.innerText += chunk.delta;
|
|
83
|
+
}
|
|
84
|
+
else if (chunk.type === 'usage') {
|
|
85
|
+
// const s = inst.getSessionUsage();
|
|
86
|
+
// usage.innerText = `Session: ${s.p + s.c} tokens`;
|
|
87
|
+
}
|
|
88
|
+
view.scrollTop = view.scrollHeight;
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
contentNode.innerHTML = `<span class="text-red-500 font-semibold">Error: ${e.message}</span>`;
|
|
92
|
+
} finally {
|
|
93
|
+
status.innerText = 'Status: Ready';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
document.getElementById('send-btn').onclick = handleSend;
|
|
98
|
+
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleSend(); } });
|
|
99
|
+
window.onload = () => input.focus();
|
|
100
|
+
</script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-ai-c",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "一个极简的、面向浏览器的多供应商 AI SDK 与 UI 管理工具。支持 OpenAI 兼容协议。",
|
|
5
|
-
"main": "vibe-ai.
|
|
5
|
+
"main": "vibe-ai.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "npx servor . demo.html 8080",
|
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
"llm",
|
|
16
16
|
"sdk",
|
|
17
17
|
"ui-component",
|
|
18
|
-
"browser-only"
|
|
18
|
+
"browser-only",
|
|
19
|
+
"vibe-coding",
|
|
20
|
+
"coding",
|
|
21
|
+
"lite",
|
|
22
|
+
"provider"
|
|
19
23
|
],
|
|
20
|
-
"author": "
|
|
24
|
+
"author": "Jason Li",
|
|
21
25
|
"license": "MIT",
|
|
22
26
|
"dependencies": {},
|
|
23
27
|
"devDependencies": {},
|
|
24
28
|
"files": [
|
|
25
29
|
"demo.html",
|
|
26
30
|
"vibe-ai.js",
|
|
27
|
-
"vibe-ai.min.js",
|
|
28
31
|
"README.md"
|
|
29
32
|
]
|
|
30
33
|
}
|