vanilla-agent 1.8.0 → 1.10.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 +246 -0
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +277 -1
- package/dist/index.d.ts +277 -1
- package/dist/index.global.js +29 -29
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +141 -12
- package/src/components/forms.ts +1 -0
- package/src/components/panel.ts +5 -1
- package/src/components/registry.ts +87 -0
- package/src/index.ts +22 -2
- package/src/plugins/registry.ts +1 -0
- package/src/plugins/types.ts +1 -0
- package/src/types.ts +150 -0
- package/src/ui.ts +80 -7
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/dom.ts +1 -0
- package/src/utils/formatting.ts +33 -8
- package/src/utils/positioning.ts +1 -0
- package/src/utils/theme.ts +1 -0
package/src/ui.ts
CHANGED
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
defaultActionHandlers,
|
|
32
32
|
defaultJsonActionParser
|
|
33
33
|
} from "./utils/actions";
|
|
34
|
+
import { createLocalStorageAdapter } from "./utils/storage";
|
|
35
|
+
import { componentRegistry } from "./components/registry";
|
|
36
|
+
import {
|
|
37
|
+
renderComponentDirective,
|
|
38
|
+
extractComponentDirectiveFromMessage,
|
|
39
|
+
hasComponentDirective
|
|
40
|
+
} from "./utils/component-middleware";
|
|
34
41
|
|
|
35
42
|
// Default localStorage key for chat history (automatically cleared on clear chat)
|
|
36
43
|
const DEFAULT_CHAT_HISTORY_STORAGE_KEY = "vanilla-agent-chat-history";
|
|
@@ -132,10 +139,15 @@ export const createAgentExperience = (
|
|
|
132
139
|
|
|
133
140
|
// Get plugins for this instance
|
|
134
141
|
const plugins = pluginRegistry.getForInstance(config.plugins);
|
|
142
|
+
|
|
143
|
+
// Register components from config
|
|
144
|
+
if (config.components) {
|
|
145
|
+
componentRegistry.registerAll(config.components);
|
|
146
|
+
}
|
|
135
147
|
const eventBus = createEventBus<AgentWidgetControllerEventMap>();
|
|
136
148
|
|
|
137
|
-
const storageAdapter: AgentWidgetStorageAdapter
|
|
138
|
-
config.storageAdapter;
|
|
149
|
+
const storageAdapter: AgentWidgetStorageAdapter =
|
|
150
|
+
config.storageAdapter ?? createLocalStorageAdapter();
|
|
139
151
|
let persistentMetadata: Record<string, unknown> = {};
|
|
140
152
|
let pendingStoredState: Promise<AgentWidgetStoredState | null> | null = null;
|
|
141
153
|
|
|
@@ -224,7 +236,9 @@ export const createAgentExperience = (
|
|
|
224
236
|
introTitle,
|
|
225
237
|
introSubtitle,
|
|
226
238
|
closeButton,
|
|
227
|
-
iconHolder
|
|
239
|
+
iconHolder,
|
|
240
|
+
headerTitle,
|
|
241
|
+
headerSubtitle
|
|
228
242
|
} = panelElements;
|
|
229
243
|
|
|
230
244
|
// Use mutable references for mic button so we can update them dynamically
|
|
@@ -300,11 +314,17 @@ export const createAgentExperience = (
|
|
|
300
314
|
: [];
|
|
301
315
|
|
|
302
316
|
function persistState(messagesOverride?: AgentWidgetMessage[]) {
|
|
303
|
-
if (!storageAdapter?.save
|
|
317
|
+
if (!storageAdapter?.save) return;
|
|
318
|
+
|
|
319
|
+
// Allow saving even if session doesn't exist yet (for metadata during init)
|
|
320
|
+
const messages = messagesOverride
|
|
321
|
+
? stripStreamingFromMessages(messagesOverride)
|
|
322
|
+
: session
|
|
323
|
+
? getMessagesForPersistence()
|
|
324
|
+
: [];
|
|
325
|
+
|
|
304
326
|
const payload = {
|
|
305
|
-
messages
|
|
306
|
-
? stripStreamingFromMessages(messagesOverride)
|
|
307
|
-
: getMessagesForPersistence(),
|
|
327
|
+
messages,
|
|
308
328
|
metadata: persistentMetadata
|
|
309
329
|
};
|
|
310
330
|
try {
|
|
@@ -516,6 +536,51 @@ export const createAgentExperience = (
|
|
|
516
536
|
}
|
|
517
537
|
}
|
|
518
538
|
|
|
539
|
+
// Check for component directive if no plugin handled it
|
|
540
|
+
if (!bubble && message.role === "assistant" && !message.variant) {
|
|
541
|
+
const enableComponentStreaming = config.enableComponentStreaming !== false; // Default to true
|
|
542
|
+
if (enableComponentStreaming && hasComponentDirective(message)) {
|
|
543
|
+
const directive = extractComponentDirectiveFromMessage(message);
|
|
544
|
+
if (directive) {
|
|
545
|
+
const componentBubble = renderComponentDirective(directive, {
|
|
546
|
+
config,
|
|
547
|
+
message,
|
|
548
|
+
transform
|
|
549
|
+
});
|
|
550
|
+
if (componentBubble) {
|
|
551
|
+
// Wrap component in standard bubble styling
|
|
552
|
+
const wrapper = document.createElement("div");
|
|
553
|
+
wrapper.className = [
|
|
554
|
+
"vanilla-message-bubble",
|
|
555
|
+
"tvw-max-w-[85%]",
|
|
556
|
+
"tvw-rounded-2xl",
|
|
557
|
+
"tvw-bg-cw-surface",
|
|
558
|
+
"tvw-border",
|
|
559
|
+
"tvw-border-cw-message-border",
|
|
560
|
+
"tvw-p-4"
|
|
561
|
+
].join(" ");
|
|
562
|
+
wrapper.setAttribute("data-message-id", message.id);
|
|
563
|
+
|
|
564
|
+
// Add text content above component if present (combined text+component response)
|
|
565
|
+
if (message.content && message.content.trim()) {
|
|
566
|
+
const textDiv = document.createElement("div");
|
|
567
|
+
textDiv.className = "tvw-mb-3 tvw-text-sm tvw-leading-relaxed";
|
|
568
|
+
textDiv.innerHTML = transform({
|
|
569
|
+
text: message.content,
|
|
570
|
+
message,
|
|
571
|
+
streaming: Boolean(message.streaming),
|
|
572
|
+
raw: message.rawContent
|
|
573
|
+
});
|
|
574
|
+
wrapper.appendChild(textDiv);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
wrapper.appendChild(componentBubble);
|
|
578
|
+
bubble = wrapper;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
519
584
|
// Fallback to default rendering if plugin returned null or no plugin matched
|
|
520
585
|
if (!bubble) {
|
|
521
586
|
if (message.variant === "reasoning" && message.reasoning) {
|
|
@@ -1295,6 +1360,14 @@ export const createAgentExperience = (
|
|
|
1295
1360
|
launcherButtonInstance.update(config);
|
|
1296
1361
|
}
|
|
1297
1362
|
|
|
1363
|
+
// Update panel header title and subtitle
|
|
1364
|
+
if (headerTitle && config.launcher?.title !== undefined) {
|
|
1365
|
+
headerTitle.textContent = config.launcher.title;
|
|
1366
|
+
}
|
|
1367
|
+
if (headerSubtitle && config.launcher?.subtitle !== undefined) {
|
|
1368
|
+
headerSubtitle.textContent = config.launcher.subtitle;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1298
1371
|
// Only update open state if launcher enabled state changed or autoExpand value changed
|
|
1299
1372
|
const launcherEnabledChanged = launcherEnabled !== prevLauncherEnabled;
|
|
1300
1373
|
const autoExpandChanged = autoExpand !== prevAutoExpand;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
2
|
+
import { componentRegistry, ComponentContext } from "../components/registry";
|
|
3
|
+
import { ComponentDirective, createComponentStreamParser } from "./component-parser";
|
|
4
|
+
import { createStandardBubble, MessageTransform } from "../components/message-bubble";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for component middleware
|
|
8
|
+
*/
|
|
9
|
+
export interface ComponentMiddlewareOptions {
|
|
10
|
+
config: AgentWidgetConfig;
|
|
11
|
+
message: AgentWidgetMessage;
|
|
12
|
+
transform: MessageTransform;
|
|
13
|
+
onPropsUpdate?: (props: Record<string, unknown>) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders a component directive into an HTMLElement
|
|
18
|
+
*/
|
|
19
|
+
export function renderComponentDirective(
|
|
20
|
+
directive: ComponentDirective,
|
|
21
|
+
options: ComponentMiddlewareOptions
|
|
22
|
+
): HTMLElement | null {
|
|
23
|
+
const { config, message, onPropsUpdate } = options;
|
|
24
|
+
|
|
25
|
+
// Get component renderer from registry
|
|
26
|
+
const renderer = componentRegistry.get(directive.component);
|
|
27
|
+
if (!renderer) {
|
|
28
|
+
// Component not found, fall back to default rendering
|
|
29
|
+
console.warn(
|
|
30
|
+
`[ComponentMiddleware] Component "${directive.component}" not found in registry. Falling back to default rendering.`
|
|
31
|
+
);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create component context
|
|
36
|
+
const context: ComponentContext = {
|
|
37
|
+
message,
|
|
38
|
+
config,
|
|
39
|
+
updateProps: (newProps: Record<string, unknown>) => {
|
|
40
|
+
if (onPropsUpdate) {
|
|
41
|
+
onPropsUpdate(newProps);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Render the component
|
|
48
|
+
const element = renderer(directive.props, context);
|
|
49
|
+
return element;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(
|
|
52
|
+
`[ComponentMiddleware] Error rendering component "${directive.component}":`,
|
|
53
|
+
error
|
|
54
|
+
);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates middleware that processes component directives from streamed JSON
|
|
61
|
+
*/
|
|
62
|
+
export function createComponentMiddleware() {
|
|
63
|
+
const parser = createComponentStreamParser();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
/**
|
|
67
|
+
* Process accumulated content and extract component directive
|
|
68
|
+
*/
|
|
69
|
+
processChunk: (accumulatedContent: string): ComponentDirective | null => {
|
|
70
|
+
return parser.processChunk(accumulatedContent);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the currently extracted directive
|
|
75
|
+
*/
|
|
76
|
+
getDirective: (): ComponentDirective | null => {
|
|
77
|
+
return parser.getExtractedDirective();
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Reset the parser state
|
|
82
|
+
*/
|
|
83
|
+
reset: () => {
|
|
84
|
+
parser.reset();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if a message contains a component directive in its raw content
|
|
91
|
+
*/
|
|
92
|
+
export function hasComponentDirective(message: AgentWidgetMessage): boolean {
|
|
93
|
+
if (!message.rawContent) return false;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(message.rawContent);
|
|
97
|
+
return (
|
|
98
|
+
typeof parsed === "object" &&
|
|
99
|
+
parsed !== null &&
|
|
100
|
+
"component" in parsed &&
|
|
101
|
+
typeof parsed.component === "string"
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extracts component directive from a complete message
|
|
110
|
+
*/
|
|
111
|
+
export function extractComponentDirectiveFromMessage(
|
|
112
|
+
message: AgentWidgetMessage
|
|
113
|
+
): ComponentDirective | null {
|
|
114
|
+
if (!message.rawContent) return null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(message.rawContent);
|
|
118
|
+
if (
|
|
119
|
+
typeof parsed === "object" &&
|
|
120
|
+
parsed !== null &&
|
|
121
|
+
"component" in parsed &&
|
|
122
|
+
typeof parsed.component === "string"
|
|
123
|
+
) {
|
|
124
|
+
return {
|
|
125
|
+
component: parsed.component,
|
|
126
|
+
props: (parsed.props && typeof parsed.props === "object" && parsed.props !== null
|
|
127
|
+
? parsed.props
|
|
128
|
+
: {}) as Record<string, unknown>,
|
|
129
|
+
raw: message.rawContent
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Not valid JSON or not a component directive
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { parse as parsePartialJson, STR, OBJ } from "partial-json";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a component directive extracted from JSON
|
|
5
|
+
*/
|
|
6
|
+
export interface ComponentDirective {
|
|
7
|
+
component: string;
|
|
8
|
+
props: Record<string, unknown>;
|
|
9
|
+
raw: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a parsed object is a component directive
|
|
14
|
+
*/
|
|
15
|
+
function isComponentDirective(obj: unknown): obj is { component: string; props?: unknown } {
|
|
16
|
+
if (!obj || typeof obj !== "object") return false;
|
|
17
|
+
if (!("component" in obj)) return false;
|
|
18
|
+
const component = (obj as { component: unknown }).component;
|
|
19
|
+
return typeof component === "string" && component.length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts component directive from parsed JSON object
|
|
24
|
+
*/
|
|
25
|
+
function extractComponentDirective(
|
|
26
|
+
parsed: unknown,
|
|
27
|
+
rawJson: string
|
|
28
|
+
): ComponentDirective | null {
|
|
29
|
+
if (!isComponentDirective(parsed)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const props = parsed.props && typeof parsed.props === "object" && parsed.props !== null
|
|
34
|
+
? (parsed.props as Record<string, unknown>)
|
|
35
|
+
: {};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
component: parsed.component,
|
|
39
|
+
props,
|
|
40
|
+
raw: rawJson
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a parser that extracts component directives from JSON streams
|
|
46
|
+
* This parser looks for objects with a "component" field and extracts
|
|
47
|
+
* the component name and props incrementally as they stream in.
|
|
48
|
+
*/
|
|
49
|
+
export function createComponentStreamParser() {
|
|
50
|
+
let extractedDirective: ComponentDirective | null = null;
|
|
51
|
+
let processedLength = 0;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
/**
|
|
55
|
+
* Get the currently extracted component directive
|
|
56
|
+
*/
|
|
57
|
+
getExtractedDirective: (): ComponentDirective | null => {
|
|
58
|
+
return extractedDirective;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Process a chunk of JSON and extract component directive if present
|
|
63
|
+
*/
|
|
64
|
+
processChunk: (accumulatedContent: string): ComponentDirective | null => {
|
|
65
|
+
// Validate that the accumulated content looks like JSON
|
|
66
|
+
const trimmed = accumulatedContent.trim();
|
|
67
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Skip if no new content
|
|
72
|
+
if (accumulatedContent.length <= processedLength) {
|
|
73
|
+
return extractedDirective;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Parse partial JSON - allow partial strings and objects during streaming
|
|
78
|
+
// STR | OBJ allows incomplete strings and objects during streaming
|
|
79
|
+
const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
|
|
80
|
+
|
|
81
|
+
// Try to extract component directive
|
|
82
|
+
const directive = extractComponentDirective(parsed, accumulatedContent);
|
|
83
|
+
if (directive) {
|
|
84
|
+
extractedDirective = directive;
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// If parsing fails completely, keep the last extracted directive
|
|
88
|
+
// This can happen with very malformed JSON during streaming
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Update processed length
|
|
92
|
+
processedLength = accumulatedContent.length;
|
|
93
|
+
|
|
94
|
+
return extractedDirective;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset the parser state
|
|
99
|
+
*/
|
|
100
|
+
reset: () => {
|
|
101
|
+
extractedDirective = null;
|
|
102
|
+
processedLength = 0;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Type guard to check if an object is a component directive
|
|
109
|
+
*/
|
|
110
|
+
export function isComponentDirectiveType(obj: unknown): obj is ComponentDirective {
|
|
111
|
+
return (
|
|
112
|
+
typeof obj === "object" &&
|
|
113
|
+
obj !== null &&
|
|
114
|
+
"component" in obj &&
|
|
115
|
+
typeof (obj as { component: unknown }).component === "string" &&
|
|
116
|
+
"props" in obj &&
|
|
117
|
+
typeof (obj as { props: unknown }).props === "object"
|
|
118
|
+
);
|
|
119
|
+
}
|
package/src/utils/constants.ts
CHANGED
package/src/utils/dom.ts
CHANGED
package/src/utils/formatting.ts
CHANGED
|
@@ -263,8 +263,8 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
263
263
|
|
|
264
264
|
// Skip if no new content
|
|
265
265
|
if (accumulatedContent.length <= processedLength) {
|
|
266
|
-
return extractedText !== null
|
|
267
|
-
? { text: extractedText, raw: accumulatedContent }
|
|
266
|
+
return extractedText !== null || extractedText === ""
|
|
267
|
+
? { text: extractedText || "", raw: accumulatedContent }
|
|
268
268
|
: null;
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -273,9 +273,21 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
273
273
|
// STR | OBJ allows incomplete strings and objects during streaming
|
|
274
274
|
const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
276
|
+
if (parsed && typeof parsed === "object") {
|
|
277
|
+
// Check for component directives - extract text if present for combined text+component
|
|
278
|
+
if (parsed.component && typeof parsed.component === "string") {
|
|
279
|
+
// For component directives, extract text if present, otherwise empty
|
|
280
|
+
extractedText = typeof parsed.text === "string" ? parsed.text : "";
|
|
281
|
+
}
|
|
282
|
+
// Check for form directives - these also don't have text fields
|
|
283
|
+
else if (parsed.type === "init" && parsed.form) {
|
|
284
|
+
// For form directives, return empty - they're handled by form postprocessor
|
|
285
|
+
extractedText = "";
|
|
286
|
+
}
|
|
287
|
+
// Extract text field if available
|
|
288
|
+
else if (typeof parsed.text === "string") {
|
|
289
|
+
extractedText = parsed.text;
|
|
290
|
+
}
|
|
279
291
|
}
|
|
280
292
|
} catch (error) {
|
|
281
293
|
// If parsing fails completely, keep the last extracted text
|
|
@@ -285,7 +297,8 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
285
297
|
// Update processed length
|
|
286
298
|
processedLength = accumulatedContent.length;
|
|
287
299
|
|
|
288
|
-
//
|
|
300
|
+
// Always return raw JSON for component/form directive detection
|
|
301
|
+
// Return empty string for text if it's a component/form directive
|
|
289
302
|
if (extractedText !== null) {
|
|
290
303
|
return {
|
|
291
304
|
text: extractedText,
|
|
@@ -318,6 +331,18 @@ export const createFlexibleJsonStreamParser = (
|
|
|
318
331
|
// Default text extractor that handles common patterns
|
|
319
332
|
const defaultExtractor = (parsed: any): string | null => {
|
|
320
333
|
if (!parsed || typeof parsed !== "object") return null;
|
|
334
|
+
|
|
335
|
+
// Check for component directives - extract text if present for combined text+component
|
|
336
|
+
if (parsed.component && typeof parsed.component === "string") {
|
|
337
|
+
// For component directives, extract text if present, otherwise empty
|
|
338
|
+
return typeof parsed.text === "string" ? parsed.text : "";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check for form directives - these also don't have text fields
|
|
342
|
+
if (parsed.type === "init" && parsed.form) {
|
|
343
|
+
// For form directives, return empty - they're handled by form postprocessor
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
321
346
|
|
|
322
347
|
// Check for action-based text fields
|
|
323
348
|
if (parsed.action) {
|
|
@@ -373,8 +398,8 @@ export const createFlexibleJsonStreamParser = (
|
|
|
373
398
|
// Update processed length
|
|
374
399
|
processedLength = accumulatedContent.length;
|
|
375
400
|
|
|
376
|
-
// Always return the raw JSON for action parsing
|
|
377
|
-
// Text may be null
|
|
401
|
+
// Always return the raw JSON for action parsing and component detection
|
|
402
|
+
// Text may be null or empty for component/form directives, that's ok
|
|
378
403
|
return {
|
|
379
404
|
text: extractedText || "",
|
|
380
405
|
raw: accumulatedContent
|
package/src/utils/positioning.ts
CHANGED
package/src/utils/theme.ts
CHANGED