mnfst 0.5.165 → 0.5.167
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/lib/manifest.appwrite.auth.js +69 -17
- package/lib/manifest.chat.js +270 -7
- package/lib/manifest.combobox.css +18 -1
- package/lib/manifest.css +231 -3
- package/lib/manifest.input.css +2 -2
- package/lib/manifest.integrity.json +3 -3
- package/lib/manifest.js +98 -49
- package/lib/manifest.min.css +1 -1
- package/package.json +2 -2
|
@@ -3215,29 +3215,50 @@ function mergeRoles(manifestRoles, userGeneratedRoles) {
|
|
|
3215
3215
|
return merged;
|
|
3216
3216
|
}
|
|
3217
3217
|
|
|
3218
|
-
//
|
|
3218
|
+
// Appwrite membership role tokens allow only [a-zA-Z0-9._-]. Role definitions are
|
|
3219
|
+
// keyed by display name (which may contain spaces/unicode), so we store an
|
|
3220
|
+
// Appwrite-safe slug on the membership and resolve by matching slug(token) against
|
|
3221
|
+
// slug(defName). Idempotent, and "owner" (Appwrite's intrinsic role) passes through.
|
|
3222
|
+
function roleSlug(name) {
|
|
3223
|
+
if (name === 'owner') return 'owner';
|
|
3224
|
+
return String(name)
|
|
3225
|
+
.trim()
|
|
3226
|
+
.toLowerCase()
|
|
3227
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
3228
|
+
.replace(/^-+|-+$/g, '');
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Normalize custom roles for Appwrite: slugify tokens (so space/unicode names stay
|
|
3232
|
+
// assignable) and add "owner" if any role requires it.
|
|
3219
3233
|
function normalizeRolesForAppwrite(customRoles, memberRoles, userGeneratedRoles = null) {
|
|
3220
3234
|
if (!Array.isArray(customRoles)) {
|
|
3221
3235
|
return customRoles;
|
|
3222
3236
|
}
|
|
3223
3237
|
|
|
3238
|
+
const slugged = customRoles.map(role => roleSlug(role));
|
|
3239
|
+
|
|
3224
3240
|
// Merge manifest and user-generated roles
|
|
3225
3241
|
const allRoles = mergeRoles(memberRoles, userGeneratedRoles);
|
|
3226
3242
|
|
|
3227
|
-
// If no roles config, return
|
|
3243
|
+
// If no roles config, return slugged tokens (Appwrite handles owner default)
|
|
3228
3244
|
if (!allRoles || Object.keys(allRoles).length === 0) {
|
|
3229
|
-
return
|
|
3245
|
+
return slugged;
|
|
3230
3246
|
}
|
|
3231
3247
|
|
|
3232
|
-
//
|
|
3233
|
-
|
|
3248
|
+
// Which definition slugs require owner — checked by slug so this works whether the
|
|
3249
|
+
// incoming tokens are display names or already slugged.
|
|
3250
|
+
const ownerSlugs = new Set();
|
|
3251
|
+
for (const name of Object.keys(allRoles)) {
|
|
3252
|
+
if (roleRequiresOwner(name, allRoles)) ownerSlugs.add(roleSlug(name));
|
|
3253
|
+
}
|
|
3254
|
+
const requiresOwner = slugged.some(s => ownerSlugs.has(s));
|
|
3234
3255
|
|
|
3235
3256
|
// If owner is already in the list, don't duplicate
|
|
3236
|
-
if (requiresOwner && !
|
|
3237
|
-
return [...
|
|
3257
|
+
if (requiresOwner && !slugged.includes('owner')) {
|
|
3258
|
+
return [...slugged, 'owner'];
|
|
3238
3259
|
}
|
|
3239
3260
|
|
|
3240
|
-
return
|
|
3261
|
+
return slugged;
|
|
3241
3262
|
}
|
|
3242
3263
|
|
|
3243
3264
|
// Normalize Appwrite roles for display (filter "owner" if a custom role replaces it)
|
|
@@ -3249,10 +3270,15 @@ function normalizeRolesForDisplay(appwriteRoles, memberRoles, userGeneratedRoles
|
|
|
3249
3270
|
// Merge manifest and user-generated roles
|
|
3250
3271
|
const allRoles = mergeRoles(memberRoles, userGeneratedRoles);
|
|
3251
3272
|
|
|
3252
|
-
// If custom roles are defined,
|
|
3253
|
-
//
|
|
3273
|
+
// If custom roles are defined, filter out "owner" (a background Appwrite role) and
|
|
3274
|
+
// translate slug tokens back to their display names (legacy exact-name tokens map
|
|
3275
|
+
// through slug() too; unknown tokens pass through as-is).
|
|
3254
3276
|
if (allRoles && Object.keys(allRoles).length > 0) {
|
|
3255
|
-
|
|
3277
|
+
const bySlug = {};
|
|
3278
|
+
for (const name of Object.keys(allRoles)) bySlug[roleSlug(name)] = name;
|
|
3279
|
+
return appwriteRoles
|
|
3280
|
+
.filter(role => role !== 'owner')
|
|
3281
|
+
.map(role => bySlug[roleSlug(role)] || role);
|
|
3256
3282
|
}
|
|
3257
3283
|
|
|
3258
3284
|
// If no custom roles config, show "owner" as-is (legacy behavior)
|
|
@@ -3303,9 +3329,13 @@ function hasPermission(userRoles, permission, memberRoles, userGeneratedRoles =
|
|
|
3303
3329
|
return true;
|
|
3304
3330
|
}
|
|
3305
3331
|
|
|
3306
|
-
// Check if any of the user's custom roles has this permission
|
|
3332
|
+
// Check if any of the user's custom roles has this permission. Match by slug so
|
|
3333
|
+
// display-name definitions resolve against slugged membership tokens, and legacy
|
|
3334
|
+
// exact-name tokens still resolve.
|
|
3335
|
+
const bySlug = {};
|
|
3336
|
+
for (const [name, perms] of Object.entries(allRoles)) bySlug[roleSlug(name)] = perms;
|
|
3307
3337
|
for (const roleName of customRoles) {
|
|
3308
|
-
const rolePermissions =
|
|
3338
|
+
const rolePermissions = bySlug[roleSlug(roleName)];
|
|
3309
3339
|
if (rolePermissions && Array.isArray(rolePermissions) && rolePermissions.includes(permission)) {
|
|
3310
3340
|
return true;
|
|
3311
3341
|
}
|
|
@@ -3360,6 +3390,10 @@ function initializeTeamsRoles() {
|
|
|
3360
3390
|
return getOwnerPermissions();
|
|
3361
3391
|
};
|
|
3362
3392
|
|
|
3393
|
+
store.roleSlug = function (name) {
|
|
3394
|
+
return roleSlug(name);
|
|
3395
|
+
};
|
|
3396
|
+
|
|
3363
3397
|
store.validateRoleConfig = async function () {
|
|
3364
3398
|
const appwriteConfig = await config.getAppwriteConfig();
|
|
3365
3399
|
const memberRoles = appwriteConfig?.memberRoles || null;
|
|
@@ -3532,6 +3566,7 @@ window.ManifestAppwriteAuthTeamsRoles = {
|
|
|
3532
3566
|
roleHasAllOwnerPermissions,
|
|
3533
3567
|
getUserGeneratedRoles,
|
|
3534
3568
|
mergeRoles,
|
|
3569
|
+
roleSlug,
|
|
3535
3570
|
normalizeRolesForAppwrite,
|
|
3536
3571
|
normalizeRolesForDisplay,
|
|
3537
3572
|
getPrimaryDisplayRole,
|
|
@@ -5446,7 +5481,19 @@ function initializeTeamsConvenience() {
|
|
|
5446
5481
|
const userMembership = this.currentTeamMemberships.find(
|
|
5447
5482
|
m => m.userId === this.user.$id
|
|
5448
5483
|
);
|
|
5449
|
-
|
|
5484
|
+
const raw = userMembership?.roles || [];
|
|
5485
|
+
// Membership tokens are Appwrite-safe slugs; translate them back to the
|
|
5486
|
+
// author's display names using the cached role map. "owner" and unknown
|
|
5487
|
+
// tokens pass through unchanged.
|
|
5488
|
+
const slug = this.roleSlug;
|
|
5489
|
+
const allRoles = (this.allTeamRoles && this.allTeamRoles(this.currentTeam)) || {};
|
|
5490
|
+
const keys = Object.keys(allRoles);
|
|
5491
|
+
if (typeof slug !== 'function' || !keys.length) {
|
|
5492
|
+
return raw;
|
|
5493
|
+
}
|
|
5494
|
+
const bySlug = {};
|
|
5495
|
+
for (const name of keys) bySlug[slug(name)] = name;
|
|
5496
|
+
return raw.map(r => (r === 'owner' ? 'owner' : (bySlug[slug(r)] || r)));
|
|
5450
5497
|
};
|
|
5451
5498
|
|
|
5452
5499
|
// Check if current user has a specific permission in the current team
|
|
@@ -5494,9 +5541,13 @@ function initializeTeamsConvenience() {
|
|
|
5494
5541
|
}
|
|
5495
5542
|
|
|
5496
5543
|
// Granted if any of the user's custom roles includes this permission
|
|
5497
|
-
// (built-in or custom key).
|
|
5544
|
+
// (built-in or custom key). Match by slug so slugged/legacy membership
|
|
5545
|
+
// tokens resolve against display-name definitions.
|
|
5546
|
+
const slug = this.roleSlug || (x => x);
|
|
5547
|
+
const bySlug = {};
|
|
5548
|
+
for (const [name, perms] of Object.entries(allRoles)) bySlug[slug(name)] = perms;
|
|
5498
5549
|
for (const roleName of customRoles) {
|
|
5499
|
-
const rolePermissions =
|
|
5550
|
+
const rolePermissions = bySlug[slug(roleName)];
|
|
5500
5551
|
if (Array.isArray(rolePermissions) && rolePermissions.includes(permission)) {
|
|
5501
5552
|
return true;
|
|
5502
5553
|
}
|
|
@@ -5510,7 +5561,8 @@ function initializeTeamsConvenience() {
|
|
|
5510
5561
|
return false;
|
|
5511
5562
|
}
|
|
5512
5563
|
const userRoles = this.getCurrentTeamRoles();
|
|
5513
|
-
|
|
5564
|
+
const slug = this.roleSlug || (x => x);
|
|
5565
|
+
return userRoles.some(r => slug(r) === slug(roleName));
|
|
5514
5566
|
};
|
|
5515
5567
|
|
|
5516
5568
|
// Get current user's primary role in current team
|
package/lib/manifest.chat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* https://manifestx.dev
|
|
4
4
|
/*
|
|
5
5
|
/* Handle = reactive conversation view fed by an adapter; drives intents. The
|
|
6
|
-
/* plugin transports/stores nothing — see
|
|
6
|
+
/* plugin transports/stores nothing — see templates/chat-adapter/README.md.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
(function () {
|
|
@@ -287,8 +287,11 @@
|
|
|
287
287
|
function mk(cid, channel, parts, msgs, closed) {
|
|
288
288
|
convs.set(cid, { id: cid, channel, closed: !!closed, participants: parts, messages: msgs });
|
|
289
289
|
}
|
|
290
|
-
const m = (cid, author, text, minsAgo, extra) =>
|
|
291
|
-
Object.assign({ id: id('m'), conversationId: cid, author, body: { text }, ts: t(minsAgo), status: 'delivered' }, extra || {});
|
|
290
|
+
const m = (cid, author, text, minsAgo, extra) => {
|
|
291
|
+
const msg = Object.assign({ id: id('m'), conversationId: cid, author, body: { text }, ts: t(minsAgo), status: 'delivered' }, extra || {});
|
|
292
|
+
if (msg.media) { msg.body.media = msg.media; delete msg.media; } // media lives on body
|
|
293
|
+
return msg;
|
|
294
|
+
};
|
|
292
295
|
|
|
293
296
|
// 1:1 AI co-pilot
|
|
294
297
|
mk('dm-ai', 'webchat', [me, bot], [
|
|
@@ -296,6 +299,26 @@
|
|
|
296
299
|
m('dm-ai', me, 'How many free credits do I get?', 11)
|
|
297
300
|
]);
|
|
298
301
|
|
|
302
|
+
// Attachment showcase — one of every media kind, both directions
|
|
303
|
+
// (harness-only conversation; asset paths resolve in the test project)
|
|
304
|
+
mk('media-1', 'webchat', [me, bot], [
|
|
305
|
+
m('media-1', me, 'Here is the artwork and the brief.', 22, {
|
|
306
|
+
media: [
|
|
307
|
+
{ kind: 'image', url: '/test/media/sample.png', name: 'artwork.png', mediaType: 'image/png' },
|
|
308
|
+
{ kind: 'document', url: '/test/media/sample.pdf', name: 'brief.pdf', mediaType: 'application/pdf' }
|
|
309
|
+
]
|
|
310
|
+
}),
|
|
311
|
+
m('media-1', bot, 'Received — and here is everything back in every format:', 20, {
|
|
312
|
+
media: [
|
|
313
|
+
{ kind: 'image', url: '/test/media/sample.png', name: 'render.png', mediaType: 'image/png' },
|
|
314
|
+
{ kind: 'audio', url: '/test/media/sample.wav', name: 'jingle.wav', mediaType: 'audio/wav' },
|
|
315
|
+
{ kind: 'voice', url: '/test/media/sample.wav', name: 'voice note', mediaType: 'audio/wav' },
|
|
316
|
+
{ kind: 'video', url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4', name: 'clip.mp4', mediaType: 'video/mp4' },
|
|
317
|
+
{ kind: 'document', url: '/test/media/sample.pdf', name: 'summary.pdf', mediaType: 'application/pdf' }
|
|
318
|
+
]
|
|
319
|
+
})
|
|
320
|
+
]);
|
|
321
|
+
|
|
299
322
|
// group with reactions + a small reply tree
|
|
300
323
|
const g1 = m('grp-1', ana, 'Ship the v2 doc today?', 30);
|
|
301
324
|
mk('grp-1', 'webchat', [me, ana, bo], [
|
|
@@ -378,7 +401,20 @@
|
|
|
378
401
|
setTimeout(() => deliver(target, msg), 60); // server echo (reconciles media/status by id)
|
|
379
402
|
return { id: msg.id, ts: msg.ts, conversationId: target.id };
|
|
380
403
|
},
|
|
381
|
-
async react(cid, mid, emoji) {
|
|
404
|
+
async react(cid, mid, emoji) {
|
|
405
|
+
// toggle: off if I reacted, join if others did, add if new
|
|
406
|
+
const c = backend.convs.get(cid); const msg = c && c.messages.find(x => x.id === mid);
|
|
407
|
+
if (msg) {
|
|
408
|
+
const rs = (msg.reactions || []).map(r => Object.assign({}, r));
|
|
409
|
+
const r = rs.find(x => x.emoji === emoji);
|
|
410
|
+
if (r && r.byMe) { r.count--; r.byMe = false; }
|
|
411
|
+
else if (r) { r.count++; r.byMe = true; }
|
|
412
|
+
else rs.push({ emoji, count: 1, byMe: true });
|
|
413
|
+
msg.reactions = rs.filter(x => x.count > 0);
|
|
414
|
+
emit(cid, h => h.onReaction && h.onReaction(mid, msg.reactions));
|
|
415
|
+
}
|
|
416
|
+
return { ok: true };
|
|
417
|
+
},
|
|
382
418
|
async addParticipant(cid, p) { const c = backend.convs.get(cid); if (c && !c.participants.some(x => x.id === p.id)) { c.participants.push(p); emit(cid, h => h.onParticipant && h.onParticipant(p, 'added')); } return { ok: true }; },
|
|
383
419
|
async transfer(cid, from, to, role) { emit(cid, h => h.onParticipant && h.onParticipant({ id: to, role: role || 'assignee' }, 'changed')); return { ok: true }; },
|
|
384
420
|
async setTyping(cid, on) { emit(cid, h => h.onTyping && h.onTyping(backend.me.id, on)); },
|
|
@@ -413,11 +449,11 @@
|
|
|
413
449
|
|
|
414
450
|
// ---- simulation hooks (button-driven; deterministic for verification) ---
|
|
415
451
|
const sim = {
|
|
416
|
-
// stream an AI reply token-by-token into a conversation
|
|
417
|
-
aiReply(cid, text) {
|
|
452
|
+
// stream an AI reply token-by-token into a conversation (media attaches on completion)
|
|
453
|
+
aiReply(cid, text, media) {
|
|
418
454
|
const c = backend.convs.get(cid); if (!c) return;
|
|
419
455
|
const mid = backend.id('m');
|
|
420
|
-
const msg = { id: mid, conversationId: cid, author: backend.bot, body: { text: '' }, ts: backend.t(0), status: 'streaming', meta: { authoredBy: 'u_me' } };
|
|
456
|
+
const msg = { id: mid, conversationId: cid, author: backend.bot, body: { text: '', media: media || undefined }, ts: backend.t(0), status: 'streaming', meta: { authoredBy: 'u_me' } };
|
|
421
457
|
c.messages.push(msg);
|
|
422
458
|
emit(cid, h => h.onMessage && h.onMessage(msg));
|
|
423
459
|
const words = (text || 'Every account starts with 5,000 free credits each month.').split(' ');
|
|
@@ -455,6 +491,233 @@
|
|
|
455
491
|
})();
|
|
456
492
|
|
|
457
493
|
|
|
494
|
+
/* Manifest Chat — optional LLM (Claude) adapter
|
|
495
|
+
* By Andrew Matlock under MIT license · https://manifestx.dev
|
|
496
|
+
*
|
|
497
|
+
* Reference `claude` adapter: replies stream from a backend proxy holding the
|
|
498
|
+
* API key (tools/chat-llm-proxy.mjs). $chat never calls the LLM — this adapter
|
|
499
|
+
* does, behind the same contract. Optional, loaded separately from the bundle.
|
|
500
|
+
* Attachments ride draft.body.media[] as base64 → image/document blocks.
|
|
501
|
+
*/
|
|
502
|
+
|
|
503
|
+
(function () {
|
|
504
|
+
'use strict';
|
|
505
|
+
|
|
506
|
+
function ready(fn) {
|
|
507
|
+
if (window.ManifestChatAdapters) return fn();
|
|
508
|
+
const t = setInterval(() => { if (window.ManifestChatAdapters) { clearInterval(t); fn(); } }, 20);
|
|
509
|
+
setTimeout(() => clearInterval(t), 5000);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
ready(function () {
|
|
513
|
+
const USER = { id: 'you', kind: 'human', role: 'user', displayName: 'You', color: '#7c3aed' };
|
|
514
|
+
const BOT = { id: 'claude', kind: 'agent', role: 'assistant', displayName: 'Claude', color: '#d97706' };
|
|
515
|
+
|
|
516
|
+
function claudeAdapter(opts) {
|
|
517
|
+
opts = opts || {};
|
|
518
|
+
// Same-origin relay mnfst-run serves for an `ai` block; override via
|
|
519
|
+
// opts.endpoint / window.CHAT_LLM_ENDPOINT.
|
|
520
|
+
const endpoint = opts.endpoint || window.CHAT_LLM_ENDPOINT || '/_ai/chat';
|
|
521
|
+
// only when set — the relay's ai block (system + grounding) is the default
|
|
522
|
+
const system = opts.system || undefined;
|
|
523
|
+
const handlers = {}; // conversationId -> subscribe handlers
|
|
524
|
+
let seq = 0;
|
|
525
|
+
const id = (p) => p + '_' + Date.now().toString(36) + '_' + (++seq);
|
|
526
|
+
const lsKey = (cid) => 'mnfst.chat.' + cid;
|
|
527
|
+
|
|
528
|
+
// text-only persistence so docs sessions survive reload (attachments stay ephemeral)
|
|
529
|
+
function loadStore(cid) { try { return JSON.parse(localStorage.getItem(lsKey(cid)) || '[]'); } catch { return []; } }
|
|
530
|
+
function saveMsg(cid, m) {
|
|
531
|
+
const all = loadStore(cid);
|
|
532
|
+
all.push({ id: m.id, role: m.author.kind === 'agent' ? 'assistant' : 'user', text: m.body.text, ts: m.ts });
|
|
533
|
+
try { localStorage.setItem(lsKey(cid), JSON.stringify(all.slice(-200))); } catch { }
|
|
534
|
+
}
|
|
535
|
+
const toMsg = (r) => ({ id: r.id, conversationId: null, author: r.role === 'assistant' ? BOT : USER, body: { text: r.text }, ts: r.ts, status: 'delivered' });
|
|
536
|
+
|
|
537
|
+
// Build the Anthropic messages array; attachments → image/document blocks.
|
|
538
|
+
// Only what the API accepts is forwarded (images + PDFs with data);
|
|
539
|
+
// audio/video/voice render locally but don't reach the model.
|
|
540
|
+
function apiMessages(cid, draft) {
|
|
541
|
+
const hist = loadStore(cid).map(r => ({ role: r.role, content: r.text }));
|
|
542
|
+
const media = ((draft.body && draft.body.media) || []).filter(a =>
|
|
543
|
+
a.data && (a.kind === 'image' || a.mediaType === 'application/pdf'));
|
|
544
|
+
const blocks = media.map(a => a.kind === 'image'
|
|
545
|
+
? { type: 'image', source: { type: 'base64', media_type: a.mediaType, data: a.data } }
|
|
546
|
+
: { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: a.data } });
|
|
547
|
+
const text = (draft.body && draft.body.text) || draft.text || '';
|
|
548
|
+
const last = blocks.length ? { role: 'user', content: [...blocks, { type: 'text', text }] } : { role: 'user', content: text };
|
|
549
|
+
return [...hist, last];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function streamReply(cid) {
|
|
553
|
+
const h = handlers[cid]; if (!h) return;
|
|
554
|
+
const messages = streamReply._pending; streamReply._pending = null;
|
|
555
|
+
const mid = id('m');
|
|
556
|
+
h.onMessage && h.onMessage({ id: mid, conversationId: cid, author: BOT, body: { text: '' }, ts: Date.now(), status: 'streaming', meta: { model: opts.model } });
|
|
557
|
+
let full = '';
|
|
558
|
+
try {
|
|
559
|
+
const resp = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ system, model: opts.model, messages }) });
|
|
560
|
+
const reader = resp.body.getReader(); const dec = new TextDecoder(); let buf = '';
|
|
561
|
+
for (; ;) {
|
|
562
|
+
const { done, value } = await reader.read(); if (done) break;
|
|
563
|
+
buf += dec.decode(value, { stream: true });
|
|
564
|
+
let i;
|
|
565
|
+
while ((i = buf.indexOf('\n\n')) >= 0) {
|
|
566
|
+
const frame = buf.slice(0, i); buf = buf.slice(i + 2);
|
|
567
|
+
const line = frame.split('\n').find(l => l.startsWith('data:')); if (!line) continue;
|
|
568
|
+
let evt; try { evt = JSON.parse(line.slice(5).trim()); } catch { continue; }
|
|
569
|
+
if (evt.type === 'content_block_delta' && evt.delta && evt.delta.type === 'text_delta') {
|
|
570
|
+
full += evt.delta.text; h.onMessagePart && h.onMessagePart(mid, { text: evt.delta.text });
|
|
571
|
+
} else if (evt.type === 'error') { full += '\n\n_(error: ' + (evt.error && evt.error.message) + ')_'; h.onMessagePart && h.onMessagePart(mid, { text: '\n\n_(error)_' }); }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch (e) { h.onMessagePart && h.onMessagePart(mid, { text: '\n\n_(proxy unreachable — is tools/chat-llm-proxy.mjs running?)_' }); full += ' (proxy unreachable)'; }
|
|
575
|
+
h.onMessagePart && h.onMessagePart(mid, { text: '', done: true });
|
|
576
|
+
saveMsg(cid, { id: mid, author: BOT, body: { text: full }, ts: Date.now() });
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
identity: () => USER,
|
|
581
|
+
async load(cid) { return { messages: loadStore(cid).map(toMsg), participants: [USER, BOT] }; },
|
|
582
|
+
subscribe(cid, h) { handlers[cid] = h; setTimeout(() => h.onConnection && h.onConnection(true), 0); return () => { delete handlers[cid]; }; },
|
|
583
|
+
async send(cid, draft) {
|
|
584
|
+
const mid = id('m'); const ts = Date.now();
|
|
585
|
+
saveMsg(cid, { id: mid, author: USER, body: { text: (draft.body && draft.body.text) || draft.text || '' }, ts });
|
|
586
|
+
streamReply._pending = apiMessages(cid, draft);
|
|
587
|
+
setTimeout(() => streamReply(cid), 30); // assistant reply streams in as a separate inbound
|
|
588
|
+
return { id: mid, ts };
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
window.ManifestChatAdapters.register('claude', claudeAdapter);
|
|
594
|
+
});
|
|
595
|
+
})();
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
/* Manifest Chat — Appwrite adapter (conversations as app data)
|
|
599
|
+
* By Andrew Matlock under MIT license
|
|
600
|
+
* https://manifestx.dev
|
|
601
|
+
*
|
|
602
|
+
* Messages live as rows in an Appwrite table; inbound arrives over Appwrite
|
|
603
|
+
* Realtime; identity comes from $auth (guest sessions cover anonymous use).
|
|
604
|
+
* Built for comments, support threads, and small-group chat — high-volume
|
|
605
|
+
* messaging belongs on a purpose-built bus behind a custom adapter.
|
|
606
|
+
*
|
|
607
|
+
* Config: opts { databaseId, tableId, ttlHours } or manifest.json
|
|
608
|
+
* chat: { appwriteDatabaseId, appwriteTableId, ttlHours }.
|
|
609
|
+
* Table columns: conversationId (indexed) · text · authorId · authorName ·
|
|
610
|
+
* authorColor? · replyTo? — ts/id come from $createdAt/$id.
|
|
611
|
+
*/
|
|
612
|
+
|
|
613
|
+
(function () {
|
|
614
|
+
'use strict';
|
|
615
|
+
|
|
616
|
+
function ready(fn) {
|
|
617
|
+
if (window.ManifestChatAdapters) return fn();
|
|
618
|
+
const t = setInterval(() => { if (window.ManifestChatAdapters) { clearInterval(t); fn(); } }, 20);
|
|
619
|
+
setTimeout(() => clearInterval(t), 5000);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
ready(function () {
|
|
623
|
+
function hashColor(id) {
|
|
624
|
+
let h = 0; for (const ch of String(id)) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
625
|
+
return `hsl(${h % 360} 60% 45%)`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function appwriteAdapter(opts) {
|
|
629
|
+
opts = opts || {};
|
|
630
|
+
let cfg = null;
|
|
631
|
+
|
|
632
|
+
async function config() {
|
|
633
|
+
if (cfg) return cfg;
|
|
634
|
+
let databaseId = opts.databaseId, tableId = opts.tableId, ttlHours = opts.ttlHours;
|
|
635
|
+
if ((!databaseId || !tableId) && window.ManifestDataConfig && window.ManifestDataConfig.ensureManifest) {
|
|
636
|
+
const m = await window.ManifestDataConfig.ensureManifest();
|
|
637
|
+
const c = (m && m.chat) || {};
|
|
638
|
+
databaseId = databaseId || c.appwriteDatabaseId;
|
|
639
|
+
tableId = tableId || c.appwriteTableId;
|
|
640
|
+
if (ttlHours == null) ttlHours = c.ttlHours;
|
|
641
|
+
}
|
|
642
|
+
if (!databaseId || !tableId) console.warn('[Manifest Chat] appwrite adapter needs databaseId + tableId (opts or manifest.json chat block).');
|
|
643
|
+
cfg = { databaseId, tableId, ttlHours };
|
|
644
|
+
return cfg;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function me() {
|
|
648
|
+
const auth = window.Alpine ? window.Alpine.store('auth') : null;
|
|
649
|
+
const u = auth && auth.user;
|
|
650
|
+
if (u && u.$id) return { id: u.$id, kind: 'human', displayName: u.name || u.email || 'Guest', color: hashColor(u.$id) };
|
|
651
|
+
let gid = null;
|
|
652
|
+
try { gid = localStorage.getItem('mnfst.chat.gid'); if (!gid) { gid = 'g_' + Math.random().toString(36).slice(2, 10); localStorage.setItem('mnfst.chat.gid', gid); } } catch (_) { gid = 'g_anon'; }
|
|
653
|
+
return { id: gid, kind: 'human', displayName: 'Guest', color: hashColor(gid) };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function toMsg(row) {
|
|
657
|
+
return {
|
|
658
|
+
id: row.$id, conversationId: row.conversationId,
|
|
659
|
+
author: { id: row.authorId, kind: 'human', displayName: row.authorName, color: row.authorColor || hashColor(row.authorId) },
|
|
660
|
+
body: { text: row.text }, replyTo: row.replyTo || undefined,
|
|
661
|
+
ts: row.$createdAt, status: 'delivered'
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
identity: () => me(),
|
|
667
|
+
|
|
668
|
+
async load(cid) {
|
|
669
|
+
const { databaseId, tableId, ttlHours } = await config();
|
|
670
|
+
if (!databaseId) return { messages: [], participants: [] };
|
|
671
|
+
// tablesDB directly (not loadTableRows): public read("any") tables
|
|
672
|
+
// must load before any session exists — no auth gate on reads.
|
|
673
|
+
const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
|
|
674
|
+
const Q = window.Appwrite.Query;
|
|
675
|
+
const queries = [Q.equal('conversationId', cid), Q.orderDesc('$createdAt'), Q.limit(100)];
|
|
676
|
+
if (ttlHours) queries.push(Q.greaterThan('$createdAt', new Date(Date.now() - ttlHours * 3600e3).toISOString()));
|
|
677
|
+
const res = await services.tablesDB.listRows({ databaseId, tableId, queries });
|
|
678
|
+
return { messages: res.rows.map(toMsg).reverse(), participants: [] };
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
subscribe(cid, handlers) {
|
|
682
|
+
let unsub = null, closed = false;
|
|
683
|
+
(async () => {
|
|
684
|
+
const { databaseId, tableId } = await config();
|
|
685
|
+
if (!databaseId) return;
|
|
686
|
+
const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
|
|
687
|
+
if (!services || !services.realtime || closed) return;
|
|
688
|
+
unsub = services.realtime.subscribe(`databases.${databaseId}.tables.${tableId}.rows`, (res) => {
|
|
689
|
+
if (!res || !res.payload || res.payload.conversationId !== cid) return;
|
|
690
|
+
const events = Array.isArray(res.events) ? res.events : [res.events];
|
|
691
|
+
if (events.some(e => typeof e === 'string' && e.endsWith('.create'))) handlers.onMessage(toMsg(res.payload));
|
|
692
|
+
});
|
|
693
|
+
if (handlers.onConnection) handlers.onConnection(true);
|
|
694
|
+
})();
|
|
695
|
+
return () => { closed = true; try { unsub && unsub(); } catch (_) { } };
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
async send(cid, draft) {
|
|
699
|
+
const { databaseId, tableId } = await config();
|
|
700
|
+
const auth = window.Alpine ? window.Alpine.store('auth') : null;
|
|
701
|
+
if (auth && !auth.isAuthenticated && typeof auth.requestGuest === 'function') {
|
|
702
|
+
try { await auth.requestGuest(); } catch (_) { } // guests may create
|
|
703
|
+
}
|
|
704
|
+
const who = me();
|
|
705
|
+
const text = (draft.body && draft.body.text) || draft.text || '';
|
|
706
|
+
const row = await window.ManifestDataAppwrite.createRow(databaseId, tableId, {
|
|
707
|
+
conversationId: cid, text,
|
|
708
|
+
authorId: who.id, authorName: who.displayName, authorColor: who.color,
|
|
709
|
+
replyTo: draft.replyTo || null
|
|
710
|
+
});
|
|
711
|
+
return { id: row.$id, ts: row.$createdAt };
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
window.ManifestChatAdapters.register('appwrite', appwriteAdapter);
|
|
717
|
+
});
|
|
718
|
+
})();
|
|
719
|
+
|
|
720
|
+
|
|
458
721
|
/* Manifest Chat — magic + init
|
|
459
722
|
/* By Andrew Matlock under MIT license
|
|
460
723
|
/* https://manifestx.dev
|
|
@@ -72,16 +72,25 @@
|
|
|
72
72
|
gap: var(--spacing, 0.25rem);
|
|
73
73
|
max-width: 100%;
|
|
74
74
|
height: calc(var(--spacing-field-height, 2.25rem) - var(--spacing, 0.25rem) * 2.5);
|
|
75
|
+
margin: 0;
|
|
76
|
+
padding: 0;
|
|
75
77
|
padding-inline-start: calc(var(--spacing, 0.25rem) * 2);
|
|
76
78
|
font-size: 0.875rem;
|
|
79
|
+
line-height: normal;
|
|
77
80
|
color: var(--color-content-stark, oklch(43.9% 0 0));
|
|
78
81
|
background-color: var(--color-popover-surface, oklch(100% 0 0));
|
|
82
|
+
border: 0;
|
|
79
83
|
border-radius: calc(var(--radius, 0.5rem) * 0.7);
|
|
80
84
|
overflow: hidden;
|
|
81
85
|
user-select: none;
|
|
82
86
|
|
|
83
87
|
/* Label */
|
|
84
88
|
&>span {
|
|
89
|
+
min-width: 0;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
border: 0;
|
|
93
|
+
font: inherit;
|
|
85
94
|
overflow: hidden;
|
|
86
95
|
text-overflow: ellipsis;
|
|
87
96
|
white-space: nowrap
|
|
@@ -92,15 +101,23 @@
|
|
|
92
101
|
display: inline-flex;
|
|
93
102
|
align-items: center;
|
|
94
103
|
justify-content: center;
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
flex: none;
|
|
106
|
+
width: auto !important;
|
|
95
107
|
min-width: 0;
|
|
108
|
+
max-width: none;
|
|
96
109
|
height: 100%;
|
|
110
|
+
min-height: 0;
|
|
97
111
|
aspect-ratio: 1 / 1;
|
|
112
|
+
margin: 0;
|
|
98
113
|
padding: 0;
|
|
114
|
+
border: 0;
|
|
115
|
+
border-radius: 0;
|
|
99
116
|
font-size: 1em;
|
|
100
117
|
line-height: 1;
|
|
101
118
|
color: var(--color-content-neutral, oklch(43.9% 0 0));
|
|
102
119
|
background: transparent;
|
|
103
|
-
|
|
120
|
+
box-shadow: none;
|
|
104
121
|
cursor: pointer;
|
|
105
122
|
|
|
106
123
|
&:hover {
|