@fayz-ai/saas 0.8.0 → 0.8.2

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.
Files changed (236) hide show
  1. package/dist/access/context.d.ts +47 -0
  2. package/dist/access/context.d.ts.map +1 -0
  3. package/dist/access/headless.d.ts +21 -0
  4. package/dist/access/headless.d.ts.map +1 -0
  5. package/dist/access/index.d.ts +12 -0
  6. package/dist/access/index.d.ts.map +1 -0
  7. package/dist/access/limits-registry.d.ts +33 -0
  8. package/dist/access/limits-registry.d.ts.map +1 -0
  9. package/dist/access/module-nav.d.ts +15 -0
  10. package/dist/access/module-nav.d.ts.map +1 -0
  11. package/dist/access/resolver.d.ts +2 -0
  12. package/dist/access/resolver.d.ts.map +1 -0
  13. package/dist/access/types.d.ts +43 -0
  14. package/dist/access/types.d.ts.map +1 -0
  15. package/dist/access/upgrade-modal-store.d.ts +23 -0
  16. package/dist/access/upgrade-modal-store.d.ts.map +1 -0
  17. package/dist/app/AdminShell.d.ts +3 -0
  18. package/dist/app/AdminShell.d.ts.map +1 -1
  19. package/dist/app/admin-app.d.ts +3 -1
  20. package/dist/app/admin-app.d.ts.map +1 -1
  21. package/dist/app/config.d.ts +47 -2
  22. package/dist/app/config.d.ts.map +1 -1
  23. package/dist/app/core-entities.d.ts +25 -0
  24. package/dist/app/core-entities.d.ts.map +1 -0
  25. package/dist/app/derive-contract.d.ts +8 -0
  26. package/dist/app/derive-contract.d.ts.map +1 -0
  27. package/dist/app/scaffold.d.ts.map +1 -1
  28. package/dist/archetype-lookup.d.ts.map +1 -1
  29. package/dist/billing/index.cjs +6 -2
  30. package/dist/billing/index.d.ts +2 -0
  31. package/dist/billing/index.d.ts.map +1 -1
  32. package/dist/billing/index.js +1 -1
  33. package/dist/billing/plan-badge.d.ts +17 -0
  34. package/dist/billing/plan-badge.d.ts.map +1 -0
  35. package/dist/billing/store.d.ts +6 -1
  36. package/dist/billing/store.d.ts.map +1 -1
  37. package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
  38. package/dist/chunk-I3DY47N5.js.map +1 -0
  39. package/dist/chunk-NNFGMUF2.cjs +39 -0
  40. package/dist/chunk-NNFGMUF2.cjs.map +1 -0
  41. package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
  42. package/dist/chunk-SAZLW54Y.cjs.map +1 -0
  43. package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
  44. package/dist/chunk-VKYZACG5.cjs.map +1 -0
  45. package/dist/chunk-VN5NTK26.js +36 -0
  46. package/dist/chunk-VN5NTK26.js.map +1 -0
  47. package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
  48. package/dist/chunk-VTEP2AQX.js.map +1 -0
  49. package/dist/components/shared/ContactPicker.d.ts +92 -0
  50. package/dist/components/shared/ContactPicker.d.ts.map +1 -0
  51. package/dist/components/shared/PersonLink.d.ts +12 -1
  52. package/dist/components/shared/PersonLink.d.ts.map +1 -1
  53. package/dist/components/shared/create-person.d.ts +20 -0
  54. package/dist/components/shared/create-person.d.ts.map +1 -0
  55. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  56. package/dist/crud/CrudFormPage.d.ts.map +1 -1
  57. package/dist/crud/CrudListView.d.ts.map +1 -1
  58. package/dist/crud/CrudPage.d.ts.map +1 -1
  59. package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
  60. package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
  61. package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
  62. package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
  63. package/dist/crud/createCrudPage.d.ts.map +1 -1
  64. package/dist/crud/index.d.ts +1 -0
  65. package/dist/crud/index.d.ts.map +1 -1
  66. package/dist/index.cjs +4300 -722
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.ts +17 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4080 -542
  71. package/dist/index.js.map +1 -1
  72. package/dist/org/index.cjs +11 -11
  73. package/dist/org/index.js +2 -2
  74. package/dist/org/store.d.ts.map +1 -1
  75. package/dist/permissions/context.d.ts +2 -0
  76. package/dist/permissions/context.d.ts.map +1 -1
  77. package/dist/permissions/index.cjs +7 -7
  78. package/dist/permissions/index.js +1 -1
  79. package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
  80. package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
  81. package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
  82. package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
  83. package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
  84. package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
  85. package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
  86. package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
  87. package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
  88. package/dist/shell/components/billing/access-contract.d.ts +18 -0
  89. package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
  90. package/dist/shell/components/billing/gates.d.ts +14 -0
  91. package/dist/shell/components/billing/gates.d.ts.map +1 -0
  92. package/dist/shell/components/billing/index.d.ts +9 -2
  93. package/dist/shell/components/billing/index.d.ts.map +1 -1
  94. package/dist/shell/components/chat/ChatFab.d.ts +3 -1
  95. package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
  96. package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
  97. package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
  98. package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
  99. package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
  100. package/dist/shell/components/chat/markdown.d.ts +5 -0
  101. package/dist/shell/components/chat/markdown.d.ts.map +1 -0
  102. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  103. package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
  104. package/dist/shell/components/organization/index.d.ts +0 -1
  105. package/dist/shell/components/organization/index.d.ts.map +1 -1
  106. package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
  107. package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
  108. package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
  109. package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
  110. package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
  111. package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
  112. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  113. package/dist/shell/hooks/index.d.ts +3 -2
  114. package/dist/shell/hooks/index.d.ts.map +1 -1
  115. package/dist/shell/hooks/useAITools.d.ts +2 -0
  116. package/dist/shell/hooks/useAITools.d.ts.map +1 -1
  117. package/dist/shell/hooks/useChat.d.ts +22 -0
  118. package/dist/shell/hooks/useChat.d.ts.map +1 -1
  119. package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
  120. package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
  121. package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
  122. package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
  123. package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
  124. package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
  125. package/dist/shell/lib/core-ai-tools.d.ts +14 -0
  126. package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
  127. package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
  128. package/dist/shell/lib/fayz-agent.d.ts +23 -0
  129. package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
  130. package/dist/shell/lib/i18n.d.ts.map +1 -1
  131. package/dist/shell/stores/chat.store.d.ts +69 -0
  132. package/dist/shell/stores/chat.store.d.ts.map +1 -1
  133. package/dist/shell/types/billing.d.ts +7 -0
  134. package/dist/shell/types/billing.d.ts.map +1 -1
  135. package/dist/shell/types/crud.d.ts +1 -1
  136. package/dist/shell/types/crud.d.ts.map +1 -1
  137. package/dist/shell/types/plugins.d.ts +2 -45
  138. package/dist/shell/types/plugins.d.ts.map +1 -1
  139. package/dist/stores/createCrudStore.d.ts.map +1 -1
  140. package/package.json +15 -12
  141. package/src/access/context.tsx +228 -0
  142. package/src/access/headless.ts +71 -0
  143. package/src/access/index.ts +27 -0
  144. package/src/access/limits-registry.ts +86 -0
  145. package/src/access/module-nav.test.ts +78 -0
  146. package/src/access/module-nav.ts +82 -0
  147. package/src/access/resolver.test.ts +143 -0
  148. package/src/access/resolver.ts +5 -0
  149. package/src/access/types.ts +47 -0
  150. package/src/access/upgrade-modal-store.ts +30 -0
  151. package/src/app/AdminShell.tsx +451 -68
  152. package/src/app/admin-app.tsx +46 -9
  153. package/src/app/config.ts +54 -1
  154. package/src/app/core-entities.ts +110 -0
  155. package/src/app/derive-contract.test.ts +155 -0
  156. package/src/app/derive-contract.ts +214 -0
  157. package/src/app/scaffold.tsx +4 -0
  158. package/src/archetype-lookup.ts +8 -2
  159. package/src/billing/index.ts +2 -0
  160. package/src/billing/plan-badge.ts +32 -0
  161. package/src/billing/store.ts +8 -1
  162. package/src/components/shared/ContactPicker.tsx +394 -0
  163. package/src/components/shared/PersonLink.tsx +11 -3
  164. package/src/components/shared/create-person.ts +74 -0
  165. package/src/crud/CrudDetailPage.tsx +8 -1
  166. package/src/crud/CrudFormPage.tsx +124 -6
  167. package/src/crud/CrudListView.tsx +16 -1
  168. package/src/crud/CrudPage.tsx +27 -5
  169. package/src/crud/DeleteConfirmDialog.tsx +1 -1
  170. package/src/crud/archetypes/AddressesTab.tsx +387 -0
  171. package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
  172. package/src/crud/createCrudPage.tsx +2 -0
  173. package/src/crud/index.ts +6 -0
  174. package/src/env.d.ts +7 -0
  175. package/src/index.ts +49 -1
  176. package/src/org/store.ts +9 -1
  177. package/src/permissions/context.tsx +29 -15
  178. package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
  179. package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
  180. package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
  181. package/src/shell/components/billing/UpgradeModal.tsx +102 -0
  182. package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
  183. package/src/shell/components/billing/access-contract.ts +64 -0
  184. package/src/shell/components/billing/gates.tsx +62 -0
  185. package/src/shell/components/billing/index.ts +18 -2
  186. package/src/shell/components/chat/ChatFab.tsx +4 -2
  187. package/src/shell/components/chat/ChatPanel.tsx +174 -23
  188. package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
  189. package/src/shell/components/chat/markdown.tsx +102 -0
  190. package/src/shell/components/organization/TeamTab.tsx +21 -4
  191. package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
  192. package/src/shell/components/organization/index.ts +0 -1
  193. package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
  194. package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
  195. package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
  196. package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
  197. package/src/shell/components/settings/SettingsPage.tsx +63 -21
  198. package/src/shell/hooks/index.ts +3 -2
  199. package/src/shell/hooks/useAITools.ts +27 -23
  200. package/src/shell/hooks/useChat.ts +381 -16
  201. package/src/shell/hooks/useSystemPermission.ts +23 -0
  202. package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
  203. package/src/shell/lib/ai-tool-handlers.ts +910 -0
  204. package/src/shell/lib/core-ai-tools.ts +234 -2
  205. package/src/shell/lib/create-client-orders-provider.ts +24 -2
  206. package/src/shell/lib/fayz-agent.ts +56 -0
  207. package/src/shell/lib/i18n.ts +71 -0
  208. package/src/shell/stores/chat.store.ts +118 -1
  209. package/src/shell/styles.css +30 -0
  210. package/src/shell/types/billing.ts +8 -0
  211. package/src/shell/types/crud.ts +1 -1
  212. package/src/shell/types/plugins.ts +13 -50
  213. package/src/stores/createCrudStore.ts +9 -0
  214. package/dist/chunk-3M25GRRU.js.map +0 -1
  215. package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
  216. package/dist/chunk-5GYQK5IW.js +0 -21
  217. package/dist/chunk-5GYQK5IW.js.map +0 -1
  218. package/dist/chunk-IPWCHJPE.cjs.map +0 -1
  219. package/dist/chunk-KYID2MR6.cjs +0 -23
  220. package/dist/chunk-KYID2MR6.cjs.map +0 -1
  221. package/dist/chunk-XNWCGJYH.js.map +0 -1
  222. package/dist/shell/components/billing/BillingPage.d.ts +0 -7
  223. package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
  224. package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
  225. package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
  226. package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
  227. package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
  228. package/dist/shell/hooks/useBilling.d.ts +0 -14
  229. package/dist/shell/hooks/useBilling.d.ts.map +0 -1
  230. package/dist/shell/hooks/usePermission.d.ts +0 -16
  231. package/dist/shell/hooks/usePermission.d.ts.map +0 -1
  232. package/src/shell/components/billing/BillingPage.tsx +0 -56
  233. package/src/shell/components/billing/PaywallGate.tsx +0 -80
  234. package/src/shell/components/organization/PermissionGate.tsx +0 -16
  235. package/src/shell/hooks/useBilling.ts +0 -151
  236. package/src/shell/hooks/usePermission.ts +0 -101
@@ -1,16 +1,24 @@
1
1
  import * as React from 'react'
2
- import { ArrowUp, Settings2 } from 'lucide-react'
2
+ import { ArrowUp, ArrowUpRight, ChevronDown, History, Loader2, SquarePen, Settings2, Wrench } from 'lucide-react'
3
3
  import { cn } from '../../lib/cn'
4
4
  import { useTranslation } from '../../hooks/useTranslation'
5
- import { useChatStore, type ChatMessage } from '../../stores/chat.store'
5
+ import { useChatStore, type ChatMessage, type ChatToolCall, type ChatRecordLink } from '../../stores/chat.store'
6
6
  import { useChat } from '../../hooks/useChat'
7
7
  import { useAITools } from '../../hooks/useAITools'
8
8
  import { ChatSuggestions, ChatToolsPanel } from './ChatSuggestions'
9
+ import { ConfirmActionCard } from './ConfirmActionCard'
10
+ import { ChatMarkdown } from './markdown'
11
+ import type { FayzAgentConnectionConfig } from '../../lib/fayz-agent'
12
+ // NOTE: src/lib copy — the one AdminShell's setEntityRouteMap fills. The
13
+ // shell/lib duplicate is a legacy shim nobody populates ('two of everything').
14
+ import { resolveEntityRoute, resolveEntityHref } from '../../../lib/entity-routes'
15
+ import { useRouter } from '../../lib/router'
9
16
 
10
17
  interface ChatPanelProps {
11
18
  title?: string
12
19
  apiEndpoint?: string
13
20
  systemPrompt?: string
21
+ agent?: FayzAgentConnectionConfig | false
14
22
  className?: string
15
23
  }
16
24
 
@@ -18,14 +26,19 @@ export function ChatPanel({
18
26
  title = 'Assistant',
19
27
  apiEndpoint,
20
28
  systemPrompt,
29
+ agent,
21
30
  className,
22
31
  }: ChatPanelProps) {
23
32
  const { isOpen, messages, isStreaming } = useChatStore()
24
- const { sendMessage } = useChat({ apiEndpoint, systemPrompt })
33
+ const {
34
+ sendMessage, isConfigured, pendingAction, resolvePendingAction,
35
+ activeTools, conversations, loadConversations, resumeConversation, startNewConversation,
36
+ } = useChat({ apiEndpoint, systemPrompt, agent })
25
37
  const { t } = useTranslation()
26
38
  const { suggestions, toolGroups } = useAITools()
27
39
  const [input, setInput] = React.useState('')
28
40
  const [showTools, setShowTools] = React.useState(false)
41
+ const [showHistory, setShowHistory] = React.useState(false)
29
42
  const scrollRef = React.useRef<HTMLDivElement>(null)
30
43
  const inputRef = React.useRef<HTMLInputElement>(null)
31
44
 
@@ -38,18 +51,20 @@ export function ChatPanel({
38
51
 
39
52
  React.useEffect(() => {
40
53
  if (isOpen) {
54
+ loadConversations()
41
55
  const timer = setTimeout(() => inputRef.current?.focus(), 100)
42
56
  return () => clearTimeout(timer)
43
57
  }
44
58
  setShowTools(false)
45
- }, [isOpen])
59
+ setShowHistory(false)
60
+ }, [isOpen, loadConversations])
46
61
 
47
62
  if (!isOpen) return null
48
63
 
49
64
  const handleSubmit = (e: React.FormEvent) => {
50
65
  e.preventDefault()
51
66
  const text = input.trim()
52
- if (!text || isStreaming) return
67
+ if (!text || isStreaming || !isConfigured) return
53
68
  setInput('')
54
69
  setShowTools(false)
55
70
  sendMessage(text)
@@ -73,9 +88,31 @@ export function ChatPanel({
73
88
  <div className="h-1.5 w-1.5 rounded-full bg-success" />
74
89
  <span className="text-xs font-semibold text-foreground">{title}</span>
75
90
  </div>
91
+ <div className="flex items-center gap-0.5">
92
+ {hasMessages && (
93
+ <button
94
+ onClick={startNewConversation}
95
+ className="inline-flex items-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
96
+ title={t('chat.newConversation')}
97
+ >
98
+ <SquarePen className="h-3 w-3" />
99
+ </button>
100
+ )}
101
+ {conversations.length > 0 && (
102
+ <button
103
+ onClick={() => { setShowHistory(!showHistory); setShowTools(false) }}
104
+ className={cn(
105
+ 'inline-flex items-center rounded-md p-1.5 transition-colors',
106
+ showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
107
+ )}
108
+ title={t('chat.history')}
109
+ >
110
+ <History className="h-3 w-3" />
111
+ </button>
112
+ )}
76
113
  {totalTools > 0 && (
77
114
  <button
78
- onClick={() => setShowTools(!showTools)}
115
+ onClick={() => { setShowTools(!showTools); setShowHistory(false) }}
79
116
  className={cn(
80
117
  'inline-flex items-center gap-1 rounded-md px-1.5 py-1 text-[10px] transition-colors',
81
118
  showTools
@@ -88,10 +125,31 @@ export function ChatPanel({
88
125
  <span className="font-medium">{totalTools}</span>
89
126
  </button>
90
127
  )}
128
+ </div>
91
129
  </div>
92
130
 
93
- {/* Tools panel overlay */}
94
- {showTools ? (
131
+ {/* History drawer */}
132
+ {showHistory ? (
133
+ <div className="min-h-0 flex-1 overflow-y-auto py-1">
134
+ {conversations.map((c) => (
135
+ <button
136
+ key={c.id}
137
+ onClick={() => {
138
+ setShowHistory(false)
139
+ resumeConversation(c.id)
140
+ }}
141
+ className="flex w-full flex-col items-start gap-0.5 px-4 py-2 text-left transition-colors hover:bg-muted/50"
142
+ >
143
+ <span className="line-clamp-1 text-[13px] text-foreground">
144
+ {c.title ?? t('chat.newConversation')}
145
+ </span>
146
+ <span className="text-[10px] text-muted-foreground">
147
+ {new Date(c.updatedAt).toLocaleString()}
148
+ </span>
149
+ </button>
150
+ ))}
151
+ </div>
152
+ ) : showTools ? (
95
153
  <ChatToolsPanel toolGroups={toolGroups} onClose={() => setShowTools(false)} />
96
154
  ) : (
97
155
  /* Messages / suggestions area */
@@ -103,18 +161,41 @@ export function ChatPanel({
103
161
  )}
104
162
  >
105
163
  {!hasMessages && (
106
- <ChatSuggestions
107
- suggestions={suggestions}
108
- onSelect={(suggestion) => {
109
- setInput('')
110
- sendMessage(suggestion.prompt ?? suggestion.label)
111
- }}
112
- />
164
+ <>
165
+ {!isConfigured && (
166
+ <div className="mx-3 mt-3 rounded-lg border border-border/60 bg-muted/40 px-3 py-2 text-[11px] leading-relaxed text-muted-foreground">
167
+ {t('chat.notConfigured')}
168
+ </div>
169
+ )}
170
+ <ChatSuggestions
171
+ suggestions={suggestions}
172
+ onSelect={(suggestion) => {
173
+ setInput('')
174
+ sendMessage(suggestion.prompt ?? suggestion.label)
175
+ }}
176
+ />
177
+ </>
113
178
  )}
114
179
  {messages.map((msg) => (
115
180
  <MessageBubble key={msg.id} message={msg} />
116
181
  ))}
117
- {isStreaming && messages[messages.length - 1]?.content === '' && (
182
+ {pendingAction && (
183
+ <ConfirmActionCard action={pendingAction} onResolve={resolvePendingAction} />
184
+ )}
185
+ {activeTools.length > 0 && (
186
+ <div className="flex flex-wrap items-center gap-1 px-2 py-1">
187
+ {activeTools.map((name) => (
188
+ <span
189
+ key={name}
190
+ className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground"
191
+ >
192
+ <Loader2 className="h-2.5 w-2.5 animate-spin" />
193
+ {name}
194
+ </span>
195
+ ))}
196
+ </div>
197
+ )}
198
+ {isStreaming && !pendingAction && activeTools.length === 0 && messages[messages.length - 1]?.content === '' && (
118
199
  <div className="flex items-center gap-1 px-2 py-1.5">
119
200
  <span className="h-1 w-1 rounded-full bg-muted-foreground/50 animate-pulse" />
120
201
  <span className="h-1 w-1 rounded-full bg-muted-foreground/50 animate-pulse [animation-delay:150ms]" />
@@ -124,7 +205,7 @@ export function ChatPanel({
124
205
  </div>
125
206
  )}
126
207
 
127
- {/* Input */}
208
+ {/* Input — disabled until a real chat.apiEndpoint is configured. */}
128
209
  <form onSubmit={handleSubmit} className="border-t border-border/40 p-2">
129
210
  <div className="flex items-center gap-1.5 rounded-full border border-border bg-background py-1 pl-3.5 pr-1 transition-colors focus-within:border-foreground/20">
130
211
  <input
@@ -132,13 +213,19 @@ export function ChatPanel({
132
213
  type="text"
133
214
  value={input}
134
215
  onChange={(e) => setInput(e.target.value)}
135
- placeholder={t('chat.messagePlaceholder')}
136
- className="min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none"
137
- disabled={isStreaming}
216
+ placeholder={
217
+ pendingAction
218
+ ? t('chat.confirmAction.blocked')
219
+ : isConfigured
220
+ ? t('chat.messagePlaceholder')
221
+ : t('chat.notConfigured')
222
+ }
223
+ className="min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none disabled:cursor-not-allowed"
224
+ disabled={isStreaming || !isConfigured || !!pendingAction}
138
225
  />
139
226
  <button
140
227
  type="submit"
141
- disabled={!input.trim() || isStreaming}
228
+ disabled={!input.trim() || isStreaming || !isConfigured || !!pendingAction}
142
229
  className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-foreground text-background transition-opacity hover:opacity-80 disabled:opacity-20"
143
230
  aria-label="Send message"
144
231
  >
@@ -150,11 +237,68 @@ export function ChatPanel({
150
237
  )
151
238
  }
152
239
 
240
+ /**
241
+ * Goto button for a record the reply concerns. The ref is semantic — the
242
+ * button only renders when THIS app's route map resolves the archetype (a
243
+ * missing link is correct; an invented one is a dead end — agents.md).
244
+ */
245
+ function RecordLinkButton({ link }: { link: ChatRecordLink }) {
246
+ const router = useRouter()
247
+ if (!resolveEntityRoute(link.archetype, link.kind)) return null
248
+ const href = resolveEntityHref(link.id, link.archetype, link.kind)
249
+ return (
250
+ <button
251
+ type="button"
252
+ onClick={() => router.navigate(href)}
253
+ className="inline-flex items-center gap-1 rounded-full border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-foreground transition-colors hover:bg-muted"
254
+ >
255
+ {link.label}
256
+ <ArrowUpRight className="h-3 w-3" />
257
+ </button>
258
+ )
259
+ }
260
+
261
+ function ToolCallRow({ call }: { call: ChatToolCall }) {
262
+ const [open, setOpen] = React.useState(false)
263
+ return (
264
+ <div className="rounded-md border border-border/60">
265
+ <button
266
+ type="button"
267
+ onClick={() => setOpen(!open)}
268
+ className="flex w-full items-center gap-1.5 px-2 py-1 text-left font-mono text-[10px] text-muted-foreground transition-colors hover:text-foreground"
269
+ >
270
+ <Wrench className="h-2.5 w-2.5 shrink-0" />
271
+ <span className="truncate">{call.name}</span>
272
+ <ChevronDown className={cn('ml-auto h-2.5 w-2.5 shrink-0 transition-transform', open && 'rotate-180')} />
273
+ </button>
274
+ {open && (
275
+ <div className="space-y-1 border-t border-border/60 px-2 py-1.5">
276
+ {call.args && (
277
+ <pre className="max-h-24 overflow-auto whitespace-pre-wrap break-all font-mono text-[9px] leading-snug text-muted-foreground">{call.args}</pre>
278
+ )}
279
+ {call.result && (
280
+ <pre className="max-h-40 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/50 p-1.5 font-mono text-[9px] leading-snug text-foreground/80">{call.result}</pre>
281
+ )}
282
+ </div>
283
+ )}
284
+ </div>
285
+ )
286
+ }
287
+
153
288
  function MessageBubble({ message }: { message: ChatMessage }) {
154
289
  const isUser = message.role === 'user'
155
290
 
156
291
  return (
157
- <div className={cn('flex', isUser ? 'justify-end' : 'justify-start')}>
292
+ <div className={cn('flex flex-col gap-1', isUser ? 'items-end' : 'items-start')}>
293
+ {/* Chronological order: the work happened BEFORE the reply — trace on
294
+ top, answer below, goto buttons (the next step) underneath. */}
295
+ {!isUser && (message.toolCalls?.length ?? 0) > 0 && (
296
+ <div className="flex max-w-[85%] flex-col gap-0.5 px-1">
297
+ {message.toolCalls!.map((call, i) => (
298
+ <ToolCallRow key={`${call.name}-${i}`} call={call} />
299
+ ))}
300
+ </div>
301
+ )}
158
302
  <div
159
303
  className={cn(
160
304
  'max-w-[85%] rounded-2xl px-3 py-1.5 text-[13px] leading-relaxed',
@@ -163,8 +307,15 @@ function MessageBubble({ message }: { message: ChatMessage }) {
163
307
  : 'rounded-bl-sm bg-muted text-foreground'
164
308
  )}
165
309
  >
166
- {message.content}
310
+ {isUser ? message.content : <ChatMarkdown content={message.content} />}
167
311
  </div>
312
+ {!isUser && (message.links?.length ?? 0) > 0 && (
313
+ <div className="flex max-w-[85%] flex-wrap gap-1 px-1">
314
+ {message.links!.map((link) => (
315
+ <RecordLinkButton key={link.id} link={link} />
316
+ ))}
317
+ </div>
318
+ )}
168
319
  </div>
169
320
  )
170
321
  }
@@ -0,0 +1,57 @@
1
+ import * as React from 'react'
2
+ import { ShieldCheck } from 'lucide-react'
3
+ import { useTranslation } from '../../hooks/useTranslation'
4
+ import type { PendingAgentAction } from '../../stores/chat.store'
5
+
6
+ /**
7
+ * The human gate in front of an agent write: shows what the agent wants to do
8
+ * (tool + arguments) and blocks the conversation until the user decides —
9
+ * the FAB's native rendering of the confirmation step every channel has
10
+ * (WhatsApp asks "responda SIM", MCP uses elicitation).
11
+ */
12
+ export function ConfirmActionCard({
13
+ action,
14
+ onResolve,
15
+ }: {
16
+ action: PendingAgentAction
17
+ onResolve: (approved: boolean) => void
18
+ }) {
19
+ const { t } = useTranslation()
20
+ const entries = Object.entries(action.params).filter(([, v]) => v !== undefined && v !== null)
21
+
22
+ return (
23
+ <div className="mx-3 my-2 rounded-lg border border-border bg-muted/40 p-3 text-sm">
24
+ <div className="mb-1 flex items-center gap-2 font-medium text-foreground">
25
+ <ShieldCheck className="h-4 w-4 shrink-0" />
26
+ {t('chat.confirmAction.title')}
27
+ </div>
28
+ <div className="mb-2 text-xs text-muted-foreground">{action.title}</div>
29
+ {entries.length > 0 && (
30
+ <dl className="mb-3 space-y-0.5">
31
+ {entries.map(([key, value]) => (
32
+ <div key={key} className="flex gap-2 text-xs">
33
+ <dt className="shrink-0 font-mono text-muted-foreground">{key}</dt>
34
+ <dd className="truncate text-foreground">{String(value)}</dd>
35
+ </div>
36
+ ))}
37
+ </dl>
38
+ )}
39
+ <div className="flex gap-2">
40
+ <button
41
+ type="button"
42
+ onClick={() => onResolve(true)}
43
+ className="inline-flex h-7 items-center rounded-md bg-foreground px-3 text-xs font-medium text-background transition-opacity hover:opacity-80"
44
+ >
45
+ {t('chat.confirmAction.confirm')}
46
+ </button>
47
+ <button
48
+ type="button"
49
+ onClick={() => onResolve(false)}
50
+ className="inline-flex h-7 items-center rounded-md border border-border px-3 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted"
51
+ >
52
+ {t('chat.confirmAction.cancel')}
53
+ </button>
54
+ </div>
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,102 @@
1
+ import * as React from 'react'
2
+
3
+ // Minimal markdown for chat bubbles — the subset agents actually emit:
4
+ // **bold**, *italic*, `code`, [links](url), - / 1. lists, ### headings and
5
+ // paragraphs. Built as React nodes (never innerHTML), so model output can't
6
+ // inject markup. Deliberately dependency-free: the saas package ships in every
7
+ // generated app and a full markdown library is ~30kb for four constructs.
8
+
9
+ function renderInline(text: string, keyBase: string): React.ReactNode[] {
10
+ const out: React.ReactNode[] = []
11
+ // Tokenize: code first (protects its content), then bold, italic, links.
12
+ const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]+\]\([^)\s]+\))/g
13
+ let last = 0
14
+ let match: RegExpExecArray | null
15
+ let i = 0
16
+ while ((match = pattern.exec(text)) !== null) {
17
+ if (match.index > last) out.push(text.slice(last, match.index))
18
+ const token = match[0]
19
+ const key = `${keyBase}-${i++}`
20
+ if (token.startsWith('`')) {
21
+ out.push(
22
+ <code key={key} className="rounded bg-black/10 px-1 font-mono text-[0.85em] dark:bg-white/10">
23
+ {token.slice(1, -1)}
24
+ </code>,
25
+ )
26
+ } else if (token.startsWith('**')) {
27
+ out.push(<strong key={key}>{renderInline(token.slice(2, -2), key)}</strong>)
28
+ } else if (token.startsWith('*')) {
29
+ out.push(<em key={key}>{renderInline(token.slice(1, -1), key)}</em>)
30
+ } else {
31
+ const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token)
32
+ if (link && /^(https?:\/\/|\/|#)/.test(link[2])) {
33
+ out.push(
34
+ <a key={key} href={link[2]} target="_blank" rel="noreferrer" className="underline underline-offset-2">
35
+ {renderInline(link[1], key)}
36
+ </a>,
37
+ )
38
+ } else {
39
+ out.push(token)
40
+ }
41
+ }
42
+ last = match.index + token.length
43
+ }
44
+ if (last < text.length) out.push(text.slice(last))
45
+ return out
46
+ }
47
+
48
+ export function ChatMarkdown({ content }: { content: string }) {
49
+ const blocks: React.ReactNode[] = []
50
+ const lines = content.split('\n')
51
+ let i = 0
52
+ let key = 0
53
+
54
+ while (i < lines.length) {
55
+ const line = lines[i]
56
+ if (!line.trim()) {
57
+ i++
58
+ continue
59
+ }
60
+ const heading = /^(#{1,4})\s+(.*)$/.exec(line)
61
+ if (heading) {
62
+ blocks.push(
63
+ <p key={key++} className="mt-1.5 font-semibold first:mt-0">
64
+ {renderInline(heading[2], `h${key}`)}
65
+ </p>,
66
+ )
67
+ i++
68
+ continue
69
+ }
70
+ if (/^\s*([-*]|\d+[.)])\s+/.test(line)) {
71
+ const items: React.ReactNode[] = []
72
+ const ordered = /^\s*\d+[.)]\s+/.test(line)
73
+ while (i < lines.length && /^\s*([-*]|\d+[.)])\s+/.test(lines[i])) {
74
+ const item = lines[i].replace(/^\s*([-*]|\d+[.)])\s+/, '')
75
+ items.push(<li key={items.length}>{renderInline(item, `li${key}-${items.length}`)}</li>)
76
+ i++
77
+ }
78
+ const listClass = 'my-1 space-y-0.5 pl-4 first:mt-0 last:mb-0'
79
+ blocks.push(
80
+ ordered ? (
81
+ <ol key={key++} className={`${listClass} list-decimal`}>{items}</ol>
82
+ ) : (
83
+ <ul key={key++} className={`${listClass} list-disc`}>{items}</ul>
84
+ ),
85
+ )
86
+ continue
87
+ }
88
+ // Paragraph: swallow consecutive plain lines with soft breaks.
89
+ const para: React.ReactNode[] = []
90
+ while (i < lines.length && lines[i].trim() && !/^\s*([-*]|\d+[.)])\s+/.test(lines[i]) && !/^#{1,4}\s+/.test(lines[i])) {
91
+ if (para.length) para.push(<br key={`br${key}-${para.length}`} />)
92
+ para.push(...renderInline(lines[i], `p${key}-${para.length}`))
93
+ i++
94
+ }
95
+ blocks.push(
96
+ <p key={key++} className="my-1 first:mt-0 last:mb-0">
97
+ {para}
98
+ </p>,
99
+ )
100
+ }
101
+ return <>{blocks}</>
102
+ }
@@ -23,9 +23,10 @@ import { useOrgAdapterOptional as useNativeOrgAdapter } from '../../../org'
23
23
  import type { OrgAdapter } from '../../types/org-adapter'
24
24
  const useOrgAdapterOptional = () => useNativeOrgAdapter() as unknown as OrgAdapter | null
25
25
  import { useAuthStore } from '@fayz-ai/auth'
26
- import { usePermission } from '../../hooks/usePermission'
26
+ import { useSystemPermission } from '../../hooks/useSystemPermission'
27
27
  import { useTranslation } from '../../hooks/useTranslation'
28
28
  import { dedup } from '../../lib/dedup'
29
+ import { useLimitGuard, invalidateLimit } from '../../../access'
29
30
 
30
31
  export function TeamTab() {
31
32
  const adapter = useOrgAdapterOptional()
@@ -36,9 +37,17 @@ export function TeamTab() {
36
37
  const invites = useInviteStore((s) => s.invites)
37
38
  const setInvites = useInviteStore((s) => s.setInvites)
38
39
  const { user } = useAuthStore()
39
- const { hasSystemPermission } = usePermission()
40
+ const hasSystemPermission = useSystemPermission()
40
41
 
41
42
  const [inviteOpen, setInviteOpen] = React.useState(false)
43
+ // Seat cap ('users' limit → tenant_members count). Guarded before opening the
44
+ // invite dialog; the guard opens the global UpgradeModal when at the cap.
45
+ const guardSeats = useLimitGuard('users')
46
+
47
+ const handleOpenInvite = async () => {
48
+ if ((await guardSeats(1)) === 'blocked') return
49
+ setInviteOpen(true)
50
+ }
42
51
 
43
52
  // Load invites on mount (deduped to avoid strict mode double-fetch)
44
53
  React.useEffect(() => {
@@ -107,7 +116,7 @@ export function TeamTab() {
107
116
  <p className="text-sm text-muted-foreground">{t('organization.team.memberCount', { count: String(members.length), plural: members.length !== 1 ? 's' : '' })}</p>
108
117
  </div>
109
118
  {canManageTeam && (
110
- <Button size="sm" onClick={() => setInviteOpen(true)}>
119
+ <Button size="sm" onClick={() => { void handleOpenInvite() }}>
111
120
  <UserPlus className="h-4 w-4 mr-1" />
112
121
  {t('organization.team.invite')}
113
122
  </Button>
@@ -264,7 +273,15 @@ export function TeamTab() {
264
273
  </div>
265
274
  )}
266
275
 
267
- <InviteMemberDialog open={inviteOpen} onOpenChange={setInviteOpen} />
276
+ <InviteMemberDialog
277
+ open={inviteOpen}
278
+ onOpenChange={(open) => {
279
+ setInviteOpen(open)
280
+ // On close (after a successful invite the dialog closes itself),
281
+ // refresh the seat count so the guard/banner reflect the new member.
282
+ if (!open) invalidateLimit('users')
283
+ }}
284
+ />
268
285
  </div>
269
286
  )
270
287
  }
@@ -11,7 +11,9 @@ import {
11
11
  } from '@fayz-ai/ui'
12
12
  import { cn } from '../../lib/cn'
13
13
  import { useTranslation } from '../../hooks/useTranslation'
14
- import { useOrgAdapter } from '../../lib/org-context'
14
+ // Native org adapter — the shell lib/org-context provider is never mounted, so
15
+ // the shell useOrgAdapter() threw here. The native <OrgProvider> is the mounted one.
16
+ import { useOrgAdapter } from '../../../org'
15
17
  import { useAuthStore } from '../../stores/auth.store'
16
18
  import { useOrganizationStore } from '../../stores/organization.store'
17
19
  import { usePermissionsStore } from '../../stores/permissions.store'
@@ -1,6 +1,5 @@
1
1
  export { OrgSwitcher } from './OrgSwitcher'
2
2
  export { OrgInitializer } from './OrgInitializer'
3
- export { PermissionGate } from './PermissionGate'
4
3
  export { PermissionMatrixEditor } from './PermissionMatrixEditor'
5
4
  export { PermissionProfilesTab } from './PermissionProfilesTab'
6
5
  export { TeamTab } from './TeamTab'
@@ -1,6 +1,9 @@
1
1
  import { CompanySettings } from './CompanySettings'
2
2
  import { useOrganizationStore } from '../../stores/organization.store'
3
- import { useOrgAdapterOptional } from '../../lib/org-context'
3
+ // Native org adapter — the shell's lib/org-context provider is never mounted,
4
+ // so importing from there returned a null adapter and every save was a no-op.
5
+ // The native <OrgProvider> (packages/saas/src/org) is the one actually mounted.
6
+ import { useOrgAdapterOptional } from '../../../org'
4
7
  import { toast } from '../notifications/ToastProvider'
5
8
  import type { Tenant, TenantSettings } from '../../types'
6
9
 
@@ -1,6 +1,8 @@
1
1
  import { FieldRulesSettings } from './FieldRulesSettings'
2
2
  import { useOrganizationStore } from '../../stores/organization.store'
3
- import { useOrgAdapterOptional } from '../../lib/org-context'
3
+ // Native org adapter — see ConnectedCompanySettings. The shell org-context
4
+ // provider is never mounted, so the old import yielded a null adapter (no-op save).
5
+ import { useOrgAdapterOptional } from '../../../org'
4
6
  import { getRegisteredEntities } from '../../lib/entity-registry'
5
7
  import { toast } from '../notifications/ToastProvider'
6
8
  import { useTranslation } from '../../hooks/useTranslation'
@@ -3,7 +3,9 @@ import type { TenantSecuritySettings } from './SecuritySettings'
3
3
  import { useAuthStore } from '../../stores/auth.store'
4
4
  import { useOrganizationStore } from '../../stores/organization.store'
5
5
  import { useAuthAdapterOptional } from '../../lib/auth-context'
6
- import { useOrgAdapterOptional } from '../../lib/org-context'
6
+ // Native org adapter — see ConnectedCompanySettings. The shell org-context
7
+ // provider is never mounted, so the old import yielded a null adapter (no-op save).
8
+ import { useOrgAdapterOptional } from '../../../org'
7
9
  import { toast } from '../notifications/ToastProvider'
8
10
 
9
11
  export function ConnectedSecuritySettings() {
@@ -1,5 +1,7 @@
1
1
  import { createCrudPage } from '../../../crud/createCrudPage'
2
- import type { EntityDef } from '../../types/crud'
2
+ // Core's EntityDef (the type createCrudPage consumes) carries `limitKey` for
3
+ // plan quantity gating; the shell alias does not.
4
+ import type { EntityDef } from '@fayz-ai/core'
3
5
 
4
6
  export const locationEntityDef: EntityDef = {
5
7
  name: 'Location',
@@ -9,6 +11,9 @@ export const locationEntityDef: EntityDef = {
9
11
  displayField: 'name',
10
12
  subtitleField: 'city',
11
13
  defaultSort: 'name',
14
+ // Plan quantity cap — the generic CRUD (CrudFormPage/CrudPage/CrudListView)
15
+ // guards create/import and dims "+ Add" at the cap. Core declares 'locations'.
16
+ limitKey: 'locations',
12
17
  fields: [
13
18
  { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true, searchable: true },
14
19
  { key: 'phone', label: 'Phone', type: 'phone', showInTable: true },