@fayz-ai/saas 0.8.1 → 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.
- package/dist/access/context.d.ts +47 -0
- package/dist/access/context.d.ts.map +1 -0
- package/dist/access/headless.d.ts +21 -0
- package/dist/access/headless.d.ts.map +1 -0
- package/dist/access/index.d.ts +12 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/limits-registry.d.ts +33 -0
- package/dist/access/limits-registry.d.ts.map +1 -0
- package/dist/access/module-nav.d.ts +15 -0
- package/dist/access/module-nav.d.ts.map +1 -0
- package/dist/access/resolver.d.ts +2 -0
- package/dist/access/resolver.d.ts.map +1 -0
- package/dist/access/types.d.ts +43 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/upgrade-modal-store.d.ts +23 -0
- package/dist/access/upgrade-modal-store.d.ts.map +1 -0
- package/dist/app/AdminShell.d.ts +3 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +3 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +47 -2
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/core-entities.d.ts +25 -0
- package/dist/app/core-entities.d.ts.map +1 -0
- package/dist/app/derive-contract.d.ts +8 -0
- package/dist/app/derive-contract.d.ts.map +1 -0
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/archetype-lookup.d.ts.map +1 -1
- package/dist/billing/index.cjs +6 -2
- package/dist/billing/index.d.ts +2 -0
- package/dist/billing/index.d.ts.map +1 -1
- package/dist/billing/index.js +1 -1
- package/dist/billing/plan-badge.d.ts +17 -0
- package/dist/billing/plan-badge.d.ts.map +1 -0
- package/dist/billing/store.d.ts +6 -1
- package/dist/billing/store.d.ts.map +1 -1
- package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
- package/dist/chunk-I3DY47N5.js.map +1 -0
- package/dist/chunk-NNFGMUF2.cjs +39 -0
- package/dist/chunk-NNFGMUF2.cjs.map +1 -0
- package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
- package/dist/chunk-SAZLW54Y.cjs.map +1 -0
- package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
- package/dist/chunk-VKYZACG5.cjs.map +1 -0
- package/dist/chunk-VN5NTK26.js +36 -0
- package/dist/chunk-VN5NTK26.js.map +1 -0
- package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
- package/dist/chunk-VTEP2AQX.js.map +1 -0
- package/dist/components/shared/ContactPicker.d.ts +92 -0
- package/dist/components/shared/ContactPicker.d.ts.map +1 -0
- package/dist/components/shared/PersonLink.d.ts +12 -1
- package/dist/components/shared/PersonLink.d.ts.map +1 -1
- package/dist/components/shared/create-person.d.ts +20 -0
- package/dist/components/shared/create-person.d.ts.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudFormPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
- package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
- package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
- package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
- package/dist/crud/createCrudPage.d.ts.map +1 -1
- package/dist/crud/index.d.ts +1 -0
- package/dist/crud/index.d.ts.map +1 -1
- package/dist/index.cjs +4281 -716
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4067 -542
- package/dist/index.js.map +1 -1
- package/dist/org/index.cjs +11 -11
- package/dist/org/index.js +2 -2
- package/dist/org/store.d.ts.map +1 -1
- package/dist/permissions/context.d.ts +2 -0
- package/dist/permissions/context.d.ts.map +1 -1
- package/dist/permissions/index.cjs +7 -7
- package/dist/permissions/index.js +1 -1
- package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
- package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
- package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
- package/dist/shell/components/billing/access-contract.d.ts +18 -0
- package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
- package/dist/shell/components/billing/gates.d.ts +14 -0
- package/dist/shell/components/billing/gates.d.ts.map +1 -0
- package/dist/shell/components/billing/index.d.ts +9 -2
- package/dist/shell/components/billing/index.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatFab.d.ts +3 -1
- package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
- package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
- package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
- package/dist/shell/components/chat/markdown.d.ts +5 -0
- package/dist/shell/components/chat/markdown.d.ts.map +1 -0
- package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
- package/dist/shell/components/organization/index.d.ts +0 -1
- package/dist/shell/components/organization/index.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
- package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
- package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/shell/hooks/index.d.ts +3 -2
- package/dist/shell/hooks/index.d.ts.map +1 -1
- package/dist/shell/hooks/useAITools.d.ts +2 -0
- package/dist/shell/hooks/useAITools.d.ts.map +1 -1
- package/dist/shell/hooks/useChat.d.ts +22 -0
- package/dist/shell/hooks/useChat.d.ts.map +1 -1
- package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
- package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
- package/dist/shell/lib/core-ai-tools.d.ts +14 -0
- package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
- package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
- package/dist/shell/lib/fayz-agent.d.ts +23 -0
- package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
- package/dist/shell/lib/i18n.d.ts.map +1 -1
- package/dist/shell/stores/chat.store.d.ts +69 -0
- package/dist/shell/stores/chat.store.d.ts.map +1 -1
- package/dist/shell/types/billing.d.ts +7 -0
- package/dist/shell/types/billing.d.ts.map +1 -1
- package/dist/shell/types/plugins.d.ts +2 -45
- package/dist/shell/types/plugins.d.ts.map +1 -1
- package/dist/stores/createCrudStore.d.ts.map +1 -1
- package/package.json +14 -11
- package/src/access/context.tsx +228 -0
- package/src/access/headless.ts +71 -0
- package/src/access/index.ts +27 -0
- package/src/access/limits-registry.ts +86 -0
- package/src/access/module-nav.test.ts +78 -0
- package/src/access/module-nav.ts +82 -0
- package/src/access/resolver.test.ts +143 -0
- package/src/access/resolver.ts +5 -0
- package/src/access/types.ts +47 -0
- package/src/access/upgrade-modal-store.ts +30 -0
- package/src/app/AdminShell.tsx +451 -68
- package/src/app/admin-app.tsx +46 -9
- package/src/app/config.ts +54 -1
- package/src/app/core-entities.ts +110 -0
- package/src/app/derive-contract.test.ts +155 -0
- package/src/app/derive-contract.ts +214 -0
- package/src/app/scaffold.tsx +4 -0
- package/src/archetype-lookup.ts +8 -2
- package/src/billing/index.ts +2 -0
- package/src/billing/plan-badge.ts +32 -0
- package/src/billing/store.ts +8 -1
- package/src/components/shared/ContactPicker.tsx +394 -0
- package/src/components/shared/PersonLink.tsx +11 -3
- package/src/components/shared/create-person.ts +74 -0
- package/src/crud/CrudDetailPage.tsx +5 -1
- package/src/crud/CrudFormPage.tsx +113 -6
- package/src/crud/CrudListView.tsx +16 -1
- package/src/crud/CrudPage.tsx +27 -5
- package/src/crud/DeleteConfirmDialog.tsx +1 -1
- package/src/crud/archetypes/AddressesTab.tsx +387 -0
- package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
- package/src/crud/createCrudPage.tsx +2 -0
- package/src/crud/index.ts +6 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +49 -1
- package/src/org/store.ts +9 -1
- package/src/permissions/context.tsx +29 -15
- package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
- package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
- package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
- package/src/shell/components/billing/UpgradeModal.tsx +102 -0
- package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
- package/src/shell/components/billing/access-contract.ts +64 -0
- package/src/shell/components/billing/gates.tsx +62 -0
- package/src/shell/components/billing/index.ts +18 -2
- package/src/shell/components/chat/ChatFab.tsx +4 -2
- package/src/shell/components/chat/ChatPanel.tsx +174 -23
- package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
- package/src/shell/components/chat/markdown.tsx +102 -0
- package/src/shell/components/organization/TeamTab.tsx +21 -4
- package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
- package/src/shell/components/organization/index.ts +0 -1
- package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
- package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
- package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
- package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
- package/src/shell/components/settings/SettingsPage.tsx +63 -21
- package/src/shell/hooks/index.ts +3 -2
- package/src/shell/hooks/useAITools.ts +27 -23
- package/src/shell/hooks/useChat.ts +381 -16
- package/src/shell/hooks/useSystemPermission.ts +23 -0
- package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
- package/src/shell/lib/ai-tool-handlers.ts +910 -0
- package/src/shell/lib/core-ai-tools.ts +234 -2
- package/src/shell/lib/create-client-orders-provider.ts +24 -2
- package/src/shell/lib/fayz-agent.ts +56 -0
- package/src/shell/lib/i18n.ts +71 -0
- package/src/shell/stores/chat.store.ts +118 -1
- package/src/shell/styles.css +30 -0
- package/src/shell/types/billing.ts +8 -0
- package/src/shell/types/plugins.ts +13 -50
- package/src/stores/createCrudStore.ts +9 -0
- package/dist/chunk-3M25GRRU.js.map +0 -1
- package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
- package/dist/chunk-5GYQK5IW.js +0 -21
- package/dist/chunk-5GYQK5IW.js.map +0 -1
- package/dist/chunk-IPWCHJPE.cjs.map +0 -1
- package/dist/chunk-KYID2MR6.cjs +0 -23
- package/dist/chunk-KYID2MR6.cjs.map +0 -1
- package/dist/chunk-XNWCGJYH.js.map +0 -1
- package/dist/shell/components/billing/BillingPage.d.ts +0 -7
- package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
- package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
- package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
- package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
- package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
- package/dist/shell/hooks/useBilling.d.ts +0 -14
- package/dist/shell/hooks/useBilling.d.ts.map +0 -1
- package/dist/shell/hooks/usePermission.d.ts +0 -16
- package/dist/shell/hooks/usePermission.d.ts.map +0 -1
- package/src/shell/components/billing/BillingPage.tsx +0 -56
- package/src/shell/components/billing/PaywallGate.tsx +0 -80
- package/src/shell/components/organization/PermissionGate.tsx +0 -16
- package/src/shell/hooks/useBilling.ts +0 -151
- package/src/shell/hooks/usePermission.ts +0 -101
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { PluginAITool, PluginRegistryDef } from '../types/plugins'
|
|
2
|
+
import type { EntityDef } from '@fayz-ai/core'
|
|
3
|
+
import { getAllEntities, type RegisteredEntity } from '@fayz-ai/core'
|
|
4
|
+
import { CORE_QUERY_ENTITIES } from '../../app/core-entities'
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Core SaaS-level AI tools — always available, no plugin required.
|
|
@@ -19,7 +22,7 @@ export const coreAITools: PluginAITool[] = [
|
|
|
19
22
|
{
|
|
20
23
|
id: 'core.team-members',
|
|
21
24
|
name: 'getTeamMembers',
|
|
22
|
-
description: 'Lists
|
|
25
|
+
description: 'Lists the WORKSPACE USERS (people with a login) and their roles. This is NOT the professionals/staff registry — for professionals, stylists or other staff the business manages as records, use the staff/people search tools instead.',
|
|
23
26
|
icon: 'Users',
|
|
24
27
|
mode: 'read',
|
|
25
28
|
parameters: {
|
|
@@ -51,6 +54,37 @@ export const coreAITools: PluginAITool[] = [
|
|
|
51
54
|
},
|
|
52
55
|
]
|
|
53
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Builds the LLM-facing function name for a registry tool.
|
|
59
|
+
*
|
|
60
|
+
* Derived from the registry id, never from the entity's display label: the
|
|
61
|
+
* label is translated, so a pt-BR app would mint `listCategoriasdeServiço` —
|
|
62
|
+
* which both violates the provider's `^[a-zA-Z0-9_-]+$` function-name rule and
|
|
63
|
+
* renames the tool whenever the user switches locale. The id is a stable ASCII
|
|
64
|
+
* slug. The human plural still drives the description, which is what the model
|
|
65
|
+
* actually reads to decide when to call it.
|
|
66
|
+
*/
|
|
67
|
+
function pascalCase(value: string): string {
|
|
68
|
+
// Transliterate diacritics BEFORE splitting: splitting on [^a-zA-Z0-9]
|
|
69
|
+
// treated an accent as a word boundary and dropped it mid-word — "Serviço"
|
|
70
|
+
// became "ServiO" and "Preço de Serviço" became "PreODeServiO", which both
|
|
71
|
+
// renames tools per locale and can trip the broker's ^[a-zA-Z0-9_-]+$ rule.
|
|
72
|
+
return value
|
|
73
|
+
.normalize('NFD')
|
|
74
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
75
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
78
|
+
.join('')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function registryToolName(pluginId: string, registryId: string): string {
|
|
82
|
+
// Namespaced by plugin because registry ids only need to be unique within
|
|
83
|
+
// their own plugin — `crm` and `inventory` can both ship a `tags` registry,
|
|
84
|
+
// and two tools sharing a function name is ambiguous to the model.
|
|
85
|
+
return `list${pascalCase(pluginId)}${pascalCase(registryId)}`
|
|
86
|
+
}
|
|
87
|
+
|
|
54
88
|
/**
|
|
55
89
|
* Auto-generates read tools from plugin registry definitions.
|
|
56
90
|
* Each registry gets a basic "list" tool so plugins get AI capabilities for free.
|
|
@@ -62,7 +96,7 @@ export function generateRegistryTools(pluginId: string, registries: PluginRegist
|
|
|
62
96
|
const plural = registry.entity.namePlural ?? `${registry.entity.name}s`
|
|
63
97
|
return {
|
|
64
98
|
id: `${pluginId}.list-${registry.id}`,
|
|
65
|
-
name:
|
|
99
|
+
name: registryToolName(pluginId, registry.id),
|
|
66
100
|
description: `Lists all ${plural.toLowerCase()} for the current business.`,
|
|
67
101
|
icon: registry.icon ?? registry.entity.icon,
|
|
68
102
|
mode: 'read' as const,
|
|
@@ -88,3 +122,201 @@ export function formatToolSignature(tool: PluginAITool): string {
|
|
|
88
122
|
: ''
|
|
89
123
|
return `${tool.name}(${params})`
|
|
90
124
|
}
|
|
125
|
+
|
|
126
|
+
export function entityToolName(entityKey: string): string {
|
|
127
|
+
return `search${pascalCase(entityKey)}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Auto-generates a search tool for every entity the app exposes as a CRUD page.
|
|
132
|
+
*
|
|
133
|
+
* Registries cover a plugin's auxiliary tables; these are the entities the
|
|
134
|
+
* business actually talks about — clients, appointments, services. Deriving
|
|
135
|
+
* them from the entity registry is what makes the agent useful in a new vertical
|
|
136
|
+
* without anyone hand-writing tools: whatever the app declares, the agent can
|
|
137
|
+
* read.
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* The TWO generic data primitives that replace the per-entity/per-registry
|
|
141
|
+
* generated tools (43 searches → 2 tools): the model picks the TARGET as a
|
|
142
|
+
* parameter (closed enum), and the executor checks the target entity's OWN
|
|
143
|
+
* read permission at call time — one tool, per-user access preserved.
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* Sentences describing entities that ATTACH to another record instead of
|
|
147
|
+
* standing alone. Appended to createRecord's description so the relationship is
|
|
148
|
+
* stated once, up front, rather than discovered through a failed write.
|
|
149
|
+
* Only entities actually present in this app's catalog are mentioned.
|
|
150
|
+
*/
|
|
151
|
+
function attachedRecordGuidance(writableKeys: string[]): string {
|
|
152
|
+
const hints = CORE_QUERY_ENTITIES
|
|
153
|
+
.filter((q) => q.agentHint && writableKeys.includes(q.key))
|
|
154
|
+
.map((q) => q.agentHint as string)
|
|
155
|
+
return hints.length ? ` Records that attach to another record: ${hints.join(' ')}` : ''
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function buildDataPrimitiveTools(input: {
|
|
159
|
+
entities: RegisteredEntity[]
|
|
160
|
+
registries: Map<string, PluginRegistryDef[]>
|
|
161
|
+
queryEntities?: Array<{ key: string; entity: EntityDef; writable?: boolean }>
|
|
162
|
+
}): PluginAITool[] {
|
|
163
|
+
const options: Array<{ key: string; label: string }> = []
|
|
164
|
+
for (const e of input.entities) {
|
|
165
|
+
if (e.entityDef && !e.entityDef.agentHidden) options.push({ key: e.entityKey, label: e.labelPlural })
|
|
166
|
+
}
|
|
167
|
+
for (const [pluginId, defs] of input.registries) {
|
|
168
|
+
for (const registry of defs) {
|
|
169
|
+
if (registry.readOnly) continue
|
|
170
|
+
options.push({
|
|
171
|
+
key: `${pluginId}:${registry.id}`,
|
|
172
|
+
label: registry.entity.namePlural ?? registry.entity.name,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Read-models are readable; base-table ones may opt into writes.
|
|
177
|
+
const writableKeys = options.map((o) => o.key)
|
|
178
|
+
// CORE_QUERY_ENTITIES is merged HERE, not at each call site: the primitives
|
|
179
|
+
// are built from three different places (deriveAgentContract, useAITools and
|
|
180
|
+
// the conversational e2e harness), and a spine entity added to only some of
|
|
181
|
+
// them is a catalog that disagrees with itself.
|
|
182
|
+
for (const q of [...(input.queryEntities ?? []), ...CORE_QUERY_ENTITIES]) {
|
|
183
|
+
options.push({ key: q.key, label: q.entity.namePlural ?? q.entity.name })
|
|
184
|
+
if (q.writable) writableKeys.push(q.key)
|
|
185
|
+
}
|
|
186
|
+
if (!options.length) return []
|
|
187
|
+
const keys = options.map((o) => o.key)
|
|
188
|
+
const catalogLine = options.map((o) => `${o.key} (${o.label.toLowerCase()})`).join(', ')
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
id: 'data.find-anything',
|
|
193
|
+
name: 'findAnything',
|
|
194
|
+
description:
|
|
195
|
+
'Global search (like the app command center): looks a name/text up across ALL record types the user may see at once and returns grouped matches. Use this FIRST whenever you do not know what kind of record a name refers to ("quem é X?", "o que é Y?") — then drill down with searchRecords.' +
|
|
196
|
+
' It matches TEXT INSIDE each record, so records attached to a person are never returned by that person\'s name — reaching them takes a second searchRecords call filtered by owner_id.' +
|
|
197
|
+
attachedRecordGuidance(keys),
|
|
198
|
+
icon: 'Command',
|
|
199
|
+
mode: 'read',
|
|
200
|
+
parameters: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
search: { type: 'string', description: 'Name or text to look up everywhere.' },
|
|
204
|
+
},
|
|
205
|
+
required: ['search'],
|
|
206
|
+
},
|
|
207
|
+
category: 'Data',
|
|
208
|
+
suggestions: [{ label: 'Quem é …?' }],
|
|
209
|
+
},
|
|
210
|
+
...(writableKeys.length
|
|
211
|
+
? [
|
|
212
|
+
{
|
|
213
|
+
id: 'data.create-record',
|
|
214
|
+
name: 'createRecord',
|
|
215
|
+
description:
|
|
216
|
+
'Creates ONE new record of an entity (client, service, supplier, …). ALWAYS search first to avoid duplicates. Field keys and required fields come from the entity — on a field error the response lists the valid fields; fix and retry. The user will confirm before anything is written.' +
|
|
217
|
+
attachedRecordGuidance(writableKeys),
|
|
218
|
+
icon: 'Plus',
|
|
219
|
+
mode: 'persist' as const,
|
|
220
|
+
// The handler orchestrates its own guard chain (permission → plan
|
|
221
|
+
// cap → confirmation → create) because the target is dynamic.
|
|
222
|
+
requiresConfirmation: false,
|
|
223
|
+
parameters: {
|
|
224
|
+
type: 'object' as const,
|
|
225
|
+
properties: {
|
|
226
|
+
entity: { type: 'string' as const, description: 'Which entity to create', enum: writableKeys },
|
|
227
|
+
values: { type: 'object' as const, description: 'Field values for the new record, e.g. {"name":"…","phone":"…"}' },
|
|
228
|
+
},
|
|
229
|
+
required: ['entity', 'values'],
|
|
230
|
+
},
|
|
231
|
+
category: 'Data',
|
|
232
|
+
} satisfies PluginAITool,
|
|
233
|
+
]
|
|
234
|
+
: []),
|
|
235
|
+
...(writableKeys.length
|
|
236
|
+
? [
|
|
237
|
+
{
|
|
238
|
+
id: 'data.update-record',
|
|
239
|
+
name: 'updateRecord',
|
|
240
|
+
description:
|
|
241
|
+
'Updates fields on an EXISTING record (add an email, fix a phone, change a name…). Use the record id already known from this conversation or from a search. Only send the fields being changed. The user confirms before anything is written.',
|
|
242
|
+
icon: 'Pencil',
|
|
243
|
+
mode: 'persist' as const,
|
|
244
|
+
requiresConfirmation: false,
|
|
245
|
+
parameters: {
|
|
246
|
+
type: 'object' as const,
|
|
247
|
+
properties: {
|
|
248
|
+
entity: { type: 'string' as const, description: 'Which entity the record belongs to', enum: writableKeys },
|
|
249
|
+
id: { type: 'string' as const, description: 'Id of the record to update' },
|
|
250
|
+
values: { type: 'object' as const, description: 'Only the fields to change, e.g. {"email":"…"}' },
|
|
251
|
+
},
|
|
252
|
+
required: ['entity', 'id', 'values'],
|
|
253
|
+
},
|
|
254
|
+
category: 'Data',
|
|
255
|
+
} satisfies PluginAITool,
|
|
256
|
+
]
|
|
257
|
+
: []),
|
|
258
|
+
{
|
|
259
|
+
id: 'data.search-records',
|
|
260
|
+
name: 'searchRecords',
|
|
261
|
+
description: `Searches or lists records of ONE entity and returns matching rows with their fields. Use \`search\` for names/text and \`filters\` for exact field values (e.g. {"status":"open"}) — never put a status word in search. Entities: ${catalogLine}.${attachedRecordGuidance(keys)}`,
|
|
262
|
+
icon: 'Search',
|
|
263
|
+
mode: 'read',
|
|
264
|
+
parameters: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
entity: { type: 'string', description: 'Which entity to search', enum: keys },
|
|
268
|
+
search: { type: 'string', description: 'Name or text to match. Omit to list recent records.' },
|
|
269
|
+
filters: { type: 'object', description: 'Optional exact-value filters {field: value}, e.g. {"status":"open"}.' },
|
|
270
|
+
orderBy: { type: 'string', description: 'Field to sort by (e.g. startsAt for "next/upcoming" questions).' },
|
|
271
|
+
direction: { type: 'string', description: 'Sort direction', enum: ['asc', 'desc'] },
|
|
272
|
+
},
|
|
273
|
+
required: ['entity'],
|
|
274
|
+
},
|
|
275
|
+
category: 'Data',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'data.query-data',
|
|
279
|
+
name: 'queryData',
|
|
280
|
+
description: `Aggregates over ONE entity: count, sum or avg of a field, with optional date range, equality filters and a group-by. Use for analytical questions (revenue this week, new clients this month, busiest professional). Entities: ${catalogLine}.`,
|
|
281
|
+
icon: 'Sigma',
|
|
282
|
+
mode: 'read',
|
|
283
|
+
parameters: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
entity: { type: 'string', description: 'Which entity to aggregate', enum: keys },
|
|
287
|
+
metric: { type: 'string', description: 'Aggregation', enum: ['count', 'sum', 'avg'] },
|
|
288
|
+
field: { type: 'string', description: 'Field to sum/avg (record field name). Ignored for count.' },
|
|
289
|
+
dateField: { type: 'string', description: "Date field to range-filter (default 'created_at')." },
|
|
290
|
+
from: { type: 'string', description: 'ISO date/datetime lower bound (inclusive).' },
|
|
291
|
+
to: { type: 'string', description: 'ISO date/datetime upper bound (inclusive).' },
|
|
292
|
+
groupBy: { type: 'string', description: 'Optional field to group results by.' },
|
|
293
|
+
filters: { type: 'object', description: 'Optional equality filters {field: value}.' },
|
|
294
|
+
},
|
|
295
|
+
required: ['entity', 'metric'],
|
|
296
|
+
},
|
|
297
|
+
category: 'Data',
|
|
298
|
+
},
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function generateEntityTools(entities: RegisteredEntity[] = getAllEntities()): PluginAITool[] {
|
|
303
|
+
return entities
|
|
304
|
+
.filter((e) => !!e.entityDef)
|
|
305
|
+
.map((entity) => ({
|
|
306
|
+
id: `entity.${entity.entityKey}`,
|
|
307
|
+
name: entityToolName(entity.entityKey),
|
|
308
|
+
description: `Searches ${entity.labelPlural.toLowerCase()} and returns matching records with their fields. Use this to answer questions about a specific ${entity.label.toLowerCase()} or to count them.`,
|
|
309
|
+
icon: entity.icon,
|
|
310
|
+
mode: 'read' as const,
|
|
311
|
+
parameters: {
|
|
312
|
+
type: 'object' as const,
|
|
313
|
+
properties: {
|
|
314
|
+
search: {
|
|
315
|
+
type: 'string' as const,
|
|
316
|
+
description: `Name or text to match. Omit to list recent ${entity.labelPlural.toLowerCase()}.`,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
category: 'Data',
|
|
321
|
+
}))
|
|
322
|
+
}
|
|
@@ -14,6 +14,23 @@ const STAGE_FILTER_MAP: Record<string, string[]> = {
|
|
|
14
14
|
paid: ['paid'],
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A shop order's lifecycle is not in `status` (open/archived/cancelled) but in
|
|
19
|
+
* the payment state the shop plugin mirrors into metadata. Without this every
|
|
20
|
+
* storefront purchase showed up as "rascunho" on the customer's record — the
|
|
21
|
+
* same tab, the same table, but reading the wrong column for that kind.
|
|
22
|
+
*/
|
|
23
|
+
function mapShopOrderToStage(row: { status?: string | null; metadata?: Record<string, unknown> | null }): ClientDocumentStage {
|
|
24
|
+
if (row.status === 'cancelled') return 'cancelled'
|
|
25
|
+
switch (String(row.metadata?.financial_status ?? '')) {
|
|
26
|
+
case 'paid': return 'paid'
|
|
27
|
+
case 'partially_refunded': return 'partial'
|
|
28
|
+
case 'refunded':
|
|
29
|
+
case 'voided': return 'cancelled'
|
|
30
|
+
default: return 'invoiced' // placed, awaiting payment
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
function mapOrderStatusToStage(status: string | null | undefined): ClientDocumentStage {
|
|
18
35
|
switch (status) {
|
|
19
36
|
case 'scheduled':
|
|
@@ -83,12 +100,17 @@ export function createClientOrdersProvider(): ClientOrdersProvider {
|
|
|
83
100
|
return {
|
|
84
101
|
id: row.id,
|
|
85
102
|
kind: row.kind as string,
|
|
86
|
-
stage:
|
|
103
|
+
stage: row.kind === 'shop'
|
|
104
|
+
? mapShopOrderToStage(row)
|
|
105
|
+
: mapOrderStatusToStage(row.status as string | null | undefined),
|
|
87
106
|
referenceNumber: row.reference_number ?? undefined,
|
|
88
107
|
date: startsAt?.slice(0, 10) ?? (row.created_at as string)?.slice(0, 10) ?? '',
|
|
89
108
|
startsAt,
|
|
90
109
|
total: Number(row.total) || 0,
|
|
91
|
-
|
|
110
|
+
// A paid shop order has no separate paidAmount; the total is what
|
|
111
|
+
// was settled.
|
|
112
|
+
paidAmount: Number(meta.paidAmount)
|
|
113
|
+
|| (row.kind === 'shop' && meta.financial_status === 'paid' ? Number(row.total) || 0 : 0),
|
|
92
114
|
description: (meta.itemsSummary as string) ?? (meta.serviceNames as string) ?? row.notes ?? undefined,
|
|
93
115
|
createdAt: row.created_at as string,
|
|
94
116
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createFayzAgentClient, type FayzAgentClient } from '@fayz-ai/sdk/agent'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the connection to the project's Fayz agent.
|
|
5
|
+
*
|
|
6
|
+
* The container writes `VITE_FAYZ_PROJECT_ID`, `VITE_FAYZ_API_BASE_URL` and
|
|
7
|
+
* `VITE_FAYZ_AGENT_KEY` into every preview's `.env`, so enabling an agent in
|
|
8
|
+
* Fayz is enough to light up the app's assistant — no app-side config edit and
|
|
9
|
+
* no key to copy. `chat.agent` overrides are for self-hosted deployments where
|
|
10
|
+
* the app is built outside a Fayz container.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface FayzAgentConnectionConfig {
|
|
14
|
+
projectId?: string
|
|
15
|
+
apiBaseUrl?: string
|
|
16
|
+
publishableKey?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResolvedFayzAgentConnection {
|
|
20
|
+
projectId: string
|
|
21
|
+
apiBaseUrl: string
|
|
22
|
+
publishableKey: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function env(key: string): string | undefined {
|
|
26
|
+
const value = (import.meta.env as Record<string, string | undefined>)[key]
|
|
27
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveFayzAgentConnection(
|
|
31
|
+
override?: FayzAgentConnectionConfig | false,
|
|
32
|
+
): ResolvedFayzAgentConnection | null {
|
|
33
|
+
if (override === false) return null
|
|
34
|
+
|
|
35
|
+
const projectId = override?.projectId ?? env('VITE_FAYZ_PROJECT_ID')
|
|
36
|
+
const apiBaseUrl = override?.apiBaseUrl ?? env('VITE_FAYZ_API_BASE_URL')
|
|
37
|
+
const publishableKey = override?.publishableKey ?? env('VITE_FAYZ_AGENT_KEY')
|
|
38
|
+
|
|
39
|
+
if (!projectId || !apiBaseUrl || !publishableKey) return null
|
|
40
|
+
return { projectId, apiBaseUrl, publishableKey }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let cached: { key: string; client: FayzAgentClient } | null = null
|
|
44
|
+
|
|
45
|
+
export function getFayzAgentClient(connection: ResolvedFayzAgentConnection): FayzAgentClient {
|
|
46
|
+
const key = `${connection.apiBaseUrl}|${connection.projectId}|${connection.publishableKey}`
|
|
47
|
+
if (cached?.key === key) return cached.client
|
|
48
|
+
|
|
49
|
+
const client = createFayzAgentClient({
|
|
50
|
+
baseUrl: connection.apiBaseUrl,
|
|
51
|
+
projectId: connection.projectId,
|
|
52
|
+
publishableKey: connection.publishableKey,
|
|
53
|
+
})
|
|
54
|
+
cached = { key, client }
|
|
55
|
+
return client
|
|
56
|
+
}
|
package/src/shell/lib/i18n.ts
CHANGED
|
@@ -595,6 +595,43 @@ export const defaultTranslations: Record<string, string> = {
|
|
|
595
595
|
'billing.paywall.requiresPlan': 'This feature requires the {{plan}} plan or higher.',
|
|
596
596
|
'billing.paywall.upgradeRequired': 'Upgrade Required',
|
|
597
597
|
'billing.paywall.viewPlans': 'View Plans',
|
|
598
|
+
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Entitlements — upgrade prompts, plan limits, soft-limit banner
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
'upgrade.title': 'Premium feature',
|
|
603
|
+
'upgrade.description': 'This feature is available on a higher plan. Upgrade to unlock it.',
|
|
604
|
+
'upgrade.descriptionFeature': 'The {feature} feature is available on a higher plan. Upgrade to unlock it.',
|
|
605
|
+
'upgrade.includes': 'Includes {feature}',
|
|
606
|
+
'upgrade.cta': 'View plans',
|
|
607
|
+
'upgrade.badgeTooltip': 'Available on a higher plan',
|
|
608
|
+
'limit.reached': "You've reached the {label} limit of the {plan} plan.",
|
|
609
|
+
'banner.overLimit': "You've exceeded the {label} limit of your plan — upgrade to keep growing.",
|
|
610
|
+
'billing.featureMatrix': 'Feature comparison',
|
|
611
|
+
'billing.unlimited': 'Unlimited',
|
|
612
|
+
'billing.upTo': 'Up to {count}',
|
|
613
|
+
// Limit labels — apps/plugins may override per key; these cover the base caps.
|
|
614
|
+
'limit.label.clients': 'clients',
|
|
615
|
+
'limit.label.users': 'team members',
|
|
616
|
+
'limit.label.locations': 'locations',
|
|
617
|
+
'limit.label.bookings_month': 'monthly bookings',
|
|
618
|
+
'limit.label.products': 'products',
|
|
619
|
+
'limit.label.students': 'students',
|
|
620
|
+
'limit.label.courses': 'courses',
|
|
621
|
+
'limit.label.contacts': 'contacts',
|
|
622
|
+
'limit.label.deals': 'deals',
|
|
623
|
+
'limit.label.menu_items': 'menu items',
|
|
624
|
+
'limit.label.orders_month': 'monthly orders',
|
|
625
|
+
'limit.label.connected_banks': 'connected banks',
|
|
626
|
+
'limit.label.movements_month': 'monthly movements',
|
|
627
|
+
'limit.label.leads': 'leads',
|
|
628
|
+
'limit.label.quotes': 'quotes',
|
|
629
|
+
'limit.label.conversations_month': 'monthly conversations',
|
|
630
|
+
'limit.label.recipes': 'recipes',
|
|
631
|
+
'limit.label.form_templates': 'form templates',
|
|
632
|
+
'limit.label.documents_month': 'monthly documents',
|
|
633
|
+
'limit.label.content_posts_month': 'monthly content posts',
|
|
634
|
+
'limit.label.blog_posts': 'blog posts',
|
|
598
635
|
'chat.defaultTitle': 'Assistant',
|
|
599
636
|
'chat.tryAsking': 'Try asking',
|
|
600
637
|
'chat.fab.tryIt': 'Try it',
|
|
@@ -2345,6 +2382,40 @@ export const builtInLocales: Record<string, Record<string, string>> = {
|
|
|
2345
2382
|
'billing.paywall.upgradeRequired': 'Upgrade Necessário',
|
|
2346
2383
|
'billing.paywall.viewPlans': 'Ver Planos',
|
|
2347
2384
|
|
|
2385
|
+
// Entitlements — upgrade prompts, limites de plano, banner de soft-limit
|
|
2386
|
+
'upgrade.title': 'Recurso premium',
|
|
2387
|
+
'upgrade.description': 'Este recurso está disponível em um plano superior. Faça upgrade para liberá-lo.',
|
|
2388
|
+
'upgrade.descriptionFeature': 'O recurso {feature} está disponível em um plano superior. Faça upgrade para liberá-lo.',
|
|
2389
|
+
'upgrade.includes': 'Inclui {feature}',
|
|
2390
|
+
'upgrade.cta': 'Ver planos',
|
|
2391
|
+
'upgrade.badgeTooltip': 'Disponível em um plano superior',
|
|
2392
|
+
'limit.reached': 'Você atingiu o limite de {label} do plano {plan}.',
|
|
2393
|
+
'banner.overLimit': 'Você excedeu o limite de {label} do seu plano — faça upgrade para continuar crescendo.',
|
|
2394
|
+
'billing.featureMatrix': 'Comparação de recursos',
|
|
2395
|
+
'billing.unlimited': 'Ilimitado',
|
|
2396
|
+
'billing.upTo': 'Até {count}',
|
|
2397
|
+
'limit.label.clients': 'clientes',
|
|
2398
|
+
'limit.label.users': 'membros da equipe',
|
|
2399
|
+
'limit.label.locations': 'locais',
|
|
2400
|
+
'limit.label.bookings_month': 'agendamentos mensais',
|
|
2401
|
+
'limit.label.leads': 'leads',
|
|
2402
|
+
'limit.label.quotes': 'orçamentos',
|
|
2403
|
+
'limit.label.conversations_month': 'conversas mensais',
|
|
2404
|
+
'limit.label.recipes': 'receitas',
|
|
2405
|
+
'limit.label.form_templates': 'modelos de formulário',
|
|
2406
|
+
'limit.label.documents_month': 'documentos mensais',
|
|
2407
|
+
'limit.label.content_posts_month': 'posts de conteúdo mensais',
|
|
2408
|
+
'limit.label.blog_posts': 'posts de blog',
|
|
2409
|
+
'limit.label.products': 'produtos',
|
|
2410
|
+
'limit.label.students': 'alunos',
|
|
2411
|
+
'limit.label.courses': 'cursos',
|
|
2412
|
+
'limit.label.contacts': 'contatos',
|
|
2413
|
+
'limit.label.deals': 'negócios',
|
|
2414
|
+
'limit.label.menu_items': 'itens do cardápio',
|
|
2415
|
+
'limit.label.orders_month': 'pedidos mensais',
|
|
2416
|
+
'limit.label.connected_banks': 'bancos conectados',
|
|
2417
|
+
'limit.label.movements_month': 'movimentações mensais',
|
|
2418
|
+
|
|
2348
2419
|
// auth (additional duplicates/variants)
|
|
2349
2420
|
'auth.login.enterCredentials': 'Insira suas credenciais para acessar sua conta',
|
|
2350
2421
|
'auth.login.failedDefault': 'Falha ao entrar. Tente novamente.',
|
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
2
|
|
|
3
|
+
/** A record the reply concerns — rendered as a "goto" button under the
|
|
4
|
+
* bubble. Semantic ref only (id + archetype/kind); the SURFACE resolves the
|
|
5
|
+
* URL against its own route map (agents.md: a missing link is correct, an
|
|
6
|
+
* invented one is a dead end). */
|
|
7
|
+
export interface ChatRecordLink {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
archetype?: string
|
|
11
|
+
kind?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatToolCall {
|
|
15
|
+
name: string
|
|
16
|
+
/** Pretty-printed arguments (may be empty). */
|
|
17
|
+
args: string
|
|
18
|
+
/** Result content, truncated for display. */
|
|
19
|
+
result?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
export interface ChatMessage {
|
|
4
23
|
id: string
|
|
5
24
|
role: 'user' | 'assistant'
|
|
6
25
|
content: string
|
|
7
26
|
timestamp: string
|
|
27
|
+
/** Tool calls this assistant reply made, accumulated across rounds —
|
|
28
|
+
* rendered as an expandable trace under the bubble. */
|
|
29
|
+
toolCalls?: ChatToolCall[]
|
|
30
|
+
/** Records to offer as navigation buttons under the reply. */
|
|
31
|
+
links?: ChatRecordLink[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A write the agent wants to perform, parked until the human decides. The turn
|
|
36
|
+
* loop awaits the promise this action's resolver settles — the chat composer
|
|
37
|
+
* stays frozen behind the card, mirroring the broker's PENDING_CONFIRMATION
|
|
38
|
+
* step for server-plane tools.
|
|
39
|
+
*/
|
|
40
|
+
export interface PendingAgentAction {
|
|
41
|
+
id: string
|
|
42
|
+
toolName: string
|
|
43
|
+
title: string
|
|
44
|
+
params: Record<string, unknown>
|
|
45
|
+
/** 'client' = executed locally after approval; 'server' = approval is sent
|
|
46
|
+
* back to the broker as confirmAction. */
|
|
47
|
+
plane: 'client' | 'server'
|
|
8
48
|
}
|
|
9
49
|
|
|
10
50
|
interface ChatState {
|
|
@@ -16,13 +56,41 @@ interface ChatState {
|
|
|
16
56
|
addMessage: (message: ChatMessage) => void
|
|
17
57
|
updateLastAssistant: (content: string) => void
|
|
18
58
|
setStreaming: (streaming: boolean) => void
|
|
59
|
+
setConversationId: (id: string | null) => void
|
|
19
60
|
reset: () => void
|
|
61
|
+
/**
|
|
62
|
+
* Server-side thread id when talking to a Fayz agent. History lives in Fayz,
|
|
63
|
+
* so subsequent turns only send the new message plus this id.
|
|
64
|
+
*/
|
|
65
|
+
conversationId: string | null
|
|
66
|
+
/** The write awaiting human confirmation (renders the ConfirmActionCard). */
|
|
67
|
+
pendingAction: PendingAgentAction | null
|
|
68
|
+
/** Park an action + register its decision resolver. */
|
|
69
|
+
requestConfirmation: (action: PendingAgentAction) => Promise<boolean>
|
|
70
|
+
/** Card buttons: settle the parked action. */
|
|
71
|
+
resolvePendingAction: (approved: boolean) => void
|
|
72
|
+
/** Tool names executing right now — rendered as live activity chips. */
|
|
73
|
+
activeTools: string[]
|
|
74
|
+
setActiveTools: (names: string[]) => void
|
|
75
|
+
/** Record the tool calls an assistant reply made (expandable per-message trace). */
|
|
76
|
+
appendToolCallsToLastAssistant: (calls: ChatToolCall[]) => void
|
|
77
|
+
/** Attach goto-record buttons to the reply (deduped by id, capped). */
|
|
78
|
+
appendLinksToLastAssistant: (links: ChatRecordLink[]) => void
|
|
79
|
+
/** The signed-in user's threads (history drawer). */
|
|
80
|
+
conversations: Array<{ id: string; title: string | null; updatedAt: string }>
|
|
81
|
+
setConversations: (rows: Array<{ id: string; title: string | null; updatedAt: string }>) => void
|
|
82
|
+
/** Replace the transcript wholesale (resuming a past conversation). */
|
|
83
|
+
setMessages: (messages: ChatMessage[]) => void
|
|
20
84
|
}
|
|
21
85
|
|
|
86
|
+
// Module-level so the resolver never round-trips through React state.
|
|
87
|
+
let pendingResolver: ((approved: boolean) => void) | null = null
|
|
88
|
+
|
|
22
89
|
export const useChatStore = create<ChatState>((set) => ({
|
|
23
90
|
messages: [],
|
|
24
91
|
isOpen: false,
|
|
25
92
|
isStreaming: false,
|
|
93
|
+
conversationId: null,
|
|
26
94
|
|
|
27
95
|
setOpen: (open) => set({ isOpen: open }),
|
|
28
96
|
toggleOpen: () => set((s) => ({ isOpen: !s.isOpen })),
|
|
@@ -41,5 +109,54 @@ export const useChatStore = create<ChatState>((set) => ({
|
|
|
41
109
|
}),
|
|
42
110
|
|
|
43
111
|
setStreaming: (isStreaming) => set({ isStreaming }),
|
|
44
|
-
|
|
112
|
+
setConversationId: (conversationId) => set({ conversationId }),
|
|
113
|
+
reset: () => {
|
|
114
|
+
pendingResolver?.(false)
|
|
115
|
+
pendingResolver = null
|
|
116
|
+
set({ messages: [], isStreaming: false, conversationId: null, pendingAction: null, activeTools: [] })
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
pendingAction: null,
|
|
120
|
+
requestConfirmation: (action) => {
|
|
121
|
+
// A newer request supersedes an unanswered one (treated as declined).
|
|
122
|
+
pendingResolver?.(false)
|
|
123
|
+
return new Promise<boolean>((resolve) => {
|
|
124
|
+
pendingResolver = resolve
|
|
125
|
+
set({ pendingAction: action })
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
resolvePendingAction: (approved) => {
|
|
129
|
+
const resolve = pendingResolver
|
|
130
|
+
pendingResolver = null
|
|
131
|
+
set({ pendingAction: null })
|
|
132
|
+
resolve?.(approved)
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
activeTools: [],
|
|
136
|
+
setActiveTools: (activeTools) => set({ activeTools }),
|
|
137
|
+
appendToolCallsToLastAssistant: (calls) =>
|
|
138
|
+
set((s) => {
|
|
139
|
+
if (!calls.length) return s
|
|
140
|
+
const msgs = [...s.messages]
|
|
141
|
+
const lastIdx = msgs.length - 1
|
|
142
|
+
if (lastIdx >= 0 && msgs[lastIdx].role === 'assistant') {
|
|
143
|
+
msgs[lastIdx] = { ...msgs[lastIdx], toolCalls: [...(msgs[lastIdx].toolCalls ?? []), ...calls] }
|
|
144
|
+
}
|
|
145
|
+
return { messages: msgs }
|
|
146
|
+
}),
|
|
147
|
+
appendLinksToLastAssistant: (links) =>
|
|
148
|
+
set((s) => {
|
|
149
|
+
if (!links.length) return s
|
|
150
|
+
const msgs = [...s.messages]
|
|
151
|
+
const lastIdx = msgs.length - 1
|
|
152
|
+
if (lastIdx >= 0 && msgs[lastIdx].role === 'assistant') {
|
|
153
|
+
const seen = new Map((msgs[lastIdx].links ?? []).map((l) => [l.id, l]))
|
|
154
|
+
for (const l of links) if (!seen.has(l.id)) seen.set(l.id, l)
|
|
155
|
+
msgs[lastIdx] = { ...msgs[lastIdx], links: Array.from(seen.values()).slice(0, 3) }
|
|
156
|
+
}
|
|
157
|
+
return { messages: msgs }
|
|
158
|
+
}),
|
|
159
|
+
conversations: [],
|
|
160
|
+
setConversations: (conversations) => set({ conversations }),
|
|
161
|
+
setMessages: (messages) => set({ messages }),
|
|
45
162
|
}))
|
package/src/shell/styles.css
CHANGED
|
@@ -428,6 +428,36 @@ select.bg-card,
|
|
|
428
428
|
to { opacity: 1; transform: translateX(0); }
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/* Sidebar rail push — see @fayz-ai/ui styles.css for the rationale. */
|
|
432
|
+
.saas-rail-in-forward { animation: saas-rail-in-forward var(--saas-rail-duration, 260ms) var(--saas-transition-easing) both; }
|
|
433
|
+
.saas-rail-out-forward { animation: saas-rail-out-forward var(--saas-rail-duration, 260ms) var(--saas-transition-easing) both; }
|
|
434
|
+
.saas-rail-in-back { animation: saas-rail-in-back var(--saas-rail-duration, 260ms) var(--saas-transition-easing) both; }
|
|
435
|
+
.saas-rail-out-back { animation: saas-rail-out-back var(--saas-rail-duration, 260ms) var(--saas-transition-easing) both; }
|
|
436
|
+
|
|
437
|
+
@keyframes saas-rail-in-forward {
|
|
438
|
+
from { opacity: 0; transform: translateX(100%); }
|
|
439
|
+
to { opacity: 1; transform: translateX(0); }
|
|
440
|
+
}
|
|
441
|
+
@keyframes saas-rail-out-forward {
|
|
442
|
+
from { opacity: 1; transform: translateX(0); }
|
|
443
|
+
to { opacity: 0; transform: translateX(-35%); }
|
|
444
|
+
}
|
|
445
|
+
@keyframes saas-rail-in-back {
|
|
446
|
+
from { opacity: 0; transform: translateX(-100%); }
|
|
447
|
+
to { opacity: 1; transform: translateX(0); }
|
|
448
|
+
}
|
|
449
|
+
@keyframes saas-rail-out-back {
|
|
450
|
+
from { opacity: 1; transform: translateX(0); }
|
|
451
|
+
to { opacity: 0; transform: translateX(35%); }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@media (prefers-reduced-motion: reduce) {
|
|
455
|
+
.saas-rail-in-forward, .saas-rail-out-forward,
|
|
456
|
+
.saas-rail-in-back, .saas-rail-out-back {
|
|
457
|
+
animation-duration: 1ms;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
431
461
|
/* ═══ Print — intentionally left minimal here. ═══ */
|
|
432
462
|
/* Runtime-injected print styles live in PrintChrome.tsx */
|
|
433
463
|
/* to guarantee they bypass any build-tool reordering. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PlanEntitlements } from '@fayz-ai/core'
|
|
2
|
+
|
|
1
3
|
export type PlanInterval = 'monthly' | 'yearly'
|
|
2
4
|
export type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'paused'
|
|
3
5
|
|
|
@@ -37,6 +39,12 @@ export interface PlanConfig {
|
|
|
37
39
|
limits?: Record<string, number>
|
|
38
40
|
popular?: boolean
|
|
39
41
|
verticalId?: string
|
|
42
|
+
/**
|
|
43
|
+
* Structured plan entitlements (feature gates + quantity caps) — the
|
|
44
|
+
* enforcement source of truth the access engine reads. Preserved verbatim by
|
|
45
|
+
* normalizeBillingPlan onto the runtime `Plan`.
|
|
46
|
+
*/
|
|
47
|
+
entitlements?: PlanEntitlements
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export function normalizePlanConfig(plan: PlanConfig, index?: number): Plan {
|