@wopr-network/platform-ui-core 1.1.4 → 1.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,6 +3,8 @@ import {
3
3
  clearChatHistory,
4
4
  getSessionId,
5
5
  loadChatHistory,
6
+ MAX_CHAT_HISTORY,
7
+ MAX_MESSAGE_CONTENT_LENGTH,
6
8
  saveChatHistory,
7
9
  } from "@/lib/chat/chat-store";
8
10
  import type { ChatMessage } from "@/lib/chat/types";
@@ -84,4 +86,94 @@ describe("chat-store", () => {
84
86
  expect(loadChatHistory()).toEqual([]);
85
87
  });
86
88
  });
89
+
90
+ describe("saveChatHistory limits", () => {
91
+ it("keeps only the last MAX_CHAT_HISTORY messages", () => {
92
+ const total = MAX_CHAT_HISTORY + 50;
93
+ const messages: ChatMessage[] = Array.from({ length: total }, (_, i) => ({
94
+ id: String(i),
95
+ role: "user" as const,
96
+ content: `msg ${i}`,
97
+ timestamp: 1000 + i,
98
+ }));
99
+ saveChatHistory(messages);
100
+ const loaded = loadChatHistory();
101
+ expect(loaded).toHaveLength(MAX_CHAT_HISTORY);
102
+ // Should keep the LAST MAX_CHAT_HISTORY (indices 50-149)
103
+ expect(loaded[0].id).toBe("50");
104
+ expect(loaded[MAX_CHAT_HISTORY - 1].id).toBe(String(total - 1));
105
+ });
106
+
107
+ it("truncates message content exceeding MAX_MESSAGE_CONTENT_LENGTH and appends ellipsis", () => {
108
+ const longContent = "x".repeat(MAX_MESSAGE_CONTENT_LENGTH + 1000);
109
+ const messages: ChatMessage[] = [
110
+ { id: "1", role: "user", content: longContent, timestamp: 1000 },
111
+ ];
112
+ saveChatHistory(messages);
113
+ const loaded = loadChatHistory();
114
+ expect(loaded).toHaveLength(1);
115
+ expect(loaded[0].content).toHaveLength(MAX_MESSAGE_CONTENT_LENGTH + 1);
116
+ expect(loaded[0].content).toBe(`${"x".repeat(MAX_MESSAGE_CONTENT_LENGTH)}…`);
117
+ });
118
+
119
+ it("does not mutate the original messages array when saving", () => {
120
+ const longContent = "x".repeat(MAX_MESSAGE_CONTENT_LENGTH + 1000);
121
+ const messages: ChatMessage[] = [
122
+ { id: "1", role: "user", content: longContent, timestamp: 1000 },
123
+ ];
124
+ const originalContent = messages[0].content;
125
+ saveChatHistory(messages);
126
+ // localStorage copy is truncated; in-memory array must be unchanged
127
+ expect(messages[0].content).toBe(originalContent);
128
+ expect(messages[0].content).toHaveLength(MAX_MESSAGE_CONTENT_LENGTH + 1000);
129
+ });
130
+
131
+ it("slices by Unicode codepoints so emoji are not split", () => {
132
+ // Each emoji is 2 UTF-16 code units but 1 codepoint.
133
+ // String.slice() cuts by code units; [...str].slice() cuts by codepoints.
134
+ // A string of MAX_MESSAGE_CONTENT_LENGTH emoji has 2*MAX code units.
135
+ // With .slice() the last emoji would be split; with spread it must not be.
136
+ const emoji = "\u{1F600}"; // 😀 — 2 code units
137
+ const longContent = emoji.repeat(MAX_MESSAGE_CONTENT_LENGTH + 100);
138
+ const messages: ChatMessage[] = [
139
+ { id: "1", role: "user", content: longContent, timestamp: 1000 },
140
+ ];
141
+ saveChatHistory(messages);
142
+ const loaded = loadChatHistory();
143
+ const stored = loaded[0].content;
144
+ // The stored content (minus ellipsis) must decode to valid codepoints only.
145
+ // If the last emoji were split, it would contain a lone surrogate which
146
+ // JSON.parse would either mangle or preserve as an invalid codepoint.
147
+ const withoutEllipsis = stored.slice(0, -1); // remove "…"
148
+ expect([...withoutEllipsis].every((ch) => ch === emoji)).toBe(true);
149
+ });
150
+
151
+ it("does not truncate content at or below the limit", () => {
152
+ const exactContent = "y".repeat(MAX_MESSAGE_CONTENT_LENGTH);
153
+ const messages: ChatMessage[] = [
154
+ { id: "1", role: "user", content: exactContent, timestamp: 1000 },
155
+ ];
156
+ saveChatHistory(messages);
157
+ const loaded = loadChatHistory();
158
+ expect(loaded[0].content).toBe(exactContent);
159
+ });
160
+
161
+ it("applies both message count and content length limits together", () => {
162
+ const total = MAX_CHAT_HISTORY + 10;
163
+ const messages: ChatMessage[] = Array.from({ length: total }, (_, i) => ({
164
+ id: String(i),
165
+ role: "bot" as const,
166
+ content: "z".repeat(MAX_MESSAGE_CONTENT_LENGTH + 1000),
167
+ timestamp: 1000 + i,
168
+ }));
169
+ saveChatHistory(messages);
170
+ const loaded = loadChatHistory();
171
+ expect(loaded).toHaveLength(MAX_CHAT_HISTORY);
172
+ expect(loaded[0].id).toBe("10");
173
+ for (const msg of loaded) {
174
+ expect(msg.content).toHaveLength(MAX_MESSAGE_CONTENT_LENGTH + 1);
175
+ expect(msg.content.endsWith("…")).toBe(true);
176
+ }
177
+ });
178
+ });
87
179
  });
@@ -163,7 +163,7 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
163
163
 
164
164
  {/* bg-white is intentional -- QR codes require white background for scanability */}
165
165
  <div className="h-40 w-40 rounded-sm bg-white p-3 min-[375px]:h-48 min-[375px]:w-48">
166
- {/* biome-ignore lint/performance/noImgElement: QR code is a base64 data URL, not optimizable by next/image */}
166
+ {/* biome-ignore lint/performance/noImgElement: QR PNG is a base64 data URI from the API next/image does not support data: URIs (cannot optimize, resize, or lazy-load inline blobs). Raw <img> is the only option here. */}
167
167
  <img
168
168
  src={qrPng}
169
169
  alt="Scan this QR code with your phone"
@@ -205,7 +205,7 @@ export function FieldQR({ field, value: _value, onChange, error, botId }: FieldQ
205
205
  {/* bg-white is intentional -- QR codes require white background for scanability */}
206
206
  <div className="relative h-40 w-40 rounded-sm bg-white p-3 min-[375px]:h-48 min-[375px]:w-48">
207
207
  {qrPng && (
208
- // biome-ignore lint/performance/noImgElement: QR code is a base64 data URL
208
+ // biome-ignore lint/performance/noImgElement: base64 data URI see comment above for rationale
209
209
  <img src={qrPng} alt="Expired QR code" className="h-full w-full opacity-40" />
210
210
  )}
211
211
  {/* Dark overlay */}
@@ -5,6 +5,12 @@ import type { ChatMessage } from "./types";
5
5
  const HISTORY_KEY = storageKey("chat-history");
6
6
  const SESSION_KEY = storageKey("chat-session");
7
7
 
8
+ /** Maximum number of messages persisted to localStorage. */
9
+ export const MAX_CHAT_HISTORY = 100;
10
+
11
+ /** Maximum character length for a single message's content field. */
12
+ export const MAX_MESSAGE_CONTENT_LENGTH = 4_000;
13
+
8
14
  const ChatMessageSchema = z.object({
9
15
  id: z.string(),
10
16
  role: z.enum(["user", "bot", "event"]),
@@ -46,9 +52,15 @@ export function loadChatHistory(): ChatMessage[] {
46
52
  export function saveChatHistory(messages: ChatMessage[]): void {
47
53
  if (typeof window === "undefined") return;
48
54
  try {
49
- localStorage.setItem(HISTORY_KEY, JSON.stringify(messages));
55
+ const trimmed = messages.slice(-MAX_CHAT_HISTORY).map((msg) => {
56
+ const codepoints = [...msg.content];
57
+ return codepoints.length > MAX_MESSAGE_CONTENT_LENGTH
58
+ ? { ...msg, content: `${codepoints.slice(0, MAX_MESSAGE_CONTENT_LENGTH).join("")}…` }
59
+ : msg;
60
+ });
61
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmed));
50
62
  } catch {
51
- // ignore
63
+ // ignore — quota exceeded or private browsing
52
64
  }
53
65
  }
54
66