@copilotz/chat-ui 0.3.0 → 0.3.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/README.md CHANGED
@@ -31,7 +31,7 @@ Then you need to show tool calls — the library doesn't support that. Streaming
31
31
  | Input | File upload (drag & drop), audio recording, attachment previews |
32
32
  | Threads | Sidebar with search, archive, date grouping, rename, delete |
33
33
  | User Profile | Dynamic fields, memories (CRUD), agent vs user distinction |
34
- | Customization | 50+ labels (i18n-ready), feature toggles, 4 presets, theming |
34
+ | Customization | 50+ labels (i18n-ready), feature toggles, theming |
35
35
 
36
36
  **One package. Backend-agnostic. Production-ready.**
37
37
 
@@ -142,26 +142,6 @@ const agents = [
142
142
 
143
143
  The configuration system lets you customize everything without touching the component internals.
144
144
 
145
- ### Presets
146
-
147
- Start with a preset and override what you need:
148
-
149
- ```tsx
150
- import { ChatUI, chatConfigPresets } from '@copilotz/chat-ui';
151
-
152
- // Minimal: no threads, no file upload, compact mode
153
- <ChatUI config={chatConfigPresets.minimal} />
154
-
155
- // Full: all features enabled, timestamps, word count
156
- <ChatUI config={chatConfigPresets.full} />
157
-
158
- // Developer: tool calls visible, timestamps, file upload
159
- <ChatUI config={chatConfigPresets.developer} />
160
-
161
- // Customer Support: threads, file upload, no message editing
162
- <ChatUI config={chatConfigPresets.customer_support} />
163
- ```
164
-
165
145
  ### Custom Configuration
166
146
 
167
147
  ```tsx
@@ -437,8 +417,7 @@ export { UserMenu } from './components/chat/UserMenu';
437
417
  export { ChatUserContextProvider, useChatUserContext } from './components/chat/UserContext';
438
418
 
439
419
  // Configuration
440
- export { defaultChatConfig, mergeConfig, chatConfigPresets, validateConfig } from './config/chatConfig';
441
- export { themeUtils, featureFlags, configUtils } from './config/chatConfig';
420
+ export { defaultChatConfig, mergeConfig } from './config/chatConfig';
442
421
 
443
422
  // Types
444
423
  export type { ChatMessage, ChatThread, ChatConfig, ChatCallbacks } from './types/chatTypes';
@@ -460,17 +439,7 @@ import '@copilotz/chat-ui/styles.css';
460
439
 
461
440
  ### Theming
462
441
 
463
- The component respects the `dark` class on your document root. Set theme programmatically:
464
-
465
- ```tsx
466
- import { themeUtils } from '@copilotz/chat-ui';
467
-
468
- // Apply theme
469
- themeUtils.applyTheme('dark'); // or 'light' or 'auto'
470
-
471
- // Get system preference
472
- const systemTheme = themeUtils.getSystemTheme(); // 'light' | 'dark'
473
- ```
442
+ The component respects the `dark` class on your document root. Set `ui.theme` to `'light'`, `'dark'`, or `'auto'` (follows system preference).
474
443
 
475
444
  ### CSS Variables
476
445
 
package/dist/index.cjs CHANGED
@@ -43,20 +43,15 @@ __export(index_exports, {
43
43
  UserMenu: () => UserMenu,
44
44
  UserProfile: () => UserProfile,
45
45
  assignAgentColors: () => assignAgentColors,
46
- chatConfigPresets: () => chatConfigPresets,
47
46
  chatUtils: () => chatUtils,
48
47
  cn: () => cn,
49
- configUtils: () => configUtils,
50
48
  createObjectUrlFromDataUrl: () => createObjectUrlFromDataUrl,
51
49
  defaultChatConfig: () => defaultChatConfig,
52
- featureFlags: () => featureFlags,
53
50
  formatDate: () => formatDate,
54
51
  getAgentColor: () => getAgentColor,
55
52
  getAgentInitials: () => getAgentInitials,
56
53
  mergeConfig: () => mergeConfig,
57
- themeUtils: () => themeUtils,
58
- useChatUserContext: () => useChatUserContext,
59
- validateConfig: () => validateConfig
54
+ useChatUserContext: () => useChatUserContext
60
55
  });
61
56
  module.exports = __toCommonJS(index_exports);
62
57
 
@@ -230,127 +225,6 @@ function mergeConfig(_baseConfig, userConfig) {
230
225
  headerActions: userConfig.headerActions || defaultChatConfig.headerActions
231
226
  };
232
227
  }
233
- var chatConfigPresets = {
234
- minimal: {
235
- features: {
236
- enableThreads: false,
237
- enableFileUpload: false,
238
- enableAudioRecording: false,
239
- enableMessageEditing: false,
240
- enableMessageCopy: true,
241
- enableRegeneration: true,
242
- enableToolCallsDisplay: false
243
- },
244
- ui: {
245
- compactMode: true,
246
- showTimestamps: false,
247
- showAvatars: false
248
- }
249
- },
250
- full: {
251
- features: {
252
- enableThreads: true,
253
- enableFileUpload: true,
254
- enableAudioRecording: true,
255
- enableMessageEditing: true,
256
- enableMessageCopy: true,
257
- enableRegeneration: true,
258
- enableToolCallsDisplay: true
259
- },
260
- ui: {
261
- showTimestamps: true,
262
- showAvatars: true,
263
- compactMode: false,
264
- showWordCount: true
265
- }
266
- },
267
- developer: {
268
- features: {
269
- enableThreads: true,
270
- enableFileUpload: true,
271
- enableAudioRecording: false,
272
- enableMessageEditing: true,
273
- enableMessageCopy: true,
274
- enableRegeneration: true,
275
- enableToolCallsDisplay: true
276
- },
277
- ui: {
278
- showTimestamps: true,
279
- showAvatars: true,
280
- compactMode: false,
281
- showWordCount: true
282
- }
283
- },
284
- customer_support: {
285
- branding: {
286
- title: "Customer Support",
287
- subtitle: "How can I help you today?"
288
- },
289
- features: {
290
- enableThreads: true,
291
- enableFileUpload: true,
292
- enableAudioRecording: false,
293
- enableMessageEditing: false,
294
- enableMessageCopy: true,
295
- enableRegeneration: false,
296
- enableToolCallsDisplay: false
297
- },
298
- ui: {
299
- showTimestamps: true,
300
- showAvatars: true,
301
- compactMode: false
302
- }
303
- }
304
- };
305
- function validateConfig(config) {
306
- const errors = [];
307
- if (config.features?.maxAttachments && config.features.maxAttachments < 1) {
308
- errors.push("maxAttachments must be at least 1");
309
- }
310
- if (config.features?.maxFileSize && config.features.maxFileSize < 1024) {
311
- errors.push("maxFileSize must be at least 1024 bytes (1KB)");
312
- }
313
- if (config.branding?.title && typeof config.branding.title !== "string") {
314
- errors.push("branding.title must be a string");
315
- }
316
- return errors;
317
- }
318
- var themeUtils = {
319
- getSystemTheme: () => {
320
- if (typeof globalThis.matchMedia === "undefined") return "light";
321
- return globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
322
- },
323
- resolveTheme: (theme) => {
324
- return theme === "auto" ? themeUtils.getSystemTheme() : theme;
325
- },
326
- applyTheme: (theme) => {
327
- if (typeof document === "undefined") return;
328
- const resolvedTheme = themeUtils.resolveTheme(theme);
329
- document.documentElement.classList.toggle("dark", resolvedTheme === "dark");
330
- }
331
- };
332
- var featureFlags = {
333
- isEnabled: (config, feature) => {
334
- return config.features[feature] === true;
335
- },
336
- getEnabledFeatures: (config) => {
337
- return Object.entries(config.features).filter(([_, enabled]) => enabled === true).map(([feature]) => feature);
338
- },
339
- hasAnyFeature: (config, features) => {
340
- return features.some((feature) => featureFlags.isEnabled(config, feature));
341
- }
342
- };
343
- var configUtils = {
344
- createConfigHook: (config) => {
345
- return {
346
- config,
347
- isFeatureEnabled: (feature) => featureFlags.isEnabled(config, feature),
348
- getLabel: (key) => config.labels[key],
349
- getBranding: () => config.branding,
350
- getUI: () => config.ui
351
- };
352
- }
353
- };
354
228
 
355
229
  // src/components/chat/Message.tsx
356
230
  var import_react = __toESM(require("react"), 1);
@@ -3181,7 +3055,7 @@ var ChatHeader = ({
3181
3055
  };
3182
3056
 
3183
3057
  // src/components/chat/ChatInput.tsx
3184
- var import_react6 = require("react");
3058
+ var import_react6 = __toESM(require("react"), 1);
3185
3059
 
3186
3060
  // src/components/chat/UserContext.tsx
3187
3061
  var import_react5 = require("react");
@@ -3776,6 +3650,29 @@ var VoiceComposer = ({
3776
3650
  // src/components/chat/ChatInput.tsx
3777
3651
  var import_lucide_react11 = require("lucide-react");
3778
3652
  var import_jsx_runtime23 = require("react/jsx-runtime");
3653
+ function getActiveMentionMatch(value, caret) {
3654
+ const prefix = value.slice(0, caret);
3655
+ const match = /(^|\s)@([\w.-]*)$/.exec(prefix);
3656
+ if (!match) return null;
3657
+ const query = match[2] ?? "";
3658
+ return {
3659
+ start: prefix.length - query.length - 1,
3660
+ end: caret,
3661
+ query
3662
+ };
3663
+ }
3664
+ function resolveTargetFromMentions(value, agents) {
3665
+ const matches = value.matchAll(/(^|\s)@([\w.-]+)/g);
3666
+ for (const match of matches) {
3667
+ const mention = match[2]?.toLowerCase();
3668
+ if (!mention) continue;
3669
+ const agent = agents.find(
3670
+ (candidate) => candidate.id.toLowerCase() === mention || candidate.name.toLowerCase() === mention
3671
+ );
3672
+ if (agent) return agent;
3673
+ }
3674
+ return null;
3675
+ }
3779
3676
  var FileUploadItem = (0, import_react6.memo)(function FileUploadItem2({ file, progress, onCancel }) {
3780
3677
  const guessTypeFromName = (name) => {
3781
3678
  const ext = (name || "").split(".").pop()?.toLowerCase();
@@ -4036,7 +3933,9 @@ var ChatInput = (0, import_react6.memo)(function ChatInput2({
4036
3933
  // 10MB
4037
3934
  acceptedFileTypes = ["image/*", "video/*", "audio/*"],
4038
3935
  className = "",
4039
- config
3936
+ config,
3937
+ mentionAgents = [],
3938
+ onTargetAgentChange
4040
3939
  }) {
4041
3940
  const voiceComposeEnabled = config?.voiceCompose?.enabled === true;
4042
3941
  const voiceDefaultMode = config?.voiceCompose?.defaultMode ?? "text";
@@ -4061,6 +3960,8 @@ var ChatInput = (0, import_react6.memo)(function ChatInput2({
4061
3960
  const [voiceCountdownMs, setVoiceCountdownMs] = (0, import_react6.useState)(0);
4062
3961
  const [isVoiceAutoSendActive, setIsVoiceAutoSendActive] = (0, import_react6.useState)(false);
4063
3962
  const [voiceError, setVoiceError] = (0, import_react6.useState)(null);
3963
+ const [activeMention, setActiveMention] = (0, import_react6.useState)(null);
3964
+ const [activeMentionIndex, setActiveMentionIndex] = (0, import_react6.useState)(0);
4064
3965
  const textareaRef = (0, import_react6.useRef)(null);
4065
3966
  const fileInputRef = (0, import_react6.useRef)(null);
4066
3967
  const mediaRecorderRef = (0, import_react6.useRef)(null);
@@ -4071,6 +3972,35 @@ var ChatInput = (0, import_react6.memo)(function ChatInput2({
4071
3972
  const voiceDraftRef = (0, import_react6.useRef)(null);
4072
3973
  const voiceAppendBaseRef = (0, import_react6.useRef)(null);
4073
3974
  const voiceAppendBaseDurationRef = (0, import_react6.useRef)(0);
3975
+ const filteredMentionAgents = import_react6.default.useMemo(() => {
3976
+ if (!activeMention || mentionAgents.length === 0) return [];
3977
+ const query = activeMention.query.trim().toLowerCase();
3978
+ const rank = (agent) => {
3979
+ const id = agent.id.toLowerCase();
3980
+ const name = agent.name.toLowerCase();
3981
+ if (!query) return 0;
3982
+ if (name.startsWith(query) || id.startsWith(query)) return 0;
3983
+ if (name.includes(query) || id.includes(query)) return 1;
3984
+ return 2;
3985
+ };
3986
+ return mentionAgents.filter((agent) => rank(agent) < 2).sort((left, right) => {
3987
+ const rankDiff = rank(left) - rank(right);
3988
+ if (rankDiff !== 0) return rankDiff;
3989
+ return left.name.localeCompare(right.name);
3990
+ }).slice(0, 6);
3991
+ }, [activeMention, mentionAgents]);
3992
+ const isMentionMenuOpen = filteredMentionAgents.length > 0;
3993
+ const syncMentionState = (0, import_react6.useCallback)((nextValue, nextCaret) => {
3994
+ const caret = typeof nextCaret === "number" ? nextCaret : textareaRef.current?.selectionStart ?? nextValue.length;
3995
+ const nextMatch = getActiveMentionMatch(nextValue, caret);
3996
+ setActiveMention((prev) => {
3997
+ if (prev?.start === nextMatch?.start && prev?.end === nextMatch?.end && prev?.query === nextMatch?.query) {
3998
+ return prev;
3999
+ }
4000
+ return nextMatch;
4001
+ });
4002
+ setActiveMentionIndex(0);
4003
+ }, []);
4074
4004
  (0, import_react6.useEffect)(() => {
4075
4005
  return () => {
4076
4006
  if (mediaStreamRef.current) {
@@ -4088,14 +4018,70 @@ var ChatInput = (0, import_react6.memo)(function ChatInput2({
4088
4018
  (0, import_react6.useEffect)(() => {
4089
4019
  voiceDraftRef.current = voiceDraft;
4090
4020
  }, [voiceDraft]);
4021
+ (0, import_react6.useEffect)(() => {
4022
+ if (!isMentionMenuOpen) {
4023
+ setActiveMentionIndex(0);
4024
+ return;
4025
+ }
4026
+ setActiveMentionIndex(
4027
+ (prev) => prev >= filteredMentionAgents.length ? 0 : prev
4028
+ );
4029
+ }, [filteredMentionAgents.length, isMentionMenuOpen]);
4030
+ const selectMentionAgent = (0, import_react6.useCallback)((agent) => {
4031
+ if (!activeMention) return;
4032
+ const replacement = `@${agent.name} `;
4033
+ const nextValue = value.slice(0, activeMention.start) + replacement + value.slice(activeMention.end);
4034
+ const nextCaret = activeMention.start + replacement.length;
4035
+ onChange(nextValue);
4036
+ onTargetAgentChange?.(agent.id);
4037
+ setActiveMention(null);
4038
+ setActiveMentionIndex(0);
4039
+ requestAnimationFrame(() => {
4040
+ textareaRef.current?.focus();
4041
+ textareaRef.current?.setSelectionRange(nextCaret, nextCaret);
4042
+ });
4043
+ }, [activeMention, onChange, onTargetAgentChange, value]);
4091
4044
  const handleSubmit = (e) => {
4092
4045
  e.preventDefault();
4093
4046
  if (!value.trim() && attachments.length === 0 || disabled || isGenerating) return;
4047
+ const mentionedAgent = resolveTargetFromMentions(value, mentionAgents);
4048
+ if (mentionedAgent) {
4049
+ onTargetAgentChange?.(mentionedAgent.id);
4050
+ }
4094
4051
  onSubmit(value.trim(), attachments);
4095
4052
  onChange("");
4096
4053
  onAttachmentsChange([]);
4054
+ setActiveMention(null);
4055
+ setActiveMentionIndex(0);
4097
4056
  };
4098
4057
  const handleKeyDown = (e) => {
4058
+ if (isMentionMenuOpen) {
4059
+ if (e.key === "ArrowDown") {
4060
+ e.preventDefault();
4061
+ setActiveMentionIndex(
4062
+ (prev) => prev >= filteredMentionAgents.length - 1 ? 0 : prev + 1
4063
+ );
4064
+ return;
4065
+ }
4066
+ if (e.key === "ArrowUp") {
4067
+ e.preventDefault();
4068
+ setActiveMentionIndex(
4069
+ (prev) => prev <= 0 ? filteredMentionAgents.length - 1 : prev - 1
4070
+ );
4071
+ return;
4072
+ }
4073
+ if ((e.key === "Enter" || e.key === "Tab") && filteredMentionAgents[activeMentionIndex]) {
4074
+ e.preventDefault();
4075
+ selectMentionAgent(filteredMentionAgents[activeMentionIndex]);
4076
+ return;
4077
+ }
4078
+ if (e.key === "Escape") {
4079
+ e.preventDefault();
4080
+ setActiveMention(null);
4081
+ setActiveMentionIndex(0);
4082
+ return;
4083
+ }
4084
+ }
4099
4085
  if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && window.innerWidth > 768) {
4100
4086
  e.preventDefault();
4101
4087
  handleSubmit(e);
@@ -4661,19 +4647,48 @@ var ChatInput = (0, import_react6.memo)(function ChatInput2({
4661
4647
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(TooltipContent, { children: config?.labels?.attachFileTooltip })
4662
4648
  ] })
4663
4649
  ] }),
4664
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4665
- Textarea,
4666
- {
4667
- ref: textareaRef,
4668
- value,
4669
- onChange: (e) => onChange(e.target.value),
4670
- onKeyDown: handleKeyDown,
4671
- placeholder,
4672
- disabled,
4673
- className: "max-h-[120px] resize-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0",
4674
- rows: 1
4675
- }
4676
- ) }),
4650
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "relative flex-1", children: [
4651
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4652
+ Textarea,
4653
+ {
4654
+ ref: textareaRef,
4655
+ value,
4656
+ onChange: (e) => {
4657
+ onChange(e.target.value);
4658
+ syncMentionState(e.target.value, e.target.selectionStart ?? e.target.value.length);
4659
+ },
4660
+ onSelect: (e) => {
4661
+ const target = e.target;
4662
+ syncMentionState(target.value, target.selectionStart ?? target.value.length);
4663
+ },
4664
+ onClick: (e) => {
4665
+ const target = e.target;
4666
+ syncMentionState(target.value, target.selectionStart ?? target.value.length);
4667
+ },
4668
+ onKeyDown: handleKeyDown,
4669
+ placeholder,
4670
+ disabled,
4671
+ className: "max-h-[120px] resize-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0",
4672
+ rows: 1
4673
+ }
4674
+ ),
4675
+ isMentionMenuOpen && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "absolute bottom-full left-0 right-0 mb-2 overflow-hidden rounded-md border bg-popover shadow-md", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "p-1", children: filteredMentionAgents.map((agent, index) => /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
4676
+ "button",
4677
+ {
4678
+ type: "button",
4679
+ className: `flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm ${index === activeMentionIndex ? "bg-accent text-accent-foreground" : "hover:bg-accent/60"}`,
4680
+ onMouseDown: (mouseEvent) => {
4681
+ mouseEvent.preventDefault();
4682
+ selectMentionAgent(agent);
4683
+ },
4684
+ children: [
4685
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "font-medium", children: agent.name }),
4686
+ agent.description && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate text-xs text-muted-foreground", children: agent.description })
4687
+ ]
4688
+ },
4689
+ agent.id
4690
+ )) }) })
4691
+ ] }),
4677
4692
  enableAudioRecording && !isRecording && canAddMoreAttachments && !value.trim() && (voiceComposeEnabled ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(Tooltip, { children: [
4678
4693
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4679
4694
  Button,
@@ -5700,7 +5715,9 @@ var ChatUI = ({
5700
5715
  enableAudioRecording: config.features.enableAudioRecording,
5701
5716
  maxAttachments: config.features.maxAttachments,
5702
5717
  maxFileSize: config.features.maxFileSize,
5703
- config
5718
+ config,
5719
+ mentionAgents: participantIds && participantIds.length > 0 ? agentOptions.filter((a) => participantIds.includes(a.id)) : agentOptions,
5720
+ onTargetAgentChange
5704
5721
  }
5705
5722
  )
5706
5723
  ] })
@@ -6038,19 +6055,14 @@ var ThreadManager = ({
6038
6055
  UserMenu,
6039
6056
  UserProfile,
6040
6057
  assignAgentColors,
6041
- chatConfigPresets,
6042
6058
  chatUtils,
6043
6059
  cn,
6044
- configUtils,
6045
6060
  createObjectUrlFromDataUrl,
6046
6061
  defaultChatConfig,
6047
- featureFlags,
6048
6062
  formatDate,
6049
6063
  getAgentColor,
6050
6064
  getAgentInitials,
6051
6065
  mergeConfig,
6052
- themeUtils,
6053
- useChatUserContext,
6054
- validateConfig
6066
+ useChatUserContext
6055
6067
  });
6056
6068
  //# sourceMappingURL=index.cjs.map