@runtypelabs/persona 1.48.0 → 2.1.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 +140 -8
- package/dist/index.cjs +90 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1098 -24
- package/dist/index.d.ts +1098 -24
- package/dist/index.global.js +134 -83
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +90 -39
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +849 -513
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +80 -0
- package/src/client.test.ts +20 -21
- package/src/client.ts +153 -4
- package/src/components/approval-bubble.ts +45 -42
- package/src/components/artifact-card.ts +91 -0
- package/src/components/artifact-pane.ts +501 -0
- package/src/components/composer-builder.ts +32 -27
- package/src/components/event-stream-view.ts +40 -40
- package/src/components/feedback.ts +36 -36
- package/src/components/forms.ts +11 -11
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +55 -36
- package/src/components/header-layouts.ts +58 -125
- package/src/components/launcher.ts +36 -21
- package/src/components/message-bubble.ts +92 -65
- package/src/components/messages.ts +2 -2
- package/src/components/panel.ts +42 -11
- package/src/components/reasoning-bubble.ts +23 -23
- package/src/components/registry.ts +4 -0
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +32 -32
- package/src/defaults.ts +30 -4
- package/src/index.ts +80 -2
- package/src/install.ts +22 -0
- package/src/plugins/types.ts +23 -0
- package/src/postprocessors.ts +2 -2
- package/src/runtime/host-layout.ts +174 -0
- package/src/runtime/init.test.ts +236 -0
- package/src/runtime/init.ts +114 -55
- package/src/session.ts +135 -2
- package/src/styles/tailwind.css +1 -1
- package/src/styles/widget.css +849 -513
- package/src/types/theme.ts +376 -0
- package/src/types.ts +338 -15
- package/src/ui.docked.test.ts +104 -0
- package/src/ui.ts +940 -227
- package/src/utils/artifact-gate.test.ts +255 -0
- package/src/utils/artifact-gate.ts +142 -0
- package/src/utils/artifact-resize.test.ts +64 -0
- package/src/utils/artifact-resize.ts +67 -0
- package/src/utils/attachment-manager.ts +10 -10
- package/src/utils/code-generators.test.ts +52 -0
- package/src/utils/code-generators.ts +40 -36
- package/src/utils/dock.ts +17 -0
- package/src/utils/dom-context.test.ts +504 -0
- package/src/utils/dom-context.ts +896 -0
- package/src/utils/dom.ts +12 -1
- package/src/utils/message-fingerprint.test.ts +187 -0
- package/src/utils/message-fingerprint.ts +105 -0
- package/src/utils/migration.ts +220 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/plugins.ts +175 -0
- package/src/utils/positioning.ts +4 -4
- package/src/utils/theme.test.ts +157 -0
- package/src/utils/theme.ts +224 -60
- package/src/utils/tokens.ts +701 -0
package/src/utils/dom.ts
CHANGED
|
@@ -12,6 +12,18 @@ export const createElement = <K extends keyof HTMLElementTagNameMap>(
|
|
|
12
12
|
return element;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export const createElementInDocument = <K extends keyof HTMLElementTagNameMap>(
|
|
16
|
+
documentRef: Document,
|
|
17
|
+
tag: K,
|
|
18
|
+
className?: string
|
|
19
|
+
): HTMLElementTagNameMap[K] => {
|
|
20
|
+
const element = documentRef.createElement(tag);
|
|
21
|
+
if (className) {
|
|
22
|
+
element.className = className;
|
|
23
|
+
}
|
|
24
|
+
return element;
|
|
25
|
+
};
|
|
26
|
+
|
|
15
27
|
export const createFragment = (): DocumentFragment => {
|
|
16
28
|
return document.createDocumentFragment();
|
|
17
29
|
};
|
|
@@ -22,4 +34,3 @@ export const createFragment = (): DocumentFragment => {
|
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
|
|
25
|
-
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
computeMessageFingerprint,
|
|
4
|
+
createMessageCache,
|
|
5
|
+
getCachedWrapper,
|
|
6
|
+
setCachedWrapper,
|
|
7
|
+
pruneCache,
|
|
8
|
+
type FingerprintableMessage,
|
|
9
|
+
} from "./message-fingerprint";
|
|
10
|
+
|
|
11
|
+
function makeMessage(overrides: Partial<FingerprintableMessage> = {}): FingerprintableMessage {
|
|
12
|
+
return {
|
|
13
|
+
id: "msg-1",
|
|
14
|
+
role: "assistant",
|
|
15
|
+
content: "Hello world",
|
|
16
|
+
streaming: false,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createFakeWrapper(id: string): HTMLElement {
|
|
22
|
+
return {
|
|
23
|
+
id: `wrapper-${id}`,
|
|
24
|
+
cloneNode: vi.fn(function (this: HTMLElement) {
|
|
25
|
+
return { ...this };
|
|
26
|
+
}),
|
|
27
|
+
} as unknown as HTMLElement;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("computeMessageFingerprint", () => {
|
|
31
|
+
it("produces a stable fingerprint for the same message", () => {
|
|
32
|
+
const msg = makeMessage();
|
|
33
|
+
const fp1 = computeMessageFingerprint(msg, 0);
|
|
34
|
+
const fp2 = computeMessageFingerprint(msg, 0);
|
|
35
|
+
expect(fp1).toBe(fp2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("changes when content changes", () => {
|
|
39
|
+
const fp1 = computeMessageFingerprint(makeMessage({ content: "Hello" }), 0);
|
|
40
|
+
const fp2 = computeMessageFingerprint(makeMessage({ content: "Hello world" }), 0);
|
|
41
|
+
expect(fp1).not.toBe(fp2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("changes when streaming changes", () => {
|
|
45
|
+
const fp1 = computeMessageFingerprint(makeMessage({ streaming: false }), 0);
|
|
46
|
+
const fp2 = computeMessageFingerprint(makeMessage({ streaming: true }), 0);
|
|
47
|
+
expect(fp1).not.toBe(fp2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("changes when role changes", () => {
|
|
51
|
+
const fp1 = computeMessageFingerprint(makeMessage({ role: "assistant" }), 0);
|
|
52
|
+
const fp2 = computeMessageFingerprint(makeMessage({ role: "user" }), 0);
|
|
53
|
+
expect(fp1).not.toBe(fp2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("changes when variant changes", () => {
|
|
57
|
+
const fp1 = computeMessageFingerprint(makeMessage({ variant: undefined }), 0);
|
|
58
|
+
const fp2 = computeMessageFingerprint(makeMessage({ variant: "reasoning" }), 0);
|
|
59
|
+
expect(fp1).not.toBe(fp2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("changes when configVersion changes", () => {
|
|
63
|
+
const msg = makeMessage();
|
|
64
|
+
const fp1 = computeMessageFingerprint(msg, 0);
|
|
65
|
+
const fp2 = computeMessageFingerprint(msg, 1);
|
|
66
|
+
expect(fp1).not.toBe(fp2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("changes when rawContent changes", () => {
|
|
70
|
+
const fp1 = computeMessageFingerprint(makeMessage({ rawContent: undefined }), 0);
|
|
71
|
+
const fp2 = computeMessageFingerprint(makeMessage({ rawContent: '{"action":"checkout"}' }), 0);
|
|
72
|
+
expect(fp1).not.toBe(fp2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("changes when llmContent changes", () => {
|
|
76
|
+
const fp1 = computeMessageFingerprint(makeMessage({ llmContent: undefined }), 0);
|
|
77
|
+
const fp2 = computeMessageFingerprint(makeMessage({ llmContent: "context for llm" }), 0);
|
|
78
|
+
expect(fp1).not.toBe(fp2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("changes when approval status changes", () => {
|
|
82
|
+
const fp1 = computeMessageFingerprint(makeMessage({ approval: { status: "pending" } }), 0);
|
|
83
|
+
const fp2 = computeMessageFingerprint(makeMessage({ approval: { status: "approved" } }), 0);
|
|
84
|
+
expect(fp1).not.toBe(fp2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("changes when toolCall status changes", () => {
|
|
88
|
+
const fp1 = computeMessageFingerprint(makeMessage({ toolCall: { status: "running" } }), 0);
|
|
89
|
+
const fp2 = computeMessageFingerprint(makeMessage({ toolCall: { status: "complete" } }), 0);
|
|
90
|
+
expect(fp1).not.toBe(fp2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("changes when reasoning chunks change", () => {
|
|
94
|
+
const fp1 = computeMessageFingerprint(makeMessage({ reasoning: { chunks: ["step 1"] } }), 0);
|
|
95
|
+
const fp2 = computeMessageFingerprint(makeMessage({ reasoning: { chunks: ["step 1", "step 2"] } }), 0);
|
|
96
|
+
expect(fp1).not.toBe(fp2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("changes when contentParts length changes", () => {
|
|
100
|
+
const fp1 = computeMessageFingerprint(makeMessage({ contentParts: [] }), 0);
|
|
101
|
+
const fp2 = computeMessageFingerprint(makeMessage({ contentParts: [{ type: "text", text: "hi" }] }), 0);
|
|
102
|
+
expect(fp1).not.toBe(fp2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("handles undefined optional fields", () => {
|
|
106
|
+
const msg: FingerprintableMessage = { id: "x", role: "user", content: "" };
|
|
107
|
+
const fp = computeMessageFingerprint(msg, 0);
|
|
108
|
+
expect(typeof fp).toBe("string");
|
|
109
|
+
expect(fp.length).toBeGreaterThan(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("detects streaming content appends via last-32-chars check", () => {
|
|
113
|
+
const fp1 = computeMessageFingerprint(
|
|
114
|
+
makeMessage({ content: "The quick brown fox jumps over the lazy dog" }),
|
|
115
|
+
0
|
|
116
|
+
);
|
|
117
|
+
const fp2 = computeMessageFingerprint(
|
|
118
|
+
makeMessage({ content: "The quick brown fox jumps over the lazy dog!" }),
|
|
119
|
+
0
|
|
120
|
+
);
|
|
121
|
+
expect(fp1).not.toBe(fp2);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("MessageCache", () => {
|
|
126
|
+
it("returns null for unknown message id", () => {
|
|
127
|
+
const cache = createMessageCache();
|
|
128
|
+
expect(getCachedWrapper(cache, "unknown", "fp")).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns cached wrapper on fingerprint match", () => {
|
|
132
|
+
const cache = createMessageCache();
|
|
133
|
+
const wrapper = createFakeWrapper("msg-1");
|
|
134
|
+
setCachedWrapper(cache, "msg-1", "fp-abc", wrapper);
|
|
135
|
+
|
|
136
|
+
const result = getCachedWrapper(cache, "msg-1", "fp-abc");
|
|
137
|
+
expect(result).toBe(wrapper);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns null on fingerprint mismatch", () => {
|
|
141
|
+
const cache = createMessageCache();
|
|
142
|
+
const wrapper = createFakeWrapper("msg-1");
|
|
143
|
+
setCachedWrapper(cache, "msg-1", "fp-abc", wrapper);
|
|
144
|
+
|
|
145
|
+
const result = getCachedWrapper(cache, "msg-1", "fp-different");
|
|
146
|
+
expect(result).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("overwrites entry on re-set", () => {
|
|
150
|
+
const cache = createMessageCache();
|
|
151
|
+
const wrapper1 = createFakeWrapper("msg-1");
|
|
152
|
+
const wrapper2 = createFakeWrapper("msg-1");
|
|
153
|
+
setCachedWrapper(cache, "msg-1", "fp-1", wrapper1);
|
|
154
|
+
setCachedWrapper(cache, "msg-1", "fp-2", wrapper2);
|
|
155
|
+
|
|
156
|
+
expect(getCachedWrapper(cache, "msg-1", "fp-1")).toBeNull();
|
|
157
|
+
expect(getCachedWrapper(cache, "msg-1", "fp-2")).toBe(wrapper2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("prunes entries for removed message IDs", () => {
|
|
161
|
+
const cache = createMessageCache();
|
|
162
|
+
setCachedWrapper(cache, "a", "fp-a", createFakeWrapper("a"));
|
|
163
|
+
setCachedWrapper(cache, "b", "fp-b", createFakeWrapper("b"));
|
|
164
|
+
setCachedWrapper(cache, "c", "fp-c", createFakeWrapper("c"));
|
|
165
|
+
|
|
166
|
+
const active = new Set(["a", "b"]);
|
|
167
|
+
pruneCache(cache, active);
|
|
168
|
+
|
|
169
|
+
expect(getCachedWrapper(cache, "a", "fp-a")).not.toBeNull();
|
|
170
|
+
expect(getCachedWrapper(cache, "b", "fp-b")).not.toBeNull();
|
|
171
|
+
expect(getCachedWrapper(cache, "c", "fp-c")).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("handles pruning with empty active set", () => {
|
|
175
|
+
const cache = createMessageCache();
|
|
176
|
+
setCachedWrapper(cache, "a", "fp-a", createFakeWrapper("a"));
|
|
177
|
+
|
|
178
|
+
pruneCache(cache, new Set());
|
|
179
|
+
expect(getCachedWrapper(cache, "a", "fp-a")).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("handles pruning an empty cache", () => {
|
|
183
|
+
const cache = createMessageCache();
|
|
184
|
+
pruneCache(cache, new Set(["a"]));
|
|
185
|
+
expect(cache.size).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message-level fingerprint cache for skipping re-renders of unchanged messages.
|
|
3
|
+
*
|
|
4
|
+
* During streaming, every SSE chunk triggers renderMessagesWithPluginsImpl which
|
|
5
|
+
* rebuilds ALL message bubbles. The fingerprint cache lets us skip rebuilding
|
|
6
|
+
* messages whose content hasn't changed, so only the actively streaming message
|
|
7
|
+
* is re-rendered each cycle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type FingerprintableMessage = {
|
|
11
|
+
id: string;
|
|
12
|
+
role: string;
|
|
13
|
+
content: string;
|
|
14
|
+
streaming?: boolean;
|
|
15
|
+
variant?: string;
|
|
16
|
+
rawContent?: string;
|
|
17
|
+
llmContent?: string;
|
|
18
|
+
approval?: { status?: string; [key: string]: unknown };
|
|
19
|
+
toolCall?: { status?: string; [key: string]: unknown };
|
|
20
|
+
reasoning?: { chunks?: string[]; status?: string; [key: string]: unknown };
|
|
21
|
+
contentParts?: unknown[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type MessageCacheEntry = {
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
wrapper: HTMLElement;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type MessageCache = Map<string, MessageCacheEntry>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compute a fast fingerprint for a message to detect changes.
|
|
33
|
+
* Uses string concatenation with a delimiter rather than JSON.stringify for performance.
|
|
34
|
+
* The configVersion parameter ensures cache invalidation when widget config changes.
|
|
35
|
+
*/
|
|
36
|
+
export function computeMessageFingerprint(
|
|
37
|
+
message: FingerprintableMessage,
|
|
38
|
+
configVersion: number
|
|
39
|
+
): string {
|
|
40
|
+
return [
|
|
41
|
+
message.id,
|
|
42
|
+
message.role,
|
|
43
|
+
message.content?.length ?? 0,
|
|
44
|
+
message.content?.slice(-32) ?? "",
|
|
45
|
+
message.streaming ? "1" : "0",
|
|
46
|
+
message.variant ?? "",
|
|
47
|
+
message.rawContent?.length ?? 0,
|
|
48
|
+
message.llmContent?.length ?? 0,
|
|
49
|
+
message.approval?.status ?? "",
|
|
50
|
+
message.toolCall?.status ?? "",
|
|
51
|
+
message.reasoning?.chunks?.length ?? 0,
|
|
52
|
+
message.contentParts?.length ?? 0,
|
|
53
|
+
configVersion,
|
|
54
|
+
].join("\x00");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a new message cache instance.
|
|
59
|
+
*/
|
|
60
|
+
export function createMessageCache(): MessageCache {
|
|
61
|
+
return new Map();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Look up a cached wrapper for a message. Returns the cached wrapper
|
|
66
|
+
* if the fingerprint matches, or null if the message needs re-rendering.
|
|
67
|
+
*/
|
|
68
|
+
export function getCachedWrapper(
|
|
69
|
+
cache: MessageCache,
|
|
70
|
+
messageId: string,
|
|
71
|
+
fingerprint: string
|
|
72
|
+
): HTMLElement | null {
|
|
73
|
+
const entry = cache.get(messageId);
|
|
74
|
+
if (entry && entry.fingerprint === fingerprint) {
|
|
75
|
+
return entry.wrapper;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Store a rendered wrapper in the cache.
|
|
82
|
+
*/
|
|
83
|
+
export function setCachedWrapper(
|
|
84
|
+
cache: MessageCache,
|
|
85
|
+
messageId: string,
|
|
86
|
+
fingerprint: string,
|
|
87
|
+
wrapper: HTMLElement
|
|
88
|
+
): void {
|
|
89
|
+
cache.set(messageId, { fingerprint, wrapper });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove cache entries for messages that no longer exist.
|
|
94
|
+
* Call after each render pass with the current message IDs.
|
|
95
|
+
*/
|
|
96
|
+
export function pruneCache(
|
|
97
|
+
cache: MessageCache,
|
|
98
|
+
activeMessageIds: Set<string>
|
|
99
|
+
): void {
|
|
100
|
+
for (const key of cache.keys()) {
|
|
101
|
+
if (!activeMessageIds.has(key)) {
|
|
102
|
+
cache.delete(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { AgentWidgetTheme } from '../types';
|
|
2
|
+
import type { PersonaTheme } from '../types/theme';
|
|
3
|
+
|
|
4
|
+
export interface V1ToV2MigrationOptions {
|
|
5
|
+
warn?: boolean;
|
|
6
|
+
colorScheme?: 'light' | 'dark' | 'auto';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const v1ToV2Mapping: Record<string, string> = {
|
|
10
|
+
primary: 'palette.colors.primary.500',
|
|
11
|
+
secondary: 'palette.colors.secondary.500',
|
|
12
|
+
accent: 'palette.colors.accent.600',
|
|
13
|
+
surface: 'palette.colors.gray.50',
|
|
14
|
+
muted: 'palette.colors.gray.500',
|
|
15
|
+
container: 'palette.colors.gray.100',
|
|
16
|
+
border: 'palette.colors.gray.200',
|
|
17
|
+
divider: 'palette.colors.gray.200',
|
|
18
|
+
messageBorder: 'palette.colors.gray.200',
|
|
19
|
+
inputBackground: 'palette.colors.gray.50',
|
|
20
|
+
callToAction: 'palette.colors.gray.900',
|
|
21
|
+
callToActionBackground: 'palette.colors.gray.50',
|
|
22
|
+
sendButtonBackgroundColor: 'semantic.colors.primary',
|
|
23
|
+
sendButtonTextColor: 'semantic.colors.textInverse',
|
|
24
|
+
sendButtonBorderColor: 'semantic.colors.primary',
|
|
25
|
+
closeButtonColor: 'palette.colors.gray.500',
|
|
26
|
+
closeButtonBackgroundColor: 'transparent',
|
|
27
|
+
clearChatIconColor: 'palette.colors.gray.500',
|
|
28
|
+
clearChatBackgroundColor: 'transparent',
|
|
29
|
+
micIconColor: 'palette.colors.gray.900',
|
|
30
|
+
micBackgroundColor: 'transparent',
|
|
31
|
+
recordingIconColor: 'palette.colors.white',
|
|
32
|
+
recordingBackgroundColor: 'palette.colors.error.500',
|
|
33
|
+
tooltipBackground: 'palette.colors.gray.900',
|
|
34
|
+
tooltipForeground: 'palette.colors.white',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const v1RadiusMapping: Record<string, string> = {
|
|
38
|
+
radiusSm: 'palette.radius.md',
|
|
39
|
+
radiusMd: 'palette.radius.lg',
|
|
40
|
+
radiusLg: 'palette.radius.xl',
|
|
41
|
+
launcherRadius: 'palette.radius.full',
|
|
42
|
+
buttonRadius: 'palette.radius.full',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function migrateV1Theme(
|
|
46
|
+
v1Theme: AgentWidgetTheme | undefined,
|
|
47
|
+
options: V1ToV2MigrationOptions = {}
|
|
48
|
+
): Partial<PersonaTheme> {
|
|
49
|
+
if (!v1Theme) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const migrated: any = {
|
|
54
|
+
palette: {
|
|
55
|
+
colors: {
|
|
56
|
+
primary: {},
|
|
57
|
+
gray: {},
|
|
58
|
+
secondary: {},
|
|
59
|
+
accent: {},
|
|
60
|
+
success: {},
|
|
61
|
+
warning: {},
|
|
62
|
+
error: {},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
semantic: {
|
|
66
|
+
colors: {
|
|
67
|
+
interactive: {},
|
|
68
|
+
feedback: {},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const [key, value] of Object.entries(v1Theme)) {
|
|
74
|
+
if (!value) continue;
|
|
75
|
+
|
|
76
|
+
if (key in v1ToV2Mapping) {
|
|
77
|
+
const v2Path = v1ToV2Mapping[key];
|
|
78
|
+
const [category, type, name, shade] = v2Path.split('.');
|
|
79
|
+
|
|
80
|
+
if (category === 'palette' && type === 'colors') {
|
|
81
|
+
const colorName = name as keyof typeof migrated.palette.colors;
|
|
82
|
+
if (migrated.palette.colors[colorName]) {
|
|
83
|
+
(migrated.palette.colors[colorName] as any)[shade || '500'] = value;
|
|
84
|
+
}
|
|
85
|
+
} else if (category === 'semantic') {
|
|
86
|
+
const pathParts = v2Path.replace('semantic.colors.', '').split('.');
|
|
87
|
+
if (pathParts.length === 1) {
|
|
88
|
+
migrated.semantic.colors[pathParts[0]] = value;
|
|
89
|
+
} else {
|
|
90
|
+
(migrated.semantic.colors as any)[pathParts[0]] = {
|
|
91
|
+
...(migrated.semantic.colors as any)[pathParts[0]],
|
|
92
|
+
[pathParts[1]]: value,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (key in v1RadiusMapping) {
|
|
97
|
+
if (!migrated.palette.radius) {
|
|
98
|
+
migrated.palette.radius = {};
|
|
99
|
+
}
|
|
100
|
+
const radiusKey = key.replace('radius', '').toLowerCase();
|
|
101
|
+
(migrated.palette.radius as any)[radiusKey || 'md'] = value;
|
|
102
|
+
} else if (key === 'inputFontFamily') {
|
|
103
|
+
if (!migrated.palette.typography) {
|
|
104
|
+
migrated.palette.typography = { fontFamily: {} };
|
|
105
|
+
}
|
|
106
|
+
migrated.palette.typography.fontFamily = {
|
|
107
|
+
sans: value === 'sans-serif' ? 'system-ui, sans-serif' : undefined,
|
|
108
|
+
serif: value === 'serif' ? 'Georgia, serif' : undefined,
|
|
109
|
+
mono: value === 'mono' ? 'monospace' : undefined,
|
|
110
|
+
};
|
|
111
|
+
} else if (key === 'inputFontWeight') {
|
|
112
|
+
if (!migrated.palette.typography) {
|
|
113
|
+
migrated.palette.typography = { fontWeight: {} };
|
|
114
|
+
}
|
|
115
|
+
migrated.palette.typography.fontWeight = {
|
|
116
|
+
normal: value,
|
|
117
|
+
};
|
|
118
|
+
} else if (key === 'panelBorder') {
|
|
119
|
+
if (!migrated.components) {
|
|
120
|
+
migrated.components = {};
|
|
121
|
+
}
|
|
122
|
+
if (!migrated.components.panel) {
|
|
123
|
+
migrated.components.panel = {};
|
|
124
|
+
}
|
|
125
|
+
migrated.components.panel.border = value;
|
|
126
|
+
} else if (key === 'panelShadow') {
|
|
127
|
+
if (!migrated.components) {
|
|
128
|
+
migrated.components = {};
|
|
129
|
+
}
|
|
130
|
+
if (!migrated.components.panel) {
|
|
131
|
+
migrated.components.panel = {};
|
|
132
|
+
}
|
|
133
|
+
migrated.components.panel.shadow = value;
|
|
134
|
+
} else if (key === 'panelBorderRadius') {
|
|
135
|
+
if (!migrated.components) {
|
|
136
|
+
migrated.components = {};
|
|
137
|
+
}
|
|
138
|
+
if (!migrated.components.panel) {
|
|
139
|
+
migrated.components.panel = {};
|
|
140
|
+
}
|
|
141
|
+
migrated.components.panel.borderRadius = value;
|
|
142
|
+
} else if (key === 'messageUserShadow') {
|
|
143
|
+
if (!migrated.components) migrated.components = {};
|
|
144
|
+
if (!migrated.components.message) migrated.components.message = {};
|
|
145
|
+
if (!migrated.components.message.user) migrated.components.message.user = {};
|
|
146
|
+
(migrated.components.message.user as { shadow?: string }).shadow = value as string;
|
|
147
|
+
} else if (key === 'messageAssistantShadow') {
|
|
148
|
+
if (!migrated.components) migrated.components = {};
|
|
149
|
+
if (!migrated.components.message) migrated.components.message = {};
|
|
150
|
+
if (!migrated.components.message.assistant) migrated.components.message.assistant = {};
|
|
151
|
+
(migrated.components.message.assistant as { shadow?: string }).shadow = value as string;
|
|
152
|
+
} else if (key === 'toolBubbleShadow') {
|
|
153
|
+
if (!migrated.components) migrated.components = {};
|
|
154
|
+
if (!migrated.components.toolBubble) migrated.components.toolBubble = {};
|
|
155
|
+
(migrated.components.toolBubble as { shadow?: string }).shadow = value as string;
|
|
156
|
+
} else if (key === 'reasoningBubbleShadow') {
|
|
157
|
+
if (!migrated.components) migrated.components = {};
|
|
158
|
+
if (!migrated.components.reasoningBubble) migrated.components.reasoningBubble = {};
|
|
159
|
+
(migrated.components.reasoningBubble as { shadow?: string }).shadow = value as string;
|
|
160
|
+
} else if (key === 'composerShadow') {
|
|
161
|
+
if (!migrated.components) migrated.components = {};
|
|
162
|
+
if (!migrated.components.composer) migrated.components.composer = {};
|
|
163
|
+
(migrated.components.composer as { shadow?: string }).shadow = value as string;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (options.warn !== false) {
|
|
168
|
+
console.warn(
|
|
169
|
+
'[Persona Widget] v1 theme configuration detected. ' +
|
|
170
|
+
'v1 themes are deprecated in v2.0.0. ' +
|
|
171
|
+
'Please migrate to the new semantic token system. ' +
|
|
172
|
+
'See https://persona.sh/docs/v2-migration for guidance.'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return migrated;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function validateV1Theme(v1Theme: unknown): {
|
|
180
|
+
valid: boolean;
|
|
181
|
+
warnings: string[];
|
|
182
|
+
} {
|
|
183
|
+
const warnings: string[] = [];
|
|
184
|
+
const theme = v1Theme as AgentWidgetTheme | undefined;
|
|
185
|
+
|
|
186
|
+
if (!theme) {
|
|
187
|
+
return { valid: true, warnings: [] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const v1ThemeChromeKeys = new Set([
|
|
191
|
+
'panelBorder',
|
|
192
|
+
'panelShadow',
|
|
193
|
+
'panelBorderRadius',
|
|
194
|
+
'messageUserShadow',
|
|
195
|
+
'messageAssistantShadow',
|
|
196
|
+
'toolBubbleShadow',
|
|
197
|
+
'reasoningBubbleShadow',
|
|
198
|
+
'composerShadow',
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
const deprecatedProperties = Object.keys(theme).filter(
|
|
202
|
+
(key) =>
|
|
203
|
+
!(
|
|
204
|
+
key in v1ToV2Mapping ||
|
|
205
|
+
key in v1RadiusMapping ||
|
|
206
|
+
key === 'inputFontFamily' ||
|
|
207
|
+
key === 'inputFontWeight' ||
|
|
208
|
+
key.startsWith('panel') ||
|
|
209
|
+
v1ThemeChromeKeys.has(key)
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (deprecatedProperties.length > 0) {
|
|
214
|
+
warnings.push(
|
|
215
|
+
`The following v1 theme properties have no v2 equivalent and will be ignored: ${deprecatedProperties.join(', ')}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { valid: true, warnings };
|
|
220
|
+
}
|
package/src/utils/morph.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const morphMessages = (
|
|
|
27
27
|
// Preserve typing indicator dots to maintain animation continuity
|
|
28
28
|
// Also preserve elements with data-preserve-animation attribute for custom loading indicators
|
|
29
29
|
if (preserveTypingAnimation) {
|
|
30
|
-
if (oldNode.classList.contains("
|
|
30
|
+
if (oldNode.classList.contains("persona-animate-typing") ||
|
|
31
31
|
oldNode.hasAttribute("data-preserve-animation")) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|