bunnyquery 1.0.8 → 1.0.10
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/bunnyquery.css +51 -0
- package/bunnyquery.js +197 -35
- package/package.json +1 -1
package/bunnyquery.css
CHANGED
|
@@ -143,6 +143,23 @@
|
|
|
143
143
|
white-space: nowrap;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
.bq-ai-status {
|
|
147
|
+
display: inline-flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
max-width: min(56vw, 26rem);
|
|
150
|
+
padding: 0.12rem 0.45rem;
|
|
151
|
+
border: 1px solid var(--bq-line);
|
|
152
|
+
background: #f7f7f7;
|
|
153
|
+
color: var(--bq-muted);
|
|
154
|
+
font-size: 0.72rem;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
letter-spacing: 0.01em;
|
|
157
|
+
line-height: 1.25;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
text-overflow: ellipsis;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
.bq-title-right {
|
|
147
164
|
display: flex;
|
|
148
165
|
align-items: center;
|
|
@@ -185,6 +202,40 @@
|
|
|
185
202
|
font-size: 0.85rem;
|
|
186
203
|
}
|
|
187
204
|
|
|
205
|
+
.bq-link-button {
|
|
206
|
+
display: inline-flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: 0.35rem;
|
|
209
|
+
padding: 0.35rem 0.65rem;
|
|
210
|
+
margin: 0.2rem 0.2rem 0.2rem 0;
|
|
211
|
+
border: 1px solid var(--bq-line);
|
|
212
|
+
border-radius: 0.15rem;
|
|
213
|
+
background: var(--bq-paper);
|
|
214
|
+
color: var(--bq-pink);
|
|
215
|
+
font-size: 0.78rem;
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
text-decoration: none;
|
|
218
|
+
white-space: nowrap;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
text-overflow: ellipsis;
|
|
221
|
+
max-width: 100%;
|
|
222
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.bq-link-button:hover {
|
|
227
|
+
background: var(--bq-hover-bg);
|
|
228
|
+
border-color: var(--bq-pink);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.bq-link-button:visited {
|
|
232
|
+
color: #a01848;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.bq-link-button:visited:hover {
|
|
236
|
+
color: var(--bq-pink);
|
|
237
|
+
}
|
|
238
|
+
|
|
188
239
|
.bq-link {
|
|
189
240
|
color: var(--bq-pink);
|
|
190
241
|
text-decoration: none;
|
package/bunnyquery.js
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
OPENAI_RESPONSES_API_URL: 'https://api.openai.com/v1/responses',
|
|
51
51
|
MAX_TOKENS: 25000,
|
|
52
52
|
POLL_INTERVAL: 1500,
|
|
53
|
-
PENDING_POLL_INTERVAL_MS:
|
|
53
|
+
PENDING_POLL_INTERVAL_MS: 1000,
|
|
54
54
|
ATTACHMENT_URL_EXPIRES_SECONDS: 600, // 10 min
|
|
55
55
|
};
|
|
56
56
|
|
|
@@ -66,6 +66,101 @@
|
|
|
66
66
|
// init()) is reflected in subsequent send/auth calls.
|
|
67
67
|
const _mcpBaseUrl = () => (global.BunnyQuery && global.BunnyQuery.MCP_BASE_URL) || DEFAULTS.MCP_BASE_URL;
|
|
68
68
|
|
|
69
|
+
// ----- link parsing helpers -----
|
|
70
|
+
// Matches markdown links `[label](url)` and bare https URLs.
|
|
71
|
+
// Handles balanced parentheses in URLs (e.g. for filenames like image (1).png).
|
|
72
|
+
const LINK_REGEX = /\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]+|\([^\s()]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
|
|
73
|
+
|
|
74
|
+
const normalizeBareUrl = (url) => {
|
|
75
|
+
if (!url) return '';
|
|
76
|
+
let out = url;
|
|
77
|
+
|
|
78
|
+
// Trim sentence punctuation that often trails pasted URLs.
|
|
79
|
+
out = out.replace(/[.,;:!?]+$/, '');
|
|
80
|
+
|
|
81
|
+
// Remove unmatched closing wrappers but preserve balanced pairs.
|
|
82
|
+
const trimUnmatched = (openCh, closeCh) => {
|
|
83
|
+
while (out.endsWith(closeCh)) {
|
|
84
|
+
const openCount = (out.match(new RegExp('\\' + openCh, 'g')) || []).length;
|
|
85
|
+
const closeCount = (out.match(new RegExp('\\' + closeCh, 'g')) || []).length;
|
|
86
|
+
if (closeCount > openCount) {
|
|
87
|
+
out = out.slice(0, -1);
|
|
88
|
+
} else {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
trimUnmatched('(', ')');
|
|
95
|
+
trimUnmatched('[', ']');
|
|
96
|
+
trimUnmatched('{', '}');
|
|
97
|
+
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const truncateLabel = (label, maxLen = 50, options = {}) => {
|
|
102
|
+
const { stripTrailing = false } = options;
|
|
103
|
+
if (!label) return '';
|
|
104
|
+
const trimmed = label.trim();
|
|
105
|
+
const unwrapped = trimmed
|
|
106
|
+
.replace(/^\*\*(.*)\*\*$/, '$1')
|
|
107
|
+
.replace(/^__(.*)__$/, '$1');
|
|
108
|
+
const cleaned = stripTrailing
|
|
109
|
+
? unwrapped.replace(/[`'"*\]\>.,;:!?]+$/, '')
|
|
110
|
+
: unwrapped;
|
|
111
|
+
if (cleaned.length <= maxLen) return cleaned;
|
|
112
|
+
return cleaned.slice(0, maxLen - 1) + '\u2026'; // ellipsis
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Helper: parse markdown links from a text segment
|
|
116
|
+
const parseMsgLinks = (text, offsetInContent, parts) => {
|
|
117
|
+
LINK_REGEX.lastIndex = 0;
|
|
118
|
+
let last = 0;
|
|
119
|
+
let lm;
|
|
120
|
+
while ((lm = LINK_REGEX.exec(text)) !== null) {
|
|
121
|
+
if (lm.index > last) {
|
|
122
|
+
const prevText = text.slice(last, lm.index);
|
|
123
|
+
if (parts.length && parts[parts.length - 1].type === 'text') {
|
|
124
|
+
parts[parts.length - 1].text += prevText;
|
|
125
|
+
} else {
|
|
126
|
+
parts.push({ type: 'text', text: prevText });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Markdown link: [label](url)
|
|
131
|
+
if (lm[1] && lm[2]) {
|
|
132
|
+
const label = truncateLabel(lm[1]);
|
|
133
|
+
const href = lm[2];
|
|
134
|
+
parts.push({
|
|
135
|
+
type: 'link',
|
|
136
|
+
label: label,
|
|
137
|
+
href: href,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Bare URL
|
|
141
|
+
else if (lm[3]) {
|
|
142
|
+
const href = normalizeBareUrl(lm[3]);
|
|
143
|
+
const label = truncateLabel(href);
|
|
144
|
+
parts.push({
|
|
145
|
+
type: 'link',
|
|
146
|
+
label: label,
|
|
147
|
+
href: href,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
last = lm.index + lm[0].length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (last < text.length) {
|
|
155
|
+
const tail = text.slice(last);
|
|
156
|
+
if (parts.length && parts[parts.length - 1].type === 'text') {
|
|
157
|
+
parts[parts.length - 1].text += tail;
|
|
158
|
+
} else {
|
|
159
|
+
parts.push({ type: 'text', text: tail });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
69
164
|
// ----- file-fence helpers (mirrors agent.vue) ---------------------------
|
|
70
165
|
// Bare language tags we treat as downloadable files. Maps to the
|
|
71
166
|
// extension used in the synthesized filename. Mirrors the file types the
|
|
@@ -152,12 +247,13 @@
|
|
|
152
247
|
const parts = [];
|
|
153
248
|
if (!content) return parts;
|
|
154
249
|
|
|
155
|
-
//
|
|
156
|
-
const
|
|
250
|
+
// First pass: extract file fences (existing logic)
|
|
251
|
+
const fenceParts = [];
|
|
252
|
+
const fenceMatches = [];
|
|
157
253
|
FENCE_FILENAME_RE.lastIndex = 0;
|
|
158
254
|
let mm;
|
|
159
255
|
while ((mm = FENCE_FILENAME_RE.exec(content)) !== null) {
|
|
160
|
-
|
|
256
|
+
fenceMatches.push({ index: mm.index, length: mm[0].length, filename: mm[1], body: mm[2], kind: 'filename' });
|
|
161
257
|
}
|
|
162
258
|
FENCE_LANG_RE.lastIndex = 0;
|
|
163
259
|
let bareCounters = _bareFenceNameCounters.get(msgKey);
|
|
@@ -171,7 +267,7 @@
|
|
|
171
267
|
const ext = FENCE_LANGUAGE_EXTENSIONS[lang];
|
|
172
268
|
const occurrence = bareSeen[ext] || 0;
|
|
173
269
|
bareSeen[ext] = occurrence + 1;
|
|
174
|
-
|
|
270
|
+
fenceMatches.push({
|
|
175
271
|
index: mm.index,
|
|
176
272
|
length: mm[0].length,
|
|
177
273
|
filename: _bareFilenameFor(msgKey, ext, occurrence),
|
|
@@ -179,30 +275,35 @@
|
|
|
179
275
|
kind: 'lang',
|
|
180
276
|
});
|
|
181
277
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
for (const m of matches) {
|
|
188
|
-
if (dedup.length && dedup[dedup.length - 1].index === m.index) continue;
|
|
189
|
-
dedup.push(m);
|
|
278
|
+
fenceMatches.sort((a, b) => a.index - b.index || (a.kind === 'filename' ? -1 : 1));
|
|
279
|
+
const dedupFences = [];
|
|
280
|
+
for (const m of fenceMatches) {
|
|
281
|
+
if (dedupFences.length && dedupFences[dedupFences.length - 1].index === m.index) continue;
|
|
282
|
+
dedupFences.push(m);
|
|
190
283
|
}
|
|
191
284
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
285
|
+
// Second pass: extract links from text segments between fences
|
|
286
|
+
let lastFenceEnd = 0;
|
|
287
|
+
for (const fence of dedupFences) {
|
|
288
|
+
// Parse links in text before this fence
|
|
289
|
+
const beforeFence = content.slice(lastFenceEnd, fence.index);
|
|
290
|
+
if (beforeFence) {
|
|
291
|
+
parseMsgLinks(beforeFence, lastFenceEnd, parts);
|
|
196
292
|
}
|
|
293
|
+
// Add the fence
|
|
197
294
|
parts.push({
|
|
198
295
|
type: 'file',
|
|
199
|
-
filename:
|
|
200
|
-
href: _getOrCreateFileHref(
|
|
296
|
+
filename: fence.filename,
|
|
297
|
+
href: _getOrCreateFileHref(fence.filename, fence.body),
|
|
201
298
|
});
|
|
202
|
-
|
|
299
|
+
lastFenceEnd = fence.index + fence.length;
|
|
203
300
|
}
|
|
204
301
|
|
|
205
|
-
|
|
302
|
+
// Parse links in remaining text after last fence
|
|
303
|
+
let tail = content.slice(lastFenceEnd);
|
|
304
|
+
if (tail) {
|
|
305
|
+
parseMsgLinks(tail, lastFenceEnd, parts);
|
|
306
|
+
}
|
|
206
307
|
|
|
207
308
|
// Mid-typewriter: hide raw source for a still-open fence so the user
|
|
208
309
|
// doesn't watch literal code stream by character-by-character.
|
|
@@ -216,12 +317,15 @@
|
|
|
216
317
|
: null;
|
|
217
318
|
if (open) {
|
|
218
319
|
const before = tail.slice(0, open.match.index);
|
|
219
|
-
if (before
|
|
220
|
-
|
|
221
|
-
|
|
320
|
+
if (before && parts.length && parts[parts.length - 1].type === 'text') {
|
|
321
|
+
parts[parts.length - 1].text += '\n[generating ' + open.label + '...]';
|
|
322
|
+
} else if (before) {
|
|
323
|
+
parts.push({ type: 'text', text: before + '\n[generating ' + open.label + '...]' });
|
|
324
|
+
} else {
|
|
325
|
+
parts.push({ type: 'text', text: '\n[generating ' + open.label + '...]' });
|
|
326
|
+
}
|
|
222
327
|
}
|
|
223
328
|
}
|
|
224
|
-
if (tail) parts.push({ type: 'text', text: tail });
|
|
225
329
|
return parts;
|
|
226
330
|
};
|
|
227
331
|
|
|
@@ -518,7 +622,7 @@
|
|
|
518
622
|
const msgList = messages && messages.length
|
|
519
623
|
? messages
|
|
520
624
|
: [{ role: 'user', content: prompt }];
|
|
521
|
-
|
|
625
|
+
const request = {
|
|
522
626
|
clientSecretName: 'claude',
|
|
523
627
|
poll: DEFAULTS.POLL_INTERVAL,
|
|
524
628
|
queue: service,
|
|
@@ -558,7 +662,8 @@
|
|
|
558
662
|
},
|
|
559
663
|
],
|
|
560
664
|
},
|
|
561
|
-
}
|
|
665
|
+
};
|
|
666
|
+
return skapi.clientSecretRequest(request);
|
|
562
667
|
}
|
|
563
668
|
|
|
564
669
|
function buildOpenAIRequest(skapi, { service, owner, prompt, messages, system, model }) {
|
|
@@ -798,6 +903,8 @@
|
|
|
798
903
|
this.pendingTimer = null;
|
|
799
904
|
this._pollingPending = false;
|
|
800
905
|
this._loadingOlder = false;
|
|
906
|
+
this._loadingFirstHistory = false;
|
|
907
|
+
this._initialHistoryLoaded = false;
|
|
801
908
|
|
|
802
909
|
this.refs = {};
|
|
803
910
|
|
|
@@ -823,6 +930,16 @@
|
|
|
823
930
|
|
|
824
931
|
// ---- bootstrap order ----------------------------------------------
|
|
825
932
|
async _bootstrap() {
|
|
933
|
+
// 0. Refresh connection info to ensure latest configuration.
|
|
934
|
+
try {
|
|
935
|
+
const info = await this.skapi.getConnectionInfo({ refresh: true });
|
|
936
|
+
const ai = String(info.ai_agent || '').split('#');
|
|
937
|
+
this.platform = (ai[0] || '').toLowerCase(); // 'claude' | 'openai'
|
|
938
|
+
this.model = ai[1] || '';
|
|
939
|
+
} catch (_) {
|
|
940
|
+
// Non-fatal; continue with existing connection info
|
|
941
|
+
}
|
|
942
|
+
|
|
826
943
|
// 1. Validate that the project has a configured AI platform.
|
|
827
944
|
if (!this.platform) {
|
|
828
945
|
this._renderFatal('This project does not have an AI agent configured yet. Please ask the project owner to set up an AI agent in the project settings page.');
|
|
@@ -964,8 +1081,20 @@
|
|
|
964
1081
|
const titleText = this.projectName
|
|
965
1082
|
? this.projectName
|
|
966
1083
|
: 'BunnyQuery';
|
|
1084
|
+
const platformLabel = this.platform === 'claude'
|
|
1085
|
+
? 'Claude'
|
|
1086
|
+
: this.platform === 'openai'
|
|
1087
|
+
? 'OpenAI'
|
|
1088
|
+
: 'Unknown';
|
|
1089
|
+
const modelLabel = this.model || (
|
|
1090
|
+
this.platform === 'claude'
|
|
1091
|
+
? DEFAULTS.DEFAULT_CLAUDE_MODEL
|
|
1092
|
+
: this.platform === 'openai'
|
|
1093
|
+
? DEFAULTS.DEFAULT_OPENAI_MODEL
|
|
1094
|
+
: 'N/A'
|
|
1095
|
+
);
|
|
967
1096
|
const titleLeft = $('div', { class: 'bq-title-left' }, [
|
|
968
|
-
$('
|
|
1097
|
+
$('span', { class: 'bq-ai-status', title: `AI: ${platformLabel} / ${modelLabel}` }, `${platformLabel} · ${modelLabel}`),
|
|
969
1098
|
]);
|
|
970
1099
|
|
|
971
1100
|
const clearIcon = $('button', {
|
|
@@ -1008,7 +1137,7 @@
|
|
|
1008
1137
|
const textarea = $('textarea', {
|
|
1009
1138
|
class: 'bq-input',
|
|
1010
1139
|
rows: '1',
|
|
1011
|
-
placeholder:
|
|
1140
|
+
placeholder: `Ask anything about ${this.projectName || 'the project'}...`,
|
|
1012
1141
|
oninput: (e) => this._autoGrow(e.target),
|
|
1013
1142
|
onkeydown: (e) => {
|
|
1014
1143
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -1060,7 +1189,15 @@
|
|
|
1060
1189
|
box.scrollHeight - box.scrollTop - box.clientHeight < 80;
|
|
1061
1190
|
box.innerHTML = '';
|
|
1062
1191
|
|
|
1063
|
-
if (!this.
|
|
1192
|
+
if (!this._initialHistoryLoaded) {
|
|
1193
|
+
box.appendChild(
|
|
1194
|
+
$('div', { class: 'bq-message is-assistant bq-empty-greeting' }, [
|
|
1195
|
+
$('div', { class: 'bq-bubble' }, [
|
|
1196
|
+
$('span', { class: 'bq-loader' }, 'Loading chat history'),
|
|
1197
|
+
]),
|
|
1198
|
+
])
|
|
1199
|
+
);
|
|
1200
|
+
} else if (!this.messages.length) {
|
|
1064
1201
|
box.appendChild(
|
|
1065
1202
|
$('div', { class: 'bq-message is-assistant bq-empty-greeting' }, [
|
|
1066
1203
|
$('div', { class: 'bq-bubble' },
|
|
@@ -1093,6 +1230,15 @@
|
|
|
1093
1230
|
download: part.filename,
|
|
1094
1231
|
}, '\u2197 ' + part.filename);
|
|
1095
1232
|
bubble.appendChild(a);
|
|
1233
|
+
} else if (part.type === 'link') {
|
|
1234
|
+
const a = $('a', {
|
|
1235
|
+
class: 'bq-link-button',
|
|
1236
|
+
href: part.href,
|
|
1237
|
+
target: '_blank',
|
|
1238
|
+
rel: 'noopener noreferrer',
|
|
1239
|
+
title: part.href,
|
|
1240
|
+
}, '\u2197 ' + part.label);
|
|
1241
|
+
bubble.appendChild(a);
|
|
1096
1242
|
} else {
|
|
1097
1243
|
bubble.appendChild(document.createTextNode(part.text));
|
|
1098
1244
|
}
|
|
@@ -1108,8 +1254,17 @@
|
|
|
1108
1254
|
box.appendChild(wrapper);
|
|
1109
1255
|
});
|
|
1110
1256
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1257
|
+
|
|
1258
|
+
// Scroll to bottom if stickToBottom, or if the last message is a resolved assistant response
|
|
1259
|
+
const lastMsg = this.messages[this.messages.length - 1];
|
|
1260
|
+
if (
|
|
1261
|
+
stickToBottom ||
|
|
1262
|
+
(lastMsg && lastMsg.role === 'assistant' && !lastMsg.isPending && !lastMsg.isError)
|
|
1263
|
+
) {
|
|
1264
|
+
// Use setTimeout to ensure DOM is updated before scrolling
|
|
1265
|
+
setTimeout(() => {
|
|
1266
|
+
box.scrollTop = box.scrollHeight;
|
|
1267
|
+
}, 0);
|
|
1113
1268
|
}
|
|
1114
1269
|
|
|
1115
1270
|
// Hide the clear-history icon when there's nothing to clear.
|
|
@@ -1125,12 +1280,12 @@
|
|
|
1125
1280
|
}
|
|
1126
1281
|
|
|
1127
1282
|
_updateInputDisabled() {
|
|
1128
|
-
const busy = this.sending || this.uploading || this._anyPending();
|
|
1283
|
+
const busy = this.sending || this.uploading || this._anyPending() || this._loadingFirstHistory;
|
|
1129
1284
|
if (this.refs.textarea) {
|
|
1130
1285
|
this.refs.textarea.disabled = busy;
|
|
1131
1286
|
this.refs.textarea.placeholder = busy
|
|
1132
1287
|
? 'Request in process. Please wait...'
|
|
1133
|
-
:
|
|
1288
|
+
: `Ask anything about ${this.projectName || 'the project'}...`;
|
|
1134
1289
|
}
|
|
1135
1290
|
if (this.refs.sendBtn) this.refs.sendBtn.disabled = busy;
|
|
1136
1291
|
if (this.refs.attachBtn) this.refs.attachBtn.disabled = busy;
|
|
@@ -1315,6 +1470,10 @@
|
|
|
1315
1470
|
|
|
1316
1471
|
// ---- history ------------------------------------------------------
|
|
1317
1472
|
async _loadFirstHistoryPage() {
|
|
1473
|
+
this._loadingFirstHistory = true;
|
|
1474
|
+
this._initialHistoryLoaded = false;
|
|
1475
|
+
this._renderMessages();
|
|
1476
|
+
|
|
1318
1477
|
const fetchPage = () => getChatHistory(
|
|
1319
1478
|
this.skapi,
|
|
1320
1479
|
{ service: this.serviceId, owner: this.ownerId, platform: this.platform },
|
|
@@ -1339,10 +1498,13 @@
|
|
|
1339
1498
|
// there are no older entries the user is allowed to see.
|
|
1340
1499
|
this.endOfList = !!(res && res.endOfList) || (this._getClearedAt() > 0 && list.length === 0);
|
|
1341
1500
|
this.messages = mapHistoryToMessages(list, this.platform);
|
|
1342
|
-
this._renderMessages();
|
|
1343
1501
|
this._schedulePendingPoll();
|
|
1344
1502
|
} catch (err) {
|
|
1345
1503
|
console.error('[BunnyQuery] history load failed', err);
|
|
1504
|
+
} finally {
|
|
1505
|
+
this._loadingFirstHistory = false;
|
|
1506
|
+
this._initialHistoryLoaded = true;
|
|
1507
|
+
this._renderMessages();
|
|
1346
1508
|
}
|
|
1347
1509
|
}
|
|
1348
1510
|
|