orchid-ai 1.3.1 → 1.3.8

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.
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { type IconName } from './Icon';
3
- import { CommandTheme, ChatMessage, ModelInfo, ChatTheme, ServerConfig } from '../types/types';
3
+ import { CommandTheme, CommandSuggestion, ChatMessage, ModelInfo, ChatTheme, ServerConfig } from '../types/types';
4
4
  interface ChatPanelProps {
5
5
  isOpen: boolean;
6
6
  setIsOpen?: (isOpen: boolean) => void;
@@ -53,6 +53,19 @@ interface ChatPanelProps {
53
53
  headerIcon?: IconName | React.ReactElement;
54
54
  headerTitle?: string;
55
55
  headerSubtitle?: string;
56
+ /** When set, replaces default behavior (setFormState + onNavigate + onClose) for command suggestions. */
57
+ onSuggestionSelect?: (s: CommandSuggestion) => void;
58
+ /**
59
+ * Applied to the full-screen modal root so the chat stacks above app chrome that uses high z-index
60
+ * (e.g. MUI AppBar ~1100, custom sidebars ~999). Default Tailwind z-50 is too low for many shells.
61
+ */
62
+ overlayZIndex?: number;
63
+ /**
64
+ * When true (default) and `showHistory` is on, only the chat history column is a narrow strip
65
+ * until hover (or tap) expands it; the main conversation area stays full width.
66
+ * Set to false to keep the history sidebar always fully expanded (~150px).
67
+ */
68
+ expandOnHover?: boolean;
56
69
  }
57
70
  /**
58
71
  * ChatPanel component
@@ -91,7 +104,7 @@ showHistory, // Default to hidden
91
104
  showProfileBubbles, // Default to hidden
92
105
  modalPosition, // Default to left position
93
106
  serverConfig, models, defaultModel, showUsageStats, maxFileSize, features, showFloatingButton, floatingButtonIcon, floatingButtonPosition, floatingButtonSize, floatingButtonClassName, chats, setChats, currentChatId, setCurrentChatId, chatLevel, initialQuery, setInitialQuery, headerIcon, // Default to bot icon
94
- headerTitle, headerSubtitle, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
107
+ headerTitle, headerSubtitle, onSuggestionSelect, overlayZIndex, expandOnHover, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
95
108
  /**
96
109
  * ChatInput component
97
110
  * @param query - The current query
@@ -118,6 +131,7 @@ type FloatingChatButtonProps = {
118
131
  className?: string;
119
132
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
120
133
  size?: 'sm' | 'md' | 'lg';
134
+ zIndex?: number;
121
135
  };
122
136
  /**
123
137
  * FloatingChatButton component
@@ -128,5 +142,5 @@ type FloatingChatButtonProps = {
128
142
  * @param position - Position of the floating button
129
143
  * @param size - Size of the floating button
130
144
  */
131
- export declare function FloatingChatButton({ onClick, theme, icon, className, position, size, }: FloatingChatButtonProps): import("react/jsx-runtime").JSX.Element;
145
+ export declare function FloatingChatButton({ onClick, theme, icon, className, position, size, zIndex: floatingZIndex, }: FloatingChatButtonProps): import("react/jsx-runtime").JSX.Element;
132
146
  export {};
@@ -61,6 +61,10 @@ interface ConversationProps {
61
61
  showClearChat?: boolean;
62
62
  onClearChat?: () => void;
63
63
  additionalContext?: string | Record<string, unknown>;
64
+ /** Optional ref to the main chat textarea (for parent focus control). */
65
+ chatInputRef?: React.RefObject<HTMLTextAreaElement | null>;
66
+ /** Increment to move focus to the main input (panel open, chat switch, new chat). */
67
+ focusChatNonce?: number;
64
68
  }
65
69
  /**
66
70
  * Conversation component - displays a single chat conversation
@@ -72,5 +76,5 @@ interface ConversationProps {
72
76
  * @param autoScroll - Whether to auto-scroll to bottom on new messages
73
77
  * @param maxHeight - Maximum height of the conversation container
74
78
  */
75
- export declare function Conversation({ chat: externalChat, onSuggestionClick, theme, showProfileBubbles, className, autoScroll, maxHeight, userId, serverConfig, formData, setFormState, onNavigate, chatLevel, chats: externalChats, setChats: externalSetChats, currentChatId: externalCurrentChatId, setCurrentChatId: externalSetCurrentChatId, query: externalQuery, setQuery: externalSetQuery, onSend: externalOnSend, isLoading: externalIsLoading, attachedFiles: externalAttachedFiles, setAttachedFiles: externalSetAttachedFiles, models, defaultModel, showUsageStats, maxFileSize, features, currentModelSelection: externalCurrentModelSelection, onModelSelectionChange: externalOnModelSelectionChange, showInput, initialQuery, setInitialQuery, showClearChat, onClearChat, additionalContext, }: ConversationProps): import("react/jsx-runtime").JSX.Element;
79
+ export declare function Conversation({ chat: externalChat, onSuggestionClick, theme, showProfileBubbles, className, autoScroll, maxHeight, userId, serverConfig, formData, chatLevel, chats: externalChats, setChats: externalSetChats, currentChatId: externalCurrentChatId, setCurrentChatId: externalSetCurrentChatId, query: externalQuery, setQuery: externalSetQuery, onSend: externalOnSend, isLoading: externalIsLoading, attachedFiles: externalAttachedFiles, setAttachedFiles: externalSetAttachedFiles, models, defaultModel, showUsageStats, maxFileSize, features, currentModelSelection: externalCurrentModelSelection, onModelSelectionChange: externalOnModelSelectionChange, showInput, initialQuery, setInitialQuery, showClearChat, onClearChat, additionalContext, chatInputRef: externalChatInputRef, focusChatNonce, }: ConversationProps): import("react/jsx-runtime").JSX.Element;
76
80
  export {};
@@ -10,6 +10,7 @@ const commandTrainingRules = {
10
10
  components: {
11
11
  patterns: {
12
12
  // React component patterns
13
+ // Has /g for matchAll in training.ts; do not use String.match(componentName) (groups break).
13
14
  componentName: /(?:export\s+)?(?:function|const)\s+(\w+)(?:\s*<[^>]*>)?\s*[=(]/g,
14
15
  props: /interface\s+(\w+)Props\s*{([^}]*)}/g,
15
16
  state: /useState<([^>]*)>/g,
@@ -34,7 +35,8 @@ const commandTrainingRules = {
34
35
  schemas: {
35
36
  patterns: {
36
37
  // Monastery schema patterns - updated for actual file format
37
- modelName: /\/\/\s*(\w+)\s+model\s+for\s+monastery/g,
38
+ // No /g: training.ts uses String.match(); with g, capture groups are omitted and modelName is always lost.
39
+ modelName: /\/\/\s*(\w+)\s+model\s+for\s+monastery/,
38
40
  fields: /fields:\s*{([^}]*)}/g,
39
41
  fieldDefinitions: /(\w+):\s*{\s*(?:required:\s*(true|false),?\s*)?type:\s*['"]([^'"]+)['"](?:,\s*required:\s*(true|false))?(?:,\s*default:\s*[^,}]+)?/g,
40
42
  indexes: /indexes:\s*\[([^\]]*)\]/g,
@@ -557,6 +559,7 @@ class CommandTrainingCollector {
557
559
  if (content.formFields?.length > 0) {
558
560
  const form = {
559
561
  name: content.componentName || 'Unknown Form',
562
+ sourceFile: path.relative(this.rootDir, component.filePath),
560
563
  purpose: `Form component for data entry`,
561
564
  fields: content.formFields.map((fieldName) => ({
562
565
  name: fieldName,
@@ -685,12 +688,15 @@ class CommandTrainingCollector {
685
688
  guessEntityFromComponent(componentName) {
686
689
  if (!componentName)
687
690
  return 'Unknown';
688
- // Simple heuristic: remove common suffixes
691
+ // Strip common create/edit wrappers, then UI suffixes (order matters)
689
692
  const cleaned = componentName
693
+ .replace(/^(?:Create|Edit|View|New|Batch)/i, '')
690
694
  .replace(/Form$/, '')
695
+ .replace(/Modal$/, '')
696
+ .replace(/Drawer$/, '')
697
+ .replace(/Dialog$/, '')
691
698
  .replace(/Page$/, '')
692
- .replace(/Component$/, '')
693
- .replace(/Modal$/, '');
699
+ .replace(/Component$/, '');
694
700
  return cleaned || 'Unknown';
695
701
  }
696
702
  guessEntitiesFromComponent(content) {
@@ -804,7 +810,7 @@ class CommandTrainingCollector {
804
810
  this.log(`āŒ Error expanding pattern "${pattern}":`, err.message);
805
811
  }
806
812
  }
807
- return expandedPaths;
813
+ return [...new Set(expandedPaths)];
808
814
  }
809
815
  extractFileInfo(fieldName, content) {
810
816
  switch (fieldName) {
@@ -848,11 +854,22 @@ class CommandTrainingCollector {
848
854
  hooks: [],
849
855
  eventHandlers: [],
850
856
  };
851
- // Extract component name
852
- const componentMatch = content.match(patterns.componentName);
853
- if (componentMatch) {
854
- result.componentName = componentMatch[1];
855
- }
857
+ // Do not use String.match() on patterns.componentName — it has /g, so match[1] is the *second*
858
+ // full match, not capture group 1 (yields strings like `const CustomerForm =`).
859
+ const declRe = /(?:export\s+)?(?:function|const)\s+(\w+)(?:\s*<[^>]*>)?\s*[=(]/g;
860
+ const declNames = [];
861
+ for (const m of content.matchAll(declRe)) {
862
+ declNames.push(m[1]);
863
+ }
864
+ const isLikelyComponentSymbol = (n) => /^[A-Z]/.test(n) &&
865
+ !/^use[A-Z]/.test(n) &&
866
+ !/^Styled/i.test(n);
867
+ const preferred = [...declNames].reverse().find((n) => isLikelyComponentSymbol(n) &&
868
+ (/(?:Form|Modal|Drawer|Dialog)$/.test(n) ||
869
+ /^FormStep/i.test(n) ||
870
+ (n.includes('Form') && n.length > 4)));
871
+ const fallback = [...declNames].reverse().find(isLikelyComponentSymbol);
872
+ result.componentName = preferred ?? fallback ?? null;
856
873
  // Extract props interfaces
857
874
  const propsMatches = content.matchAll(patterns.props);
858
875
  for (const match of propsMatches) {
@@ -887,6 +904,7 @@ class CommandTrainingCollector {
887
904
  for (const match of fieldMatches) {
888
905
  result.formFields.push(match[1]);
889
906
  }
907
+ result.formFields = [...new Set(result.formFields)];
890
908
  // Extract imports
891
909
  const importMatches = content.matchAll(patterns.imports);
892
910
  for (const match of importMatches) {
@@ -1635,6 +1653,7 @@ class SchemaGenerator extends CommandTrainingCollector {
1635
1653
  };
1636
1654
  this.log(` āœ… Generated schema with ${Object.keys(properties).length} properties`);
1637
1655
  }
1656
+ this.mergeFormUiFieldsIntoEntitySchemas(trainingData, schemas);
1638
1657
  // If no entities found, try to extract from components
1639
1658
  if (Object.keys(schemas).length === 0) {
1640
1659
  this.log(' āš ļø No entities found in training data, extracting from components...');
@@ -1642,6 +1661,87 @@ class SchemaGenerator extends CommandTrainingCollector {
1642
1661
  }
1643
1662
  return schemas;
1644
1663
  }
1664
+ /**
1665
+ * Merge `name="..."` fields collected from UI forms/modals into existing entity schemas
1666
+ * (server models stay canonical; this adds frontend surface hints).
1667
+ * Optional `customTrainingData.options.formEntityHints`: map form component name -> schema entity key.
1668
+ * Optional `formEntityPathPatterns`: `{ includes, entity }` — first match on `form.sourceFile` wins
1669
+ * (disambiguates reused names like `FormStepDetails` in different feature folders).
1670
+ */
1671
+ mergeFormUiFieldsIntoEntitySchemas(trainingData, schemas) {
1672
+ const forms = trainingData.customTrainingData?.structuredData?.components?.forms || [];
1673
+ if (!forms.length || !Object.keys(schemas).length)
1674
+ return;
1675
+ const hints = (trainingData.customTrainingData?.options?.formEntityHints || {});
1676
+ const pathPatterns = (trainingData.customTrainingData?.options?.formEntityPathPatterns ||
1677
+ []);
1678
+ /** Short UI stems that don't match lowered model names */
1679
+ const STEM_ALIASES = {
1680
+ hs: 'hsreport',
1681
+ incident: 'incidentreport',
1682
+ };
1683
+ const schemaKeys = new Set(Object.keys(schemas));
1684
+ const resolveKey = (form) => {
1685
+ if (form.sourceFile) {
1686
+ for (const p of pathPatterns) {
1687
+ if (!p?.includes || !p?.entity)
1688
+ continue;
1689
+ if (form.sourceFile.includes(p.includes)) {
1690
+ const k = p.entity.toLowerCase();
1691
+ if (schemaKeys.has(k))
1692
+ return k;
1693
+ }
1694
+ }
1695
+ }
1696
+ const tryKey = (raw) => {
1697
+ if (!raw)
1698
+ return undefined;
1699
+ const mapped = hints[raw];
1700
+ const stem = raw
1701
+ .replace(/^(?:Create|Edit|View|New|Batch)/i, '')
1702
+ .replace(/(?:Form|Modal|Drawer|Dialog)$/i, '');
1703
+ const stemLc = stem.toLowerCase();
1704
+ const alias = STEM_ALIASES[stemLc];
1705
+ if (alias && schemaKeys.has(alias))
1706
+ return alias;
1707
+ const candidates = [
1708
+ (mapped || raw).toLowerCase(),
1709
+ stemLc,
1710
+ ];
1711
+ for (const c of candidates) {
1712
+ if (c && schemaKeys.has(c))
1713
+ return c;
1714
+ }
1715
+ return undefined;
1716
+ };
1717
+ return tryKey(form.name) || tryKey(form.relatedEntity) || undefined;
1718
+ };
1719
+ let mergedForms = 0;
1720
+ let mergedFields = 0;
1721
+ for (const form of forms) {
1722
+ const entityKey = resolveKey(form);
1723
+ if (!entityKey)
1724
+ continue;
1725
+ const gen = schemas[entityKey];
1726
+ if (!gen?.schema?.properties)
1727
+ continue;
1728
+ const props = gen.schema.properties;
1729
+ for (const f of form.fields || []) {
1730
+ const n = f?.name;
1731
+ if (!n || typeof n !== 'string' || props[n])
1732
+ continue;
1733
+ props[n] = {
1734
+ type: 'string',
1735
+ description: `UI form field (from ${form.name}). Cross-check server model; may overlap sharing/base shapes.`,
1736
+ };
1737
+ mergedFields += 1;
1738
+ }
1739
+ mergedForms += 1;
1740
+ }
1741
+ if (mergedForms > 0) {
1742
+ this.log(`\nšŸ“Ž Merged UI form fields into ${mergedForms} entity schema(s) (+${mergedFields} properties)`);
1743
+ }
1744
+ }
1645
1745
  /**
1646
1746
  * Extract schemas from component analysis if no explicit entities found
1647
1747
  */
@@ -1955,6 +2055,9 @@ class SchemaGenerator extends CommandTrainingCollector {
1955
2055
  * Map training data field types to JSON Schema types
1956
2056
  */
1957
2057
  mapFieldType(type) {
2058
+ if (type == null || typeof type !== 'string') {
2059
+ return 'string';
2060
+ }
1958
2061
  const typeMap = {
1959
2062
  string: 'string',
1960
2063
  number: 'number',
@@ -1,3 +1,4 @@
1
+ import './orchid-chat.css';
1
2
  export { useStreamingAI } from './hooks/useStreamingAI';
2
3
  export type { StreamingModelSelection } from './hooks/useStreamingAI';
3
4
  export { useSuggestions, useDebouncedSuggestions, } from './hooks/useSuggestions';
@@ -55,6 +55,14 @@ export declare class SchemaGenerator extends CommandTrainingCollector {
55
55
  * Generate schemas from collected training data
56
56
  */
57
57
  private generateSchemas;
58
+ /**
59
+ * Merge `name="..."` fields collected from UI forms/modals into existing entity schemas
60
+ * (server models stay canonical; this adds frontend surface hints).
61
+ * Optional `customTrainingData.options.formEntityHints`: map form component name -> schema entity key.
62
+ * Optional `formEntityPathPatterns`: `{ includes, entity }` — first match on `form.sourceFile` wins
63
+ * (disambiguates reused names like `FormStepDetails` in different feature folders).
64
+ */
65
+ private mergeFormUiFieldsIntoEntitySchemas;
58
66
  /**
59
67
  * Extract schemas from component analysis if no explicit entities found
60
68
  */
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { type IconName } from './Icon';
3
- import { CommandTheme, ChatMessage, ModelInfo, ChatTheme, ServerConfig } from '../types/types';
3
+ import { CommandTheme, CommandSuggestion, ChatMessage, ModelInfo, ChatTheme, ServerConfig } from '../types/types';
4
4
  interface ChatPanelProps {
5
5
  isOpen: boolean;
6
6
  setIsOpen?: (isOpen: boolean) => void;
@@ -53,6 +53,19 @@ interface ChatPanelProps {
53
53
  headerIcon?: IconName | React.ReactElement;
54
54
  headerTitle?: string;
55
55
  headerSubtitle?: string;
56
+ /** When set, replaces default behavior (setFormState + onNavigate + onClose) for command suggestions. */
57
+ onSuggestionSelect?: (s: CommandSuggestion) => void;
58
+ /**
59
+ * Applied to the full-screen modal root so the chat stacks above app chrome that uses high z-index
60
+ * (e.g. MUI AppBar ~1100, custom sidebars ~999). Default Tailwind z-50 is too low for many shells.
61
+ */
62
+ overlayZIndex?: number;
63
+ /**
64
+ * When true (default) and `showHistory` is on, only the chat history column is a narrow strip
65
+ * until hover (or tap) expands it; the main conversation area stays full width.
66
+ * Set to false to keep the history sidebar always fully expanded (~150px).
67
+ */
68
+ expandOnHover?: boolean;
56
69
  }
57
70
  /**
58
71
  * ChatPanel component
@@ -91,7 +104,7 @@ showHistory, // Default to hidden
91
104
  showProfileBubbles, // Default to hidden
92
105
  modalPosition, // Default to left position
93
106
  serverConfig, models, defaultModel, showUsageStats, maxFileSize, features, showFloatingButton, floatingButtonIcon, floatingButtonPosition, floatingButtonSize, floatingButtonClassName, chats, setChats, currentChatId, setCurrentChatId, chatLevel, initialQuery, setInitialQuery, headerIcon, // Default to bot icon
94
- headerTitle, headerSubtitle, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
107
+ headerTitle, headerSubtitle, onSuggestionSelect, overlayZIndex, expandOnHover, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
95
108
  /**
96
109
  * ChatInput component
97
110
  * @param query - The current query
@@ -118,6 +131,7 @@ type FloatingChatButtonProps = {
118
131
  className?: string;
119
132
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
120
133
  size?: 'sm' | 'md' | 'lg';
134
+ zIndex?: number;
121
135
  };
122
136
  /**
123
137
  * FloatingChatButton component
@@ -128,5 +142,5 @@ type FloatingChatButtonProps = {
128
142
  * @param position - Position of the floating button
129
143
  * @param size - Size of the floating button
130
144
  */
131
- export declare function FloatingChatButton({ onClick, theme, icon, className, position, size, }: FloatingChatButtonProps): import("react/jsx-runtime").JSX.Element;
145
+ export declare function FloatingChatButton({ onClick, theme, icon, className, position, size, zIndex: floatingZIndex, }: FloatingChatButtonProps): import("react/jsx-runtime").JSX.Element;
132
146
  export {};
@@ -61,6 +61,10 @@ interface ConversationProps {
61
61
  showClearChat?: boolean;
62
62
  onClearChat?: () => void;
63
63
  additionalContext?: string | Record<string, unknown>;
64
+ /** Optional ref to the main chat textarea (for parent focus control). */
65
+ chatInputRef?: React.RefObject<HTMLTextAreaElement | null>;
66
+ /** Increment to move focus to the main input (panel open, chat switch, new chat). */
67
+ focusChatNonce?: number;
64
68
  }
65
69
  /**
66
70
  * Conversation component - displays a single chat conversation
@@ -72,5 +76,5 @@ interface ConversationProps {
72
76
  * @param autoScroll - Whether to auto-scroll to bottom on new messages
73
77
  * @param maxHeight - Maximum height of the conversation container
74
78
  */
75
- export declare function Conversation({ chat: externalChat, onSuggestionClick, theme, showProfileBubbles, className, autoScroll, maxHeight, userId, serverConfig, formData, setFormState, onNavigate, chatLevel, chats: externalChats, setChats: externalSetChats, currentChatId: externalCurrentChatId, setCurrentChatId: externalSetCurrentChatId, query: externalQuery, setQuery: externalSetQuery, onSend: externalOnSend, isLoading: externalIsLoading, attachedFiles: externalAttachedFiles, setAttachedFiles: externalSetAttachedFiles, models, defaultModel, showUsageStats, maxFileSize, features, currentModelSelection: externalCurrentModelSelection, onModelSelectionChange: externalOnModelSelectionChange, showInput, initialQuery, setInitialQuery, showClearChat, onClearChat, additionalContext, }: ConversationProps): import("react/jsx-runtime").JSX.Element;
79
+ export declare function Conversation({ chat: externalChat, onSuggestionClick, theme, showProfileBubbles, className, autoScroll, maxHeight, userId, serverConfig, formData, chatLevel, chats: externalChats, setChats: externalSetChats, currentChatId: externalCurrentChatId, setCurrentChatId: externalSetCurrentChatId, query: externalQuery, setQuery: externalSetQuery, onSend: externalOnSend, isLoading: externalIsLoading, attachedFiles: externalAttachedFiles, setAttachedFiles: externalSetAttachedFiles, models, defaultModel, showUsageStats, maxFileSize, features, currentModelSelection: externalCurrentModelSelection, onModelSelectionChange: externalOnModelSelectionChange, showInput, initialQuery, setInitialQuery, showClearChat, onClearChat, additionalContext, chatInputRef: externalChatInputRef, focusChatNonce, }: ConversationProps): import("react/jsx-runtime").JSX.Element;
76
80
  export {};
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import './orchid-chat.css';
1
2
  export { useStreamingAI } from './hooks/useStreamingAI';
2
3
  export type { StreamingModelSelection } from './hooks/useStreamingAI';
3
4
  export { useSuggestions, useDebouncedSuggestions, } from './hooks/useSuggestions';