@joewinke/jatui 0.35.0 → 0.36.1
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/CLAUDE.md +8 -3
- package/dist/actions/reveal.d.ts +72 -11
- package/dist/actions/reveal.js +103 -14
- package/dist/components/AudioRecorder.svelte +23 -1
- package/dist/components/AudioRecorder.svelte.d.ts +13 -1
- package/dist/components/BulkActionBar.svelte +17 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +20 -1
- package/dist/components/Drawer.svelte.d.ts +11 -0
- package/dist/components/FloatingActionBar.svelte +26 -11
- package/dist/components/FloatingActionBar.svelte.d.ts +5 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +180 -7
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +21 -0
- package/dist/components/ThumbnailGrid.svelte +34 -7
- package/dist/components/ThumbnailGrid.svelte.d.ts +17 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/motion/index.d.ts +1 -1
- package/dist/motion/index.js +1 -1
- package/dist/utils/audioRecorder.d.ts +6 -1
- package/dist/utils/audioRecorder.js +5 -0
- package/dist/utils/audioRecorderCore.d.ts +34 -0
- package/dist/utils/audioRecorderCore.js +43 -6
- package/dist/utils/keyboardShortcutHub.d.ts +73 -0
- package/dist/utils/keyboardShortcutHub.js +89 -0
- package/dist/utils/mentionParser.d.ts +44 -2
- package/dist/utils/mentionParser.js +84 -4
- package/package.json +2 -1
|
@@ -61,7 +61,7 @@ export function computeElapsedMs(startedAt, pausedTotalMs, pauseStartedAt, now)
|
|
|
61
61
|
return Math.max(0, now - startedAt - pausedMs);
|
|
62
62
|
}
|
|
63
63
|
export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
64
|
-
const { mode = 'toggle', maxDurationMs = 0, onStart, onPause, onResume, onStop, onCancel, onError, onFrame } = options;
|
|
64
|
+
const { mode = 'toggle', maxDurationMs = 0, timesliceMs = 1000, onStart, onPause, onResume, onStop, onCancel, onError, onFrame, onChunk } = options;
|
|
65
65
|
// Lazy globalThis defaults — resolved per factory call, never at import time.
|
|
66
66
|
const g = (v) => v;
|
|
67
67
|
const getUserMedia = deps.getUserMedia ??
|
|
@@ -87,6 +87,8 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
87
87
|
let level = 0;
|
|
88
88
|
let sizeBytes = 0;
|
|
89
89
|
let error = null;
|
|
90
|
+
/** 0-based index of the next chunk in the current take. */
|
|
91
|
+
let chunkIndex = 0;
|
|
90
92
|
// Recorder internals
|
|
91
93
|
let recorder = null;
|
|
92
94
|
let stream = null;
|
|
@@ -105,7 +107,15 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
105
107
|
let pendingCancel = false;
|
|
106
108
|
const listeners = new Set();
|
|
107
109
|
function snapshot() {
|
|
108
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
phase,
|
|
112
|
+
paused,
|
|
113
|
+
level,
|
|
114
|
+
sizeBytes,
|
|
115
|
+
elapsedMs: computeElapsedMs(startedAt, pausedTotalMs, pauseStartedAt, now()),
|
|
116
|
+
error,
|
|
117
|
+
stream
|
|
118
|
+
};
|
|
109
119
|
}
|
|
110
120
|
function notify() {
|
|
111
121
|
const s = snapshot();
|
|
@@ -273,11 +283,32 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
273
283
|
}
|
|
274
284
|
chunks = [];
|
|
275
285
|
sizeBytes = 0;
|
|
286
|
+
chunkIndex = 0;
|
|
276
287
|
recorder.ondataavailable = (e) => {
|
|
277
288
|
if (e.data && e.data.size > 0) {
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
const blob = e.data;
|
|
290
|
+
chunks.push(blob);
|
|
291
|
+
sizeBytes += blob.size;
|
|
292
|
+
const chunk = {
|
|
293
|
+
blob,
|
|
294
|
+
mimeType,
|
|
295
|
+
sizeBytes,
|
|
296
|
+
elapsedMs: elapsed(),
|
|
297
|
+
index: chunkIndex++
|
|
298
|
+
};
|
|
280
299
|
notify();
|
|
300
|
+
if (onChunk) {
|
|
301
|
+
try {
|
|
302
|
+
onChunk(chunk);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
// A throwing chunk handler must not lose the take — report and keep capturing.
|
|
306
|
+
fail({
|
|
307
|
+
kind: 'handler-failed',
|
|
308
|
+
message: `Chunk kept, but the onChunk handler failed: ${err instanceof Error ? err.message : String(err)}`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
281
312
|
}
|
|
282
313
|
};
|
|
283
314
|
recorder.onstop = () => {
|
|
@@ -300,7 +331,7 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
300
331
|
analyser = null;
|
|
301
332
|
}
|
|
302
333
|
}
|
|
303
|
-
recorder.start(
|
|
334
|
+
recorder.start(timesliceMs > 0 ? timesliceMs : undefined);
|
|
304
335
|
startedAt = now();
|
|
305
336
|
pausedTotalMs = 0;
|
|
306
337
|
pauseStartedAt = 0;
|
|
@@ -381,7 +412,10 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
381
412
|
if (wasCancelled)
|
|
382
413
|
return;
|
|
383
414
|
if (chunks.length === 0) {
|
|
384
|
-
// Zero chunks (e.g. stopped before any data arrived) — fail loud, not a
|
|
415
|
+
// Zero chunks (e.g. stopped before any data arrived) — fail loud, not a
|
|
416
|
+
// silent no-op. Release the mic + audio graph like every other exit, so
|
|
417
|
+
// a failed take can't leave a hot microphone behind.
|
|
418
|
+
teardown();
|
|
385
419
|
phase = 'idle';
|
|
386
420
|
notify();
|
|
387
421
|
fail({ kind: 'too-short', message: 'Recording was too short — nothing was captured. Try again.' });
|
|
@@ -434,6 +468,9 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
434
468
|
return supported;
|
|
435
469
|
},
|
|
436
470
|
mode,
|
|
471
|
+
get stream() {
|
|
472
|
+
return stream;
|
|
473
|
+
},
|
|
437
474
|
snapshot,
|
|
438
475
|
subscribe,
|
|
439
476
|
start,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcut hub — the "one overlay" coordination layer for
|
|
3
|
+
* KeyboardShortcutsOverlay's host/contributor mode.
|
|
4
|
+
*
|
|
5
|
+
* Page-level overlay instances usually self-render (the pre-existing
|
|
6
|
+
* non-host fallback path). But an app shell can mount ONE "global" host
|
|
7
|
+
* overlay; every other overlay on the page then becomes a contributor:
|
|
8
|
+
* instead of self-rendering it contributes its sections (under a
|
|
9
|
+
* `sectionTitle`) to the host's panel and forwards its `open` requests to
|
|
10
|
+
* the host, so exactly one `?`-overlay exists no matter how many surfaces
|
|
11
|
+
* mount the component.
|
|
12
|
+
*
|
|
13
|
+
* Pure TypeScript (no Svelte), subscriber-based like ShortcutRegistry —
|
|
14
|
+
* reactivity lives in the overlay components, which re-read on notify.
|
|
15
|
+
*
|
|
16
|
+
* Usage (app shell):
|
|
17
|
+
*
|
|
18
|
+
* <KeyboardShortcutsOverlay mode="host" /> // owns the `?` toggle
|
|
19
|
+
*
|
|
20
|
+
* Usage (page / portal / contract page):
|
|
21
|
+
*
|
|
22
|
+
* <KeyboardShortcutsOverlay
|
|
23
|
+
* {sections}
|
|
24
|
+
* sectionTitle="Inbox"
|
|
25
|
+
* showHint={false}
|
|
26
|
+
* />
|
|
27
|
+
* // self-renders when no host exists; contributes when one does
|
|
28
|
+
*/
|
|
29
|
+
export interface ShortcutContribution {
|
|
30
|
+
/** Stable id for the contributing surface (page route, portal name…). */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Label under which the contributed sections appear in the host panel. */
|
|
33
|
+
sectionTitle: string;
|
|
34
|
+
/** Contributed sections (same shape as KeyboardShortcutsOverlay's `sections`). */
|
|
35
|
+
sections: ShortcutSectionLike[];
|
|
36
|
+
/** Registry snapshot (context-keyed sections) — optional alternative to `sections`. */
|
|
37
|
+
registrySections?: ShortcutSectionLike[];
|
|
38
|
+
}
|
|
39
|
+
export interface ShortcutSectionLike {
|
|
40
|
+
title: string;
|
|
41
|
+
shortcuts: {
|
|
42
|
+
key: string;
|
|
43
|
+
description: string;
|
|
44
|
+
color?: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export interface KeyboardShortcutHost {
|
|
48
|
+
/** Toggle the host overlay open/closed. */
|
|
49
|
+
toggle: () => void;
|
|
50
|
+
/** Open the host overlay (idempotent while already open). */
|
|
51
|
+
open: () => void;
|
|
52
|
+
}
|
|
53
|
+
declare class KeyboardShortcutHub {
|
|
54
|
+
private host;
|
|
55
|
+
private contributions;
|
|
56
|
+
private listeners;
|
|
57
|
+
/** True when a host overlay is mounted (contributing instances defer to it). */
|
|
58
|
+
get hasHost(): boolean;
|
|
59
|
+
/** Mount the host. Returns the unmount function. */
|
|
60
|
+
registerHost(host: KeyboardShortcutHost): () => void;
|
|
61
|
+
/** Contribute sections from a page-level overlay instance. Returns unregister. */
|
|
62
|
+
contribute(contribution: ShortcutContribution): () => void;
|
|
63
|
+
/** Contributed sections in registration order, labelled by sectionTitle. */
|
|
64
|
+
getContributions(): ShortcutContribution[];
|
|
65
|
+
/** Ask the host overlay to open. No-op when there is no host. */
|
|
66
|
+
requestOpen(): boolean;
|
|
67
|
+
/** Subscribe to hub changes (host mount/unmount, contributions). */
|
|
68
|
+
subscribe(listener: () => void): () => void;
|
|
69
|
+
private notify;
|
|
70
|
+
}
|
|
71
|
+
export declare const keyboardShortcutHub: KeyboardShortcutHub;
|
|
72
|
+
export declare const KEYBOARD_SHORTCUT_HUB_ID = "keyboard-shortcut-hub";
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcut hub — the "one overlay" coordination layer for
|
|
3
|
+
* KeyboardShortcutsOverlay's host/contributor mode.
|
|
4
|
+
*
|
|
5
|
+
* Page-level overlay instances usually self-render (the pre-existing
|
|
6
|
+
* non-host fallback path). But an app shell can mount ONE "global" host
|
|
7
|
+
* overlay; every other overlay on the page then becomes a contributor:
|
|
8
|
+
* instead of self-rendering it contributes its sections (under a
|
|
9
|
+
* `sectionTitle`) to the host's panel and forwards its `open` requests to
|
|
10
|
+
* the host, so exactly one `?`-overlay exists no matter how many surfaces
|
|
11
|
+
* mount the component.
|
|
12
|
+
*
|
|
13
|
+
* Pure TypeScript (no Svelte), subscriber-based like ShortcutRegistry —
|
|
14
|
+
* reactivity lives in the overlay components, which re-read on notify.
|
|
15
|
+
*
|
|
16
|
+
* Usage (app shell):
|
|
17
|
+
*
|
|
18
|
+
* <KeyboardShortcutsOverlay mode="host" /> // owns the `?` toggle
|
|
19
|
+
*
|
|
20
|
+
* Usage (page / portal / contract page):
|
|
21
|
+
*
|
|
22
|
+
* <KeyboardShortcutsOverlay
|
|
23
|
+
* {sections}
|
|
24
|
+
* sectionTitle="Inbox"
|
|
25
|
+
* showHint={false}
|
|
26
|
+
* />
|
|
27
|
+
* // self-renders when no host exists; contributes when one does
|
|
28
|
+
*/
|
|
29
|
+
const HUB_ID = 'keyboard-shortcut-hub';
|
|
30
|
+
class KeyboardShortcutHub {
|
|
31
|
+
host = null;
|
|
32
|
+
contributions = new Map();
|
|
33
|
+
listeners = new Set();
|
|
34
|
+
/** True when a host overlay is mounted (contributing instances defer to it). */
|
|
35
|
+
get hasHost() {
|
|
36
|
+
return this.host !== null;
|
|
37
|
+
}
|
|
38
|
+
/** Mount the host. Returns the unmount function. */
|
|
39
|
+
registerHost(host) {
|
|
40
|
+
if (this.host && this.host !== host) {
|
|
41
|
+
// Two hosts — the later one wins (last-mounted). Warn loudly: a
|
|
42
|
+
// page accidentally mounting a second global overlay is a bug.
|
|
43
|
+
console.warn('[keyboardShortcutHub] replacing an existing host overlay — did you mount the global overlay twice?');
|
|
44
|
+
}
|
|
45
|
+
this.host = host;
|
|
46
|
+
this.notify();
|
|
47
|
+
return () => {
|
|
48
|
+
if (this.host === host) {
|
|
49
|
+
this.host = null;
|
|
50
|
+
this.notify();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Contribute sections from a page-level overlay instance. Returns unregister. */
|
|
55
|
+
contribute(contribution) {
|
|
56
|
+
this.contributions.set(contribution.id, contribution);
|
|
57
|
+
this.notify();
|
|
58
|
+
return () => {
|
|
59
|
+
if (this.contributions.get(contribution.id) === contribution) {
|
|
60
|
+
this.contributions.delete(contribution.id);
|
|
61
|
+
this.notify();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Contributed sections in registration order, labelled by sectionTitle. */
|
|
66
|
+
getContributions() {
|
|
67
|
+
return [...this.contributions.values()];
|
|
68
|
+
}
|
|
69
|
+
/** Ask the host overlay to open. No-op when there is no host. */
|
|
70
|
+
requestOpen() {
|
|
71
|
+
if (!this.host)
|
|
72
|
+
return false;
|
|
73
|
+
this.host.open();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
/** Subscribe to hub changes (host mount/unmount, contributions). */
|
|
77
|
+
subscribe(listener) {
|
|
78
|
+
this.listeners.add(listener);
|
|
79
|
+
return () => {
|
|
80
|
+
this.listeners.delete(listener);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
notify() {
|
|
84
|
+
for (const l of this.listeners)
|
|
85
|
+
l();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const keyboardShortcutHub = new KeyboardShortcutHub();
|
|
89
|
+
export const KEYBOARD_SHORTCUT_HUB_ID = HUB_ID;
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* Handles parsing and rendering of @-mentions in messages.
|
|
5
5
|
* Format: [@Display Name](type:id) -> clickable links
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* URL routing has three layers (most specific first):
|
|
8
|
+
* 1. the `urlGenerator` parameter on a given parse/render call
|
|
9
|
+
* 2. a per-type registration — registerMentionType(type, generator) —
|
|
10
|
+
* so each mention type carries its own routing (e.g. @agent mentions
|
|
11
|
+
* route to /admin/settings/agent while @user routes elsewhere)
|
|
12
|
+
* 3. the single app-wide default — setMentionUrlGenerator
|
|
13
|
+
*
|
|
14
|
+
* The per-type registry is what lets a consuming app retire a local
|
|
15
|
+
* mentionParser copy (mos-i8nnd.18): one default generator cannot express
|
|
16
|
+
* per-type routing.
|
|
9
17
|
*/
|
|
10
18
|
export interface MentionMatch {
|
|
11
19
|
type: string;
|
|
@@ -17,6 +25,22 @@ export interface MentionMatch {
|
|
|
17
25
|
fullMatch: string;
|
|
18
26
|
}
|
|
19
27
|
export type MentionUrlGenerator = (type: string, id: string) => string;
|
|
28
|
+
/** The mention type used to reference agents (extractAgentMentions). */
|
|
29
|
+
export declare const AGENT_MENTION_TYPE = "agent";
|
|
30
|
+
/** Per-type registration record held by the registry. */
|
|
31
|
+
export interface MentionTypeConfig {
|
|
32
|
+
urlGenerator: MentionUrlGenerator;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register a URL generator for a specific mention type.
|
|
36
|
+
*
|
|
37
|
+
* Call once at app startup per routed type (e.g. registerMentionType('agent',
|
|
38
|
+
* () => '/admin/settings/agent')). Re-registering a type replaces its
|
|
39
|
+
* generator; the returned function unregisters exactly that registration.
|
|
40
|
+
* The type must be an identifier token — the mention regex only accepts
|
|
41
|
+
* `[A-Za-z0-9_-]+`, so anything else could never match a mention.
|
|
42
|
+
*/
|
|
43
|
+
export declare function registerMentionType(type: string, urlGenerator: MentionUrlGenerator): () => void;
|
|
20
44
|
/**
|
|
21
45
|
* Set the default URL generator for mentions.
|
|
22
46
|
* Call this once at app startup to configure mention link routing.
|
|
@@ -73,6 +97,24 @@ export declare function replaceMentionInContent(content: string, startIndex: num
|
|
|
73
97
|
newContent: string;
|
|
74
98
|
newCursorPosition: number;
|
|
75
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Extract only agent mentions (type='agent').
|
|
102
|
+
*/
|
|
103
|
+
export declare function extractAgentMentions(content: string): MentionMatch[];
|
|
104
|
+
/**
|
|
105
|
+
* Cheap boolean check — does the content contain at least one mention?
|
|
106
|
+
*/
|
|
107
|
+
export declare function hasMentions(content: string): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Unique ids of @user mentions, in order of first appearance.
|
|
110
|
+
* Agent mentions are separate — use extractAgentMentions.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getMentionedUserIds(content: string): string[];
|
|
113
|
+
/**
|
|
114
|
+
* Mentions deduped by type+id (first occurrence wins, appearance order
|
|
115
|
+
* preserved), optionally filtered to a single type.
|
|
116
|
+
*/
|
|
117
|
+
export declare function getUniqueMentionsByType(content: string, type?: string): MentionMatch[];
|
|
76
118
|
/**
|
|
77
119
|
* Simple HTML escape function
|
|
78
120
|
*/
|
|
@@ -4,9 +4,43 @@
|
|
|
4
4
|
* Handles parsing and rendering of @-mentions in messages.
|
|
5
5
|
* Format: [@Display Name](type:id) -> clickable links
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* URL routing has three layers (most specific first):
|
|
8
|
+
* 1. the `urlGenerator` parameter on a given parse/render call
|
|
9
|
+
* 2. a per-type registration — registerMentionType(type, generator) —
|
|
10
|
+
* so each mention type carries its own routing (e.g. @agent mentions
|
|
11
|
+
* route to /admin/settings/agent while @user routes elsewhere)
|
|
12
|
+
* 3. the single app-wide default — setMentionUrlGenerator
|
|
13
|
+
*
|
|
14
|
+
* The per-type registry is what lets a consuming app retire a local
|
|
15
|
+
* mentionParser copy (mos-i8nnd.18): one default generator cannot express
|
|
16
|
+
* per-type routing.
|
|
17
|
+
*/
|
|
18
|
+
/** The mention type used to reference agents (extractAgentMentions). */
|
|
19
|
+
export const AGENT_MENTION_TYPE = 'agent';
|
|
20
|
+
// Per-type registry — populated by registerMentionType; wins over the
|
|
21
|
+
// app-wide default for that type, loses to an explicit generator argument.
|
|
22
|
+
const mentionTypes = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* Register a URL generator for a specific mention type.
|
|
25
|
+
*
|
|
26
|
+
* Call once at app startup per routed type (e.g. registerMentionType('agent',
|
|
27
|
+
* () => '/admin/settings/agent')). Re-registering a type replaces its
|
|
28
|
+
* generator; the returned function unregisters exactly that registration.
|
|
29
|
+
* The type must be an identifier token — the mention regex only accepts
|
|
30
|
+
* `[A-Za-z0-9_-]+`, so anything else could never match a mention.
|
|
9
31
|
*/
|
|
32
|
+
export function registerMentionType(type, urlGenerator) {
|
|
33
|
+
if (!/^[A-Za-z0-9_-]+$/.test(type)) {
|
|
34
|
+
throw new TypeError(`mention type ${JSON.stringify(type)} must match /^[A-Za-z0-9_-]+$/ — ` +
|
|
35
|
+
'the mention regex only matches identifier tokens');
|
|
36
|
+
}
|
|
37
|
+
mentionTypes.set(type, { urlGenerator });
|
|
38
|
+
return () => {
|
|
39
|
+
// Only remove OUR registration — a later re-registration must survive.
|
|
40
|
+
if (mentionTypes.get(type)?.urlGenerator === urlGenerator)
|
|
41
|
+
mentionTypes.delete(type);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
10
44
|
// Regex to match mention pattern: [@Display Name](type:id)
|
|
11
45
|
//
|
|
12
46
|
// `type` is an identifier token, never free text (mos-3xvn1): it lands in
|
|
@@ -28,7 +62,6 @@ export function setMentionUrlGenerator(generator) {
|
|
|
28
62
|
*/
|
|
29
63
|
export function parseMentions(content, urlGenerator) {
|
|
30
64
|
const mentions = [];
|
|
31
|
-
const gen = urlGenerator ?? defaultUrlGenerator;
|
|
32
65
|
let match;
|
|
33
66
|
MENTION_REGEX.lastIndex = 0;
|
|
34
67
|
while ((match = MENTION_REGEX.exec(content)) !== null) {
|
|
@@ -37,7 +70,9 @@ export function parseMentions(content, urlGenerator) {
|
|
|
37
70
|
type,
|
|
38
71
|
id,
|
|
39
72
|
displayName,
|
|
40
|
-
|
|
73
|
+
// Resolution: explicit call-site generator > per-type registered
|
|
74
|
+
// generator > the app-wide default.
|
|
75
|
+
url: (urlGenerator ?? mentionTypes.get(type)?.urlGenerator ?? defaultUrlGenerator)(type, id),
|
|
41
76
|
startIndex: match.index,
|
|
42
77
|
endIndex: match.index + fullMatch.length,
|
|
43
78
|
fullMatch
|
|
@@ -147,6 +182,51 @@ export function replaceMentionInContent(content, startIndex, endIndex, mentionSt
|
|
|
147
182
|
const newCursorPosition = startIndex + mentionString.length;
|
|
148
183
|
return { newContent, newCursorPosition };
|
|
149
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Extract only agent mentions (type='agent').
|
|
187
|
+
*/
|
|
188
|
+
export function extractAgentMentions(content) {
|
|
189
|
+
return parseMentions(content).filter((mention) => mention.type === AGENT_MENTION_TYPE);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Cheap boolean check — does the content contain at least one mention?
|
|
193
|
+
*/
|
|
194
|
+
export function hasMentions(content) {
|
|
195
|
+
MENTION_REGEX.lastIndex = 0;
|
|
196
|
+
return MENTION_REGEX.test(content);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Unique ids of @user mentions, in order of first appearance.
|
|
200
|
+
* Agent mentions are separate — use extractAgentMentions.
|
|
201
|
+
*/
|
|
202
|
+
export function getMentionedUserIds(content) {
|
|
203
|
+
const ids = [];
|
|
204
|
+
for (const mention of parseMentions(content)) {
|
|
205
|
+
if (mention.type !== 'user')
|
|
206
|
+
continue;
|
|
207
|
+
if (!ids.includes(mention.id))
|
|
208
|
+
ids.push(mention.id);
|
|
209
|
+
}
|
|
210
|
+
return ids;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Mentions deduped by type+id (first occurrence wins, appearance order
|
|
214
|
+
* preserved), optionally filtered to a single type.
|
|
215
|
+
*/
|
|
216
|
+
export function getUniqueMentionsByType(content, type) {
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const mention of parseMentions(content)) {
|
|
220
|
+
if (type && mention.type !== type)
|
|
221
|
+
continue;
|
|
222
|
+
const key = `${mention.type}:${mention.id}`;
|
|
223
|
+
if (seen.has(key))
|
|
224
|
+
continue;
|
|
225
|
+
seen.add(key);
|
|
226
|
+
out.push(mention);
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
150
230
|
/**
|
|
151
231
|
* Simple HTML escape function
|
|
152
232
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joewinke/jatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Svelte 5 component library for JAT projects",
|
|
6
6
|
"type": "module",
|
|
@@ -188,6 +188,7 @@
|
|
|
188
188
|
"lenis": "^1.3.25",
|
|
189
189
|
"livekit-client": "^2.20.1",
|
|
190
190
|
"motion": "^12.42.2",
|
|
191
|
+
"@playwright/test": "^1.62.1",
|
|
191
192
|
"playwright": "^1.62.1",
|
|
192
193
|
"svelte": "^5.0.0",
|
|
193
194
|
"tailwindcss": "^4.0.0",
|