GHL MCP full update — 2026-02-06
=== DONE === - MCP Apps UI system added (11 apps with _meta.ui.resourceUri) - 19 new tool modules added - Tool count: 269 → 461 across 38 categories - Upstream changes merged - All tools tagged with _meta labels - Package lock updated === TO-DO === - [ ] Fix 42 failing edge case tests (BLOCKER — Stage 11) - [ ] Live API testing with GHL credentials - [ ] App design approval for Stage 7→8
This commit is contained in:
parent
c1fbbdd95b
commit
4f2a8d6ab5
177
AGENT-TASKS.md
Normal file
177
AGENT-TASKS.md
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# Agent Task Briefs (for Phase 2 spawn)
|
||||||
|
|
||||||
|
## Shared Context for ALL Phase 2 Agents
|
||||||
|
- Working dir: `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/src/ui/react-app/`
|
||||||
|
- Read the full plan: `../../REACT-REWRITE-PLAN.md`
|
||||||
|
- Read types: `src/types.ts` (created by Alpha — has all prop interfaces)
|
||||||
|
- Read registry: `src/renderer/registry.ts` (has stub components — replace yours)
|
||||||
|
- Read current string implementations: `../json-render-app/src/components.ts` and `../json-render-app/src/charts.ts`
|
||||||
|
- Use MCPAppContext via `import { useMCPApp } from '../context/MCPAppContext'` for callTool
|
||||||
|
- CSS goes in `src/styles/components.css` (display) or `src/styles/interactive.css` (interactive)
|
||||||
|
- Each component is a React FC exported from its own file
|
||||||
|
- After building all components, UPDATE `src/renderer/registry.ts` to import your real components instead of stubs
|
||||||
|
- NO GHL references — everything is generic MCP UI Kit
|
||||||
|
- Compact sizing: 12px base, tight padding, designed for chat inline display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Bravo — Layout + Core Data (15 components)
|
||||||
|
|
||||||
|
### Files to create:
|
||||||
|
```
|
||||||
|
src/components/layout/PageHeader.tsx
|
||||||
|
src/components/layout/Card.tsx
|
||||||
|
src/components/layout/StatsGrid.tsx
|
||||||
|
src/components/layout/SplitLayout.tsx
|
||||||
|
src/components/layout/Section.tsx
|
||||||
|
src/components/data/DataTable.tsx
|
||||||
|
src/components/data/KanbanBoard.tsx ← DRAG AND DROP (key component)
|
||||||
|
src/components/data/MetricCard.tsx
|
||||||
|
src/components/data/StatusBadge.tsx
|
||||||
|
src/components/data/Timeline.tsx
|
||||||
|
src/components/data/DetailHeader.tsx
|
||||||
|
src/components/data/KeyValueList.tsx
|
||||||
|
src/components/data/LineItemsTable.tsx
|
||||||
|
src/components/data/InfoBlock.tsx
|
||||||
|
src/components/data/ProgressBar.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special attention:
|
||||||
|
- **KanbanBoard** — Must have REAL drag-and-drop via React state (onDragStart/onDragOver/onDrop).
|
||||||
|
- Cards are draggable between columns
|
||||||
|
- Optimistic UI: move card immediately in state, revert on error
|
||||||
|
- Accepts `moveTool?: string` prop — if provided, calls `callTool(moveTool, { opportunityId, pipelineStageId })` on drop
|
||||||
|
- Drop zone highlights with dashed border
|
||||||
|
- Cards show hover lift effect
|
||||||
|
- **DataTable** — Sortable columns (click header), clickable rows (emit onRowClick), pagination
|
||||||
|
- All layout components accept `children` prop for nested UITree elements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Charlie — Extended Data + Navigation + Actions (15 components)
|
||||||
|
|
||||||
|
### Files to create:
|
||||||
|
```
|
||||||
|
src/components/data/CurrencyDisplay.tsx
|
||||||
|
src/components/data/TagList.tsx
|
||||||
|
src/components/data/CardGrid.tsx
|
||||||
|
src/components/data/AvatarGroup.tsx
|
||||||
|
src/components/data/StarRating.tsx
|
||||||
|
src/components/data/StockIndicator.tsx
|
||||||
|
src/components/shared/SearchBar.tsx
|
||||||
|
src/components/shared/FilterChips.tsx
|
||||||
|
src/components/shared/TabGroup.tsx
|
||||||
|
src/components/shared/ActionButton.tsx
|
||||||
|
src/components/shared/ActionBar.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus from data/:
|
||||||
|
```
|
||||||
|
src/components/data/ChecklistView.tsx
|
||||||
|
src/components/data/AudioPlayer.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special attention:
|
||||||
|
- **SearchBar** — Controlled input, fires onChange with debounce. Accepts `onSearch?: (query: string) => void`
|
||||||
|
- **FilterChips** — Toggle active state on click, fires onFilter
|
||||||
|
- **TabGroup** — Controlled tab state, fires onTabChange
|
||||||
|
- **ActionButton** — Accepts `onClick` + optional `toolName` + `toolArgs` props. If toolName provided, calls callTool on click.
|
||||||
|
- **ChecklistView** — Checkboxes toggle completed state. Accepts `onToggle?: (itemId, completed) => void`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Delta — Comms + Viz + Charts (16 components)
|
||||||
|
|
||||||
|
### Files to create:
|
||||||
|
```
|
||||||
|
src/components/comms/ChatThread.tsx
|
||||||
|
src/components/comms/EmailPreview.tsx
|
||||||
|
src/components/comms/ContentPreview.tsx
|
||||||
|
src/components/comms/TranscriptView.tsx
|
||||||
|
src/components/viz/CalendarView.tsx
|
||||||
|
src/components/viz/FlowDiagram.tsx
|
||||||
|
src/components/viz/TreeView.tsx
|
||||||
|
src/components/viz/MediaGallery.tsx
|
||||||
|
src/components/viz/DuplicateCompare.tsx
|
||||||
|
src/components/charts/BarChart.tsx
|
||||||
|
src/components/charts/LineChart.tsx
|
||||||
|
src/components/charts/PieChart.tsx
|
||||||
|
src/components/charts/FunnelChart.tsx
|
||||||
|
src/components/charts/SparklineChart.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special attention:
|
||||||
|
- **All charts** — Use inline SVG (same approach as current charts.ts). Convert from template strings to JSX SVG elements.
|
||||||
|
- **CalendarView** — Grid layout, today highlight, event dots. Accepts `onDateClick?: (date: string) => void`
|
||||||
|
- **TreeView** — Expandable nodes with click-to-toggle. Track expanded state in local component state.
|
||||||
|
- **ChatThread** — Outbound messages right-aligned indigo, inbound left-aligned gray. Auto-scroll to bottom.
|
||||||
|
- **FlowDiagram** — Horizontal/vertical node→arrow→node layout with SVG connectors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Echo — Interactive Components + Forms (8 components + shared UI)
|
||||||
|
|
||||||
|
### Files to create:
|
||||||
|
```
|
||||||
|
src/components/interactive/ContactPicker.tsx
|
||||||
|
src/components/interactive/InvoiceBuilder.tsx
|
||||||
|
src/components/interactive/OpportunityEditor.tsx
|
||||||
|
src/components/interactive/AppointmentBooker.tsx
|
||||||
|
src/components/interactive/EditableField.tsx
|
||||||
|
src/components/interactive/SelectDropdown.tsx
|
||||||
|
src/components/interactive/FormGroup.tsx
|
||||||
|
src/components/interactive/AmountInput.tsx
|
||||||
|
src/components/shared/Toast.tsx
|
||||||
|
src/components/shared/Modal.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critical design:
|
||||||
|
ALL interactive components are **CRM-agnostic**. They receive tool names as props.
|
||||||
|
|
||||||
|
- **ContactPicker** — Searchable dropdown.
|
||||||
|
- Props: `searchTool: string` (e.g., "search_contacts"), `onSelect: (contact) => void`, `placeholder?: string`
|
||||||
|
- On keystroke (debounced 300ms): calls `callTool(searchTool, { query })` → displays results as dropdown options
|
||||||
|
- On select: calls onSelect with full contact object, closes dropdown
|
||||||
|
- Shows loading spinner during search
|
||||||
|
|
||||||
|
- **InvoiceBuilder** — Multi-section form.
|
||||||
|
- Props: `createTool?: string`, `contactSearchTool?: string`
|
||||||
|
- Sections: Contact (uses ContactPicker), Line Items (add/remove rows), Totals (auto-calc)
|
||||||
|
- Each line item: description, quantity, unit price, total
|
||||||
|
- "Create Invoice" button calls `callTool(createTool, { contactId, lineItems, ... })`
|
||||||
|
|
||||||
|
- **OpportunityEditor** — Inline edit form.
|
||||||
|
- Props: `saveTool: string` (e.g., "update_opportunity"), `opportunity: { id, name, value, status, stageId }`
|
||||||
|
- Renders current values with click-to-edit behavior
|
||||||
|
- Save button calls `callTool(saveTool, { opportunityId, ...changes })`
|
||||||
|
|
||||||
|
- **AppointmentBooker** — Date/time picker + form.
|
||||||
|
- Props: `calendarTool?: string`, `bookTool?: string`, `contactSearchTool?: string`
|
||||||
|
- Calendar grid for date selection, time slot list, contact picker, notes field
|
||||||
|
- Book button calls `callTool(bookTool, { calendarId, contactId, date, time, ... })`
|
||||||
|
|
||||||
|
- **EditableField** — Click-to-edit wrapper.
|
||||||
|
- Props: `value: string`, `saveTool?: string`, `saveArgs?: Record`, `fieldName: string`
|
||||||
|
- Click → shows input, blur/enter → saves via callTool if provided
|
||||||
|
|
||||||
|
- **SelectDropdown** — Generic async select.
|
||||||
|
- Props: `loadTool?: string`, `options?: Array`, `onChange`, `placeholder`
|
||||||
|
- If loadTool provided, fetches options on mount/open
|
||||||
|
|
||||||
|
- **FormGroup** — Layout for labeled form fields.
|
||||||
|
- Props: `fields: Array<{ key, label, type, value, required? }>`, `onSubmit`, `submitLabel`
|
||||||
|
|
||||||
|
- **AmountInput** — Currency-formatted number input.
|
||||||
|
- Props: `value, onChange, currency?, locale?`
|
||||||
|
- Formats display as $1,234.56, stores raw number
|
||||||
|
|
||||||
|
- **Toast** — Notification component (renders via React portal).
|
||||||
|
- Export `useToast()` hook: `const { showToast } = useToast()`
|
||||||
|
- `showToast('Deal moved!', 'success')` — auto-dismisses
|
||||||
|
|
||||||
|
- **Modal** — Dialog component (renders via React portal).
|
||||||
|
- Props: `isOpen, onClose, title, children, footer?`
|
||||||
|
- Backdrop click to close, escape key to close
|
||||||
|
|
||||||
|
### Styles:
|
||||||
|
Write `src/styles/interactive.css` with all interactive styles (modals, toasts, dropdowns, drag states, form inputs, etc.)
|
||||||
263
REACT-REWRITE-PLAN.md
Normal file
263
REACT-REWRITE-PLAN.md
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
# MCP UI Kit — React Rewrite Plan
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
Build a **generic, reusable MCP UI component library** using React + ext-apps SDK. Any MCP server (GHL, HubSpot, Salesforce, etc.) can use this component kit to render AI-generated interactive UIs. GHL is the first implementation. The library is CRM-agnostic — interactive components accept tool names as props so each server configures its own tool mappings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Goose / Claude Desktop (MCP Host) │
|
||||||
|
│ │
|
||||||
|
│ tools/call → GHL MCP Server → GHL API │
|
||||||
|
│ ↑ ↓ │
|
||||||
|
│ tools/call structuredContent (JSON UI tree) │
|
||||||
|
│ ↑ ↓ │
|
||||||
|
│ ┌─────────────────────────────────────────────┐ │
|
||||||
|
│ │ React App (iframe via ext-apps SDK) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ useApp() hook ← ontoolresult (UI tree) │ │
|
||||||
|
│ │ ↓ │ │
|
||||||
|
│ │ MCPAppProvider (React Context) │ │
|
||||||
|
│ │ - uiTree state │ │
|
||||||
|
│ │ - formState (inputs, selections) │ │
|
||||||
|
│ │ - callTool(name, args) → MCP server │ │
|
||||||
|
│ │ ↓ │ │
|
||||||
|
│ │ <UITreeRenderer tree={uiTree} /> │ │
|
||||||
|
│ │ - Looks up component by type │ │
|
||||||
|
│ │ - Renders React component with props │ │
|
||||||
|
│ │ - Recursively renders children │ │
|
||||||
|
│ │ ↓ │ │
|
||||||
|
│ │ 42 Display Components (pure, CRM-agnostic) │ │
|
||||||
|
│ │ + 8 Interactive Components (tool-configurable)│ │
|
||||||
|
│ │ - ContactPicker(searchTool="search_contacts")│ │
|
||||||
|
│ │ - InvoiceBuilder(createTool="create_invoice")│ │
|
||||||
|
│ │ - KanbanBoard(onMoveTool="update_opportunity")│ │
|
||||||
|
│ │ - EditableField(saveTool=props.saveTool) │ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure (New)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/ui/react-app/ # GENERIC MCP UI KIT (no GHL references)
|
||||||
|
├── package.json # React + ext-apps SDK deps
|
||||||
|
├── vite.config.ts # Vite + singlefile + React
|
||||||
|
├── tsconfig.json
|
||||||
|
├── index.html # Entry point
|
||||||
|
├── src/
|
||||||
|
│ ├── App.tsx # Root — useApp hook, MCPAppProvider
|
||||||
|
│ ├── types.ts # UITree, UIElement, component prop interfaces
|
||||||
|
│ ├── context/
|
||||||
|
│ │ └── MCPAppContext.tsx # React Context — uiTree, formState, callTool
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ ├── useCallTool.ts # Hook wrapping app.callServerTool
|
||||||
|
│ │ ├── useFormState.ts # Shared form state management
|
||||||
|
│ │ └── useSizeReporter.ts # Auto-report content size to host
|
||||||
|
│ ├── renderer/
|
||||||
|
│ │ ├── UITreeRenderer.tsx # Recursive tree → React component resolver
|
||||||
|
│ │ └── registry.ts # Component name → React component map
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── layout/ # PageHeader, Card, SplitLayout, Section, StatsGrid
|
||||||
|
│ │ ├── data/ # DataTable, KanbanBoard, MetricCard, StatusBadge,
|
||||||
|
│ │ │ # Timeline, ProgressBar, KeyValueList, etc.
|
||||||
|
│ │ ├── charts/ # BarChart, LineChart, PieChart, FunnelChart, Sparkline
|
||||||
|
│ │ ├── comms/ # ChatThread, EmailPreview, TranscriptView, etc.
|
||||||
|
│ │ ├── viz/ # CalendarView, FlowDiagram, TreeView, MediaGallery, etc.
|
||||||
|
│ │ ├── interactive/ # ContactPicker, InvoiceBuilder, EditableField, etc.
|
||||||
|
│ │ │ # All accept tool names as PROPS (CRM-agnostic)
|
||||||
|
│ │ └── shared/ # ActionButton, Toast, Modal (React portals)
|
||||||
|
│ └── styles/
|
||||||
|
│ ├── base.css # Reset, variables, typography
|
||||||
|
│ ├── components.css # Component-specific styles (compact for chat)
|
||||||
|
│ └── interactive.css # Drag/drop, modals, toasts, form styles
|
||||||
|
```
|
||||||
|
|
||||||
|
### CRM-Agnostic Design Principle
|
||||||
|
- NO component imports GHL types or references GHL tool names
|
||||||
|
- Interactive components receive tool names via props:
|
||||||
|
- `ContactPicker` → `searchTool="search_contacts"` (GHL) or `"hubspot_search_contacts"` (HubSpot)
|
||||||
|
- `KanbanBoard` → `moveTool="update_opportunity"` (GHL) or `"move_deal"` (Pipedrive)
|
||||||
|
- `InvoiceBuilder` → `createTool="create_invoice"` (any billing system)
|
||||||
|
- The MCP server's AI prompt tells Claude which tool names to use in the UI tree
|
||||||
|
- Components call `callTool(props.toolName, args)` — they don't know or care what CRM is behind it
|
||||||
|
|
||||||
|
## Component Inventory
|
||||||
|
|
||||||
|
### Existing 42 (string → React conversion)
|
||||||
|
|
||||||
|
**Layout (5):** PageHeader, Card, StatsGrid, SplitLayout, Section
|
||||||
|
**Data Display (10):** DataTable, KanbanBoard, MetricCard, StatusBadge, Timeline, ProgressBar, DetailHeader, KeyValueList, LineItemsTable, InfoBlock
|
||||||
|
**Navigation (3):** SearchBar, FilterChips, TabGroup
|
||||||
|
**Actions (2):** ActionButton, ActionBar
|
||||||
|
**Extended Data (6):** CurrencyDisplay, TagList, CardGrid, AvatarGroup, StarRating, StockIndicator
|
||||||
|
**Communications (6):** ChatThread, EmailPreview, ContentPreview, TranscriptView, AudioPlayer, ChecklistView
|
||||||
|
**Visualization (5):** CalendarView, FlowDiagram, TreeView, MediaGallery, DuplicateCompare
|
||||||
|
**Charts (5):** BarChart, LineChart, PieChart, FunnelChart, SparklineChart
|
||||||
|
|
||||||
|
### New Interactive Components (8)
|
||||||
|
|
||||||
|
| Component | Purpose | MCP Tools Used |
|
||||||
|
|-----------|---------|----------------|
|
||||||
|
| **ContactPicker** | Searchable dropdown, fetches contacts on type | `search_contacts` |
|
||||||
|
| **InvoiceBuilder** | Line items, totals, contact auto-fill | `create_invoice`, `get_contact` |
|
||||||
|
| **OpportunityEditor** | Inline edit deal name/value/status/stage | `update_opportunity` |
|
||||||
|
| **AppointmentBooker** | Calendar slot picker + booking form | `get_calendar`, `create_appointment` |
|
||||||
|
| **EditableField** | Click-to-edit any text/number field | varies (generic) |
|
||||||
|
| **SelectDropdown** | Generic select with async option loading | varies |
|
||||||
|
| **FormGroup** | Group of form fields with validation | varies |
|
||||||
|
| **AmountInput** | Currency-formatted number input | — (local state) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Team Plan
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Sequential — 1 agent)
|
||||||
|
|
||||||
|
**Agent Alpha — Project Scaffold + App Shell**
|
||||||
|
- Create `src/ui/react-app/` with package.json, vite.config, tsconfig
|
||||||
|
- Install deps: react, react-dom, @modelcontextprotocol/ext-apps, @vitejs/plugin-react, vite-plugin-singlefile
|
||||||
|
- Build `App.tsx` with `useApp` hook — handles ontoolresult, ontoolinput, host context
|
||||||
|
- Build `GHLContext.tsx` — React context providing uiTree, formState, callTool
|
||||||
|
- Build `useCallTool.ts` — wrapper around `app.callServerTool` with loading/error states
|
||||||
|
- Build `useFormState.ts` — shared form state hook
|
||||||
|
- Build `useSizeReporter.ts` — auto-measures content, sends `ui/notifications/size-changed`
|
||||||
|
- Build `UITreeRenderer.tsx` — recursive renderer that resolves component types from registry
|
||||||
|
- Build `registry.ts` — component map (stubs for now, filled by other agents)
|
||||||
|
- Build `types.ts` — UITree, UIElement, all component prop interfaces
|
||||||
|
- Build base CSS (`base.css`) — reset, variables, compact typography
|
||||||
|
- Update outer build pipeline in GoHighLevel-MCP `package.json` to build React app
|
||||||
|
- **Output:** Working scaffold that renders a loading state, connects to host via ext-apps
|
||||||
|
|
||||||
|
### Phase 2: Components (Parallel — 4 agents)
|
||||||
|
|
||||||
|
**Agent Bravo — Layout + Core Data Components (15)**
|
||||||
|
Files: `components/layout/`, `components/data/` (first half)
|
||||||
|
- PageHeader, Card, StatsGrid, SplitLayout, Section
|
||||||
|
- DataTable (with clickable rows, sortable columns)
|
||||||
|
- KanbanBoard (with FULL drag-and-drop via React state — no DOM hacking)
|
||||||
|
- MetricCard, StatusBadge, Timeline
|
||||||
|
- Register all in registry.ts
|
||||||
|
- Component CSS in `components.css`
|
||||||
|
|
||||||
|
**Agent Charlie — Data Display + Navigation + Actions (15)**
|
||||||
|
Files: `components/data/` (second half), `components/shared/`
|
||||||
|
- ProgressBar, DetailHeader, KeyValueList, LineItemsTable, InfoBlock
|
||||||
|
- SearchBar, FilterChips, TabGroup
|
||||||
|
- ActionButton, ActionBar
|
||||||
|
- CurrencyDisplay, TagList, CardGrid, AvatarGroup, StarRating, StockIndicator
|
||||||
|
- Register all in registry.ts
|
||||||
|
|
||||||
|
**Agent Delta — Comms + Viz + Charts (16)**
|
||||||
|
Files: `components/comms/`, `components/viz/`, `components/charts/`
|
||||||
|
- ChatThread, EmailPreview, ContentPreview, TranscriptView, AudioPlayer, ChecklistView
|
||||||
|
- CalendarView, FlowDiagram, TreeView, MediaGallery, DuplicateCompare
|
||||||
|
- BarChart, LineChart, PieChart, FunnelChart, SparklineChart
|
||||||
|
- All chart components use inline SVG (same approach, just JSX)
|
||||||
|
- Register all in registry.ts
|
||||||
|
|
||||||
|
**Agent Echo — Interactive Components + Forms (8)**
|
||||||
|
Files: `components/interactive/`, `hooks/`
|
||||||
|
- ContactPicker — searchable dropdown, calls `search_contacts` on keystroke with debounce
|
||||||
|
- InvoiceBuilder — line items table + contact selection + auto-total
|
||||||
|
- OpportunityEditor — inline edit form for deal fields, saves via `update_opportunity`
|
||||||
|
- AppointmentBooker — date/time picker + contact + calendar selection
|
||||||
|
- EditableField — click-to-edit wrapper for any field
|
||||||
|
- SelectDropdown — generic async select
|
||||||
|
- FormGroup — form layout with labels + validation
|
||||||
|
- AmountInput — formatted currency input
|
||||||
|
- Shared: Toast component, Modal component (proper React portals)
|
||||||
|
- Integrate with GHLContext for tool calling
|
||||||
|
|
||||||
|
### Phase 3: Integration (Sequential — 1 agent)
|
||||||
|
|
||||||
|
**Agent Foxtrot — Wire Everything Together**
|
||||||
|
- Merge all component registrations into `registry.ts`
|
||||||
|
- Update `src/apps/index.ts`:
|
||||||
|
- Add new tool definitions for interactive components (`create_invoice`, `create_appointment`)
|
||||||
|
- Update resource handler for `ui://ghl/dynamic-view` to serve React build
|
||||||
|
- Add new resource URIs if needed
|
||||||
|
- Update `src/server.ts` if new tools need routing
|
||||||
|
- Update system prompt: add new interactive component catalog entries
|
||||||
|
- Update Goose config `available_tools` if needed
|
||||||
|
- Full build: React app → singlefile HTML → server TypeScript
|
||||||
|
- Test: verify JSON UI trees render correctly, interactive components call tools, drag-and-drop works
|
||||||
|
- Write brief README for the new architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- **React Context** (not Redux) — app is small enough, context + useReducer is perfect
|
||||||
|
- `GHLContext` holds: current UITree, form values, loading states, selected entities
|
||||||
|
- Any component can `const { callTool } = useGHL()` to interact with the MCP server
|
||||||
|
|
||||||
|
### Tool Calling Pattern
|
||||||
|
```tsx
|
||||||
|
// Any component can call MCP tools:
|
||||||
|
const { callTool, isLoading } = useCallTool();
|
||||||
|
|
||||||
|
const handleDrop = async (cardId: string, newStageId: string) => {
|
||||||
|
await callTool('update_opportunity', {
|
||||||
|
opportunityId: cardId,
|
||||||
|
pipelineStageId: newStageId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drag & Drop (KanbanBoard)
|
||||||
|
- Pure React state — no global DOM event handlers
|
||||||
|
- `onDragStart`, `onDragOver`, `onDrop` on React elements
|
||||||
|
- Optimistic UI update (move card immediately, revert on error)
|
||||||
|
|
||||||
|
### Dynamic Sizing
|
||||||
|
- `useSizeReporter` hook — ResizeObserver on `#app`
|
||||||
|
- Sends `ui/notifications/size-changed` on every size change
|
||||||
|
- Caps at 600px height
|
||||||
|
|
||||||
|
### CSS Strategy
|
||||||
|
- Plain CSS files (not CSS modules, not Tailwind) — keeps bundle simple
|
||||||
|
- Same compact sizing as current (12px base, tight padding)
|
||||||
|
- All in `styles/` directory, imported in App.tsx
|
||||||
|
- Interactive styles (drag states, modals, toasts) in separate file
|
||||||
|
|
||||||
|
### Build Pipeline
|
||||||
|
```bash
|
||||||
|
# In src/ui/react-app/
|
||||||
|
npm run build
|
||||||
|
# → Vite builds React app → vite-plugin-singlefile → single HTML file
|
||||||
|
# → Output: ../../dist/app-ui/dynamic-view.html
|
||||||
|
|
||||||
|
# In GoHighLevel-MCP root
|
||||||
|
npm run build
|
||||||
|
# → Builds React UI first, then compiles TypeScript server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline Estimate
|
||||||
|
|
||||||
|
| Phase | Agents | Est. Time | Depends On |
|
||||||
|
|-------|--------|-----------|------------|
|
||||||
|
| Phase 1: Foundation | Alpha (1) | ~20 min | — |
|
||||||
|
| Phase 2: Components | Bravo, Charlie, Delta, Echo (4 parallel) | ~25 min | Phase 1 |
|
||||||
|
| Phase 3: Integration | Foxtrot (1) | ~15 min | Phase 2 |
|
||||||
|
| **Total** | **6 agents** | **~60 min** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
1. ✅ All 42 existing components render identically to current version
|
||||||
|
2. ✅ JSON UI trees from Claude work without any format changes
|
||||||
|
3. ✅ KanbanBoard drag-and-drop moves deals and persists via `update_opportunity`
|
||||||
|
4. ✅ ContactPicker fetches real contacts from GHL on keystroke
|
||||||
|
5. ✅ InvoiceBuilder creates invoices with real contact data
|
||||||
|
6. ✅ EditableField saves changes via appropriate MCP tool
|
||||||
|
7. ✅ Dynamic sizing works — views fit in chat
|
||||||
|
8. ✅ Single HTML file output (vite-plugin-singlefile)
|
||||||
|
9. ✅ ext-apps handshake completes with Goose
|
||||||
|
10. ✅ All existing `view_*` tools still work alongside `generate_ghl_view`
|
||||||
369
REACT-STATE-ANALYSIS.md
Normal file
369
REACT-STATE-ANALYSIS.md
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
# React State & Hydration Analysis — MCP App Lifecycle
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**The interactive state destruction is caused by a cascading chain of three interconnected bugs:**
|
||||||
|
1. Every tool call result replaces the entire UITree → full component tree teardown
|
||||||
|
2. Element keys from the server may not be stable across calls → React unmounts everything
|
||||||
|
3. All interactive components store state locally (`useState`) instead of in the shared context that was built specifically to survive re-renders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #1: `ontoolresult` → `setUITree()` Nuclear Replacement
|
||||||
|
|
||||||
|
### The Code (App.tsx:80-86)
|
||||||
|
```tsx
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const tree = extractUITree(result);
|
||||||
|
if (tree) {
|
||||||
|
setUITree(tree); // ← REPLACES the entire tree object
|
||||||
|
setToolInput(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Happens
|
||||||
|
Every tool result that contains a UITree — even from a button click, search query, or tab switch — causes `setUITree(brandNewTreeObject)`. This triggers:
|
||||||
|
|
||||||
|
1. `App.tsx` re-renders with new `uiTree` state
|
||||||
|
2. `<UITreeRenderer tree={uiTree} />` receives a **new object reference**
|
||||||
|
3. `ElementRenderer` receives a **new `elements` map** (new reference, even if data is identical)
|
||||||
|
4. React walks the entire tree and reconciles
|
||||||
|
|
||||||
|
**The critical question:** does it unmount/remount or just re-render?
|
||||||
|
|
||||||
|
That depends entirely on **Bug #2** (keys).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #2: `key: element.key` — Keys Control Component Identity
|
||||||
|
|
||||||
|
### The Code (UITreeRenderer.tsx:37-40)
|
||||||
|
```tsx
|
||||||
|
// Each component gets its key from the JSON tree
|
||||||
|
return React.createElement(Component, { key: element.key, ...element.props }, childElements);
|
||||||
|
|
||||||
|
// Children also keyed from the tree
|
||||||
|
const childElements = element.children?.map((childKey) =>
|
||||||
|
React.createElement(ElementRenderer, {
|
||||||
|
key: childKey, // ← from JSON
|
||||||
|
elementKey: childKey, // ← from JSON
|
||||||
|
elements,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
React uses `key` to determine component identity. When keys change between renders:
|
||||||
|
- **Same key** → React re-renders the existing component (state preserved)
|
||||||
|
- **Different key** → React unmounts old, mounts new (STATE DESTROYED)
|
||||||
|
|
||||||
|
If the MCP server generates keys like `contact-list-abc123` on call #1 and `contact-list-def456` on call #2, React sees them as **completely different components** and tears down the entire subtree.
|
||||||
|
|
||||||
|
**Even if keys are stable**, the `elements` object reference changes every time, forcing re-renders down the entire tree. Not a state-loss issue by itself, but causes performance problems and can trigger effects/callbacks unnecessarily.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #3: Dual `uiTree` State — App.tsx vs MCPAppContext
|
||||||
|
|
||||||
|
### The Code
|
||||||
|
```tsx
|
||||||
|
// App.tsx — has its own uiTree state
|
||||||
|
const [uiTree, setUITree] = useState<UITree | null>(null);
|
||||||
|
|
||||||
|
// MCPAppContext.tsx — ALSO has its own uiTree state
|
||||||
|
const [uiTree, setUITree] = useState<UITree | null>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
There are **two completely independent `uiTree` states**:
|
||||||
|
- `App.tsx` manages the actual rendering tree (used by `<UITreeRenderer tree={uiTree} />`)
|
||||||
|
- `MCPAppContext` has its own `uiTree` + `setUITree` exposed via context, but **nobody calls the context's `setUITree`**
|
||||||
|
|
||||||
|
The context's `uiTree` is **always null**. The context was designed to provide shared state (`formState`, `setFormValue`, `callTool`), but the tree management is disconnected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #4: Interactive Components Use Local State That Gets Destroyed
|
||||||
|
|
||||||
|
### FormGroup (forms reset)
|
||||||
|
```tsx
|
||||||
|
// FormGroup.tsx — all form values in LOCAL useState
|
||||||
|
const [values, setValues] = useState<Record<string, string>>(() => {
|
||||||
|
const init: Record<string, string> = {};
|
||||||
|
for (const f of fields) { init[f.key] = ""; }
|
||||||
|
return init;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain:** User types → submits → `execute(submitTool, values)` → server returns new tree → `ontoolresult` → `setUITree(newTree)` → FormGroup unmounts → remounts → `values` resets to `{}`
|
||||||
|
|
||||||
|
### KanbanBoard (drag-and-drop fails)
|
||||||
|
```tsx
|
||||||
|
// KanbanBoard.tsx — drag state + columns in LOCAL state
|
||||||
|
const [columns, setColumns] = useState<KanbanColumn[]>(initialColumns);
|
||||||
|
const [dropTargetStage, setDropTargetStage] = useState<string | null>(null);
|
||||||
|
const [draggingCardId, setDraggingCardId] = useState<string | null>(null);
|
||||||
|
const dragRef = useRef<DragState | null>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain:** User drags card → optimistic update → `callTool(moveTool, ...)` → server returns new tree → `ontoolresult` → `setUITree(newTree)` → KanbanBoard unmounts → remounts → drag state gone, optimistic move reverted, columns reset to server data
|
||||||
|
|
||||||
|
The KanbanBoard even has a mitigation attempt that fails:
|
||||||
|
```tsx
|
||||||
|
// This ref-based sync ONLY works if React re-renders without unmounting
|
||||||
|
const prevColumnsRef = useRef(initialColumns);
|
||||||
|
if (prevColumnsRef.current !== initialColumns) {
|
||||||
|
prevColumnsRef.current = initialColumns;
|
||||||
|
setColumns(initialColumns); // ← This runs, but after unmount/remount it's moot
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SearchBar (search clears)
|
||||||
|
```tsx
|
||||||
|
// SearchBar.tsx — query in LOCAL state
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain:** User types query → debounced `callTool(searchTool, ...)` → server returns new tree → SearchBar unmounts → remounts → search input clears
|
||||||
|
|
||||||
|
### ActionButton (stops working)
|
||||||
|
```tsx
|
||||||
|
// ActionButton.tsx — loading state in LOCAL state
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain:** User clicks → `setLoading(true)` → `callTool(...)` → server returns new tree → ActionButton unmounts → remounts → `loading` stuck at initial `false`, but the async `callTool` still has a stale closure reference. The `finally { setLoading(false) }` calls `setLoading` on an unmounted component.
|
||||||
|
|
||||||
|
### TabGroup (tab selection lost)
|
||||||
|
```tsx
|
||||||
|
const [localActive, setLocalActive] = useState<string>(
|
||||||
|
controlledActiveTab || tabs[0]?.value || "",
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chain:** User clicks tab → `setLocalActive(value)` → `callTool(switchTool, ...)` → new tree → TabGroup remounts → `localActive` resets to first tab
|
||||||
|
|
||||||
|
### ContactPicker (search results disappear)
|
||||||
|
```tsx
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [results, setResults] = useState<Contact[]>([]);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<Contact | null>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
### InvoiceBuilder (entire form wiped)
|
||||||
|
```tsx
|
||||||
|
const [selectedContact, setSelectedContact] = useState<SelectedContact | null>(null);
|
||||||
|
const [lineItems, setLineItems] = useState<InvoiceLineItem[]>(...);
|
||||||
|
const [taxRate, setTaxRate] = useState(8.5);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #5: Hydration Path Mismatch
|
||||||
|
|
||||||
|
### The Code (App.tsx:60-66)
|
||||||
|
```tsx
|
||||||
|
useEffect(() => {
|
||||||
|
const preInjected = getPreInjectedTree();
|
||||||
|
if (preInjected && !uiTree) {
|
||||||
|
setUITree(preInjected);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
Not a React hydration mismatch (we use `createRoot`, not `hydrateRoot`), but a **data transition issue**:
|
||||||
|
|
||||||
|
1. App mounts → `uiTree = null` → shows "Connecting..."
|
||||||
|
2. `useEffect` fires → finds `window.__MCP_APP_DATA__` → `setUITree(preInjectedTree)`
|
||||||
|
3. Tree renders with pre-injected keys
|
||||||
|
4. `ontoolresult` fires with server-generated tree → `setUITree(serverTree)`
|
||||||
|
5. If keys differ between pre-injected and server tree → **full unmount/remount**
|
||||||
|
|
||||||
|
The pre-injected path and the dynamic path produce trees with potentially different key schemas, causing a jarring full teardown on the first real tool result.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug #6: MCPAppProvider Context Is Stable (NOT a bug)
|
||||||
|
|
||||||
|
`MCPAppProvider` wrapping does **not** cause context loss. It's rendered consistently in all branches of the conditional render in App.tsx. The provider identity is stable.
|
||||||
|
|
||||||
|
However, the provider creates a **new `value` object on every render**:
|
||||||
|
```tsx
|
||||||
|
const value: MCPAppContextValue = {
|
||||||
|
uiTree, setUITree, formState, setFormValue, resetFormState, callTool, isLoading, app,
|
||||||
|
};
|
||||||
|
return <MCPAppContext.Provider value={value}>{children}</MCPAppContext.Provider>;
|
||||||
|
```
|
||||||
|
|
||||||
|
This causes every `useMCPApp()` consumer to re-render on every provider render, but it doesn't cause state loss — just unnecessary renders.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Kill Chain (Full Interaction Flow)
|
||||||
|
|
||||||
|
```
|
||||||
|
User clicks ActionButton("View Contact", toolName="get_contact", toolArgs={id: "123"})
|
||||||
|
│
|
||||||
|
├─ ActionButton.handleClick()
|
||||||
|
│ ├─ setLoading(true)
|
||||||
|
│ └─ callTool("get_contact", {id: "123"})
|
||||||
|
│ └─ app.callServerTool({name: "get_contact", arguments: {id: "123"}})
|
||||||
|
│
|
||||||
|
├─ MCP Server processes tool call, returns CallToolResult with NEW UITree
|
||||||
|
│
|
||||||
|
├─ app.ontoolresult fires
|
||||||
|
│ ├─ extractUITree(result) → newTree (new keys, new elements, new object)
|
||||||
|
│ └─ setUITree(newTree) ← App.tsx state update
|
||||||
|
│
|
||||||
|
├─ App.tsx re-renders
|
||||||
|
│ └─ <UITreeRenderer tree={newTree} />
|
||||||
|
│ └─ ElementRenderer receives new elements map
|
||||||
|
│ └─ For each element: React.createElement(Component, {key: NEW_KEY, ...})
|
||||||
|
│ └─ React sees different key → UNMOUNT old component, MOUNT new one
|
||||||
|
│
|
||||||
|
├─ ALL components unmount:
|
||||||
|
│ ├─ FormGroup: values={} (reset)
|
||||||
|
│ ├─ KanbanBoard: columns=initial, dragState=null (reset)
|
||||||
|
│ ├─ SearchBar: value="" (reset)
|
||||||
|
│ ├─ TabGroup: localActive=first tab (reset)
|
||||||
|
│ ├─ ContactPicker: query="", results=[], selected=null (reset)
|
||||||
|
│ └─ InvoiceBuilder: lineItems=[default], contact=null (reset)
|
||||||
|
│
|
||||||
|
└─ Meanwhile, ActionButton's finally{} block calls setLoading(false)
|
||||||
|
on an UNMOUNTED component → React warning + no-op
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
### Fix 1: Deterministic Stable Keys (Server-Side)
|
||||||
|
The MCP server must generate **deterministic, position-stable keys** for UITree elements. Keys should be based on component type + semantic identity, not random IDs.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: keys change every call
|
||||||
|
{ key: `card-${crypto.randomUUID()}`, type: "Card", ... }
|
||||||
|
|
||||||
|
// GOOD: keys are stable across calls
|
||||||
|
{ key: "contact-detail-card", type: "Card", ... }
|
||||||
|
{ key: "pipeline-kanban", type: "KanbanBoard", ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 2: Tree Diffing / Partial Updates Instead of Full Replace
|
||||||
|
Don't replace the entire tree on every tool result. Diff the new tree against the old one and only update changed branches:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// App.tsx — instead of wholesale replace:
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const newTree = extractUITree(result);
|
||||||
|
if (newTree) {
|
||||||
|
setUITree(prev => {
|
||||||
|
if (!prev) return newTree;
|
||||||
|
// Merge: keep unchanged elements, update changed ones
|
||||||
|
return mergeUITrees(prev, newTree);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function mergeUITrees(oldTree: UITree, newTree: UITree): UITree {
|
||||||
|
const mergedElements: Record<string, UIElement> = {};
|
||||||
|
for (const [key, newEl] of Object.entries(newTree.elements)) {
|
||||||
|
const oldEl = oldTree.elements[key];
|
||||||
|
// Keep old reference if data is identical (prevents re-render)
|
||||||
|
if (oldEl && JSON.stringify(oldEl) === JSON.stringify(newEl)) {
|
||||||
|
mergedElements[key] = oldEl;
|
||||||
|
} else {
|
||||||
|
mergedElements[key] = newEl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { root: newTree.root, elements: mergedElements };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 3: Separate Data Results from UI Results
|
||||||
|
Not every tool call should trigger a tree replacement. ActionButton clicks that return data (not a new view) should be handled by the component, not by `ontoolresult`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ActionButton.tsx — handle result locally, don't let ontoolresult replace tree
|
||||||
|
const handleClick = useCallback(async () => {
|
||||||
|
if (!toolName || loading || disabled) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await callTool(toolName, toolArgs || {});
|
||||||
|
// Result handled locally — only navigate if result contains a NEW view
|
||||||
|
if (result && hasNavigationIntent(result)) {
|
||||||
|
// Let ontoolresult handle it
|
||||||
|
}
|
||||||
|
// Otherwise, toast/notification with result, don't replace tree
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [toolName, toolArgs, loading, disabled, callTool]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 4: Move Interactive State to Context
|
||||||
|
Use the existing `formState` in MCPAppContext instead of local `useState`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// FormGroup.tsx — use shared form state that survives remounts
|
||||||
|
const { formState, setFormValue } = useMCPApp();
|
||||||
|
|
||||||
|
// Instead of local useState, derive from context
|
||||||
|
const getValue = (key: string) => formState[`form:${formId}:${key}`] ?? "";
|
||||||
|
const setValue = (key: string, val: string) => setFormValue(`form:${formId}:${key}`, val);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 5: Fix the Dual uiTree State
|
||||||
|
Either:
|
||||||
|
- **Option A:** Remove `uiTree` from MCPAppContext entirely (it's unused)
|
||||||
|
- **Option B:** Have App.tsx use the context's uiTree and remove its local state
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Option B: App.tsx uses context state
|
||||||
|
export function App() {
|
||||||
|
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
|
||||||
|
// Remove local uiTree state — let context own it
|
||||||
|
|
||||||
|
// In MCPAppProvider, connect ontoolresult to context's setUITree
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 6: Memoize Context Value
|
||||||
|
Prevent unnecessary re-renders of all context consumers:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// MCPAppContext.tsx
|
||||||
|
const value = useMemo<MCPAppContextValue>(() => ({
|
||||||
|
uiTree, setUITree, formState, setFormValue, resetFormState, callTool, isLoading, app,
|
||||||
|
}), [uiTree, formState, callTool, isLoading, app]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 7: Add React.memo to Leaf Components
|
||||||
|
Prevent re-renders when props haven't actually changed:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export const MetricCard = React.memo<MetricCardProps>(({ label, value, trend, ... }) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StatusBadge = React.memo<StatusBadgeProps>(({ label, variant }) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
| # | Fix | Impact | Effort |
|
||||||
|
|---|-----|--------|--------|
|
||||||
|
| 1 | Stable keys (server-side) | 🔴 Critical — fixes unmount/remount | Medium |
|
||||||
|
| 2 | Tree diffing/merge | 🔴 Critical — prevents unnecessary teardown | Medium |
|
||||||
|
| 3 | Separate data vs UI results | 🟡 High — stops button clicks from nuking the view | Low |
|
||||||
|
| 4 | Context-based interactive state | 🟡 High — state survives even if components remount | Medium |
|
||||||
|
| 5 | Fix dual uiTree state | 🟢 Medium — removes confusion, single source of truth | Low |
|
||||||
|
| 6 | Memoize context value | 🟢 Medium — performance improvement | Low |
|
||||||
|
| 7 | React.memo leaf components | 🟢 Low — performance polish | Low |
|
||||||
639
docs/FALLBACK-ARCHITECTURE.md
Normal file
639
docs/FALLBACK-ARCHITECTURE.md
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
# MCP App Fallback Architecture
|
||||||
|
## Graceful Degradation for Hosts Without Full Interactive Support
|
||||||
|
|
||||||
|
**Date:** 2026-02-03
|
||||||
|
**Status:** Proposed
|
||||||
|
**Applies to:** GoHighLevel MCP UI Kit React App
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The ext-apps SDK provides explicit **host capability negotiation** via `McpUiHostCapabilities`. After the `ui/initialize` handshake, the View knows exactly which features the host supports. We use this to implement a **three-tier progressive enhancement** model: Static → Context-Synced → Fully Interactive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Feature Detection (The Foundation)
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
When the View connects, the host returns `McpUiHostCapabilities` in the `ui/initialize` response. The `App` class exposes this via `app.getHostCapabilities()`.
|
||||||
|
|
||||||
|
**The key capability flags:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface McpUiHostCapabilities {
|
||||||
|
serverTools?: { listChanged?: boolean }; // Can proxy tools/call to MCP server
|
||||||
|
serverResources?: { listChanged?: boolean }; // Can proxy resources/read
|
||||||
|
updateModelContext?: {}; // Accepts ui/update-model-context
|
||||||
|
message?: {}; // Accepts ui/message (send chat messages)
|
||||||
|
openLinks?: {}; // Can open external URLs
|
||||||
|
logging?: {}; // Accepts log messages
|
||||||
|
sandbox?: { permissions?: {...}; csp?: {...} };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The critical check: `serverTools` tells you if `callServerTool` will work.**
|
||||||
|
|
||||||
|
### Implementation: `useHostCapabilities` Hook
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// hooks/useHostCapabilities.ts
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useMCPApp } from "../context/MCPAppContext.js";
|
||||||
|
import type { McpUiHostCapabilities } from "@modelcontextprotocol/ext-apps";
|
||||||
|
|
||||||
|
export type InteractionTier = "static" | "context-synced" | "full";
|
||||||
|
|
||||||
|
export interface HostCapabilityInfo {
|
||||||
|
/** Raw capabilities from the host */
|
||||||
|
raw: McpUiHostCapabilities | undefined;
|
||||||
|
/** Whether callServerTool() will work */
|
||||||
|
canCallTools: boolean;
|
||||||
|
/** Whether updateModelContext() will work */
|
||||||
|
canUpdateContext: boolean;
|
||||||
|
/** Whether sendMessage() will work */
|
||||||
|
canSendMessages: boolean;
|
||||||
|
/** The interaction tier this host supports */
|
||||||
|
tier: InteractionTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHostCapabilities(): HostCapabilityInfo {
|
||||||
|
const { app } = useMCPApp();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const raw = app?.getHostCapabilities?.() as McpUiHostCapabilities | undefined;
|
||||||
|
|
||||||
|
const canCallTools = !!raw?.serverTools;
|
||||||
|
const canUpdateContext = !!raw?.updateModelContext;
|
||||||
|
const canSendMessages = !!raw?.message;
|
||||||
|
|
||||||
|
// Determine tier
|
||||||
|
let tier: InteractionTier = "static";
|
||||||
|
if (canCallTools) {
|
||||||
|
tier = "full";
|
||||||
|
} else if (canUpdateContext) {
|
||||||
|
tier = "context-synced";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { raw, canCallTools, canUpdateContext, canSendMessages, tier };
|
||||||
|
}, [app]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add to MCPAppContext
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In MCPAppContext.tsx — extend the context value:
|
||||||
|
|
||||||
|
export interface MCPAppContextValue {
|
||||||
|
// ... existing fields ...
|
||||||
|
|
||||||
|
/** Host capability info, available after connection */
|
||||||
|
capabilities: HostCapabilityInfo;
|
||||||
|
|
||||||
|
/** Safe tool call: uses callServerTool if available,
|
||||||
|
falls back to updateModelContext, or no-ops */
|
||||||
|
safeCallTool: (
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, any>,
|
||||||
|
options?: { localOnly?: boolean }
|
||||||
|
) => Promise<CallToolResult | null>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The Three-Tier Model
|
||||||
|
|
||||||
|
### Tier 1: Static (No host capabilities)
|
||||||
|
- Pure data visualization — charts, tables, badges, timelines
|
||||||
|
- All data comes from the initial `ontoolresult` payload
|
||||||
|
- No server communication, no context updates
|
||||||
|
- **Components work as-is:** BarChart, PieChart, StatusBadge, DataTable, Timeline, etc.
|
||||||
|
|
||||||
|
### Tier 2: Context-Synced (`updateModelContext` available, no `serverTools`)
|
||||||
|
- Local interactivity works (drag, edit, toggle, form fills)
|
||||||
|
- User actions update LOCAL state immediately
|
||||||
|
- State changes are synced to the LLM via `updateModelContext`
|
||||||
|
- The LLM can then decide to call tools itself on the next turn
|
||||||
|
- **Key pattern:** User drags a Kanban card → local state updates → `updateModelContext` tells the LLM what happened → LLM calls `move_opportunity` on its own
|
||||||
|
|
||||||
|
### Tier 3: Full Interactive (`serverTools` available)
|
||||||
|
- Everything works as currently designed
|
||||||
|
- Components call `callServerTool` directly for real-time server mutations
|
||||||
|
- Optimistic UI with server confirmation
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ TIER 3: FULL │
|
||||||
|
│ callServerTool ✓ updateModelContext ✓ │
|
||||||
|
│ Direct server mutations, optimistic UI │
|
||||||
|
│ ┌─────────────────────────────────────────────┐ │
|
||||||
|
│ │ TIER 2: CONTEXT-SYNCED │ │
|
||||||
|
│ │ callServerTool ✗ updateModelContext ✓ │ │
|
||||||
|
│ │ Local state + LLM-informed sync │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────┐│ │
|
||||||
|
│ │ │ TIER 1: STATIC ││ │
|
||||||
|
│ │ │ Read-only data visualization ││ │
|
||||||
|
│ │ │ Charts, tables, badges, timelines ││ │
|
||||||
|
│ │ └─────────────────────────────────────────┘│ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Component Classification
|
||||||
|
|
||||||
|
### Always Static (work everywhere)
|
||||||
|
These components are pure data display — they render from the UITree data and never call tools:
|
||||||
|
|
||||||
|
| Component | Category |
|
||||||
|
|-----------|----------|
|
||||||
|
| BarChart, LineChart, PieChart, SparklineChart, FunnelChart | charts |
|
||||||
|
| StatusBadge, ProgressBar, CurrencyDisplay, StarRating | data |
|
||||||
|
| DataTable, KeyValueList, TagList, AvatarGroup | data |
|
||||||
|
| Timeline, MetricCard, DetailHeader, InfoBlock | data |
|
||||||
|
| PageHeader, Section, Card, StatsGrid, SplitLayout | layout |
|
||||||
|
| ChatThread, TranscriptView, EmailPreview, ContentPreview | comms |
|
||||||
|
| CalendarView, TreeView, FlowDiagram, DuplicateCompare | viz |
|
||||||
|
|
||||||
|
### Context-Syncable (Tier 2+)
|
||||||
|
These work with local state and sync via `updateModelContext`:
|
||||||
|
|
||||||
|
| Component | Local Behavior | Context Sync |
|
||||||
|
|-----------|---------------|--------------|
|
||||||
|
| KanbanBoard | Drag cards between columns locally | Report new board state to LLM |
|
||||||
|
| EditableField | Edit text inline locally | Report edited values to LLM |
|
||||||
|
| FormGroup | Fill form fields locally | Report form state to LLM |
|
||||||
|
| SelectDropdown | Select options locally | Report selection to LLM |
|
||||||
|
| AmountInput | Adjust amounts locally | Report new amounts to LLM |
|
||||||
|
| ChecklistView | Toggle checkboxes locally | Report checklist state to LLM |
|
||||||
|
| FilterChips | Toggle filters locally | Report active filters to LLM |
|
||||||
|
| TabGroup | Switch tabs locally | Report active tab to LLM |
|
||||||
|
| SearchBar | Type search queries locally | Report search to LLM |
|
||||||
|
|
||||||
|
### Full Interactive Only (Tier 3)
|
||||||
|
These **require** server tool calls and degrade gracefully at lower tiers:
|
||||||
|
|
||||||
|
| Component | Tier 3 | Tier 2 Fallback | Tier 1 Fallback |
|
||||||
|
|-----------|--------|-----------------|-----------------|
|
||||||
|
| ActionButton | Calls tool | Shows "Ask assistant" hint | Disabled/hidden |
|
||||||
|
| AppointmentBooker | Books via tool | Shows form, reports to LLM | Shows read-only schedule |
|
||||||
|
| InvoiceBuilder | Creates via tool | Builds locally, reports to LLM | Shows existing invoice data |
|
||||||
|
| OpportunityEditor | Saves via tool | Edits locally, reports to LLM | Shows read-only data |
|
||||||
|
| ContactPicker | Searches via tool | Shows static options list | Shows read-only display |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Concrete Implementation
|
||||||
|
|
||||||
|
### 4a. `safeCallTool` — The Universal Action Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In MCPAppProvider
|
||||||
|
|
||||||
|
const safeCallTool = useCallback(
|
||||||
|
async (
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, any>,
|
||||||
|
options?: { localOnly?: boolean }
|
||||||
|
): Promise<CallToolResult | null> => {
|
||||||
|
// Tier 3: Full tool call
|
||||||
|
if (capabilities.canCallTools && !options?.localOnly) {
|
||||||
|
return callTool(toolName, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 2: Inform LLM via updateModelContext
|
||||||
|
if (capabilities.canUpdateContext && app) {
|
||||||
|
const markdown = [
|
||||||
|
`---`,
|
||||||
|
`action: ${toolName}`,
|
||||||
|
`timestamp: ${new Date().toISOString()}`,
|
||||||
|
...Object.entries(args).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
|
||||||
|
`---`,
|
||||||
|
`User performed action "${toolName}" in the UI.`,
|
||||||
|
`Please execute the corresponding server-side operation.`,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
await app.updateModelContext({
|
||||||
|
content: [{ type: "text", text: markdown }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a synthetic "pending" result
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Action "${toolName}" reported to assistant. It will be processed on the next turn.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as CallToolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 1: No-op, return null
|
||||||
|
console.warn(`[MCPApp] Cannot execute ${toolName}: no host support`);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[capabilities, callTool, app]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4b. `<InteractiveGate>` — Tier-Aware Wrapper
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// components/shared/InteractiveGate.tsx
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useHostCapabilities, type InteractionTier } from "../../hooks/useHostCapabilities.js";
|
||||||
|
|
||||||
|
interface InteractiveGateProps {
|
||||||
|
/** Minimum tier required to show children */
|
||||||
|
requires: InteractionTier;
|
||||||
|
/** What to render if the tier isn't met */
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
/** If true, hide entirely instead of showing fallback */
|
||||||
|
hideOnUnsupported?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierLevel: Record<InteractionTier, number> = {
|
||||||
|
static: 0,
|
||||||
|
"context-synced": 1,
|
||||||
|
full: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InteractiveGate: React.FC<InteractiveGateProps> = ({
|
||||||
|
requires,
|
||||||
|
fallback,
|
||||||
|
hideOnUnsupported = false,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const { tier } = useHostCapabilities();
|
||||||
|
|
||||||
|
if (tierLevel[tier] >= tierLevel[requires]) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideOnUnsupported) return null;
|
||||||
|
|
||||||
|
if (fallback) return <>{fallback}</>;
|
||||||
|
|
||||||
|
// Default fallback: subtle message
|
||||||
|
return (
|
||||||
|
<div className="interactive-unavailable" role="note">
|
||||||
|
<span className="interactive-unavailable-icon">ℹ️</span>
|
||||||
|
<span>This feature requires a supported host. Ask the assistant to perform this action.</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4c. Refactored `ActionButton` with Fallback
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// components/shared/ActionButton.tsx (revised)
|
||||||
|
|
||||||
|
export const ActionButton: React.FC<ActionButtonProps> = ({
|
||||||
|
label,
|
||||||
|
variant = "secondary",
|
||||||
|
size = "md",
|
||||||
|
disabled,
|
||||||
|
toolName,
|
||||||
|
toolArgs,
|
||||||
|
}) => {
|
||||||
|
const { safeCallTool } = useMCPApp();
|
||||||
|
const { tier, canCallTools, canSendMessages } = useHostCapabilities();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleClick = useCallback(async () => {
|
||||||
|
if (!toolName || loading || disabled) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setPendingMessage(null);
|
||||||
|
try {
|
||||||
|
const result = await safeCallTool(toolName, toolArgs || {});
|
||||||
|
|
||||||
|
// Tier 2: show "reported to assistant" feedback
|
||||||
|
if (!canCallTools && result) {
|
||||||
|
setPendingMessage("Reported to assistant");
|
||||||
|
setTimeout(() => setPendingMessage(null), 3000);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// handled by context
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [toolName, toolArgs, loading, disabled, safeCallTool, canCallTools]);
|
||||||
|
|
||||||
|
// Tier 1 with no context sync: show disabled with hint
|
||||||
|
if (tier === "static" && toolName) {
|
||||||
|
return (
|
||||||
|
<button className={`btn ${vCls} ${sCls} btn-unsupported`} disabled title="Ask the assistant to perform this action">
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={`btn ${vCls} ${sCls}`} disabled={disabled || loading} onClick={handleClick}>
|
||||||
|
{loading && <span className="loading-spinner" style={{ width: 14, height: 14, marginRight: 6, borderWidth: 2, display: "inline-block" }} />}
|
||||||
|
{label}
|
||||||
|
{pendingMessage && <span className="btn-pending-badge">{pendingMessage}</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4d. Refactored `KanbanBoard` — Local-First + Context Sync
|
||||||
|
|
||||||
|
The KanbanBoard already uses optimistic local state. The only change: fall back to `updateModelContext` when `moveTool` can't be called directly.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// In KanbanBoard onDrop handler (revised):
|
||||||
|
|
||||||
|
const onDrop = useCallback(async (e: React.DragEvent, toStageId: string) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDropTargetStage(null);
|
||||||
|
|
||||||
|
const drag = dragRef.current;
|
||||||
|
if (!drag) return;
|
||||||
|
const { cardId, fromStageId } = drag;
|
||||||
|
dragRef.current = null;
|
||||||
|
setDraggingCardId(null);
|
||||||
|
if (fromStageId === toStageId) return;
|
||||||
|
|
||||||
|
// 1. Optimistic local update (same as before)
|
||||||
|
let movedCard: KanbanCard | undefined;
|
||||||
|
const prevColumns = columns;
|
||||||
|
setColumns(prev => { /* ... same optimistic logic ... */ });
|
||||||
|
|
||||||
|
// 2. Use safeCallTool — works at Tier 2 AND Tier 3
|
||||||
|
if (moveTool) {
|
||||||
|
try {
|
||||||
|
const result = await safeCallTool(moveTool, {
|
||||||
|
opportunityId: cardId,
|
||||||
|
pipelineStageId: toStageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tier 2: result is a "reported" message — no revert needed
|
||||||
|
// Local state IS the source of truth until LLM processes it
|
||||||
|
if (!capabilities.canCallTools && result) {
|
||||||
|
// Also report the full board state so LLM has context
|
||||||
|
await reportBoardState();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Only revert if we attempted a real tool call (Tier 3)
|
||||||
|
if (capabilities.canCallTools) {
|
||||||
|
console.error("KanbanBoard: move failed, reverting", err);
|
||||||
|
setColumns(prevColumns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [columns, moveTool, safeCallTool, capabilities]);
|
||||||
|
|
||||||
|
// Report full board state to LLM for context
|
||||||
|
const reportBoardState = useCallback(async () => {
|
||||||
|
if (!app || !capabilities.canUpdateContext) return;
|
||||||
|
|
||||||
|
const summary = columns.map(col =>
|
||||||
|
`**${col.title}** (${col.cards?.length ?? 0}):\n` +
|
||||||
|
(col.cards || []).map(c => ` - ${c.title} (${c.value || 'no value'})`).join('\n')
|
||||||
|
).join('\n\n');
|
||||||
|
|
||||||
|
await app.updateModelContext({
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `---\ncomponent: kanban-board\nupdated: ${new Date().toISOString()}\n---\nCurrent pipeline state:\n\n${summary}`,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}, [app, capabilities, columns]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4e. Refactored `EditableField` — Works at All Tiers
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// In EditableField (revised handleSave):
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
if (editValue === value) return;
|
||||||
|
|
||||||
|
if (saveTool) {
|
||||||
|
const result = await safeCallTool(saveTool, {
|
||||||
|
...saveArgs,
|
||||||
|
value: editValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tier 2: field stays locally updated; LLM is informed
|
||||||
|
// Tier 3: server confirms; UI may refresh via ontoolresult
|
||||||
|
// Tier 1: nothing happens — but user sees their edit locally
|
||||||
|
|
||||||
|
if (!result && tier === "static") {
|
||||||
|
// Show hint that the change is display-only
|
||||||
|
setLocalOnlyHint(true);
|
||||||
|
setTimeout(() => setLocalOnlyHint(false), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Alternative Interaction: `updateModelContext` as Primary Channel
|
||||||
|
|
||||||
|
For hosts that support `updateModelContext` but not `serverTools`, we can use a **declarative intent** pattern instead of imperative tool calls:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// hooks/useReportInteraction.ts
|
||||||
|
|
||||||
|
export function useReportInteraction() {
|
||||||
|
const { app } = useMCPApp();
|
||||||
|
const { canUpdateContext } = useHostCapabilities();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async (interaction: {
|
||||||
|
component: string;
|
||||||
|
action: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
suggestion?: string; // What we suggest the LLM should do
|
||||||
|
}) => {
|
||||||
|
if (!canUpdateContext || !app) return;
|
||||||
|
|
||||||
|
const text = [
|
||||||
|
`---`,
|
||||||
|
`component: ${interaction.component}`,
|
||||||
|
`action: ${interaction.action}`,
|
||||||
|
`timestamp: ${new Date().toISOString()}`,
|
||||||
|
...Object.entries(interaction.data).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
|
||||||
|
`---`,
|
||||||
|
interaction.suggestion || `User performed "${interaction.action}" in ${interaction.component}.`,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
await app.updateModelContext({
|
||||||
|
content: [{ type: "text", text }],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[app, canUpdateContext]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in any component:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const reportInteraction = useReportInteraction();
|
||||||
|
|
||||||
|
// In a form submit handler:
|
||||||
|
await reportInteraction({
|
||||||
|
component: "AppointmentBooker",
|
||||||
|
action: "book_appointment",
|
||||||
|
data: { contactId, calendarId, startTime, endTime },
|
||||||
|
suggestion: "User wants to book this appointment. Please call create_appointment with these details.",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Server-Side: Dual-Mode Tool Registration
|
||||||
|
|
||||||
|
The MCP server should register tools that work both ways:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server-side: Register tool as visible to both model and app
|
||||||
|
registerAppTool(server, "move_opportunity", {
|
||||||
|
description: "Move an opportunity to a different pipeline stage",
|
||||||
|
inputSchema: {
|
||||||
|
opportunityId: z.string(),
|
||||||
|
pipelineStageId: z.string(),
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
ui: {
|
||||||
|
resourceUri: "ui://ghl/pipeline-view",
|
||||||
|
visibility: ["model", "app"], // ← Both can call it
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, handler);
|
||||||
|
```
|
||||||
|
|
||||||
|
With `visibility: ["model", "app"]`:
|
||||||
|
- **Tier 3 hosts:** App calls it directly via `callServerTool`
|
||||||
|
- **Tier 2 hosts:** App reports intent via `updateModelContext`, LLM sees the tool in its tool list and calls it on the next turn
|
||||||
|
- **Tier 1 hosts:** Tool still works as text-only through normal MCP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. `window.__MCP_APP_DATA__` Pre-Injection (Tier 0)
|
||||||
|
|
||||||
|
The existing `getPreInjectedTree()` in App.tsx already supports a non-MCP path. For environments where the iframe loads but the ext-apps SDK never connects:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// App.tsx already handles this:
|
||||||
|
useEffect(() => {
|
||||||
|
const preInjected = getPreInjectedTree();
|
||||||
|
if (preInjected && !uiTree) {
|
||||||
|
setUITree(preInjected);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
This serves as **Tier 0**: pure server-side rendering. The server injects the UITree directly into the HTML. No SDK connection needed. All components render in static mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. CSS for Degraded States
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* styles/fallback.css */
|
||||||
|
|
||||||
|
/* Unsupported interactive elements */
|
||||||
|
.btn-unsupported {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.btn-unsupported::after {
|
||||||
|
content: "↗";
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-left: 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Reported to assistant" badge */
|
||||||
|
.btn-pending-badge {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--color-text-info, #3b82f6);
|
||||||
|
animation: fadeIn 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unavailable feature hint */
|
||||||
|
.interactive-unavailable {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
background: var(--color-background-info, #eff6ff);
|
||||||
|
color: var(--color-text-secondary, #6b7280);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Local-only edit hint */
|
||||||
|
.ef-local-hint {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--color-text-warning, #f59e0b);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editable fields in static mode: remove edit affordance */
|
||||||
|
[data-tier="static"] .ef-edit-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[data-tier="static"] .ef-display {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Decision Matrix
|
||||||
|
|
||||||
|
| Scenario | Detection | Behavior |
|
||||||
|
|----------|-----------|----------|
|
||||||
|
| Claude Desktop (full support) | `serverTools` ✓ | Tier 3: all features work |
|
||||||
|
| Host with context-only support | `updateModelContext` ✓, `serverTools` ✗ | Tier 2: local + LLM sync |
|
||||||
|
| Minimal host / basic iframe | No capabilities | Tier 1: static display |
|
||||||
|
| Pre-injected `__MCP_APP_DATA__` | No SDK connection | Tier 0: SSR static |
|
||||||
|
| `callServerTool` fails at runtime | Error caught | Downgrade to Tier 2 dynamically |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Summary: What to Implement
|
||||||
|
|
||||||
|
### Phase 1 (Essential)
|
||||||
|
1. **`useHostCapabilities` hook** — reads `app.getHostCapabilities()`, computes tier
|
||||||
|
2. **`safeCallTool` in MCPAppContext** — wraps callTool with fallback to updateModelContext
|
||||||
|
3. **Update `ActionButton`** — use safeCallTool, show disabled state at Tier 1
|
||||||
|
4. **Update `KanbanBoard`** — already local-first, just wire up safeCallTool + reportBoardState
|
||||||
|
5. **`fallback.css`** — styles for degraded states
|
||||||
|
|
||||||
|
### Phase 2 (Full Coverage)
|
||||||
|
6. **`useReportInteraction` hook** — standard way to inform LLM of UI actions
|
||||||
|
7. **`<InteractiveGate>` component** — declarative tier gating in templates
|
||||||
|
8. **Update all interactive components** — EditableField, FormGroup, AppointmentBooker, etc.
|
||||||
|
9. **Runtime downgrade** — if a Tier 3 call fails, auto-downgrade to Tier 2
|
||||||
|
|
||||||
|
### Phase 3 (Polish)
|
||||||
|
10. **Toast notifications** for tier-specific feedback ("Action sent to assistant")
|
||||||
|
11. **`data-tier` attribute** on root for CSS-only degradation
|
||||||
|
12. **Testing matrix** — automated tests per tier per component
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Insight
|
||||||
|
|
||||||
|
The ext-apps spec was **designed** for this: *"UI is a progressive enhancement, not a requirement."* Our architecture mirrors this philosophy — every component starts as a static data display and progressively gains interactivity based on what the host reports it can do. The KanbanBoard's existing optimistic-update pattern is already the correct Tier 2 pattern; we just need to formalize it and apply it consistently.
|
||||||
87
package-lock.json
generated
87
package-lock.json
generated
@ -1,22 +1,15 @@
|
|||||||
{
|
{
|
||||||
<<<<<<< HEAD
|
|
||||||
"name": "ghl-mcp",
|
|
||||||
=======
|
|
||||||
"name": "@mastanley13/ghl-mcp-server",
|
"name": "@mastanley13/ghl-mcp-server",
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
<<<<<<< HEAD
|
|
||||||
"name": "ghl-mcp",
|
|
||||||
=======
|
|
||||||
"name": "@mastanley13/ghl-mcp-server",
|
"name": "@mastanley13/ghl-mcp-server",
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.72.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"@types/cors": "^2.8.18",
|
"@types/cors": "^2.8.18",
|
||||||
"@types/express": "^5.0.2",
|
"@types/express": "^5.0.2",
|
||||||
@ -25,12 +18,9 @@
|
|||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0"
|
||||||
},
|
},
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"ghl-mcp-server": "dist/server.js"
|
"ghl-mcp-server": "dist/server.js"
|
||||||
},
|
},
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.29",
|
"@types/node": "^22.15.29",
|
||||||
@ -39,12 +29,9 @@
|
|||||||
"ts-jest": "^29.3.4",
|
"ts-jest": "^29.3.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@ -60,6 +47,26 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
|
"version": "0.72.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.72.1.tgz",
|
||||||
|
"integrity": "sha512-MiUnue7qN7DvLIoYHgkedN2z05mRf2CutBzjXXY2krzOhG2r/rIfISS2uVkNLikgToB5hYIzw+xp2jdOtRkqYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema-to-ts": "^3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"anthropic-ai-sdk": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@ -88,10 +95,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@ -465,6 +469,15 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.27.2",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||||
@ -1102,10 +1115,7 @@
|
|||||||
"version": "22.15.29",
|
"version": "22.15.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
||||||
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@ -1487,10 +1497,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001718",
|
"caniuse-lite": "^1.0.30001718",
|
||||||
"electron-to-chromium": "^1.5.160",
|
"electron-to-chromium": "^1.5.160",
|
||||||
@ -2155,10 +2162,7 @@
|
|||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
@ -2878,10 +2882,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
||||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.7.0",
|
"@jest/core": "^29.7.0",
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
@ -3481,6 +3482,19 @@
|
|||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-to-ts": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"ts-algebra": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
@ -4613,6 +4627,12 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-algebra": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-jest": {
|
"node_modules/ts-jest": {
|
||||||
"version": "29.3.4",
|
"version": "29.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
|
||||||
@ -4691,10 +4711,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@ -4772,10 +4789,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -4994,10 +5008,7 @@
|
|||||||
"version": "3.25.51",
|
"version": "3.25.51",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz",
|
||||||
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==",
|
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==",
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"peer": true,
|
"peer": true,
|
||||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,8 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build:dynamic-ui": "cd src/ui/react-app && npm run build",
|
||||||
|
"build": "npm run build:dynamic-ui && tsc",
|
||||||
"dev": "nodemon --exec ts-node src/http-server.ts",
|
"dev": "nodemon --exec ts-node src/http-server.ts",
|
||||||
"start": "node dist/http-server.js",
|
"start": "node dist/http-server.js",
|
||||||
"start:stdio": "node dist/server.js",
|
"start:stdio": "node dist/server.js",
|
||||||
@ -45,6 +46,7 @@
|
|||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.72.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"@types/cors": "^2.8.18",
|
"@types/cors": "^2.8.18",
|
||||||
"@types/express": "^5.0.2",
|
"@types/express": "^5.0.2",
|
||||||
|
|||||||
1168
src/apps/index.ts
1168
src/apps/index.ts
File diff suppressed because it is too large
Load Diff
106
src/apps/templates/agent-stats.template.ts
Normal file
106
src/apps/templates/agent-stats.template.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildAgentStatsTree(data: {
|
||||||
|
userId?: string;
|
||||||
|
dateRange: string;
|
||||||
|
location: any;
|
||||||
|
locationId: string;
|
||||||
|
}): UITree {
|
||||||
|
const location = data.location || {};
|
||||||
|
const locName = location.name || 'Location';
|
||||||
|
|
||||||
|
// Since GHL API doesn't have direct agent stats, build from available location data
|
||||||
|
const dateRangeLabel =
|
||||||
|
data.dateRange === 'last7days' ? 'Last 7 Days'
|
||||||
|
: data.dateRange === 'last30days' ? 'Last 30 Days'
|
||||||
|
: data.dateRange === 'last90days' ? 'Last 90 Days'
|
||||||
|
: data.dateRange || 'Last 30 Days';
|
||||||
|
|
||||||
|
// Build a placeholder stats view using available data
|
||||||
|
const elements: UITree['elements'] = {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: data.userId ? `Agent: ${data.userId}` : 'Agent Overview',
|
||||||
|
subtitle: `${locName} · ${dateRangeLabel}`,
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Location', value: locName },
|
||||||
|
{ label: 'Period', value: dateRangeLabel },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['metrics', 'layout'],
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
key: 'metrics',
|
||||||
|
type: 'StatsGrid',
|
||||||
|
props: { columns: 4 },
|
||||||
|
children: ['mTotal', 'mActive', 'mRes', 'mAvg'],
|
||||||
|
},
|
||||||
|
mTotal: {
|
||||||
|
key: 'mTotal',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Total Interactions', value: '—', color: 'blue' },
|
||||||
|
},
|
||||||
|
mActive: {
|
||||||
|
key: 'mActive',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Active Contacts', value: '—', color: 'green' },
|
||||||
|
},
|
||||||
|
mRes: {
|
||||||
|
key: 'mRes',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Response Rate', value: '—', color: 'purple' },
|
||||||
|
},
|
||||||
|
mAvg: {
|
||||||
|
key: 'mAvg',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Avg Response Time', value: '—', color: 'yellow' },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
key: 'layout',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '50/50', gap: 'md' },
|
||||||
|
children: ['chart', 'activityTable'],
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
key: 'chart',
|
||||||
|
type: 'LineChart',
|
||||||
|
props: {
|
||||||
|
points: [
|
||||||
|
{ label: 'Mon', value: 0 },
|
||||||
|
{ label: 'Tue', value: 0 },
|
||||||
|
{ label: 'Wed', value: 0 },
|
||||||
|
{ label: 'Thu', value: 0 },
|
||||||
|
{ label: 'Fri', value: 0 },
|
||||||
|
],
|
||||||
|
title: 'Activity Trend',
|
||||||
|
showPoints: true,
|
||||||
|
showArea: true,
|
||||||
|
yAxisLabel: 'Interactions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activityTable: {
|
||||||
|
key: 'activityTable',
|
||||||
|
type: 'DataTable',
|
||||||
|
props: {
|
||||||
|
columns: [
|
||||||
|
{ key: 'type', label: 'Activity', format: 'text' },
|
||||||
|
{ key: 'count', label: 'Count', format: 'text', sortable: true },
|
||||||
|
{ key: 'trend', label: 'Trend', format: 'text' },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
{ type: 'Calls', count: '—', trend: '—' },
|
||||||
|
{ type: 'Emails', count: '—', trend: '—' },
|
||||||
|
{ type: 'SMS', count: '—', trend: '—' },
|
||||||
|
{ type: 'Tasks', count: '—', trend: '—' },
|
||||||
|
],
|
||||||
|
emptyMessage: 'No activity data available',
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { root: 'page', elements };
|
||||||
|
}
|
||||||
57
src/apps/templates/calendar-view.template.ts
Normal file
57
src/apps/templates/calendar-view.template.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildCalendarViewTree(data: {
|
||||||
|
calendar: any;
|
||||||
|
events: any[];
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
}): UITree {
|
||||||
|
const calendar = data.calendar || {};
|
||||||
|
const events = data.events || [];
|
||||||
|
|
||||||
|
// Map GHL events to CalendarView component events
|
||||||
|
const calEvents = events.map((evt: any) => ({
|
||||||
|
date: evt.startTime || evt.start || evt.date || '',
|
||||||
|
title: evt.title || evt.name || 'Event',
|
||||||
|
time: evt.startTime
|
||||||
|
? new Date(evt.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
|
: undefined,
|
||||||
|
type: evt.appointmentStatus === 'confirmed' ? 'meeting' as const : 'event' as const,
|
||||||
|
color: evt.appointmentStatus === 'cancelled' ? '#dc2626' : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const start = new Date(data.startDate);
|
||||||
|
const confirmedCount = events.filter((e: any) => e.appointmentStatus === 'confirmed').length;
|
||||||
|
const cancelledCount = events.filter((e: any) => e.appointmentStatus === 'cancelled').length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: calendar.name || 'Calendar',
|
||||||
|
subtitle: `${events.length} events`,
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Total Events', value: String(events.length) },
|
||||||
|
{ label: 'Confirmed', value: String(confirmedCount) },
|
||||||
|
{ label: 'Cancelled', value: String(cancelledCount) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['calendar'],
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
key: 'calendar',
|
||||||
|
type: 'CalendarView',
|
||||||
|
props: {
|
||||||
|
year: start.getFullYear(),
|
||||||
|
month: start.getMonth() + 1,
|
||||||
|
events: calEvents,
|
||||||
|
highlightToday: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
118
src/apps/templates/campaign-stats.template.ts
Normal file
118
src/apps/templates/campaign-stats.template.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildCampaignStatsTree(data: {
|
||||||
|
campaign: any;
|
||||||
|
campaigns: any[];
|
||||||
|
campaignId: string;
|
||||||
|
locationId: string;
|
||||||
|
}): UITree {
|
||||||
|
const campaign = data.campaign || {};
|
||||||
|
const campaigns = data.campaigns || [];
|
||||||
|
|
||||||
|
const stats = campaign.statistics || campaign.stats || {};
|
||||||
|
const sent = stats.sent || stats.delivered || 0;
|
||||||
|
const opened = stats.opened || stats.opens || 0;
|
||||||
|
const clicked = stats.clicked || stats.clicks || 0;
|
||||||
|
const bounced = stats.bounced || stats.bounces || 0;
|
||||||
|
const unsubscribed = stats.unsubscribed || stats.unsubscribes || 0;
|
||||||
|
const openRate = sent > 0 ? ((opened / sent) * 100).toFixed(1) : '0.0';
|
||||||
|
const clickRate = sent > 0 ? ((clicked / sent) * 100).toFixed(1) : '0.0';
|
||||||
|
|
||||||
|
// Bar chart of performance metrics
|
||||||
|
const bars = [
|
||||||
|
{ label: 'Sent', value: sent, color: '#3b82f6' },
|
||||||
|
{ label: 'Opened', value: opened, color: '#059669' },
|
||||||
|
{ label: 'Clicked', value: clicked, color: '#7c3aed' },
|
||||||
|
{ label: 'Bounced', value: bounced, color: '#f59e0b' },
|
||||||
|
{ label: 'Unsubscribed', value: unsubscribed, color: '#dc2626' },
|
||||||
|
].filter(b => b.value > 0);
|
||||||
|
|
||||||
|
// Other campaigns table
|
||||||
|
const campaignRows = campaigns.slice(0, 8).map((c: any) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.name || 'Untitled',
|
||||||
|
status: c.status || 'draft',
|
||||||
|
sent: c.statistics?.sent || 0,
|
||||||
|
opens: c.statistics?.opened || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: campaign.name || 'Campaign',
|
||||||
|
subtitle: campaign.subject || 'Email Campaign',
|
||||||
|
status: campaign.status || 'draft',
|
||||||
|
statusVariant: campaign.status === 'completed' ? 'complete' : campaign.status === 'active' ? 'active' : 'draft',
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Sent', value: sent.toLocaleString() },
|
||||||
|
{ label: 'Open Rate', value: `${openRate}%` },
|
||||||
|
{ label: 'Click Rate', value: `${clickRate}%` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['metrics', 'layout'],
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
key: 'metrics',
|
||||||
|
type: 'StatsGrid',
|
||||||
|
props: { columns: 4 },
|
||||||
|
children: ['mSent', 'mOpened', 'mClicked', 'mBounced'],
|
||||||
|
},
|
||||||
|
mSent: {
|
||||||
|
key: 'mSent',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Sent', value: sent.toLocaleString(), color: 'blue' },
|
||||||
|
},
|
||||||
|
mOpened: {
|
||||||
|
key: 'mOpened',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Opened', value: opened.toLocaleString(), color: 'green' },
|
||||||
|
},
|
||||||
|
mClicked: {
|
||||||
|
key: 'mClicked',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Clicked', value: clicked.toLocaleString(), color: 'purple' },
|
||||||
|
},
|
||||||
|
mBounced: {
|
||||||
|
key: 'mBounced',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Bounced', value: bounced.toLocaleString(), color: 'yellow' },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
key: 'layout',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '50/50', gap: 'md' },
|
||||||
|
children: ['chart', 'campaignTable'],
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
key: 'chart',
|
||||||
|
type: 'BarChart',
|
||||||
|
props: {
|
||||||
|
bars,
|
||||||
|
orientation: 'horizontal',
|
||||||
|
showValues: true,
|
||||||
|
title: 'Performance Breakdown',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
campaignTable: {
|
||||||
|
key: 'campaignTable',
|
||||||
|
type: 'DataTable',
|
||||||
|
props: {
|
||||||
|
columns: [
|
||||||
|
{ key: 'name', label: 'Campaign', format: 'text', sortable: true },
|
||||||
|
{ key: 'status', label: 'Status', format: 'status' },
|
||||||
|
{ key: 'sent', label: 'Sent', format: 'text', sortable: true },
|
||||||
|
{ key: 'opens', label: 'Opens', format: 'text' },
|
||||||
|
],
|
||||||
|
rows: campaignRows,
|
||||||
|
emptyMessage: 'No other campaigns',
|
||||||
|
pageSize: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
62
src/apps/templates/contact-grid.template.ts
Normal file
62
src/apps/templates/contact-grid.template.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildContactGridTree(data: { contacts: any[]; query?: string }): UITree {
|
||||||
|
const contacts = data.contacts || [];
|
||||||
|
|
||||||
|
const taggedCount = contacts.filter((c: any) => c.tags && c.tags.length > 0).length;
|
||||||
|
const withEmail = contacts.filter((c: any) => c.email).length;
|
||||||
|
|
||||||
|
const rows = contacts.map((c: any) => ({
|
||||||
|
id: c.id || '',
|
||||||
|
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
|
||||||
|
email: c.email || '—',
|
||||||
|
phone: c.phone || '—',
|
||||||
|
tags: c.tags || [],
|
||||||
|
dateAdded: c.dateAdded ? new Date(c.dateAdded).toLocaleDateString() : '—',
|
||||||
|
source: c.source || '—',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: 'Contacts',
|
||||||
|
subtitle: data.query ? `Search: "${data.query}"` : 'All contacts',
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Total', value: String(contacts.length) },
|
||||||
|
{ label: 'With Email', value: String(withEmail) },
|
||||||
|
{ label: 'Tagged', value: String(taggedCount) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['search', 'table'],
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
key: 'search',
|
||||||
|
type: 'SearchBar',
|
||||||
|
props: { placeholder: 'Search contacts...', valuePath: 'query' },
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
key: 'table',
|
||||||
|
type: 'DataTable',
|
||||||
|
props: {
|
||||||
|
columns: [
|
||||||
|
{ key: 'name', label: 'Name', format: 'avatar', sortable: true },
|
||||||
|
{ key: 'email', label: 'Email', format: 'email', sortable: true },
|
||||||
|
{ key: 'phone', label: 'Phone', format: 'phone' },
|
||||||
|
{ key: 'tags', label: 'Tags', format: 'tags' },
|
||||||
|
{ key: 'dateAdded', label: 'Added', format: 'date', sortable: true },
|
||||||
|
{ key: 'source', label: 'Source', format: 'text' },
|
||||||
|
],
|
||||||
|
rows,
|
||||||
|
selectable: true,
|
||||||
|
emptyMessage: 'No contacts found',
|
||||||
|
pageSize: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
127
src/apps/templates/contact-timeline.template.ts
Normal file
127
src/apps/templates/contact-timeline.template.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildContactTimelineTree(data: {
|
||||||
|
contact: any;
|
||||||
|
notes: any;
|
||||||
|
tasks: any;
|
||||||
|
}): UITree {
|
||||||
|
const contact = data.contact || {};
|
||||||
|
const notes = Array.isArray(data.notes) ? data.notes : data.notes?.notes || [];
|
||||||
|
const tasks = Array.isArray(data.tasks) ? data.tasks : data.tasks?.tasks || [];
|
||||||
|
|
||||||
|
const contactName = `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown Contact';
|
||||||
|
const email = contact.email || '—';
|
||||||
|
const phone = contact.phone || '—';
|
||||||
|
|
||||||
|
// Build timeline events from notes + tasks, sorted by date
|
||||||
|
const events: any[] = [];
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
events.push({
|
||||||
|
id: note.id || `note-${events.length}`,
|
||||||
|
title: 'Note Added',
|
||||||
|
description: note.body || note.content || note.text || 'Note',
|
||||||
|
timestamp: note.dateAdded || note.createdAt ? new Date(note.dateAdded || note.createdAt).toLocaleString() : '—',
|
||||||
|
icon: 'note',
|
||||||
|
variant: 'default',
|
||||||
|
_sort: new Date(note.dateAdded || note.createdAt || 0).getTime(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
events.push({
|
||||||
|
id: task.id || `task-${events.length}`,
|
||||||
|
title: task.title || task.name || 'Task',
|
||||||
|
description: task.description || task.body || (task.completed ? 'Completed' : 'Pending'),
|
||||||
|
timestamp: task.dueDate || task.createdAt ? new Date(task.dueDate || task.createdAt).toLocaleString() : '—',
|
||||||
|
icon: 'task',
|
||||||
|
variant: task.completed ? 'success' : 'default',
|
||||||
|
_sort: new Date(task.dueDate || task.createdAt || 0).getTime(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add contact creation event
|
||||||
|
if (contact.dateAdded || contact.createdAt) {
|
||||||
|
events.push({
|
||||||
|
id: 'contact-created',
|
||||||
|
title: 'Contact Created',
|
||||||
|
description: `${contactName} was added to the CRM`,
|
||||||
|
timestamp: new Date(contact.dateAdded || contact.createdAt).toLocaleString(),
|
||||||
|
icon: 'system',
|
||||||
|
variant: 'default',
|
||||||
|
_sort: new Date(contact.dateAdded || contact.createdAt).getTime(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date descending (newest first)
|
||||||
|
events.sort((a, b) => (b._sort || 0) - (a._sort || 0));
|
||||||
|
|
||||||
|
// Clean _sort from events
|
||||||
|
const cleanEvents = events.map(({ _sort, ...rest }) => rest);
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'DetailHeader',
|
||||||
|
props: {
|
||||||
|
title: contactName,
|
||||||
|
subtitle: email !== '—' ? email : phone,
|
||||||
|
entityId: contact.id,
|
||||||
|
status: contact.type || 'lead',
|
||||||
|
statusVariant: 'active',
|
||||||
|
},
|
||||||
|
children: ['tabs', 'layout'],
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
key: 'tabs',
|
||||||
|
type: 'TabGroup',
|
||||||
|
props: {
|
||||||
|
tabs: [
|
||||||
|
{ label: 'Timeline', value: 'timeline', count: cleanEvents.length },
|
||||||
|
{ label: 'Notes', value: 'notes', count: notes.length },
|
||||||
|
{ label: 'Tasks', value: 'tasks', count: tasks.length },
|
||||||
|
],
|
||||||
|
activeTab: 'timeline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
key: 'layout',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '33/67', gap: 'md' },
|
||||||
|
children: ['contactInfo', 'timeline'],
|
||||||
|
},
|
||||||
|
contactInfo: {
|
||||||
|
key: 'contactInfo',
|
||||||
|
type: 'KeyValueList',
|
||||||
|
props: {
|
||||||
|
items: [
|
||||||
|
{ label: 'Name', value: contactName },
|
||||||
|
{ label: 'Email', value: email },
|
||||||
|
{ label: 'Phone', value: phone },
|
||||||
|
{ label: 'Tags', value: (contact.tags || []).join(', ') || '—' },
|
||||||
|
{ label: 'Source', value: contact.source || '—' },
|
||||||
|
{ label: 'Added', value: contact.dateAdded ? new Date(contact.dateAdded).toLocaleDateString() : '—' },
|
||||||
|
],
|
||||||
|
compact: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
key: 'timeline',
|
||||||
|
type: 'Timeline',
|
||||||
|
props: {
|
||||||
|
events: cleanEvents.length > 0
|
||||||
|
? cleanEvents
|
||||||
|
: [{
|
||||||
|
id: 'placeholder',
|
||||||
|
title: 'No activity yet',
|
||||||
|
description: 'Notes, tasks, and activity will appear here',
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
icon: 'system',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
122
src/apps/templates/dashboard.template.ts
Normal file
122
src/apps/templates/dashboard.template.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildDashboardTree(data: {
|
||||||
|
recentContacts: any[];
|
||||||
|
pipelines: any[];
|
||||||
|
calendars: any[];
|
||||||
|
locationId: string;
|
||||||
|
}): UITree {
|
||||||
|
const contacts = data.recentContacts || [];
|
||||||
|
const pipelines = data.pipelines || [];
|
||||||
|
const calendars = data.calendars || [];
|
||||||
|
|
||||||
|
const totalContacts = contacts.length;
|
||||||
|
const totalPipelines = pipelines.length;
|
||||||
|
const totalCalendars = calendars.length;
|
||||||
|
|
||||||
|
// Recent contacts table rows
|
||||||
|
const contactRows = contacts.slice(0, 8).map((c: any) => ({
|
||||||
|
id: c.id || '',
|
||||||
|
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
|
||||||
|
email: c.email || '—',
|
||||||
|
phone: c.phone || '—',
|
||||||
|
added: c.dateAdded ? new Date(c.dateAdded).toLocaleDateString() : '—',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Pipeline summary rows
|
||||||
|
const pipelineRows = pipelines.slice(0, 5).map((p: any) => ({
|
||||||
|
id: p.id || '',
|
||||||
|
name: p.name || 'Untitled',
|
||||||
|
stages: (p.stages || []).length,
|
||||||
|
status: 'active',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: 'GHL Dashboard',
|
||||||
|
subtitle: 'Overview',
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Contacts', value: String(totalContacts) },
|
||||||
|
{ label: 'Pipelines', value: String(totalPipelines) },
|
||||||
|
{ label: 'Calendars', value: String(totalCalendars) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['metrics', 'layout'],
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
key: 'metrics',
|
||||||
|
type: 'StatsGrid',
|
||||||
|
props: { columns: 3 },
|
||||||
|
children: ['mContacts', 'mPipelines', 'mCalendars'],
|
||||||
|
},
|
||||||
|
mContacts: {
|
||||||
|
key: 'mContacts',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Contacts', value: String(totalContacts), color: 'blue' },
|
||||||
|
},
|
||||||
|
mPipelines: {
|
||||||
|
key: 'mPipelines',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Pipelines', value: String(totalPipelines), color: 'purple' },
|
||||||
|
},
|
||||||
|
mCalendars: {
|
||||||
|
key: 'mCalendars',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Calendars', value: String(totalCalendars), color: 'green' },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
key: 'layout',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '67/33', gap: 'md' },
|
||||||
|
children: ['contactsCard', 'pipelinesCard'],
|
||||||
|
},
|
||||||
|
contactsCard: {
|
||||||
|
key: 'contactsCard',
|
||||||
|
type: 'Card',
|
||||||
|
props: { title: 'Recent Contacts', padding: 'none' },
|
||||||
|
children: ['contactsTable'],
|
||||||
|
},
|
||||||
|
contactsTable: {
|
||||||
|
key: 'contactsTable',
|
||||||
|
type: 'DataTable',
|
||||||
|
props: {
|
||||||
|
columns: [
|
||||||
|
{ key: 'name', label: 'Name', format: 'avatar', sortable: true },
|
||||||
|
{ key: 'email', label: 'Email', format: 'email' },
|
||||||
|
{ key: 'phone', label: 'Phone', format: 'phone' },
|
||||||
|
{ key: 'added', label: 'Added', format: 'date' },
|
||||||
|
],
|
||||||
|
rows: contactRows,
|
||||||
|
emptyMessage: 'No contacts yet',
|
||||||
|
pageSize: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pipelinesCard: {
|
||||||
|
key: 'pipelinesCard',
|
||||||
|
type: 'Card',
|
||||||
|
props: { title: 'Pipelines', padding: 'none' },
|
||||||
|
children: ['pipelinesTable'],
|
||||||
|
},
|
||||||
|
pipelinesTable: {
|
||||||
|
key: 'pipelinesTable',
|
||||||
|
type: 'DataTable',
|
||||||
|
props: {
|
||||||
|
columns: [
|
||||||
|
{ key: 'name', label: 'Pipeline', format: 'text' },
|
||||||
|
{ key: 'stages', label: 'Stages', format: 'text' },
|
||||||
|
{ key: 'status', label: 'Status', format: 'status' },
|
||||||
|
],
|
||||||
|
rows: pipelineRows,
|
||||||
|
emptyMessage: 'No pipelines',
|
||||||
|
pageSize: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
11
src/apps/templates/index.ts
Normal file
11
src/apps/templates/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { buildContactGridTree } from './contact-grid.template.js';
|
||||||
|
export { buildPipelineBoardTree } from './pipeline-board.template.js';
|
||||||
|
export { buildQuickBookTree } from './quick-book.template.js';
|
||||||
|
export { buildOpportunityCardTree } from './opportunity-card.template.js';
|
||||||
|
export { buildCalendarViewTree } from './calendar-view.template.js';
|
||||||
|
export { buildInvoicePreviewTree } from './invoice-preview.template.js';
|
||||||
|
export { buildCampaignStatsTree } from './campaign-stats.template.js';
|
||||||
|
export { buildAgentStatsTree } from './agent-stats.template.js';
|
||||||
|
export { buildContactTimelineTree } from './contact-timeline.template.js';
|
||||||
|
export { buildWorkflowStatusTree } from './workflow-status.template.js';
|
||||||
|
export { buildDashboardTree } from './dashboard.template.js';
|
||||||
112
src/apps/templates/invoice-preview.template.ts
Normal file
112
src/apps/templates/invoice-preview.template.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildInvoicePreviewTree(data: any): UITree {
|
||||||
|
const invoice = data || {};
|
||||||
|
const contact = invoice.contact || invoice.contactDetails || {};
|
||||||
|
const businessInfo = invoice.businessDetails || {};
|
||||||
|
const items = invoice.items || invoice.lineItems || [];
|
||||||
|
const currency = invoice.currency || 'USD';
|
||||||
|
|
||||||
|
const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown';
|
||||||
|
const businessName = businessInfo.name || invoice.businessName || 'Business';
|
||||||
|
|
||||||
|
// Build line items
|
||||||
|
const lineItems = items.map((item: any) => ({
|
||||||
|
name: item.name || item.description || 'Item',
|
||||||
|
description: item.description !== item.name ? item.description : undefined,
|
||||||
|
quantity: item.quantity || item.qty || 1,
|
||||||
|
unitPrice: item.price || item.unitPrice || item.amount || 0,
|
||||||
|
total: (item.quantity || 1) * (item.price || item.unitPrice || item.amount || 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const subtotal = lineItems.reduce((s: number, i: any) => s + i.total, 0);
|
||||||
|
const discount = invoice.discount || 0;
|
||||||
|
const tax = invoice.taxAmount || invoice.tax || 0;
|
||||||
|
const total = invoice.total || invoice.amount || subtotal - discount + tax;
|
||||||
|
const amountDue = invoice.amountDue ?? total;
|
||||||
|
|
||||||
|
const fmtCurrency = (n: number) => {
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n);
|
||||||
|
} catch {
|
||||||
|
return `$${n.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const totals: Array<{ label: string; value: string; bold?: boolean; variant?: string; isTotalRow?: boolean }> = [
|
||||||
|
{ label: 'Subtotal', value: fmtCurrency(subtotal) },
|
||||||
|
];
|
||||||
|
if (discount > 0) {
|
||||||
|
totals.push({ label: 'Discount', value: `-${fmtCurrency(discount)}`, variant: 'danger' });
|
||||||
|
}
|
||||||
|
if (tax > 0) {
|
||||||
|
totals.push({ label: 'Tax', value: fmtCurrency(tax) });
|
||||||
|
}
|
||||||
|
totals.push({ label: 'Total', value: fmtCurrency(total), bold: true, isTotalRow: true });
|
||||||
|
if (amountDue !== total) {
|
||||||
|
totals.push({ label: 'Amount Due', value: fmtCurrency(amountDue), variant: 'highlight' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'DetailHeader',
|
||||||
|
props: {
|
||||||
|
title: `Invoice #${invoice.invoiceNumber || invoice.number || '—'}`,
|
||||||
|
subtitle: invoice.title || `For ${contactName}`,
|
||||||
|
entityId: invoice.id,
|
||||||
|
status: invoice.status || 'draft',
|
||||||
|
statusVariant: invoice.status === 'paid' ? 'paid' : invoice.status === 'sent' ? 'sent' : 'draft',
|
||||||
|
},
|
||||||
|
children: ['infoRow', 'lineItemsTable', 'totals'],
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
key: 'infoRow',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '50/50', gap: 'md' },
|
||||||
|
children: ['fromInfo', 'toInfo'],
|
||||||
|
},
|
||||||
|
fromInfo: {
|
||||||
|
key: 'fromInfo',
|
||||||
|
type: 'InfoBlock',
|
||||||
|
props: {
|
||||||
|
label: 'From',
|
||||||
|
name: businessName,
|
||||||
|
lines: [
|
||||||
|
businessInfo.email || '',
|
||||||
|
businessInfo.phone || '',
|
||||||
|
businessInfo.address || '',
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toInfo: {
|
||||||
|
key: 'toInfo',
|
||||||
|
type: 'InfoBlock',
|
||||||
|
props: {
|
||||||
|
label: 'To',
|
||||||
|
name: contactName,
|
||||||
|
lines: [
|
||||||
|
contact.email || '',
|
||||||
|
contact.phone || '',
|
||||||
|
contact.address || '',
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lineItemsTable: {
|
||||||
|
key: 'lineItemsTable',
|
||||||
|
type: 'LineItemsTable',
|
||||||
|
props: {
|
||||||
|
items: lineItems,
|
||||||
|
currency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
totals: {
|
||||||
|
key: 'totals',
|
||||||
|
type: 'KeyValueList',
|
||||||
|
props: { items: totals },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
135
src/apps/templates/opportunity-card.template.ts
Normal file
135
src/apps/templates/opportunity-card.template.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildOpportunityCardTree(data: any): UITree {
|
||||||
|
const opp = data || {};
|
||||||
|
const contact = opp.contact || {};
|
||||||
|
const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown';
|
||||||
|
const monetaryValue = opp.monetaryValue || 0;
|
||||||
|
|
||||||
|
const kvItems = [
|
||||||
|
{ label: 'Contact', value: contactName },
|
||||||
|
{ label: 'Email', value: contact.email || '—' },
|
||||||
|
{ label: 'Phone', value: contact.phone || '—' },
|
||||||
|
{ label: 'Value', value: `$${Number(monetaryValue).toLocaleString()}`, bold: true },
|
||||||
|
{ label: 'Status', value: (opp.status || 'open').charAt(0).toUpperCase() + (opp.status || 'open').slice(1) },
|
||||||
|
{ label: 'Source', value: opp.source || '—' },
|
||||||
|
{ label: 'Created', value: opp.createdAt ? new Date(opp.createdAt).toLocaleDateString() : '—' },
|
||||||
|
{ label: 'Updated', value: opp.updatedAt ? new Date(opp.updatedAt).toLocaleDateString() : '—' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build timeline from available data
|
||||||
|
const timelineEvents: any[] = [];
|
||||||
|
if (opp.createdAt) {
|
||||||
|
timelineEvents.push({
|
||||||
|
id: 'created',
|
||||||
|
title: 'Opportunity Created',
|
||||||
|
description: `Created with value $${Number(monetaryValue).toLocaleString()}`,
|
||||||
|
timestamp: new Date(opp.createdAt).toLocaleString(),
|
||||||
|
icon: 'system',
|
||||||
|
variant: 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (opp.updatedAt && opp.updatedAt !== opp.createdAt) {
|
||||||
|
timelineEvents.push({
|
||||||
|
id: 'updated',
|
||||||
|
title: 'Last Updated',
|
||||||
|
description: `Status: ${opp.status || 'open'}`,
|
||||||
|
timestamp: new Date(opp.updatedAt).toLocaleString(),
|
||||||
|
icon: 'note',
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (opp.lastStatusChangeAt) {
|
||||||
|
timelineEvents.push({
|
||||||
|
id: 'status-change',
|
||||||
|
title: 'Status Changed',
|
||||||
|
description: `Changed to ${opp.status || 'open'}`,
|
||||||
|
timestamp: new Date(opp.lastStatusChangeAt).toLocaleString(),
|
||||||
|
icon: 'task',
|
||||||
|
variant: opp.status === 'won' ? 'success' : opp.status === 'lost' ? 'error' : 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements: UITree['elements'] = {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'DetailHeader',
|
||||||
|
props: {
|
||||||
|
title: opp.name || 'Untitled Opportunity',
|
||||||
|
subtitle: contactName,
|
||||||
|
entityId: opp.id,
|
||||||
|
status: opp.status || 'open',
|
||||||
|
statusVariant: opp.status || 'open',
|
||||||
|
},
|
||||||
|
children: ['layout'],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
key: 'layout',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '50/50', gap: 'md' },
|
||||||
|
children: ['details', 'rightCol'],
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
key: 'details',
|
||||||
|
type: 'KeyValueList',
|
||||||
|
props: { items: kvItems, compact: true },
|
||||||
|
},
|
||||||
|
rightCol: {
|
||||||
|
key: 'rightCol',
|
||||||
|
type: 'Card',
|
||||||
|
props: { title: 'Activity', padding: 'sm' },
|
||||||
|
children: ['timeline'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timelineEvents.length > 0) {
|
||||||
|
elements.timeline = {
|
||||||
|
key: 'timeline',
|
||||||
|
type: 'Timeline',
|
||||||
|
props: { events: timelineEvents },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
elements.timeline = {
|
||||||
|
key: 'timeline',
|
||||||
|
type: 'Timeline',
|
||||||
|
props: {
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
id: 'placeholder',
|
||||||
|
title: 'No activity recorded',
|
||||||
|
description: 'Activity will appear here as events are logged',
|
||||||
|
timestamp: new Date().toLocaleString(),
|
||||||
|
icon: 'system',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add action bar
|
||||||
|
elements.actions = {
|
||||||
|
key: 'actions',
|
||||||
|
type: 'ActionBar',
|
||||||
|
props: { align: 'right' },
|
||||||
|
children: ['editBtn', 'statusBtn'],
|
||||||
|
};
|
||||||
|
elements.editBtn = {
|
||||||
|
key: 'editBtn',
|
||||||
|
type: 'ActionButton',
|
||||||
|
props: { label: 'Edit', variant: 'secondary', size: 'sm' },
|
||||||
|
};
|
||||||
|
elements.statusBtn = {
|
||||||
|
key: 'statusBtn',
|
||||||
|
type: 'ActionButton',
|
||||||
|
props: {
|
||||||
|
label: opp.status === 'won' ? 'Reopen' : 'Mark Won',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add actions to page children
|
||||||
|
elements.page.children!.push('actions');
|
||||||
|
|
||||||
|
return { root: 'page', elements };
|
||||||
|
}
|
||||||
67
src/apps/templates/pipeline-board.template.ts
Normal file
67
src/apps/templates/pipeline-board.template.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildPipelineBoardTree(data: {
|
||||||
|
pipeline: any;
|
||||||
|
opportunities: any[];
|
||||||
|
stages: any[];
|
||||||
|
}): UITree {
|
||||||
|
const pipeline = data.pipeline || {};
|
||||||
|
const opportunities = data.opportunities || [];
|
||||||
|
const stages = data.stages || [];
|
||||||
|
|
||||||
|
const totalValue = opportunities.reduce((s: number, o: any) => s + (o.monetaryValue || 0), 0);
|
||||||
|
const openCount = opportunities.filter((o: any) => o.status === 'open').length;
|
||||||
|
const wonCount = opportunities.filter((o: any) => o.status === 'won').length;
|
||||||
|
|
||||||
|
// Build kanban columns from real pipeline stages
|
||||||
|
const columns = stages.map((stage: any) => {
|
||||||
|
const stageOpps = opportunities.filter((o: any) => o.pipelineStageId === stage.id);
|
||||||
|
const stageValue = stageOpps.reduce((s: number, o: any) => s + (o.monetaryValue || 0), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: stage.id,
|
||||||
|
title: stage.name,
|
||||||
|
count: stageOpps.length,
|
||||||
|
totalValue: stageValue > 0 ? `$${stageValue.toLocaleString()}` : undefined,
|
||||||
|
cards: stageOpps.slice(0, 8).map((opp: any) => ({
|
||||||
|
id: opp.id,
|
||||||
|
title: opp.name || 'Untitled',
|
||||||
|
subtitle: opp.contact?.name || opp.contact?.email || undefined,
|
||||||
|
value: opp.monetaryValue ? `$${Number(opp.monetaryValue).toLocaleString()}` : undefined,
|
||||||
|
status: opp.status || 'open',
|
||||||
|
statusVariant: opp.status || 'open',
|
||||||
|
date: opp.updatedAt ? new Date(opp.updatedAt).toLocaleDateString() : undefined,
|
||||||
|
avatarInitials: opp.contact?.name
|
||||||
|
? opp.contact.name.split(' ').map((n: string) => n[0]).join('').slice(0, 2).toUpperCase()
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: pipeline.name || 'Pipeline',
|
||||||
|
subtitle: `${opportunities.length} opportunities`,
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Total Value', value: `$${totalValue.toLocaleString()}` },
|
||||||
|
{ label: 'Open', value: String(openCount) },
|
||||||
|
{ label: 'Won', value: String(wonCount) },
|
||||||
|
{ label: 'Stages', value: String(stages.length) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['board'],
|
||||||
|
},
|
||||||
|
board: {
|
||||||
|
key: 'board',
|
||||||
|
type: 'KanbanBoard',
|
||||||
|
props: { columns },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
91
src/apps/templates/quick-book.template.ts
Normal file
91
src/apps/templates/quick-book.template.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildQuickBookTree(data: {
|
||||||
|
calendar: any;
|
||||||
|
contact: any;
|
||||||
|
locationId: string;
|
||||||
|
}): UITree {
|
||||||
|
const calendar = data.calendar || {};
|
||||||
|
const contact = data.contact || null;
|
||||||
|
const contactName = contact
|
||||||
|
? `${contact.firstName || ''} ${contact.lastName || ''}`.trim()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: 'Quick Book',
|
||||||
|
subtitle: calendar.name || 'Appointment Booking',
|
||||||
|
gradient: true,
|
||||||
|
stats: contact
|
||||||
|
? [
|
||||||
|
{ label: 'Contact', value: contactName || 'Selected' },
|
||||||
|
{ label: 'Calendar', value: calendar.name || '—' },
|
||||||
|
]
|
||||||
|
: [{ label: 'Calendar', value: calendar.name || '—' }],
|
||||||
|
},
|
||||||
|
children: ['layout'],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
key: 'layout',
|
||||||
|
type: 'SplitLayout',
|
||||||
|
props: { ratio: '50/50', gap: 'md' },
|
||||||
|
children: ['calendarView', 'bookingForm'],
|
||||||
|
},
|
||||||
|
calendarView: {
|
||||||
|
key: 'calendarView',
|
||||||
|
type: 'CalendarView',
|
||||||
|
props: {
|
||||||
|
year: now.getFullYear(),
|
||||||
|
month: now.getMonth() + 1,
|
||||||
|
events: [],
|
||||||
|
highlightToday: true,
|
||||||
|
title: 'Select a Date',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bookingForm: {
|
||||||
|
key: 'bookingForm',
|
||||||
|
type: 'FormGroup',
|
||||||
|
props: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'contactId',
|
||||||
|
label: 'Contact',
|
||||||
|
type: 'text',
|
||||||
|
value: contactName || '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'date',
|
||||||
|
label: 'Date',
|
||||||
|
type: 'date',
|
||||||
|
value: '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'time',
|
||||||
|
label: 'Time',
|
||||||
|
type: 'text',
|
||||||
|
value: '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notes',
|
||||||
|
label: 'Notes',
|
||||||
|
type: 'text',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
submitLabel: 'Book Appointment',
|
||||||
|
submitTool: 'create_appointment',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
120
src/apps/templates/workflow-status.template.ts
Normal file
120
src/apps/templates/workflow-status.template.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { UITree } from '../types.js';
|
||||||
|
|
||||||
|
export function buildWorkflowStatusTree(data: {
|
||||||
|
workflow: any;
|
||||||
|
workflows: any[];
|
||||||
|
workflowId: string;
|
||||||
|
locationId: string;
|
||||||
|
}): UITree {
|
||||||
|
const workflow = data.workflow || {};
|
||||||
|
const workflows = data.workflows || [];
|
||||||
|
const wfName = workflow.name || 'Workflow';
|
||||||
|
|
||||||
|
// Build flow diagram from workflow structure if available
|
||||||
|
const flowNodes: any[] = [];
|
||||||
|
const flowEdges: any[] = [];
|
||||||
|
|
||||||
|
// Add trigger node
|
||||||
|
flowNodes.push({
|
||||||
|
id: 'trigger',
|
||||||
|
label: workflow.trigger?.type || 'Trigger',
|
||||||
|
type: 'start',
|
||||||
|
description: workflow.trigger?.name || 'Workflow trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
// If workflow has actions/steps, map them
|
||||||
|
const actions = workflow.actions || workflow.steps || [];
|
||||||
|
let prevId = 'trigger';
|
||||||
|
for (let i = 0; i < Math.min(actions.length, 8); i++) {
|
||||||
|
const action = actions[i];
|
||||||
|
const nodeId = `action-${i}`;
|
||||||
|
const isCondition = action.type === 'condition' || action.type === 'if_else';
|
||||||
|
|
||||||
|
flowNodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
label: action.name || action.type || `Step ${i + 1}`,
|
||||||
|
type: isCondition ? 'condition' : 'action',
|
||||||
|
description: action.description || undefined,
|
||||||
|
});
|
||||||
|
flowEdges.push({ from: prevId, to: nodeId });
|
||||||
|
prevId = nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End node
|
||||||
|
flowNodes.push({ id: 'end', label: 'End', type: 'end' });
|
||||||
|
flowEdges.push({ from: prevId, to: 'end' });
|
||||||
|
|
||||||
|
// If no actions were found, create a simple placeholder flow
|
||||||
|
if (actions.length === 0 && !workflow.trigger) {
|
||||||
|
flowNodes.length = 0;
|
||||||
|
flowEdges.length = 0;
|
||||||
|
flowNodes.push(
|
||||||
|
{ id: 'start', label: 'Start', type: 'start' },
|
||||||
|
{ id: 'process', label: wfName, type: 'action', description: workflow.status || 'Active' },
|
||||||
|
{ id: 'end', label: 'End', type: 'end' },
|
||||||
|
);
|
||||||
|
flowEdges.push(
|
||||||
|
{ from: 'start', to: 'process' },
|
||||||
|
{ from: 'process', to: 'end' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow stats
|
||||||
|
const activeCount = workflows.filter((w: any) => w.status === 'active').length;
|
||||||
|
const draftCount = workflows.filter((w: any) => w.status === 'draft').length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: 'page',
|
||||||
|
elements: {
|
||||||
|
page: {
|
||||||
|
key: 'page',
|
||||||
|
type: 'PageHeader',
|
||||||
|
props: {
|
||||||
|
title: wfName,
|
||||||
|
subtitle: 'Workflow Status',
|
||||||
|
status: workflow.status || 'active',
|
||||||
|
statusVariant: workflow.status === 'active' ? 'active' : 'draft',
|
||||||
|
gradient: true,
|
||||||
|
stats: [
|
||||||
|
{ label: 'Status', value: (workflow.status || 'active').charAt(0).toUpperCase() + (workflow.status || 'active').slice(1) },
|
||||||
|
{ label: 'Total Workflows', value: String(workflows.length) },
|
||||||
|
{ label: 'Active', value: String(activeCount) },
|
||||||
|
{ label: 'Draft', value: String(draftCount) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: ['flow', 'statsGrid'],
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
key: 'flow',
|
||||||
|
type: 'FlowDiagram',
|
||||||
|
props: {
|
||||||
|
nodes: flowNodes,
|
||||||
|
edges: flowEdges,
|
||||||
|
direction: 'horizontal',
|
||||||
|
title: `${wfName} Flow`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statsGrid: {
|
||||||
|
key: 'statsGrid',
|
||||||
|
type: 'StatsGrid',
|
||||||
|
props: { columns: 3 },
|
||||||
|
children: ['sActive', 'sDraft', 'sTotal'],
|
||||||
|
},
|
||||||
|
sActive: {
|
||||||
|
key: 'sActive',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Active Workflows', value: String(activeCount), color: 'green' },
|
||||||
|
},
|
||||||
|
sDraft: {
|
||||||
|
key: 'sDraft',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Draft Workflows', value: String(draftCount), color: 'yellow' },
|
||||||
|
},
|
||||||
|
sTotal: {
|
||||||
|
key: 'sTotal',
|
||||||
|
type: 'MetricCard',
|
||||||
|
props: { label: 'Total', value: String(workflows.length), color: 'blue' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
397
src/apps/types.ts
Normal file
397
src/apps/types.ts
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
/**
|
||||||
|
* UITree Type Definitions for GHL MCP Apps
|
||||||
|
* Shared types for the universal renderer and template system.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─── Core UITree Types ──────────────────────────────────────
|
||||||
|
|
||||||
|
export interface UIElement {
|
||||||
|
key: string;
|
||||||
|
type: ComponentType;
|
||||||
|
props: Record<string, any>;
|
||||||
|
children?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UITree {
|
||||||
|
root: string;
|
||||||
|
elements: Record<string, UIElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component Type Union ───────────────────────────────────
|
||||||
|
|
||||||
|
export type ComponentType =
|
||||||
|
// Layout
|
||||||
|
| 'PageHeader'
|
||||||
|
| 'Card'
|
||||||
|
| 'StatsGrid'
|
||||||
|
| 'SplitLayout'
|
||||||
|
| 'Section'
|
||||||
|
// Data Display
|
||||||
|
| 'DataTable'
|
||||||
|
| 'KanbanBoard'
|
||||||
|
| 'MetricCard'
|
||||||
|
| 'StatusBadge'
|
||||||
|
| 'Timeline'
|
||||||
|
| 'ProgressBar'
|
||||||
|
// Detail View
|
||||||
|
| 'DetailHeader'
|
||||||
|
| 'KeyValueList'
|
||||||
|
| 'LineItemsTable'
|
||||||
|
| 'InfoBlock'
|
||||||
|
// Interactive
|
||||||
|
| 'SearchBar'
|
||||||
|
| 'FilterChips'
|
||||||
|
| 'TabGroup'
|
||||||
|
| 'ActionButton'
|
||||||
|
| 'ActionBar'
|
||||||
|
// Extended Data Display
|
||||||
|
| 'CurrencyDisplay'
|
||||||
|
| 'TagList'
|
||||||
|
| 'CardGrid'
|
||||||
|
| 'AvatarGroup'
|
||||||
|
| 'StarRating'
|
||||||
|
| 'StockIndicator'
|
||||||
|
// Communication
|
||||||
|
| 'ChatThread'
|
||||||
|
| 'EmailPreview'
|
||||||
|
| 'ContentPreview'
|
||||||
|
| 'TranscriptView'
|
||||||
|
| 'AudioPlayer'
|
||||||
|
| 'ChecklistView'
|
||||||
|
// Visualization
|
||||||
|
| 'CalendarView'
|
||||||
|
| 'FlowDiagram'
|
||||||
|
| 'TreeView'
|
||||||
|
| 'MediaGallery'
|
||||||
|
| 'DuplicateCompare'
|
||||||
|
// Charts
|
||||||
|
| 'BarChart'
|
||||||
|
| 'LineChart'
|
||||||
|
| 'PieChart'
|
||||||
|
| 'FunnelChart'
|
||||||
|
| 'SparklineChart'
|
||||||
|
// Interactive Editors
|
||||||
|
| 'ContactPicker'
|
||||||
|
| 'InvoiceBuilder'
|
||||||
|
| 'OpportunityEditor'
|
||||||
|
| 'AppointmentBooker'
|
||||||
|
| 'EditableField'
|
||||||
|
| 'SelectDropdown'
|
||||||
|
| 'FormGroup'
|
||||||
|
| 'AmountInput';
|
||||||
|
|
||||||
|
// ─── All valid component names for validation ───────────────
|
||||||
|
|
||||||
|
export const VALID_COMPONENT_TYPES: ReadonlySet<string> = new Set<ComponentType>([
|
||||||
|
'PageHeader', 'Card', 'StatsGrid', 'SplitLayout', 'Section',
|
||||||
|
'DataTable', 'KanbanBoard', 'MetricCard', 'StatusBadge', 'Timeline', 'ProgressBar',
|
||||||
|
'DetailHeader', 'KeyValueList', 'LineItemsTable', 'InfoBlock',
|
||||||
|
'SearchBar', 'FilterChips', 'TabGroup', 'ActionButton', 'ActionBar',
|
||||||
|
'CurrencyDisplay', 'TagList', 'CardGrid', 'AvatarGroup', 'StarRating', 'StockIndicator',
|
||||||
|
'ChatThread', 'EmailPreview', 'ContentPreview', 'TranscriptView', 'AudioPlayer', 'ChecklistView',
|
||||||
|
'CalendarView', 'FlowDiagram', 'TreeView', 'MediaGallery', 'DuplicateCompare',
|
||||||
|
'BarChart', 'LineChart', 'PieChart', 'FunnelChart', 'SparklineChart',
|
||||||
|
'ContactPicker', 'InvoiceBuilder', 'OpportunityEditor', 'AppointmentBooker',
|
||||||
|
'EditableField', 'SelectDropdown', 'FormGroup', 'AmountInput',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ─── Components that can contain children ───────────────────
|
||||||
|
|
||||||
|
export const CONTAINER_COMPONENTS: ReadonlySet<string> = new Set<ComponentType>([
|
||||||
|
'PageHeader', 'Card', 'StatsGrid', 'SplitLayout', 'Section',
|
||||||
|
'DetailHeader', 'ActionBar',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ─── Required props per component (minimal set) ─────────────
|
||||||
|
|
||||||
|
export const REQUIRED_PROPS: Readonly<Record<string, readonly string[]>> = {
|
||||||
|
PageHeader: ['title'],
|
||||||
|
DataTable: ['columns', 'rows'],
|
||||||
|
KanbanBoard: ['columns'],
|
||||||
|
MetricCard: ['label', 'value'],
|
||||||
|
StatusBadge: ['label', 'variant'],
|
||||||
|
Timeline: ['events'],
|
||||||
|
ProgressBar: ['label', 'value'],
|
||||||
|
DetailHeader: ['title'],
|
||||||
|
KeyValueList: ['items'],
|
||||||
|
LineItemsTable: ['items'],
|
||||||
|
InfoBlock: ['label', 'name', 'lines'],
|
||||||
|
CalendarView: [],
|
||||||
|
FlowDiagram: ['nodes', 'edges'],
|
||||||
|
BarChart: ['bars'],
|
||||||
|
LineChart: ['points'],
|
||||||
|
PieChart: ['segments'],
|
||||||
|
FunnelChart: ['stages'],
|
||||||
|
SparklineChart: ['values'],
|
||||||
|
CurrencyDisplay: ['amount'],
|
||||||
|
TagList: ['tags'],
|
||||||
|
CardGrid: ['cards'],
|
||||||
|
AvatarGroup: ['avatars'],
|
||||||
|
StarRating: ['rating'],
|
||||||
|
StockIndicator: ['quantity'],
|
||||||
|
ChatThread: ['messages'],
|
||||||
|
EmailPreview: ['from', 'to', 'subject', 'date', 'body'],
|
||||||
|
ChecklistView: ['items'],
|
||||||
|
FormGroup: ['fields'],
|
||||||
|
AmountInput: ['value'],
|
||||||
|
EditableField: ['value', 'fieldName'],
|
||||||
|
OpportunityEditor: ['saveTool', 'opportunity'],
|
||||||
|
ContactPicker: ['searchTool'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Prop Interfaces for Template-Used Components ───────────
|
||||||
|
|
||||||
|
export interface PageHeaderProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
status?: string;
|
||||||
|
statusVariant?: 'active' | 'complete' | 'paused' | 'draft' | 'error' | 'sent' | 'paid' | 'pending';
|
||||||
|
gradient?: boolean;
|
||||||
|
stats?: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableColumn {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
sortable?: boolean;
|
||||||
|
align?: string;
|
||||||
|
format?: 'text' | 'email' | 'phone' | 'date' | 'currency' | 'tags' | 'avatar' | 'status';
|
||||||
|
width?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableProps {
|
||||||
|
columns: DataTableColumn[];
|
||||||
|
rows: Record<string, any>[];
|
||||||
|
selectable?: boolean;
|
||||||
|
rowAction?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KanbanCard {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
value?: string;
|
||||||
|
status?: string;
|
||||||
|
statusVariant?: string;
|
||||||
|
date?: string;
|
||||||
|
avatarInitials?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KanbanColumn {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
count?: number;
|
||||||
|
totalValue?: string;
|
||||||
|
color?: string;
|
||||||
|
cards: KanbanCard[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KanbanBoardProps {
|
||||||
|
columns: KanbanColumn[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricCardProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
format?: 'number' | 'currency' | 'percent';
|
||||||
|
trend?: 'up' | 'down' | 'flat';
|
||||||
|
trendValue?: string;
|
||||||
|
color?: 'default' | 'green' | 'blue' | 'purple' | 'yellow' | 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineEvent {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
timestamp: string;
|
||||||
|
icon?: 'email' | 'phone' | 'note' | 'meeting' | 'task' | 'system';
|
||||||
|
variant?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineProps {
|
||||||
|
events: TimelineEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyValueItem {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
bold?: boolean;
|
||||||
|
variant?: 'default' | 'highlight' | 'muted' | 'success' | 'danger';
|
||||||
|
isTotalRow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyValueListProps {
|
||||||
|
items: KeyValueItem[];
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineItem {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
quantity: number;
|
||||||
|
unitPrice: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineItemsTableProps {
|
||||||
|
items: LineItem[];
|
||||||
|
currency?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InfoBlockProps {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
lines: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DetailHeaderProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
entityId?: string;
|
||||||
|
status?: string;
|
||||||
|
statusVariant?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchBarProps {
|
||||||
|
placeholder?: string;
|
||||||
|
valuePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendarEvent {
|
||||||
|
date: string;
|
||||||
|
title: string;
|
||||||
|
time?: string;
|
||||||
|
color?: string;
|
||||||
|
type?: 'meeting' | 'call' | 'task' | 'deadline' | 'event';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendarViewProps {
|
||||||
|
year?: number;
|
||||||
|
month?: number;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
highlightToday?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowNode {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
type?: 'start' | 'action' | 'condition' | 'end';
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowEdge {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowDiagramProps {
|
||||||
|
nodes: FlowNode[];
|
||||||
|
edges: FlowEdge[];
|
||||||
|
direction?: 'horizontal' | 'vertical';
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BarChartBar {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BarChartProps {
|
||||||
|
bars: BarChartBar[];
|
||||||
|
orientation?: 'vertical' | 'horizontal';
|
||||||
|
maxValue?: number;
|
||||||
|
showValues?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineChartPoint {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineChartProps {
|
||||||
|
points: LineChartPoint[];
|
||||||
|
color?: string;
|
||||||
|
showPoints?: boolean;
|
||||||
|
showArea?: boolean;
|
||||||
|
title?: string;
|
||||||
|
yAxisLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── UITree Validation ──────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ValidationError {
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a UITree for correctness:
|
||||||
|
* - Root key exists in elements
|
||||||
|
* - All children references resolve
|
||||||
|
* - All component types are valid
|
||||||
|
* - Required props are present
|
||||||
|
*/
|
||||||
|
export function validateUITree(tree: UITree): ValidationError[] {
|
||||||
|
const errors: ValidationError[] = [];
|
||||||
|
|
||||||
|
if (!tree || typeof tree !== 'object') {
|
||||||
|
errors.push({ path: '', message: 'UITree must be a non-null object' });
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.root) {
|
||||||
|
errors.push({ path: 'root', message: 'Missing root key' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.elements || typeof tree.elements !== 'object') {
|
||||||
|
errors.push({ path: 'elements', message: 'Missing or invalid elements map' });
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check root exists in elements
|
||||||
|
if (tree.root && !tree.elements[tree.root]) {
|
||||||
|
errors.push({ path: 'root', message: `Root key "${tree.root}" not found in elements` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each element
|
||||||
|
for (const [key, element] of Object.entries(tree.elements)) {
|
||||||
|
const ePath = `elements.${key}`;
|
||||||
|
|
||||||
|
// Check key matches
|
||||||
|
if (element.key !== key) {
|
||||||
|
errors.push({ path: ePath, message: `Element key mismatch: "${element.key}" vs map key "${key}"` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check component type
|
||||||
|
if (!VALID_COMPONENT_TYPES.has(element.type)) {
|
||||||
|
errors.push({ path: `${ePath}.type`, message: `Unknown component type: "${element.type}"` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required props
|
||||||
|
const requiredProps = REQUIRED_PROPS[element.type];
|
||||||
|
if (requiredProps) {
|
||||||
|
for (const prop of requiredProps) {
|
||||||
|
if (element.props[prop] === undefined || element.props[prop] === null) {
|
||||||
|
errors.push({ path: `${ePath}.props.${prop}`, message: `Missing required prop "${prop}" for ${element.type}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children references
|
||||||
|
if (element.children) {
|
||||||
|
for (const childKey of element.children) {
|
||||||
|
if (!tree.elements[childKey]) {
|
||||||
|
errors.push({ path: `${ePath}.children`, message: `Child reference "${childKey}" not found in elements` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
@ -22,6 +22,13 @@ export class AffiliatesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -31,7 +38,14 @@ export class AffiliatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Affiliate Campaign ID' },
|
campaignId: { type: 'string', description: 'Affiliate Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -48,7 +62,14 @@ export class AffiliatesTools {
|
|||||||
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
|
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
|
||||||
commissionValue: { type: 'number', description: 'Commission value (percentage or fixed amount)' },
|
commissionValue: { type: 'number', description: 'Commission value (percentage or fixed amount)' },
|
||||||
cookieDays: { type: 'number', description: 'Cookie tracking duration in days' },
|
cookieDays: { type: 'number', description: 'Cookie tracking duration in days' },
|
||||||
productIds: { type: 'array', items: { type: 'string' }, description: 'Product IDs for this campaign' }
|
productIds: { type: 'array', items: { type: 'string' }, description: 'Product IDs for this campaign' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'commissionType', 'commissionValue']
|
required: ['name', 'commissionType', 'commissionValue']
|
||||||
}
|
}
|
||||||
@ -65,7 +86,14 @@ export class AffiliatesTools {
|
|||||||
description: { type: 'string', description: 'Campaign description' },
|
description: { type: 'string', description: 'Campaign description' },
|
||||||
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
|
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
|
||||||
commissionValue: { type: 'number', description: 'Commission value' },
|
commissionValue: { type: 'number', description: 'Commission value' },
|
||||||
status: { type: 'string', enum: ['active', 'inactive'], description: 'Campaign status' }
|
status: { type: 'string', enum: ['active', 'inactive'], description: 'Campaign status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -77,7 +105,14 @@ export class AffiliatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -96,6 +131,13 @@ export class AffiliatesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -105,7 +147,14 @@ export class AffiliatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -120,7 +169,14 @@ export class AffiliatesTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID to make affiliate' },
|
contactId: { type: 'string', description: 'Contact ID to make affiliate' },
|
||||||
campaignId: { type: 'string', description: 'Campaign to assign to' },
|
campaignId: { type: 'string', description: 'Campaign to assign to' },
|
||||||
customCode: { type: 'string', description: 'Custom affiliate code' },
|
customCode: { type: 'string', description: 'Custom affiliate code' },
|
||||||
status: { type: 'string', enum: ['pending', 'approved'], description: 'Initial status' }
|
status: { type: 'string', enum: ['pending', 'approved'], description: 'Initial status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId', 'campaignId']
|
required: ['contactId', 'campaignId']
|
||||||
}
|
}
|
||||||
@ -134,7 +190,14 @@ export class AffiliatesTools {
|
|||||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
status: { type: 'string', enum: ['pending', 'approved', 'rejected'], description: 'Status' },
|
status: { type: 'string', enum: ['pending', 'approved', 'rejected'], description: 'Status' },
|
||||||
customCode: { type: 'string', description: 'Custom affiliate code' }
|
customCode: { type: 'string', description: 'Custom affiliate code' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -146,7 +209,14 @@ export class AffiliatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -159,7 +229,14 @@ export class AffiliatesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
reason: { type: 'string', description: 'Rejection reason' }
|
reason: { type: 'string', description: 'Rejection reason' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -171,7 +248,14 @@ export class AffiliatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -190,7 +274,14 @@ export class AffiliatesTools {
|
|||||||
startDate: { type: 'string', description: 'Start date' },
|
startDate: { type: 'string', description: 'Start date' },
|
||||||
endDate: { type: 'string', description: 'End date' },
|
endDate: { type: 'string', description: 'End date' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -204,7 +295,14 @@ export class AffiliatesTools {
|
|||||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
startDate: { type: 'string', description: 'Start date' },
|
startDate: { type: 'string', description: 'Start date' },
|
||||||
endDate: { type: 'string', description: 'End date' }
|
endDate: { type: 'string', description: 'End date' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId']
|
required: ['affiliateId']
|
||||||
}
|
}
|
||||||
@ -219,7 +317,14 @@ export class AffiliatesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
amount: { type: 'number', description: 'Payout amount' },
|
amount: { type: 'number', description: 'Payout amount' },
|
||||||
commissionIds: { type: 'array', items: { type: 'string' }, description: 'Commission IDs to include' },
|
commissionIds: { type: 'array', items: { type: 'string' }, description: 'Commission IDs to include' },
|
||||||
note: { type: 'string', description: 'Payout note' }
|
note: { type: 'string', description: 'Payout note' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['affiliateId', 'amount']
|
required: ['affiliateId', 'amount']
|
||||||
}
|
}
|
||||||
@ -236,6 +341,13 @@ export class AffiliatesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "affiliates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,13 @@ export class AssociationTools {
|
|||||||
default: 20
|
default: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -67,7 +74,14 @@ export class AssociationTools {
|
|||||||
},
|
},
|
||||||
secondObjectKey: {
|
secondObjectKey: {
|
||||||
description: 'Key for the second object (e.g., "contact")'
|
description: 'Key for the second object (e.g., "contact")'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['key', 'firstObjectLabel', 'firstObjectKey', 'secondObjectLabel', 'secondObjectKey']
|
required: ['key', 'firstObjectLabel', 'firstObjectKey', 'secondObjectLabel', 'secondObjectKey']
|
||||||
}
|
}
|
||||||
@ -81,7 +95,14 @@ export class AssociationTools {
|
|||||||
associationId: {
|
associationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The ID of the association to retrieve'
|
description: 'The ID of the association to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['associationId']
|
required: ['associationId']
|
||||||
}
|
}
|
||||||
@ -101,7 +122,14 @@ export class AssociationTools {
|
|||||||
},
|
},
|
||||||
secondObjectLabel: {
|
secondObjectLabel: {
|
||||||
description: 'New label for the second object in the association'
|
description: 'New label for the second object in the association'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['associationId', 'firstObjectLabel', 'secondObjectLabel']
|
required: ['associationId', 'firstObjectLabel', 'secondObjectLabel']
|
||||||
}
|
}
|
||||||
@ -115,7 +143,14 @@ export class AssociationTools {
|
|||||||
associationId: {
|
associationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The ID of the association to delete'
|
description: 'The ID of the association to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['associationId']
|
required: ['associationId']
|
||||||
}
|
}
|
||||||
@ -133,7 +168,14 @@ export class AssociationTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['keyName']
|
required: ['keyName']
|
||||||
}
|
}
|
||||||
@ -151,7 +193,14 @@ export class AssociationTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (optional)'
|
description: 'GoHighLevel location ID (optional)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['objectKey']
|
required: ['objectKey']
|
||||||
}
|
}
|
||||||
@ -178,7 +227,14 @@ export class AssociationTools {
|
|||||||
secondRecordId: {
|
secondRecordId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'ID of the second record (e.g., custom object record ID if custom object is second object)'
|
description: 'ID of the second record (e.g., custom object record ID if custom object is second object)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['associationId', 'firstRecordId', 'secondRecordId']
|
required: ['associationId', 'firstRecordId', 'secondRecordId']
|
||||||
}
|
}
|
||||||
@ -213,7 +269,14 @@ export class AssociationTools {
|
|||||||
type: 'string'
|
type: 'string'
|
||||||
},
|
},
|
||||||
description: 'Optional array of association IDs to filter relations'
|
description: 'Optional array of association IDs to filter relations'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['recordId']
|
required: ['recordId']
|
||||||
}
|
}
|
||||||
@ -231,7 +294,14 @@ export class AssociationTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "associations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['relationId']
|
required: ['relationId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,13 @@ export class BlogTools {
|
|||||||
publishedAt: {
|
publishedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Optional ISO timestamp for publication date (defaults to now for PUBLISHED status)'
|
description: 'Optional ISO timestamp for publication date (defaults to now for PUBLISHED status)'
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['title', 'blogId', 'content', 'description', 'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories']
|
required: ['title', 'blogId', 'content', 'description', 'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories']
|
||||||
@ -165,6 +172,13 @@ export class BlogTools {
|
|||||||
publishedAt: {
|
publishedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Updated ISO timestamp for publication date'
|
description: 'Updated ISO timestamp for publication date'
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['postId', 'blogId']
|
required: ['postId', 'blogId']
|
||||||
@ -200,6 +214,13 @@ export class BlogTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'],
|
enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'],
|
||||||
description: 'Optional filter by publication status'
|
description: 'Optional filter by publication status'
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['blogId']
|
required: ['blogId']
|
||||||
@ -228,7 +249,14 @@ export class BlogTools {
|
|||||||
description: 'Optional search term to filter blogs by name'
|
description: 'Optional search term to filter blogs by name'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 5. Get Blog Authors
|
// 5. Get Blog Authors
|
||||||
@ -249,7 +277,14 @@ export class BlogTools {
|
|||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 6. Get Blog Categories
|
// 6. Get Blog Categories
|
||||||
@ -270,7 +305,14 @@ export class BlogTools {
|
|||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 7. Check URL Slug
|
// 7. Check URL Slug
|
||||||
@ -287,6 +329,13 @@ export class BlogTools {
|
|||||||
postId: {
|
postId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Optional post ID when updating an existing post (to exclude itself from the check)'
|
description: 'Optional post ID when updating an existing post (to exclude itself from the check)'
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "blogs",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['urlSlug']
|
required: ['urlSlug']
|
||||||
|
|||||||
@ -21,6 +21,13 @@ export class BusinessesTools {
|
|||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "businesses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -36,7 +43,14 @@ export class BusinessesTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "businesses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['businessId']
|
required: ['businessId']
|
||||||
}
|
}
|
||||||
@ -94,7 +108,14 @@ export class BusinessesTools {
|
|||||||
logoUrl: {
|
logoUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'URL to business logo image'
|
description: 'URL to business logo image'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "businesses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name']
|
required: ['name']
|
||||||
}
|
}
|
||||||
@ -156,7 +177,14 @@ export class BusinessesTools {
|
|||||||
logoUrl: {
|
logoUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'URL to business logo image'
|
description: 'URL to business logo image'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "businesses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['businessId']
|
required: ['businessId']
|
||||||
}
|
}
|
||||||
@ -174,7 +202,14 @@ export class BusinessesTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "businesses",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['businessId']
|
required: ['businessId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,13 @@ export class CalendarTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {}
|
properties: {}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,6 +87,13 @@ export class CalendarTools {
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -136,7 +150,14 @@ export class CalendarTools {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Make calendar active immediately (default: true)',
|
description: 'Make calendar active immediately (default: true)',
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'calendarType']
|
required: ['name', 'calendarType']
|
||||||
}
|
}
|
||||||
@ -150,7 +171,14 @@ export class CalendarTools {
|
|||||||
calendarId: {
|
calendarId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the calendar to retrieve'
|
description: 'The unique ID of the calendar to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId']
|
required: ['calendarId']
|
||||||
}
|
}
|
||||||
@ -192,7 +220,14 @@ export class CalendarTools {
|
|||||||
isActive: {
|
isActive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Updated active status'
|
description: 'Updated active status'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId']
|
required: ['calendarId']
|
||||||
}
|
}
|
||||||
@ -206,7 +241,14 @@ export class CalendarTools {
|
|||||||
calendarId: {
|
calendarId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the calendar to delete'
|
description: 'The unique ID of the calendar to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId']
|
required: ['calendarId']
|
||||||
}
|
}
|
||||||
@ -236,7 +278,14 @@ export class CalendarTools {
|
|||||||
groupId: {
|
groupId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Filter events by calendar group ID'
|
description: 'Filter events by calendar group ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startTime', 'endTime']
|
required: ['startTime', 'endTime']
|
||||||
}
|
}
|
||||||
@ -266,7 +315,14 @@ export class CalendarTools {
|
|||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Specific user ID to check availability for'
|
description: 'Specific user ID to check availability for'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId', 'startDate', 'endDate']
|
required: ['calendarId', 'startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -326,7 +382,14 @@ export class CalendarTools {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Send notifications for this appointment',
|
description: 'Send notifications for this appointment',
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId', 'contactId', 'startTime']
|
required: ['calendarId', 'contactId', 'startTime']
|
||||||
}
|
}
|
||||||
@ -340,7 +403,14 @@ export class CalendarTools {
|
|||||||
appointmentId: {
|
appointmentId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the appointment to retrieve'
|
description: 'The unique ID of the appointment to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId']
|
required: ['appointmentId']
|
||||||
}
|
}
|
||||||
@ -384,7 +454,14 @@ export class CalendarTools {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Send notifications for this update',
|
description: 'Send notifications for this update',
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId']
|
required: ['appointmentId']
|
||||||
}
|
}
|
||||||
@ -398,7 +475,14 @@ export class CalendarTools {
|
|||||||
appointmentId: {
|
appointmentId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the appointment to delete'
|
description: 'The unique ID of the appointment to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId']
|
required: ['appointmentId']
|
||||||
}
|
}
|
||||||
@ -428,7 +512,14 @@ export class CalendarTools {
|
|||||||
assignedUserId: {
|
assignedUserId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'User ID to apply the block for'
|
description: 'User ID to apply the block for'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startTime', 'endTime']
|
required: ['startTime', 'endTime']
|
||||||
}
|
}
|
||||||
@ -462,7 +553,14 @@ export class CalendarTools {
|
|||||||
assignedUserId: {
|
assignedUserId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Updated assigned user ID'
|
description: 'Updated assigned user ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['blockSlotId']
|
required: ['blockSlotId']
|
||||||
}
|
}
|
||||||
@ -476,7 +574,14 @@ export class CalendarTools {
|
|||||||
name: { type: 'string', description: 'Group name' },
|
name: { type: 'string', description: 'Group name' },
|
||||||
description: { type: 'string', description: 'Group description' },
|
description: { type: 'string', description: 'Group description' },
|
||||||
slug: { type: 'string', description: 'URL slug for the group' },
|
slug: { type: 'string', description: 'URL slug for the group' },
|
||||||
isActive: { type: 'boolean', description: 'Whether group is active', default: true }
|
isActive: { type: 'boolean', description: 'Whether group is active', default: true },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'description', 'slug']
|
required: ['name', 'description', 'slug']
|
||||||
}
|
}
|
||||||
@ -488,7 +593,14 @@ export class CalendarTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
slug: { type: 'string', description: 'Slug to validate' },
|
slug: { type: 'string', description: 'Slug to validate' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['slug']
|
required: ['slug']
|
||||||
}
|
}
|
||||||
@ -502,7 +614,14 @@ export class CalendarTools {
|
|||||||
groupId: { type: 'string', description: 'Calendar group ID' },
|
groupId: { type: 'string', description: 'Calendar group ID' },
|
||||||
name: { type: 'string', description: 'Group name' },
|
name: { type: 'string', description: 'Group name' },
|
||||||
description: { type: 'string', description: 'Group description' },
|
description: { type: 'string', description: 'Group description' },
|
||||||
slug: { type: 'string', description: 'URL slug for the group' }
|
slug: { type: 'string', description: 'URL slug for the group' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['groupId', 'name', 'description', 'slug']
|
required: ['groupId', 'name', 'description', 'slug']
|
||||||
}
|
}
|
||||||
@ -513,7 +632,14 @@ export class CalendarTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
groupId: { type: 'string', description: 'Calendar group ID' }
|
groupId: { type: 'string', description: 'Calendar group ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['groupId']
|
required: ['groupId']
|
||||||
}
|
}
|
||||||
@ -525,7 +651,14 @@ export class CalendarTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
groupId: { type: 'string', description: 'Calendar group ID' },
|
groupId: { type: 'string', description: 'Calendar group ID' },
|
||||||
isActive: { type: 'boolean', description: 'Whether to enable (true) or disable (false) the group' }
|
isActive: { type: 'boolean', description: 'Whether to enable (true) or disable (false) the group' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['groupId', 'isActive']
|
required: ['groupId', 'isActive']
|
||||||
}
|
}
|
||||||
@ -538,7 +671,14 @@ export class CalendarTools {
|
|||||||
properties: {
|
properties: {
|
||||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||||
limit: { type: 'number', description: 'Maximum number of notes to return', default: 10 },
|
limit: { type: 'number', description: 'Maximum number of notes to return', default: 10 },
|
||||||
offset: { type: 'number', description: 'Number of notes to skip', default: 0 }
|
offset: { type: 'number', description: 'Number of notes to skip', default: 0 },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId']
|
required: ['appointmentId']
|
||||||
}
|
}
|
||||||
@ -551,7 +691,14 @@ export class CalendarTools {
|
|||||||
properties: {
|
properties: {
|
||||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||||
body: { type: 'string', description: 'Note content' },
|
body: { type: 'string', description: 'Note content' },
|
||||||
userId: { type: 'string', description: 'User ID creating the note' }
|
userId: { type: 'string', description: 'User ID creating the note' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId', 'body']
|
required: ['appointmentId', 'body']
|
||||||
}
|
}
|
||||||
@ -565,7 +712,14 @@ export class CalendarTools {
|
|||||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||||
noteId: { type: 'string', description: 'Note ID' },
|
noteId: { type: 'string', description: 'Note ID' },
|
||||||
body: { type: 'string', description: 'Updated note content' },
|
body: { type: 'string', description: 'Updated note content' },
|
||||||
userId: { type: 'string', description: 'User ID updating the note' }
|
userId: { type: 'string', description: 'User ID updating the note' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId', 'noteId', 'body']
|
required: ['appointmentId', 'noteId', 'body']
|
||||||
}
|
}
|
||||||
@ -577,7 +731,14 @@ export class CalendarTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||||
noteId: { type: 'string', description: 'Note ID' }
|
noteId: { type: 'string', description: 'Note ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appointmentId', 'noteId']
|
required: ['appointmentId', 'noteId']
|
||||||
}
|
}
|
||||||
@ -591,6 +752,13 @@ export class CalendarTools {
|
|||||||
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
|
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
|
||||||
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -604,7 +772,14 @@ export class CalendarTools {
|
|||||||
quantity: { type: 'number', description: 'Total quantity available' },
|
quantity: { type: 'number', description: 'Total quantity available' },
|
||||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||||
capacity: { type: 'number', description: 'Capacity per unit' },
|
capacity: { type: 'number', description: 'Capacity per unit' },
|
||||||
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' }
|
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
|
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
|
||||||
}
|
}
|
||||||
@ -615,7 +790,14 @@ export class CalendarTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
resourceId: { type: 'string', description: 'Equipment resource ID' }
|
resourceId: { type: 'string', description: 'Equipment resource ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['resourceId']
|
required: ['resourceId']
|
||||||
}
|
}
|
||||||
@ -633,7 +815,14 @@ export class CalendarTools {
|
|||||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||||
capacity: { type: 'number', description: 'Capacity per unit' },
|
capacity: { type: 'number', description: 'Capacity per unit' },
|
||||||
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
|
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
|
||||||
isActive: { type: 'boolean', description: 'Whether resource is active' }
|
isActive: { type: 'boolean', description: 'Whether resource is active' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['resourceId']
|
required: ['resourceId']
|
||||||
}
|
}
|
||||||
@ -644,7 +833,14 @@ export class CalendarTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
resourceId: { type: 'string', description: 'Equipment resource ID' }
|
resourceId: { type: 'string', description: 'Equipment resource ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['resourceId']
|
required: ['resourceId']
|
||||||
}
|
}
|
||||||
@ -658,6 +854,13 @@ export class CalendarTools {
|
|||||||
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
|
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
|
||||||
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -671,7 +874,14 @@ export class CalendarTools {
|
|||||||
quantity: { type: 'number', description: 'Total quantity available' },
|
quantity: { type: 'number', description: 'Total quantity available' },
|
||||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||||
capacity: { type: 'number', description: 'Room capacity' },
|
capacity: { type: 'number', description: 'Room capacity' },
|
||||||
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' }
|
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
|
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
|
||||||
}
|
}
|
||||||
@ -682,7 +892,14 @@ export class CalendarTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
resourceId: { type: 'string', description: 'Room resource ID' }
|
resourceId: { type: 'string', description: 'Room resource ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['resourceId']
|
required: ['resourceId']
|
||||||
}
|
}
|
||||||
@ -700,7 +917,14 @@ export class CalendarTools {
|
|||||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||||
capacity: { type: 'number', description: 'Room capacity' },
|
capacity: { type: 'number', description: 'Room capacity' },
|
||||||
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
|
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
|
||||||
isActive: { type: 'boolean', description: 'Whether resource is active' }
|
isActive: { type: 'boolean', description: 'Whether resource is active' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['resourceId']
|
required: ['resourceId']
|
||||||
}
|
}
|
||||||
@ -711,7 +935,14 @@ export class CalendarTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
resourceId: { type: 'string', description: 'Room resource ID' }
|
resourceId: { type: 'string', description: 'Room resource ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['resourceId']
|
required: ['resourceId']
|
||||||
}
|
}
|
||||||
@ -726,7 +957,14 @@ export class CalendarTools {
|
|||||||
isActive: { type: 'boolean', description: 'Filter by active status' },
|
isActive: { type: 'boolean', description: 'Filter by active status' },
|
||||||
deleted: { type: 'boolean', description: 'Include deleted notifications' },
|
deleted: { type: 'boolean', description: 'Include deleted notifications' },
|
||||||
limit: { type: 'number', description: 'Maximum number to return' },
|
limit: { type: 'number', description: 'Maximum number to return' },
|
||||||
skip: { type: 'number', description: 'Number to skip' }
|
skip: { type: 'number', description: 'Number to skip' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId']
|
required: ['calendarId']
|
||||||
}
|
}
|
||||||
@ -749,7 +987,14 @@ export class CalendarTools {
|
|||||||
isActive: { type: 'boolean', description: 'Whether notification is active' },
|
isActive: { type: 'boolean', description: 'Whether notification is active' },
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
body: { type: 'string', description: 'Notification body' },
|
body: { type: 'string', description: 'Notification body' },
|
||||||
subject: { type: 'string', description: 'Notification subject' }
|
subject: { type: 'string', description: 'Notification subject' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['receiverType', 'channel', 'notificationType']
|
required: ['receiverType', 'channel', 'notificationType']
|
||||||
},
|
},
|
||||||
@ -766,7 +1011,14 @@ export class CalendarTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
calendarId: { type: 'string', description: 'Calendar ID' },
|
calendarId: { type: 'string', description: 'Calendar ID' },
|
||||||
notificationId: { type: 'string', description: 'Notification ID' }
|
notificationId: { type: 'string', description: 'Notification ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId', 'notificationId']
|
required: ['calendarId', 'notificationId']
|
||||||
}
|
}
|
||||||
@ -786,7 +1038,14 @@ export class CalendarTools {
|
|||||||
deleted: { type: 'boolean', description: 'Whether notification is deleted' },
|
deleted: { type: 'boolean', description: 'Whether notification is deleted' },
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
body: { type: 'string', description: 'Notification body' },
|
body: { type: 'string', description: 'Notification body' },
|
||||||
subject: { type: 'string', description: 'Notification subject' }
|
subject: { type: 'string', description: 'Notification subject' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId', 'notificationId']
|
required: ['calendarId', 'notificationId']
|
||||||
}
|
}
|
||||||
@ -798,7 +1057,14 @@ export class CalendarTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
calendarId: { type: 'string', description: 'Calendar ID' },
|
calendarId: { type: 'string', description: 'Calendar ID' },
|
||||||
notificationId: { type: 'string', description: 'Notification ID' }
|
notificationId: { type: 'string', description: 'Notification ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['calendarId', 'notificationId']
|
required: ['calendarId', 'notificationId']
|
||||||
}
|
}
|
||||||
@ -813,7 +1079,14 @@ export class CalendarTools {
|
|||||||
calendarId: { type: 'string', description: 'Filter by calendar ID' },
|
calendarId: { type: 'string', description: 'Filter by calendar ID' },
|
||||||
groupId: { type: 'string', description: 'Filter by group ID' },
|
groupId: { type: 'string', description: 'Filter by group ID' },
|
||||||
startTime: { type: 'string', description: 'Start time for the query range' },
|
startTime: { type: 'string', description: 'Start time for the query range' },
|
||||||
endTime: { type: 'string', description: 'End time for the query range' }
|
endTime: { type: 'string', description: 'End time for the query range' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "calendar",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startTime', 'endTime']
|
required: ['startTime', 'endTime']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,13 @@ export class CampaignsTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -31,7 +38,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -45,7 +59,14 @@ export class CampaignsTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Campaign name' },
|
name: { type: 'string', description: 'Campaign name' },
|
||||||
type: { type: 'string', enum: ['email', 'sms', 'voicemail'], description: 'Campaign type' },
|
type: { type: 'string', enum: ['email', 'sms', 'voicemail'], description: 'Campaign type' },
|
||||||
status: { type: 'string', enum: ['draft', 'scheduled'], description: 'Initial status' }
|
status: { type: 'string', enum: ['draft', 'scheduled'], description: 'Initial status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'type']
|
required: ['name', 'type']
|
||||||
}
|
}
|
||||||
@ -59,7 +80,14 @@ export class CampaignsTools {
|
|||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Campaign name' },
|
name: { type: 'string', description: 'Campaign name' },
|
||||||
status: { type: 'string', enum: ['draft', 'scheduled', 'paused'], description: 'Campaign status' }
|
status: { type: 'string', enum: ['draft', 'scheduled', 'paused'], description: 'Campaign status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -71,7 +99,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -85,7 +120,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -97,7 +139,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -109,7 +158,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -123,7 +179,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -138,7 +201,14 @@ export class CampaignsTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
status: { type: 'string', enum: ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'unsubscribed'], description: 'Filter by recipient status' },
|
status: { type: 'string', enum: ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'unsubscribed'], description: 'Filter by recipient status' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['campaignId']
|
required: ['campaignId']
|
||||||
}
|
}
|
||||||
@ -155,6 +225,13 @@ export class CampaignsTools {
|
|||||||
contactId: { type: 'string', description: 'Filter by contact ID' },
|
contactId: { type: 'string', description: 'Filter by contact ID' },
|
||||||
campaignId: { type: 'string', description: 'Filter by campaign ID' }
|
campaignId: { type: 'string', description: 'Filter by campaign ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -164,7 +241,14 @@ export class CampaignsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
messageId: { type: 'string', description: 'Scheduled message ID' },
|
messageId: { type: 'string', description: 'Scheduled message ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "campaigns",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['messageId']
|
required: ['messageId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,13 @@ export class CompaniesTools {
|
|||||||
description: 'Search query to filter companies'
|
description: 'Search query to filter companies'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "general",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -48,7 +55,14 @@ export class CompaniesTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "general",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId']
|
required: ['companyId']
|
||||||
}
|
}
|
||||||
@ -127,7 +141,14 @@ export class CompaniesTools {
|
|||||||
id: { type: 'string' },
|
id: { type: 'string' },
|
||||||
key: { type: 'string' },
|
key: { type: 'string' },
|
||||||
value: { type: 'string' }
|
value: { type: 'string' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "general",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Custom field values'
|
description: 'Custom field values'
|
||||||
},
|
},
|
||||||
@ -214,7 +235,14 @@ export class CompaniesTools {
|
|||||||
id: { type: 'string' },
|
id: { type: 'string' },
|
||||||
key: { type: 'string' },
|
key: { type: 'string' },
|
||||||
value: { type: 'string' }
|
value: { type: 'string' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "general",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Custom field values'
|
description: 'Custom field values'
|
||||||
},
|
},
|
||||||
@ -240,7 +268,14 @@ export class CompaniesTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "general",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId']
|
required: ['companyId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,13 @@ export class ContactTools {
|
|||||||
source: { type: 'string', description: 'Source of the contact' }
|
source: { type: 'string', description: 'Source of the contact' }
|
||||||
},
|
},
|
||||||
required: ['email']
|
required: ['email']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,6 +101,13 @@ export class ContactTools {
|
|||||||
phone: { type: 'string', description: 'Filter by phone number' },
|
phone: { type: 'string', description: 'Filter by phone number' },
|
||||||
limit: { type: 'number', description: 'Maximum number of results (default: 25)' }
|
limit: { type: 'number', description: 'Maximum number of results (default: 25)' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -105,6 +119,13 @@ export class ContactTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID' }
|
contactId: { type: 'string', description: 'Contact ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -121,6 +142,13 @@ export class ContactTools {
|
|||||||
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' }
|
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -132,6 +160,13 @@ export class ContactTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID' }
|
contactId: { type: 'string', description: 'Contact ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -144,6 +179,13 @@ export class ContactTools {
|
|||||||
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to add' }
|
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to add' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'tags']
|
required: ['contactId', 'tags']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,6 +198,13 @@ export class ContactTools {
|
|||||||
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to remove' }
|
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to remove' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'tags']
|
required: ['contactId', 'tags']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -169,6 +218,13 @@ export class ContactTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID' }
|
contactId: { type: 'string', description: 'Contact ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -185,6 +241,13 @@ export class ContactTools {
|
|||||||
assignedTo: { type: 'string', description: 'User ID to assign task to' }
|
assignedTo: { type: 'string', description: 'User ID to assign task to' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'title', 'dueDate']
|
required: ['contactId', 'title', 'dueDate']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -197,6 +260,13 @@ export class ContactTools {
|
|||||||
taskId: { type: 'string', description: 'Task ID' }
|
taskId: { type: 'string', description: 'Task ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'taskId']
|
required: ['contactId', 'taskId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -214,6 +284,13 @@ export class ContactTools {
|
|||||||
assignedTo: { type: 'string', description: 'User ID to assign task to' }
|
assignedTo: { type: 'string', description: 'User ID to assign task to' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'taskId']
|
required: ['contactId', 'taskId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -226,6 +303,13 @@ export class ContactTools {
|
|||||||
taskId: { type: 'string', description: 'Task ID' }
|
taskId: { type: 'string', description: 'Task ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'taskId']
|
required: ['contactId', 'taskId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -239,6 +323,13 @@ export class ContactTools {
|
|||||||
completed: { type: 'boolean', description: 'Completion status' }
|
completed: { type: 'boolean', description: 'Completion status' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'taskId', 'completed']
|
required: ['contactId', 'taskId', 'completed']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -252,6 +343,13 @@ export class ContactTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID' }
|
contactId: { type: 'string', description: 'Contact ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -265,6 +363,13 @@ export class ContactTools {
|
|||||||
userId: { type: 'string', description: 'User ID creating the note' }
|
userId: { type: 'string', description: 'User ID creating the note' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'body']
|
required: ['contactId', 'body']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -277,6 +382,13 @@ export class ContactTools {
|
|||||||
noteId: { type: 'string', description: 'Note ID' }
|
noteId: { type: 'string', description: 'Note ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'noteId']
|
required: ['contactId', 'noteId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -291,6 +403,13 @@ export class ContactTools {
|
|||||||
userId: { type: 'string', description: 'User ID updating the note' }
|
userId: { type: 'string', description: 'User ID updating the note' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'noteId', 'body']
|
required: ['contactId', 'noteId', 'body']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -303,6 +422,13 @@ export class ContactTools {
|
|||||||
noteId: { type: 'string', description: 'Note ID' }
|
noteId: { type: 'string', description: 'Note ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'noteId']
|
required: ['contactId', 'noteId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -321,6 +447,13 @@ export class ContactTools {
|
|||||||
source: { type: 'string', description: 'Source of the contact' },
|
source: { type: 'string', description: 'Source of the contact' },
|
||||||
assignedTo: { type: 'string', description: 'User ID to assign contact to' }
|
assignedTo: { type: 'string', description: 'User ID to assign contact to' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "complex"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -332,6 +465,13 @@ export class ContactTools {
|
|||||||
email: { type: 'string', description: 'Email to check for duplicates' },
|
email: { type: 'string', description: 'Email to check for duplicates' },
|
||||||
phone: { type: 'string', description: 'Phone to check for duplicates' }
|
phone: { type: 'string', description: 'Phone to check for duplicates' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -346,6 +486,13 @@ export class ContactTools {
|
|||||||
query: { type: 'string', description: 'Search query' }
|
query: { type: 'string', description: 'Search query' }
|
||||||
},
|
},
|
||||||
required: ['businessId']
|
required: ['businessId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -357,6 +504,13 @@ export class ContactTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID' }
|
contactId: { type: 'string', description: 'Contact ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -373,6 +527,13 @@ export class ContactTools {
|
|||||||
removeAllTags: { type: 'boolean', description: 'Remove all existing tags before adding new ones' }
|
removeAllTags: { type: 'boolean', description: 'Remove all existing tags before adding new ones' }
|
||||||
},
|
},
|
||||||
required: ['contactIds', 'tags', 'operation']
|
required: ['contactIds', 'tags', 'operation']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -385,6 +546,13 @@ export class ContactTools {
|
|||||||
businessId: { type: 'string', description: 'Business ID (null to remove from business)' }
|
businessId: { type: 'string', description: 'Business ID (null to remove from business)' }
|
||||||
},
|
},
|
||||||
required: ['contactIds']
|
required: ['contactIds']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -399,6 +567,13 @@ export class ContactTools {
|
|||||||
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to add as followers' }
|
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to add as followers' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'followers']
|
required: ['contactId', 'followers']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -411,6 +586,13 @@ export class ContactTools {
|
|||||||
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to remove as followers' }
|
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to remove as followers' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'followers']
|
required: ['contactId', 'followers']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -425,6 +607,13 @@ export class ContactTools {
|
|||||||
campaignId: { type: 'string', description: 'Campaign ID' }
|
campaignId: { type: 'string', description: 'Campaign ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'campaignId']
|
required: ['contactId', 'campaignId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -437,6 +626,13 @@ export class ContactTools {
|
|||||||
campaignId: { type: 'string', description: 'Campaign ID' }
|
campaignId: { type: 'string', description: 'Campaign ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'campaignId']
|
required: ['contactId', 'campaignId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -448,6 +644,13 @@ export class ContactTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID' }
|
contactId: { type: 'string', description: 'Contact ID' }
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -463,6 +666,13 @@ export class ContactTools {
|
|||||||
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
|
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'workflowId']
|
required: ['contactId', 'workflowId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -476,6 +686,13 @@ export class ContactTools {
|
|||||||
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
|
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
|
||||||
},
|
},
|
||||||
required: ['contactId', 'workflowId']
|
required: ['contactId', 'workflowId']
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "contacts",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -69,7 +69,14 @@ export class ConversationTools {
|
|||||||
fromNumber: {
|
fromNumber: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Optional: Phone number to send from (must be configured in GHL)'
|
description: 'Optional: Phone number to send from (must be configured in GHL)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId', 'message']
|
required: ['contactId', 'message']
|
||||||
}
|
}
|
||||||
@ -115,7 +122,14 @@ export class ConversationTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Optional: Array of BCC email addresses'
|
description: 'Optional: Array of BCC email addresses'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId', 'subject']
|
required: ['contactId', 'subject']
|
||||||
}
|
}
|
||||||
@ -152,6 +166,13 @@ export class ConversationTools {
|
|||||||
description: 'Filter by user ID assigned to conversations'
|
description: 'Filter by user ID assigned to conversations'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -181,7 +202,14 @@ export class ConversationTools {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
description: 'Filter messages by type (optional)'
|
description: 'Filter messages by type (optional)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['conversationId']
|
required: ['conversationId']
|
||||||
}
|
}
|
||||||
@ -195,7 +223,14 @@ export class ConversationTools {
|
|||||||
contactId: {
|
contactId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the contact to create conversation with'
|
description: 'The unique ID of the contact to create conversation with'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId']
|
required: ['contactId']
|
||||||
}
|
}
|
||||||
@ -218,7 +253,14 @@ export class ConversationTools {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Set the unread message count (0 to mark as read)',
|
description: 'Set the unread message count (0 to mark as read)',
|
||||||
minimum: 0
|
minimum: 0
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['conversationId']
|
required: ['conversationId']
|
||||||
}
|
}
|
||||||
@ -243,6 +285,13 @@ export class ConversationTools {
|
|||||||
default: 'unread'
|
default: 'unread'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -254,7 +303,14 @@ export class ConversationTools {
|
|||||||
conversationId: {
|
conversationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the conversation to delete'
|
description: 'The unique ID of the conversation to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['conversationId']
|
required: ['conversationId']
|
||||||
}
|
}
|
||||||
@ -270,7 +326,14 @@ export class ConversationTools {
|
|||||||
emailMessageId: {
|
emailMessageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the email message to retrieve'
|
description: 'The unique ID of the email message to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['emailMessageId']
|
required: ['emailMessageId']
|
||||||
}
|
}
|
||||||
@ -284,7 +347,14 @@ export class ConversationTools {
|
|||||||
messageId: {
|
messageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the message to retrieve'
|
description: 'The unique ID of the message to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['messageId']
|
required: ['messageId']
|
||||||
}
|
}
|
||||||
@ -303,7 +373,14 @@ export class ConversationTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Array of file URLs to upload as attachments'
|
description: 'Array of file URLs to upload as attachments'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['conversationId', 'attachmentUrls']
|
required: ['conversationId', 'attachmentUrls']
|
||||||
}
|
}
|
||||||
@ -330,7 +407,14 @@ export class ConversationTools {
|
|||||||
code: { type: 'string' },
|
code: { type: 'string' },
|
||||||
type: { type: 'string' },
|
type: { type: 'string' },
|
||||||
message: { type: 'string' }
|
message: { type: 'string' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emailMessageId: {
|
emailMessageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -425,7 +509,14 @@ export class ConversationTools {
|
|||||||
description: 'Call status'
|
description: 'Call status'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['type', 'conversationId', 'conversationProviderId']
|
required: ['type', 'conversationId', 'conversationProviderId']
|
||||||
}
|
}
|
||||||
@ -469,7 +560,14 @@ export class ConversationTools {
|
|||||||
date: {
|
date: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Date of the call (ISO format)'
|
description: 'Date of the call (ISO format)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status']
|
required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status']
|
||||||
}
|
}
|
||||||
@ -485,7 +583,14 @@ export class ConversationTools {
|
|||||||
messageId: {
|
messageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the call message to get recording for'
|
description: 'The unique ID of the call message to get recording for'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['messageId']
|
required: ['messageId']
|
||||||
}
|
}
|
||||||
@ -499,7 +604,14 @@ export class ConversationTools {
|
|||||||
messageId: {
|
messageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the call message to get transcription for'
|
description: 'The unique ID of the call message to get transcription for'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['messageId']
|
required: ['messageId']
|
||||||
}
|
}
|
||||||
@ -513,7 +625,14 @@ export class ConversationTools {
|
|||||||
messageId: {
|
messageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the call message to download transcription for'
|
description: 'The unique ID of the call message to download transcription for'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['messageId']
|
required: ['messageId']
|
||||||
}
|
}
|
||||||
@ -529,7 +648,14 @@ export class ConversationTools {
|
|||||||
messageId: {
|
messageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the scheduled message to cancel'
|
description: 'The unique ID of the scheduled message to cancel'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['messageId']
|
required: ['messageId']
|
||||||
}
|
}
|
||||||
@ -543,7 +669,14 @@ export class ConversationTools {
|
|||||||
emailMessageId: {
|
emailMessageId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the scheduled email to cancel'
|
description: 'The unique ID of the scheduled email to cancel'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['emailMessageId']
|
required: ['emailMessageId']
|
||||||
}
|
}
|
||||||
@ -567,7 +700,14 @@ export class ConversationTools {
|
|||||||
isTyping: {
|
isTyping: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether the agent is currently typing'
|
description: 'Whether the agent is currently typing'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "conversations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['visitorId', 'conversationId', 'isTyping']
|
required: ['visitorId', 'conversationId', 'isTyping']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,13 @@ export class CoursesTools {
|
|||||||
limit: { type: 'number', description: 'Max results to return' },
|
limit: { type: 'number', description: 'Max results to return' },
|
||||||
offset: { type: 'number', description: 'Offset for pagination' }
|
offset: { type: 'number', description: 'Offset for pagination' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -32,7 +39,14 @@ export class CoursesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Import job name' },
|
name: { type: 'string', description: 'Import job name' },
|
||||||
sourceUrl: { type: 'string', description: 'Source URL to import from' },
|
sourceUrl: { type: 'string', description: 'Source URL to import from' },
|
||||||
type: { type: 'string', description: 'Import type' }
|
type: { type: 'string', description: 'Import type' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name']
|
required: ['name']
|
||||||
}
|
}
|
||||||
@ -49,6 +63,13 @@ export class CoursesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -58,7 +79,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
productId: { type: 'string', description: 'Course product ID' },
|
productId: { type: 'string', description: 'Course product ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -73,7 +101,14 @@ export class CoursesTools {
|
|||||||
title: { type: 'string', description: 'Product title' },
|
title: { type: 'string', description: 'Product title' },
|
||||||
description: { type: 'string', description: 'Product description' },
|
description: { type: 'string', description: 'Product description' },
|
||||||
imageUrl: { type: 'string', description: 'Product image URL' },
|
imageUrl: { type: 'string', description: 'Product image URL' },
|
||||||
statementDescriptor: { type: 'string', description: 'Payment statement descriptor' }
|
statementDescriptor: { type: 'string', description: 'Payment statement descriptor' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['title']
|
required: ['title']
|
||||||
}
|
}
|
||||||
@ -88,7 +123,14 @@ export class CoursesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
title: { type: 'string', description: 'Product title' },
|
title: { type: 'string', description: 'Product title' },
|
||||||
description: { type: 'string', description: 'Product description' },
|
description: { type: 'string', description: 'Product description' },
|
||||||
imageUrl: { type: 'string', description: 'Product image URL' }
|
imageUrl: { type: 'string', description: 'Product image URL' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -100,7 +142,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
productId: { type: 'string', description: 'Course product ID' },
|
productId: { type: 'string', description: 'Course product ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -117,6 +166,13 @@ export class CoursesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -126,7 +182,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
title: { type: 'string', description: 'Category title' }
|
title: { type: 'string', description: 'Category title' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['title']
|
required: ['title']
|
||||||
}
|
}
|
||||||
@ -139,7 +202,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
categoryId: { type: 'string', description: 'Category ID' },
|
categoryId: { type: 'string', description: 'Category ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
title: { type: 'string', description: 'Category title' }
|
title: { type: 'string', description: 'Category title' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['categoryId', 'title']
|
required: ['categoryId', 'title']
|
||||||
}
|
}
|
||||||
@ -151,7 +221,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
categoryId: { type: 'string', description: 'Category ID' },
|
categoryId: { type: 'string', description: 'Category ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['categoryId']
|
required: ['categoryId']
|
||||||
}
|
}
|
||||||
@ -169,6 +246,13 @@ export class CoursesTools {
|
|||||||
offset: { type: 'number', description: 'Pagination offset' },
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
categoryId: { type: 'string', description: 'Filter by category' }
|
categoryId: { type: 'string', description: 'Filter by category' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -178,7 +262,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -194,7 +285,14 @@ export class CoursesTools {
|
|||||||
description: { type: 'string', description: 'Course description' },
|
description: { type: 'string', description: 'Course description' },
|
||||||
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
|
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
|
||||||
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
|
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
|
||||||
categoryId: { type: 'string', description: 'Category ID to place course in' }
|
categoryId: { type: 'string', description: 'Category ID to place course in' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['title']
|
required: ['title']
|
||||||
}
|
}
|
||||||
@ -210,7 +308,14 @@ export class CoursesTools {
|
|||||||
title: { type: 'string', description: 'Course title' },
|
title: { type: 'string', description: 'Course title' },
|
||||||
description: { type: 'string', description: 'Course description' },
|
description: { type: 'string', description: 'Course description' },
|
||||||
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
|
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
|
||||||
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' }
|
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -222,7 +327,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -236,7 +348,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -251,7 +370,14 @@ export class CoursesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
userId: { type: 'string', description: 'User ID of instructor' },
|
userId: { type: 'string', description: 'User ID of instructor' },
|
||||||
name: { type: 'string', description: 'Instructor display name' },
|
name: { type: 'string', description: 'Instructor display name' },
|
||||||
bio: { type: 'string', description: 'Instructor bio' }
|
bio: { type: 'string', description: 'Instructor bio' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -267,7 +393,14 @@ export class CoursesTools {
|
|||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -280,7 +413,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
postId: { type: 'string', description: 'Post/Lesson ID' },
|
postId: { type: 'string', description: 'Post/Lesson ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'postId']
|
required: ['courseId', 'postId']
|
||||||
}
|
}
|
||||||
@ -297,7 +437,14 @@ export class CoursesTools {
|
|||||||
contentType: { type: 'string', enum: ['video', 'text', 'quiz', 'assignment'], description: 'Content type' },
|
contentType: { type: 'string', enum: ['video', 'text', 'quiz', 'assignment'], description: 'Content type' },
|
||||||
content: { type: 'string', description: 'Post content (text/HTML)' },
|
content: { type: 'string', description: 'Post content (text/HTML)' },
|
||||||
videoUrl: { type: 'string', description: 'Video URL (if video type)' },
|
videoUrl: { type: 'string', description: 'Video URL (if video type)' },
|
||||||
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' }
|
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'title']
|
required: ['courseId', 'title']
|
||||||
}
|
}
|
||||||
@ -314,7 +461,14 @@ export class CoursesTools {
|
|||||||
title: { type: 'string', description: 'Post/lesson title' },
|
title: { type: 'string', description: 'Post/lesson title' },
|
||||||
content: { type: 'string', description: 'Post content' },
|
content: { type: 'string', description: 'Post content' },
|
||||||
videoUrl: { type: 'string', description: 'Video URL' },
|
videoUrl: { type: 'string', description: 'Video URL' },
|
||||||
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' }
|
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'postId']
|
required: ['courseId', 'postId']
|
||||||
}
|
}
|
||||||
@ -327,7 +481,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
postId: { type: 'string', description: 'Post/Lesson ID' },
|
postId: { type: 'string', description: 'Post/Lesson ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'postId']
|
required: ['courseId', 'postId']
|
||||||
}
|
}
|
||||||
@ -341,7 +502,14 @@ export class CoursesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
productId: { type: 'string', description: 'Course product ID' },
|
productId: { type: 'string', description: 'Course product ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -358,7 +526,14 @@ export class CoursesTools {
|
|||||||
price: { type: 'number', description: 'Price in cents' },
|
price: { type: 'number', description: 'Price in cents' },
|
||||||
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
||||||
type: { type: 'string', enum: ['one-time', 'subscription'], description: 'Payment type' },
|
type: { type: 'string', enum: ['one-time', 'subscription'], description: 'Payment type' },
|
||||||
interval: { type: 'string', enum: ['month', 'year'], description: 'Subscription interval (if subscription)' }
|
interval: { type: 'string', enum: ['month', 'year'], description: 'Subscription interval (if subscription)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId', 'name', 'price']
|
required: ['productId', 'name', 'price']
|
||||||
}
|
}
|
||||||
@ -373,7 +548,14 @@ export class CoursesTools {
|
|||||||
offerId: { type: 'string', description: 'Offer ID' },
|
offerId: { type: 'string', description: 'Offer ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Offer name' },
|
name: { type: 'string', description: 'Offer name' },
|
||||||
price: { type: 'number', description: 'Price in cents' }
|
price: { type: 'number', description: 'Price in cents' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId', 'offerId']
|
required: ['productId', 'offerId']
|
||||||
}
|
}
|
||||||
@ -386,7 +568,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
productId: { type: 'string', description: 'Course product ID' },
|
productId: { type: 'string', description: 'Course product ID' },
|
||||||
offerId: { type: 'string', description: 'Offer ID' },
|
offerId: { type: 'string', description: 'Offer ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId', 'offerId']
|
required: ['productId', 'offerId']
|
||||||
}
|
}
|
||||||
@ -402,7 +591,14 @@ export class CoursesTools {
|
|||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId']
|
required: ['courseId']
|
||||||
}
|
}
|
||||||
@ -415,7 +611,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
contactId: { type: 'string', description: 'Contact ID to enroll' },
|
contactId: { type: 'string', description: 'Contact ID to enroll' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'contactId']
|
required: ['courseId', 'contactId']
|
||||||
}
|
}
|
||||||
@ -428,7 +631,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
contactId: { type: 'string', description: 'Contact ID' },
|
contactId: { type: 'string', description: 'Contact ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'contactId']
|
required: ['courseId', 'contactId']
|
||||||
}
|
}
|
||||||
@ -443,7 +653,14 @@ export class CoursesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
courseId: { type: 'string', description: 'Course ID' },
|
courseId: { type: 'string', description: 'Course ID' },
|
||||||
contactId: { type: 'string', description: 'Contact/Student ID' },
|
contactId: { type: 'string', description: 'Contact/Student ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'contactId']
|
required: ['courseId', 'contactId']
|
||||||
}
|
}
|
||||||
@ -458,7 +675,14 @@ export class CoursesTools {
|
|||||||
postId: { type: 'string', description: 'Post/Lesson ID' },
|
postId: { type: 'string', description: 'Post/Lesson ID' },
|
||||||
contactId: { type: 'string', description: 'Contact/Student ID' },
|
contactId: { type: 'string', description: 'Contact/Student ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
completed: { type: 'boolean', description: 'Whether lesson is completed' }
|
completed: { type: 'boolean', description: 'Whether lesson is completed' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "courses",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['courseId', 'postId', 'contactId', 'completed']
|
required: ['courseId', 'postId', 'contactId', 'completed']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,14 @@ export class CustomFieldV2Tools {
|
|||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The ID of the custom field or folder to retrieve'
|
description: 'The ID of the custom field or folder to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['id']
|
required: ['id']
|
||||||
}
|
}
|
||||||
@ -74,7 +81,14 @@ export class CustomFieldV2Tools {
|
|||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'URL associated with the option (only for RADIO type)'
|
description: 'URL associated with the option (only for RADIO type)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['key', 'label']
|
required: ['key', 'label']
|
||||||
},
|
},
|
||||||
@ -160,7 +174,14 @@ export class CustomFieldV2Tools {
|
|||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'URL associated with the option (only for RADIO type)'
|
description: 'URL associated with the option (only for RADIO type)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['key', 'label']
|
required: ['key', 'label']
|
||||||
},
|
},
|
||||||
@ -188,7 +209,14 @@ export class CustomFieldV2Tools {
|
|||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The ID of the custom field to delete'
|
description: 'The ID of the custom field to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['id']
|
required: ['id']
|
||||||
}
|
}
|
||||||
@ -206,7 +234,14 @@ export class CustomFieldV2Tools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['objectKey']
|
required: ['objectKey']
|
||||||
}
|
}
|
||||||
@ -229,7 +264,14 @@ export class CustomFieldV2Tools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['objectKey', 'name']
|
required: ['objectKey', 'name']
|
||||||
}
|
}
|
||||||
@ -251,7 +293,14 @@ export class CustomFieldV2Tools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['id', 'name']
|
required: ['id', 'name']
|
||||||
}
|
}
|
||||||
@ -269,7 +318,14 @@ export class CustomFieldV2Tools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "custom-fields",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['id']
|
required: ['id']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,14 @@ export class EmailISVTools {
|
|||||||
verify: {
|
verify: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Email address to verify (if type=email) or contact ID (if type=contact)'
|
description: 'Email address to verify (if type=email) or contact ID (if type=contact)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "email",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'type', 'verify']
|
required: ['locationId', 'type', 'verify']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,13 @@ export class EmailTools {
|
|||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "email",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,7 +77,14 @@ export class EmailTools {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether the template is plain text.',
|
description: 'Whether the template is plain text.',
|
||||||
default: false
|
default: false
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "email",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['title', 'html']
|
required: ['title', 'html']
|
||||||
}
|
}
|
||||||
@ -92,6 +106,13 @@ export class EmailTools {
|
|||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "email",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -111,7 +132,14 @@ export class EmailTools {
|
|||||||
previewText: {
|
previewText: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The updated preview text for the template.'
|
description: 'The updated preview text for the template.'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "email",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId', 'html']
|
required: ['templateId', 'html']
|
||||||
}
|
}
|
||||||
@ -125,7 +153,14 @@ export class EmailTools {
|
|||||||
templateId: {
|
templateId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The ID of the template to delete.'
|
description: 'The ID of the template to delete.'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "email",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,13 @@ export class FormsTools {
|
|||||||
description: 'Filter by form type (e.g., "form", "survey")'
|
description: 'Filter by form type (e.g., "form", "survey")'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "forms",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,7 +75,14 @@ export class FormsTools {
|
|||||||
page: {
|
page: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Page number for pagination'
|
description: 'Page number for pagination'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "forms",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['formId']
|
required: ['formId']
|
||||||
}
|
}
|
||||||
@ -86,7 +100,14 @@ export class FormsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "forms",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['formId']
|
required: ['formId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,13 @@ export class FunnelsTools {
|
|||||||
description: 'Filter by type (funnel or website)'
|
description: 'Filter by type (funnel or website)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -61,7 +68,14 @@ export class FunnelsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId']
|
required: ['funnelId']
|
||||||
}
|
}
|
||||||
@ -87,7 +101,14 @@ export class FunnelsTools {
|
|||||||
limit: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum number of pages to return'
|
description: 'Maximum number of pages to return'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId']
|
required: ['funnelId']
|
||||||
}
|
}
|
||||||
@ -105,7 +126,14 @@ export class FunnelsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId']
|
required: ['funnelId']
|
||||||
}
|
}
|
||||||
@ -136,7 +164,14 @@ export class FunnelsTools {
|
|||||||
pathName: {
|
pathName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Source path for the redirect'
|
description: 'Source path for the redirect'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId', 'target', 'action']
|
required: ['funnelId', 'target', 'action']
|
||||||
}
|
}
|
||||||
@ -171,7 +206,14 @@ export class FunnelsTools {
|
|||||||
pathName: {
|
pathName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Source path for the redirect'
|
description: 'Source path for the redirect'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId', 'redirectId']
|
required: ['funnelId', 'redirectId']
|
||||||
}
|
}
|
||||||
@ -193,7 +235,14 @@ export class FunnelsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId', 'redirectId']
|
required: ['funnelId', 'redirectId']
|
||||||
}
|
}
|
||||||
@ -219,7 +268,14 @@ export class FunnelsTools {
|
|||||||
limit: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum number of redirects to return'
|
description: 'Maximum number of redirects to return'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "funnels",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['funnelId']
|
required: ['funnelId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,7 +85,14 @@ export class InvoicesTools {
|
|||||||
title: { type: 'string', description: 'Invoice title' },
|
title: { type: 'string', description: 'Invoice title' },
|
||||||
currency: { type: 'string', description: 'Currency code' },
|
currency: { type: 'string', description: 'Currency code' },
|
||||||
issueDate: { type: 'string', description: 'Issue date' },
|
issueDate: { type: 'string', description: 'Issue date' },
|
||||||
dueDate: { type: 'string', description: 'Due date' }
|
dueDate: { type: 'string', description: 'Due date' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name']
|
required: ['name']
|
||||||
}
|
}
|
||||||
@ -101,7 +108,14 @@ export class InvoicesTools {
|
|||||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||||
status: { type: 'string', description: 'Filter by status' },
|
status: { type: 'string', description: 'Filter by status' },
|
||||||
search: { type: 'string', description: 'Search term' },
|
search: { type: 'string', description: 'Search term' },
|
||||||
paymentMode: { type: 'string', enum: ['default', 'live', 'test'], description: 'Payment mode' }
|
paymentMode: { type: 'string', enum: ['default', 'live', 'test'], description: 'Payment mode' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['limit', 'offset']
|
required: ['limit', 'offset']
|
||||||
}
|
}
|
||||||
@ -113,7 +127,14 @@ export class InvoicesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
altId: { type: 'string', description: 'Location ID' }
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -128,7 +149,14 @@ export class InvoicesTools {
|
|||||||
altId: { type: 'string', description: 'Location ID' },
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Template name' },
|
name: { type: 'string', description: 'Template name' },
|
||||||
title: { type: 'string', description: 'Invoice title' },
|
title: { type: 'string', description: 'Invoice title' },
|
||||||
currency: { type: 'string', description: 'Currency code' }
|
currency: { type: 'string', description: 'Currency code' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -140,7 +168,14 @@ export class InvoicesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
altId: { type: 'string', description: 'Location ID' }
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -157,7 +192,14 @@ export class InvoicesTools {
|
|||||||
name: { type: 'string', description: 'Schedule name' },
|
name: { type: 'string', description: 'Schedule name' },
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
contactId: { type: 'string', description: 'Contact ID' },
|
contactId: { type: 'string', description: 'Contact ID' },
|
||||||
frequency: { type: 'string', description: 'Schedule frequency' }
|
frequency: { type: 'string', description: 'Schedule frequency' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'templateId', 'contactId']
|
required: ['name', 'templateId', 'contactId']
|
||||||
}
|
}
|
||||||
@ -172,7 +214,14 @@ export class InvoicesTools {
|
|||||||
limit: { type: 'string', description: 'Number of results per page', default: '10' },
|
limit: { type: 'string', description: 'Number of results per page', default: '10' },
|
||||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||||
status: { type: 'string', description: 'Filter by status' },
|
status: { type: 'string', description: 'Filter by status' },
|
||||||
search: { type: 'string', description: 'Search term' }
|
search: { type: 'string', description: 'Search term' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['limit', 'offset']
|
required: ['limit', 'offset']
|
||||||
}
|
}
|
||||||
@ -184,7 +233,14 @@ export class InvoicesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
scheduleId: { type: 'string', description: 'Schedule ID' },
|
scheduleId: { type: 'string', description: 'Schedule ID' },
|
||||||
altId: { type: 'string', description: 'Location ID' }
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['scheduleId']
|
required: ['scheduleId']
|
||||||
}
|
}
|
||||||
@ -203,7 +259,14 @@ export class InvoicesTools {
|
|||||||
currency: { type: 'string', description: 'Currency code' },
|
currency: { type: 'string', description: 'Currency code' },
|
||||||
issueDate: { type: 'string', description: 'Issue date' },
|
issueDate: { type: 'string', description: 'Issue date' },
|
||||||
dueDate: { type: 'string', description: 'Due date' },
|
dueDate: { type: 'string', description: 'Due date' },
|
||||||
items: { type: 'array', description: 'Invoice items' }
|
items: { type: 'array', description: 'Invoice items' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId', 'title']
|
required: ['contactId', 'title']
|
||||||
}
|
}
|
||||||
@ -219,7 +282,14 @@ export class InvoicesTools {
|
|||||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||||
status: { type: 'string', description: 'Filter by status' },
|
status: { type: 'string', description: 'Filter by status' },
|
||||||
contactId: { type: 'string', description: 'Filter by contact ID' },
|
contactId: { type: 'string', description: 'Filter by contact ID' },
|
||||||
search: { type: 'string', description: 'Search term' }
|
search: { type: 'string', description: 'Search term' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['limit', 'offset']
|
required: ['limit', 'offset']
|
||||||
}
|
}
|
||||||
@ -231,7 +301,14 @@ export class InvoicesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||||
altId: { type: 'string', description: 'Location ID' }
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['invoiceId']
|
required: ['invoiceId']
|
||||||
}
|
}
|
||||||
@ -246,7 +323,14 @@ export class InvoicesTools {
|
|||||||
altId: { type: 'string', description: 'Location ID' },
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
emailTo: { type: 'string', description: 'Email address to send to' },
|
emailTo: { type: 'string', description: 'Email address to send to' },
|
||||||
subject: { type: 'string', description: 'Email subject' },
|
subject: { type: 'string', description: 'Email subject' },
|
||||||
message: { type: 'string', description: 'Email message' }
|
message: { type: 'string', description: 'Email message' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['invoiceId']
|
required: ['invoiceId']
|
||||||
}
|
}
|
||||||
@ -264,7 +348,14 @@ export class InvoicesTools {
|
|||||||
title: { type: 'string', description: 'Estimate title' },
|
title: { type: 'string', description: 'Estimate title' },
|
||||||
currency: { type: 'string', description: 'Currency code' },
|
currency: { type: 'string', description: 'Currency code' },
|
||||||
issueDate: { type: 'string', description: 'Issue date' },
|
issueDate: { type: 'string', description: 'Issue date' },
|
||||||
validUntil: { type: 'string', description: 'Valid until date' }
|
validUntil: { type: 'string', description: 'Valid until date' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId', 'title']
|
required: ['contactId', 'title']
|
||||||
}
|
}
|
||||||
@ -280,7 +371,14 @@ export class InvoicesTools {
|
|||||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||||
status: { type: 'string', enum: ['all', 'draft', 'sent', 'accepted', 'declined', 'invoiced', 'viewed'], description: 'Filter by status' },
|
status: { type: 'string', enum: ['all', 'draft', 'sent', 'accepted', 'declined', 'invoiced', 'viewed'], description: 'Filter by status' },
|
||||||
contactId: { type: 'string', description: 'Filter by contact ID' },
|
contactId: { type: 'string', description: 'Filter by contact ID' },
|
||||||
search: { type: 'string', description: 'Search term' }
|
search: { type: 'string', description: 'Search term' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['limit', 'offset']
|
required: ['limit', 'offset']
|
||||||
}
|
}
|
||||||
@ -295,7 +393,14 @@ export class InvoicesTools {
|
|||||||
altId: { type: 'string', description: 'Location ID' },
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
emailTo: { type: 'string', description: 'Email address to send to' },
|
emailTo: { type: 'string', description: 'Email address to send to' },
|
||||||
subject: { type: 'string', description: 'Email subject' },
|
subject: { type: 'string', description: 'Email subject' },
|
||||||
message: { type: 'string', description: 'Email message' }
|
message: { type: 'string', description: 'Email message' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['estimateId']
|
required: ['estimateId']
|
||||||
}
|
}
|
||||||
@ -309,7 +414,14 @@ export class InvoicesTools {
|
|||||||
estimateId: { type: 'string', description: 'Estimate ID' },
|
estimateId: { type: 'string', description: 'Estimate ID' },
|
||||||
altId: { type: 'string', description: 'Location ID' },
|
altId: { type: 'string', description: 'Location ID' },
|
||||||
issueDate: { type: 'string', description: 'Invoice issue date' },
|
issueDate: { type: 'string', description: 'Invoice issue date' },
|
||||||
dueDate: { type: 'string', description: 'Invoice due date' }
|
dueDate: { type: 'string', description: 'Invoice due date' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['estimateId']
|
required: ['estimateId']
|
||||||
}
|
}
|
||||||
@ -324,6 +436,13 @@ export class InvoicesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
altId: { type: 'string', description: 'Location ID' }
|
altId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "invoices",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -29,6 +29,13 @@ export class LinksTools {
|
|||||||
description: 'Maximum number of links to return'
|
description: 'Maximum number of links to return'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "links",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -44,7 +51,14 @@ export class LinksTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "links",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['linkId']
|
required: ['linkId']
|
||||||
}
|
}
|
||||||
@ -74,7 +88,14 @@ export class LinksTools {
|
|||||||
fieldValue: {
|
fieldValue: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Value to set for the custom field'
|
description: 'Value to set for the custom field'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "links",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'redirectTo']
|
required: ['name', 'redirectTo']
|
||||||
}
|
}
|
||||||
@ -108,7 +129,14 @@ export class LinksTools {
|
|||||||
fieldValue: {
|
fieldValue: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Value to set for the custom field'
|
description: 'Value to set for the custom field'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "links",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['linkId']
|
required: ['linkId']
|
||||||
}
|
}
|
||||||
@ -126,7 +154,14 @@ export class LinksTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "links",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['linkId']
|
required: ['linkId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,13 @@ export class LocationTools {
|
|||||||
format: 'email'
|
format: 'email'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -92,7 +99,14 @@ export class LocationTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the location to retrieve'
|
description: 'The unique ID of the location to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -148,7 +162,14 @@ export class LocationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
firstName: { type: 'string', description: 'Prospect first name' },
|
firstName: { type: 'string', description: 'Prospect first name' },
|
||||||
lastName: { type: 'string', description: 'Prospect last name' },
|
lastName: { type: 'string', description: 'Prospect last name' },
|
||||||
email: { type: 'string', format: 'email', description: 'Prospect email' }
|
email: { type: 'string', format: 'email', description: 'Prospect email' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['firstName', 'lastName', 'email'],
|
required: ['firstName', 'lastName', 'email'],
|
||||||
description: 'Prospect information for the location'
|
description: 'Prospect information for the location'
|
||||||
@ -210,7 +231,14 @@ export class LocationTools {
|
|||||||
timezone: {
|
timezone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Updated timezone'
|
description: 'Updated timezone'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'companyId']
|
required: ['locationId', 'companyId']
|
||||||
}
|
}
|
||||||
@ -229,7 +257,14 @@ export class LocationTools {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether to delete associated Twilio account',
|
description: 'Whether to delete associated Twilio account',
|
||||||
default: false
|
default: false
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'deleteTwilioAccount']
|
required: ['locationId', 'deleteTwilioAccount']
|
||||||
}
|
}
|
||||||
@ -245,7 +280,14 @@ export class LocationTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The location ID to get tags from'
|
description: 'The location ID to get tags from'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -263,7 +305,14 @@ export class LocationTools {
|
|||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Name of the tag to create'
|
description: 'Name of the tag to create'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'name']
|
required: ['locationId', 'name']
|
||||||
}
|
}
|
||||||
@ -281,7 +330,14 @@ export class LocationTools {
|
|||||||
tagId: {
|
tagId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The tag ID to retrieve'
|
description: 'The tag ID to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'tagId']
|
required: ['locationId', 'tagId']
|
||||||
}
|
}
|
||||||
@ -303,7 +359,14 @@ export class LocationTools {
|
|||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Updated name for the tag'
|
description: 'Updated name for the tag'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'tagId', 'name']
|
required: ['locationId', 'tagId', 'name']
|
||||||
}
|
}
|
||||||
@ -321,7 +384,14 @@ export class LocationTools {
|
|||||||
tagId: {
|
tagId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The tag ID to delete'
|
description: 'The tag ID to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'tagId']
|
required: ['locationId', 'tagId']
|
||||||
}
|
}
|
||||||
@ -369,7 +439,14 @@ export class LocationTools {
|
|||||||
businessId: {
|
businessId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Business ID filter'
|
description: 'Business ID filter'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -391,7 +468,14 @@ export class LocationTools {
|
|||||||
enum: ['contact', 'opportunity', 'all'],
|
enum: ['contact', 'opportunity', 'all'],
|
||||||
description: 'Filter by model type (default: all)',
|
description: 'Filter by model type (default: all)',
|
||||||
default: 'all'
|
default: 'all'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -428,7 +512,14 @@ export class LocationTools {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Position/order of the field (default: 0)',
|
description: 'Position/order of the field (default: 0)',
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'name', 'dataType']
|
required: ['locationId', 'name', 'dataType']
|
||||||
}
|
}
|
||||||
@ -446,7 +537,14 @@ export class LocationTools {
|
|||||||
customFieldId: {
|
customFieldId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The custom field ID to retrieve'
|
description: 'The custom field ID to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'customFieldId']
|
required: ['locationId', 'customFieldId']
|
||||||
}
|
}
|
||||||
@ -476,7 +574,14 @@ export class LocationTools {
|
|||||||
position: {
|
position: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Updated position/order'
|
description: 'Updated position/order'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'customFieldId', 'name']
|
required: ['locationId', 'customFieldId', 'name']
|
||||||
}
|
}
|
||||||
@ -494,7 +599,14 @@ export class LocationTools {
|
|||||||
customFieldId: {
|
customFieldId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The custom field ID to delete'
|
description: 'The custom field ID to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'customFieldId']
|
required: ['locationId', 'customFieldId']
|
||||||
}
|
}
|
||||||
@ -510,7 +622,14 @@ export class LocationTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The location ID'
|
description: 'The location ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -532,7 +651,14 @@ export class LocationTools {
|
|||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Value to assign'
|
description: 'Value to assign'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'name', 'value']
|
required: ['locationId', 'name', 'value']
|
||||||
}
|
}
|
||||||
@ -550,7 +676,14 @@ export class LocationTools {
|
|||||||
customValueId: {
|
customValueId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The custom value ID to retrieve'
|
description: 'The custom value ID to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'customValueId']
|
required: ['locationId', 'customValueId']
|
||||||
}
|
}
|
||||||
@ -576,7 +709,14 @@ export class LocationTools {
|
|||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Updated value'
|
description: 'Updated value'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'customValueId', 'name', 'value']
|
required: ['locationId', 'customValueId', 'name', 'value']
|
||||||
}
|
}
|
||||||
@ -594,7 +734,14 @@ export class LocationTools {
|
|||||||
customValueId: {
|
customValueId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The custom value ID to delete'
|
description: 'The custom value ID to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'customValueId']
|
required: ['locationId', 'customValueId']
|
||||||
}
|
}
|
||||||
@ -634,7 +781,14 @@ export class LocationTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['sms', 'email', 'whatsapp'],
|
enum: ['sms', 'email', 'whatsapp'],
|
||||||
description: 'Filter by template type'
|
description: 'Filter by template type'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'originId']
|
required: ['locationId', 'originId']
|
||||||
}
|
}
|
||||||
@ -652,7 +806,14 @@ export class LocationTools {
|
|||||||
templateId: {
|
templateId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The template ID to delete'
|
description: 'The template ID to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "locations",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'templateId']
|
required: ['locationId', 'templateId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,7 +80,14 @@ export class MediaTools {
|
|||||||
parentId: {
|
parentId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Parent folder ID to list files within a specific folder'
|
description: 'Parent folder ID to list files within a specific folder'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
@ -121,7 +128,14 @@ export class MediaTools {
|
|||||||
altId: {
|
altId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location or Agency ID (uses default location if not provided)'
|
description: 'Location or Agency ID (uses default location if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "media",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
@ -145,7 +159,14 @@ export class MediaTools {
|
|||||||
altId: {
|
altId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location or Agency ID (uses default location if not provided)'
|
description: 'Location or Agency ID (uses default location if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "media",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['id']
|
required: ['id']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,13 @@ export class OAuthTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
companyId: { type: 'string', description: 'Company ID for agency-level apps' }
|
companyId: { type: 'string', description: 'Company ID for agency-level apps' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -29,7 +36,14 @@ export class OAuthTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
appId: { type: 'string', description: 'OAuth App ID' },
|
appId: { type: 'string', description: 'OAuth App ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appId']
|
required: ['appId']
|
||||||
}
|
}
|
||||||
@ -45,7 +59,14 @@ export class OAuthTools {
|
|||||||
skip: { type: 'number', description: 'Records to skip' },
|
skip: { type: 'number', description: 'Records to skip' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
query: { type: 'string', description: 'Search query' },
|
query: { type: 'string', description: 'Search query' },
|
||||||
isInstalled: { type: 'boolean', description: 'Filter by installation status' }
|
isInstalled: { type: 'boolean', description: 'Filter by installation status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['appId', 'companyId']
|
required: ['appId', 'companyId']
|
||||||
}
|
}
|
||||||
@ -58,6 +79,13 @@ export class OAuthTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {}
|
properties: {}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -67,7 +95,14 @@ export class OAuthTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
companyId: { type: 'string', description: 'Company/Agency ID' },
|
companyId: { type: 'string', description: 'Company/Agency ID' },
|
||||||
locationId: { type: 'string', description: 'Target Location ID' }
|
locationId: { type: 'string', description: 'Target Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId', 'locationId']
|
required: ['companyId', 'locationId']
|
||||||
}
|
}
|
||||||
@ -82,6 +117,13 @@ export class OAuthTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -91,7 +133,14 @@ export class OAuthTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
integrationId: { type: 'string', description: 'Integration ID to disconnect' },
|
integrationId: { type: 'string', description: 'Integration ID to disconnect' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['integrationId']
|
required: ['integrationId']
|
||||||
}
|
}
|
||||||
@ -106,6 +155,13 @@ export class OAuthTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -120,7 +176,14 @@ export class OAuthTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Permission scopes for the key'
|
description: 'Permission scopes for the key'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name']
|
required: ['name']
|
||||||
}
|
}
|
||||||
@ -132,7 +195,14 @@ export class OAuthTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
keyId: { type: 'string', description: 'API Key ID' },
|
keyId: { type: 'string', description: 'API Key ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "oauth",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['keyId']
|
required: ['keyId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,14 @@ export class ObjectTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "read",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
@ -64,7 +71,14 @@ export class ObjectTools {
|
|||||||
description: 'Singular and plural names for the custom object',
|
description: 'Singular and plural names for the custom object',
|
||||||
properties: {
|
properties: {
|
||||||
singular: { type: 'string', description: 'Singular name (e.g., "Pet")' },
|
singular: { type: 'string', description: 'Singular name (e.g., "Pet")' },
|
||||||
plural: { type: 'string', description: 'Plural name (e.g., "Pets")' }
|
plural: { type: 'string', description: 'Plural name (e.g., "Pets")' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['singular', 'plural']
|
required: ['singular', 'plural']
|
||||||
},
|
},
|
||||||
@ -112,7 +126,14 @@ export class ObjectTools {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether to fetch all standard/custom fields of the object',
|
description: 'Whether to fetch all standard/custom fields of the object',
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['key']
|
required: ['key']
|
||||||
}
|
}
|
||||||
@ -133,7 +154,14 @@ export class ObjectTools {
|
|||||||
properties: {
|
properties: {
|
||||||
singular: { type: 'string', description: 'Updated singular name' },
|
singular: { type: 'string', description: 'Updated singular name' },
|
||||||
plural: { type: 'string', description: 'Updated plural name' }
|
plural: { type: 'string', description: 'Updated plural name' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -181,7 +209,14 @@ export class ObjectTools {
|
|||||||
description: 'Array of user IDs who follow this record (limited to 10)',
|
description: 'Array of user IDs who follow this record (limited to 10)',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
maxItems: 10
|
maxItems: 10
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['schemaKey', 'properties']
|
required: ['schemaKey', 'properties']
|
||||||
}
|
}
|
||||||
@ -199,7 +234,14 @@ export class ObjectTools {
|
|||||||
recordId: {
|
recordId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'ID of the record to retrieve'
|
description: 'ID of the record to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['schemaKey', 'recordId']
|
required: ['schemaKey', 'recordId']
|
||||||
}
|
}
|
||||||
@ -237,7 +279,14 @@ export class ObjectTools {
|
|||||||
description: 'Updated array of user IDs who follow this record',
|
description: 'Updated array of user IDs who follow this record',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
maxItems: 10
|
maxItems: 10
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['schemaKey', 'recordId']
|
required: ['schemaKey', 'recordId']
|
||||||
}
|
}
|
||||||
@ -255,7 +304,14 @@ export class ObjectTools {
|
|||||||
recordId: {
|
recordId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'ID of the record to delete'
|
description: 'ID of the record to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['schemaKey', 'recordId']
|
required: ['schemaKey', 'recordId']
|
||||||
}
|
}
|
||||||
@ -295,7 +351,14 @@ export class ObjectTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Cursor for pagination (returned from previous search)',
|
description: 'Cursor for pagination (returned from previous search)',
|
||||||
items: { type: 'string' }
|
items: { type: 'string' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "objects",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['schemaKey', 'query']
|
required: ['schemaKey', 'query']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,13 @@ export class OpportunityTools {
|
|||||||
default: 20
|
default: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -77,6 +84,13 @@ export class OpportunityTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {}
|
properties: {}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -88,7 +102,14 @@ export class OpportunityTools {
|
|||||||
opportunityId: {
|
opportunityId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the opportunity to retrieve'
|
description: 'The unique ID of the opportunity to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['opportunityId']
|
required: ['opportunityId']
|
||||||
}
|
}
|
||||||
@ -124,7 +145,14 @@ export class OpportunityTools {
|
|||||||
assignedTo: {
|
assignedTo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'User ID to assign this opportunity to'
|
description: 'User ID to assign this opportunity to'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'pipelineId', 'contactId']
|
required: ['name', 'pipelineId', 'contactId']
|
||||||
}
|
}
|
||||||
@ -143,7 +171,14 @@ export class OpportunityTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'New status for the opportunity',
|
description: 'New status for the opportunity',
|
||||||
enum: ['open', 'won', 'lost', 'abandoned']
|
enum: ['open', 'won', 'lost', 'abandoned']
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['opportunityId', 'status']
|
required: ['opportunityId', 'status']
|
||||||
}
|
}
|
||||||
@ -157,7 +192,14 @@ export class OpportunityTools {
|
|||||||
opportunityId: {
|
opportunityId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The unique ID of the opportunity to delete'
|
description: 'The unique ID of the opportunity to delete'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['opportunityId']
|
required: ['opportunityId']
|
||||||
}
|
}
|
||||||
@ -196,7 +238,14 @@ export class OpportunityTools {
|
|||||||
assignedTo: {
|
assignedTo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Updated assigned user ID'
|
description: 'Updated assigned user ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['opportunityId']
|
required: ['opportunityId']
|
||||||
}
|
}
|
||||||
@ -236,7 +285,14 @@ export class OpportunityTools {
|
|||||||
assignedTo: {
|
assignedTo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'User ID to assign this opportunity to'
|
description: 'User ID to assign this opportunity to'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "write",
|
||||||
|
complexity: "complex"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['pipelineId', 'contactId']
|
required: ['pipelineId', 'contactId']
|
||||||
}
|
}
|
||||||
@ -255,7 +311,14 @@ export class OpportunityTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Array of user IDs to add as followers'
|
description: 'Array of user IDs to add as followers'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['opportunityId', 'followers']
|
required: ['opportunityId', 'followers']
|
||||||
}
|
}
|
||||||
@ -274,7 +337,14 @@ export class OpportunityTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Array of user IDs to remove as followers'
|
description: 'Array of user IDs to remove as followers'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "deals",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['opportunityId', 'followers']
|
required: ['opportunityId', 'followers']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,14 @@ export class PaymentsTools {
|
|||||||
imageUrl: {
|
imageUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The URL to an image representing the integration provider'
|
description: 'The URL to an image representing the integration provider'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType', 'uniqueName', 'title', 'provider', 'description', 'imageUrl']
|
required: ['altId', 'altType', 'uniqueName', 'title', 'provider', 'description', 'imageUrl']
|
||||||
}
|
}
|
||||||
@ -98,7 +105,14 @@ export class PaymentsTools {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Starting index for pagination',
|
description: 'Starting index for pagination',
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType']
|
required: ['altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -160,7 +174,14 @@ export class PaymentsTools {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Starting index for pagination',
|
description: 'Starting index for pagination',
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType']
|
required: ['altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -186,7 +207,14 @@ export class PaymentsTools {
|
|||||||
altType: {
|
altType: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Alt Type (type of identifier)'
|
description: 'Alt Type (type of identifier)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['orderId', 'altId', 'altType']
|
required: ['orderId', 'altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -231,7 +259,14 @@ export class PaymentsTools {
|
|||||||
description: 'Tracking URL'
|
description: 'Tracking URL'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
@ -277,7 +312,14 @@ export class PaymentsTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['location'],
|
enum: ['location'],
|
||||||
description: 'Alt Type'
|
description: 'Alt Type'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['orderId', 'altId', 'altType']
|
required: ['orderId', 'altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -347,7 +389,14 @@ export class PaymentsTools {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Starting index for pagination',
|
description: 'Starting index for pagination',
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType']
|
required: ['altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -373,7 +422,14 @@ export class PaymentsTools {
|
|||||||
altType: {
|
altType: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Alt Type (type of identifier)'
|
description: 'Alt Type (type of identifier)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['transactionId', 'altId', 'altType']
|
required: ['transactionId', 'altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -436,7 +492,14 @@ export class PaymentsTools {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Starting index for pagination',
|
description: 'Starting index for pagination',
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType']
|
required: ['altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -459,7 +522,14 @@ export class PaymentsTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['location'],
|
enum: ['location'],
|
||||||
description: 'Alt Type'
|
description: 'Alt Type'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['subscriptionId', 'altId', 'altType']
|
required: ['subscriptionId', 'altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -499,7 +569,14 @@ export class PaymentsTools {
|
|||||||
search: {
|
search: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Search term to filter coupons by name or code'
|
description: 'Search term to filter coupons by name or code'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType']
|
required: ['altId', 'altType']
|
||||||
}
|
}
|
||||||
@ -553,7 +630,14 @@ export class PaymentsTools {
|
|||||||
description: 'Product IDs that the coupon applies to',
|
description: 'Product IDs that the coupon applies to',
|
||||||
items: {
|
items: {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
applyToFuturePayments: {
|
applyToFuturePayments: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -643,7 +727,14 @@ export class PaymentsTools {
|
|||||||
description: 'Product IDs that the coupon applies to',
|
description: 'Product IDs that the coupon applies to',
|
||||||
items: {
|
items: {
|
||||||
type: 'string'
|
type: 'string'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
applyToFuturePayments: {
|
applyToFuturePayments: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -696,7 +787,14 @@ export class PaymentsTools {
|
|||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Coupon ID'
|
description: 'Coupon ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType', 'id']
|
required: ['altId', 'altType', 'id']
|
||||||
}
|
}
|
||||||
@ -723,7 +821,14 @@ export class PaymentsTools {
|
|||||||
code: {
|
code: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Coupon code'
|
description: 'Coupon code'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['altId', 'altType', 'id', 'code']
|
required: ['altId', 'altType', 'id', 'code']
|
||||||
}
|
}
|
||||||
@ -759,7 +864,14 @@ export class PaymentsTools {
|
|||||||
imageUrl: {
|
imageUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Public image URL for the payment gateway logo'
|
description: 'Public image URL for the payment gateway logo'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'name', 'description', 'paymentsUrl', 'queryUrl', 'imageUrl']
|
required: ['locationId', 'name', 'description', 'paymentsUrl', 'queryUrl', 'imageUrl']
|
||||||
}
|
}
|
||||||
@ -773,7 +885,14 @@ export class PaymentsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID'
|
description: 'Location ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -787,7 +906,14 @@ export class PaymentsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID'
|
description: 'Location ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId']
|
required: ['locationId']
|
||||||
}
|
}
|
||||||
@ -813,7 +939,14 @@ export class PaymentsTools {
|
|||||||
publishableKey: {
|
publishableKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Publishable key for live payments'
|
description: 'Publishable key for live payments'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['apiKey', 'publishableKey']
|
required: ['apiKey', 'publishableKey']
|
||||||
},
|
},
|
||||||
@ -849,7 +982,14 @@ export class PaymentsTools {
|
|||||||
liveMode: {
|
liveMode: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether to disconnect live or test mode config'
|
description: 'Whether to disconnect live or test mode config'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "payments",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['locationId', 'liveMode']
|
required: ['locationId', 'liveMode']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,13 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,7 +35,14 @@ export class PhoneTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
|
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumberId']
|
required: ['phoneNumberId']
|
||||||
}
|
}
|
||||||
@ -43,7 +57,14 @@ export class PhoneTools {
|
|||||||
country: { type: 'string', description: 'Country code (e.g., US, CA)' },
|
country: { type: 'string', description: 'Country code (e.g., US, CA)' },
|
||||||
areaCode: { type: 'string', description: 'Area code to search' },
|
areaCode: { type: 'string', description: 'Area code to search' },
|
||||||
contains: { type: 'string', description: 'Number pattern to search for' },
|
contains: { type: 'string', description: 'Number pattern to search for' },
|
||||||
type: { type: 'string', enum: ['local', 'tollfree', 'mobile'], description: 'Number type' }
|
type: { type: 'string', enum: ['local', 'tollfree', 'mobile'], description: 'Number type' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['country']
|
required: ['country']
|
||||||
}
|
}
|
||||||
@ -56,7 +77,14 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
phoneNumber: { type: 'string', description: 'Phone number to purchase' },
|
phoneNumber: { type: 'string', description: 'Phone number to purchase' },
|
||||||
name: { type: 'string', description: 'Friendly name for the number' }
|
name: { type: 'string', description: 'Friendly name for the number' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumber']
|
required: ['phoneNumber']
|
||||||
}
|
}
|
||||||
@ -72,7 +100,14 @@ export class PhoneTools {
|
|||||||
name: { type: 'string', description: 'Friendly name' },
|
name: { type: 'string', description: 'Friendly name' },
|
||||||
forwardingNumber: { type: 'string', description: 'Number to forward calls to' },
|
forwardingNumber: { type: 'string', description: 'Number to forward calls to' },
|
||||||
callRecording: { type: 'boolean', description: 'Enable call recording' },
|
callRecording: { type: 'boolean', description: 'Enable call recording' },
|
||||||
whisperMessage: { type: 'string', description: 'Whisper message played to agent' }
|
whisperMessage: { type: 'string', description: 'Whisper message played to agent' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumberId']
|
required: ['phoneNumberId']
|
||||||
}
|
}
|
||||||
@ -84,7 +119,14 @@ export class PhoneTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
|
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumberId']
|
required: ['phoneNumberId']
|
||||||
}
|
}
|
||||||
@ -98,7 +140,14 @@ export class PhoneTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
|
phoneNumberId: { type: 'string', description: 'Phone Number ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumberId']
|
required: ['phoneNumberId']
|
||||||
}
|
}
|
||||||
@ -114,7 +163,14 @@ export class PhoneTools {
|
|||||||
enabled: { type: 'boolean', description: 'Enable forwarding' },
|
enabled: { type: 'boolean', description: 'Enable forwarding' },
|
||||||
forwardTo: { type: 'string', description: 'Number to forward to' },
|
forwardTo: { type: 'string', description: 'Number to forward to' },
|
||||||
ringTimeout: { type: 'number', description: 'Ring timeout in seconds' },
|
ringTimeout: { type: 'number', description: 'Ring timeout in seconds' },
|
||||||
voicemailEnabled: { type: 'boolean', description: 'Enable voicemail on no answer' }
|
voicemailEnabled: { type: 'boolean', description: 'Enable voicemail on no answer' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumberId']
|
required: ['phoneNumberId']
|
||||||
}
|
}
|
||||||
@ -129,6 +185,13 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,7 +211,14 @@ export class PhoneTools {
|
|||||||
digit: { type: 'string', description: 'Digit to press (0-9, *, #)' },
|
digit: { type: 'string', description: 'Digit to press (0-9, *, #)' },
|
||||||
action: { type: 'string', description: 'Action type' },
|
action: { type: 'string', description: 'Action type' },
|
||||||
destination: { type: 'string', description: 'Action destination' }
|
destination: { type: 'string', description: 'Action destination' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Menu options'
|
description: 'Menu options'
|
||||||
}
|
}
|
||||||
@ -166,7 +236,14 @@ export class PhoneTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Menu name' },
|
name: { type: 'string', description: 'Menu name' },
|
||||||
greeting: { type: 'string', description: 'Greeting message' },
|
greeting: { type: 'string', description: 'Greeting message' },
|
||||||
options: { type: 'array', description: 'Menu options' }
|
options: { type: 'array', description: 'Menu options' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['menuId']
|
required: ['menuId']
|
||||||
}
|
}
|
||||||
@ -178,7 +255,14 @@ export class PhoneTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
menuId: { type: 'string', description: 'IVR Menu ID' },
|
menuId: { type: 'string', description: 'IVR Menu ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['menuId']
|
required: ['menuId']
|
||||||
}
|
}
|
||||||
@ -193,6 +277,13 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -207,6 +298,13 @@ export class PhoneTools {
|
|||||||
transcriptionEnabled: { type: 'boolean', description: 'Enable transcription' },
|
transcriptionEnabled: { type: 'boolean', description: 'Enable transcription' },
|
||||||
notificationEmail: { type: 'string', description: 'Email for voicemail notifications' }
|
notificationEmail: { type: 'string', description: 'Email for voicemail notifications' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -221,6 +319,13 @@ export class PhoneTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -230,7 +335,14 @@ export class PhoneTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
voicemailId: { type: 'string', description: 'Voicemail ID' },
|
voicemailId: { type: 'string', description: 'Voicemail ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['voicemailId']
|
required: ['voicemailId']
|
||||||
}
|
}
|
||||||
@ -245,6 +357,13 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -255,7 +374,14 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
phoneNumber: { type: 'string', description: 'Phone number to verify' },
|
phoneNumber: { type: 'string', description: 'Phone number to verify' },
|
||||||
name: { type: 'string', description: 'Friendly name' }
|
name: { type: 'string', description: 'Friendly name' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['phoneNumber']
|
required: ['phoneNumber']
|
||||||
}
|
}
|
||||||
@ -268,7 +394,14 @@ export class PhoneTools {
|
|||||||
properties: {
|
properties: {
|
||||||
callerIdId: { type: 'string', description: 'Caller ID record ID' },
|
callerIdId: { type: 'string', description: 'Caller ID record ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
code: { type: 'string', description: 'Verification code' }
|
code: { type: 'string', description: 'Verification code' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['callerIdId', 'code']
|
required: ['callerIdId', 'code']
|
||||||
}
|
}
|
||||||
@ -280,7 +413,14 @@ export class PhoneTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
callerIdId: { type: 'string', description: 'Caller ID record ID' },
|
callerIdId: { type: 'string', description: 'Caller ID record ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "phone-numbers",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['callerIdId']
|
required: ['callerIdId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,7 +184,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
description: { type: 'string', description: 'Product description' },
|
description: { type: 'string', description: 'Product description' },
|
||||||
image: { type: 'string', description: 'Product image URL' },
|
image: { type: 'string', description: 'Product image URL' },
|
||||||
availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
|
availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
|
||||||
slug: { type: 'string', description: 'Product URL slug' }
|
slug: { type: 'string', description: 'Product URL slug' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'productType']
|
required: ['name', 'productType']
|
||||||
}
|
}
|
||||||
@ -201,7 +208,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
search: { type: 'string', description: 'Search term for product names' },
|
search: { type: 'string', description: 'Search term for product names' },
|
||||||
storeId: { type: 'string', description: 'Filter by store ID' },
|
storeId: { type: 'string', description: 'Filter by store ID' },
|
||||||
includedInStore: { type: 'boolean', description: 'Filter by store inclusion status' },
|
includedInStore: { type: 'boolean', description: 'Filter by store inclusion status' },
|
||||||
availableInStore: { type: 'boolean', description: 'Filter by store availability' }
|
availableInStore: { type: 'boolean', description: 'Filter by store availability' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
@ -213,7 +227,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
productId: { type: 'string', description: 'Product ID to retrieve' },
|
productId: { type: 'string', description: 'Product ID to retrieve' },
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -234,7 +255,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
},
|
},
|
||||||
description: { type: 'string', description: 'Product description' },
|
description: { type: 'string', description: 'Product description' },
|
||||||
image: { type: 'string', description: 'Product image URL' },
|
image: { type: 'string', description: 'Product image URL' },
|
||||||
availableInStore: { type: 'boolean', description: 'Whether product is available in store' }
|
availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -246,7 +274,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
productId: { type: 'string', description: 'Product ID to delete' },
|
productId: { type: 'string', description: 'Product ID to delete' },
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -269,7 +304,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
||||||
amount: { type: 'number', description: 'Price amount in cents' },
|
amount: { type: 'number', description: 'Price amount in cents' },
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
compareAtPrice: { type: 'number', description: 'Compare at price (for discounts)' }
|
compareAtPrice: { type: 'number', description: 'Compare at price (for discounts)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId', 'name', 'type', 'currency', 'amount']
|
required: ['productId', 'name', 'type', 'currency', 'amount']
|
||||||
}
|
}
|
||||||
@ -283,7 +325,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
productId: { type: 'string', description: 'Product ID to list prices for' },
|
productId: { type: 'string', description: 'Product ID to list prices for' },
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
limit: { type: 'number', description: 'Maximum number of prices to return' },
|
limit: { type: 'number', description: 'Maximum number of prices to return' },
|
||||||
offset: { type: 'number', description: 'Number of prices to skip' }
|
offset: { type: 'number', description: 'Number of prices to skip' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['productId']
|
required: ['productId']
|
||||||
}
|
}
|
||||||
@ -299,7 +348,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
limit: { type: 'number', description: 'Maximum number of items to return' },
|
limit: { type: 'number', description: 'Maximum number of items to return' },
|
||||||
offset: { type: 'number', description: 'Number of items to skip' },
|
offset: { type: 'number', description: 'Number of items to skip' },
|
||||||
search: { type: 'string', description: 'Search term for inventory items' }
|
search: { type: 'string', description: 'Search term for inventory items' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
@ -322,7 +378,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
title: { type: 'string', description: 'SEO title' },
|
title: { type: 'string', description: 'SEO title' },
|
||||||
description: { type: 'string', description: 'SEO description' }
|
description: { type: 'string', description: 'SEO description' }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'slug']
|
required: ['name', 'slug']
|
||||||
}
|
}
|
||||||
@ -336,7 +399,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
|||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
limit: { type: 'number', description: 'Maximum number of collections to return' },
|
limit: { type: 'number', description: 'Maximum number of collections to return' },
|
||||||
offset: { type: 'number', description: 'Number of collections to skip' },
|
offset: { type: 'number', description: 'Number of collections to skip' },
|
||||||
name: { type: 'string', description: 'Search by collection name' }
|
name: { type: 'string', description: 'Search by collection name' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "products",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,14 @@ export class ReportingTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -36,7 +43,14 @@ export class ReportingTools {
|
|||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
userId: { type: 'string', description: 'Filter by user ID' },
|
userId: { type: 'string', description: 'Filter by user ID' },
|
||||||
type: { type: 'string', enum: ['inbound', 'outbound', 'all'], description: 'Call type filter' }
|
type: { type: 'string', enum: ['inbound', 'outbound', 'all'], description: 'Call type filter' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -53,7 +67,14 @@ export class ReportingTools {
|
|||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
calendarId: { type: 'string', description: 'Filter by calendar ID' },
|
calendarId: { type: 'string', description: 'Filter by calendar ID' },
|
||||||
status: { type: 'string', enum: ['booked', 'confirmed', 'showed', 'noshow', 'cancelled'], description: 'Appointment status filter' }
|
status: { type: 'string', enum: ['booked', 'confirmed', 'showed', 'noshow', 'cancelled'], description: 'Appointment status filter' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -70,7 +91,14 @@ export class ReportingTools {
|
|||||||
pipelineId: { type: 'string', description: 'Filter by pipeline ID' },
|
pipelineId: { type: 'string', description: 'Filter by pipeline ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
userId: { type: 'string', description: 'Filter by assigned user' }
|
userId: { type: 'string', description: 'Filter by assigned user' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -85,7 +113,14 @@ export class ReportingTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -98,7 +133,14 @@ export class ReportingTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -114,7 +156,14 @@ export class ReportingTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
funnelId: { type: 'string', description: 'Filter by funnel ID' },
|
funnelId: { type: 'string', description: 'Filter by funnel ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -130,7 +179,14 @@ export class ReportingTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
platform: { type: 'string', enum: ['google', 'facebook', 'all'], description: 'Ad platform' },
|
platform: { type: 'string', enum: ['google', 'facebook', 'all'], description: 'Ad platform' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -146,7 +202,14 @@ export class ReportingTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
userId: { type: 'string', description: 'Filter by user ID' },
|
userId: { type: 'string', description: 'Filter by user ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -164,6 +227,13 @@ export class ReportingTools {
|
|||||||
startDate: { type: 'string', description: 'Start date for custom range' },
|
startDate: { type: 'string', description: 'Start date for custom range' },
|
||||||
endDate: { type: 'string', description: 'End date for custom range' }
|
endDate: { type: 'string', description: 'End date for custom range' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -177,7 +247,14 @@ export class ReportingTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
source: { type: 'string', description: 'Filter by source' }
|
source: { type: 'string', description: 'Filter by source' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
@ -193,7 +270,14 @@ export class ReportingTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||||
groupBy: { type: 'string', enum: ['day', 'week', 'month'], description: 'Group results by' }
|
groupBy: { type: 'string', enum: ['day', 'week', 'month'], description: 'Group results by' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "analytics",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['startDate', 'endDate']
|
required: ['startDate', 'endDate']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,13 @@ export class ReputationTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,7 +42,14 @@ export class ReputationTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
reviewId: { type: 'string', description: 'Review ID' },
|
reviewId: { type: 'string', description: 'Review ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['reviewId']
|
required: ['reviewId']
|
||||||
}
|
}
|
||||||
@ -48,7 +62,14 @@ export class ReputationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
reviewId: { type: 'string', description: 'Review ID' },
|
reviewId: { type: 'string', description: 'Review ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
reply: { type: 'string', description: 'Reply text' }
|
reply: { type: 'string', description: 'Reply text' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['reviewId', 'reply']
|
required: ['reviewId', 'reply']
|
||||||
}
|
}
|
||||||
@ -61,7 +82,14 @@ export class ReputationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
reviewId: { type: 'string', description: 'Review ID' },
|
reviewId: { type: 'string', description: 'Review ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
reply: { type: 'string', description: 'Updated reply text' }
|
reply: { type: 'string', description: 'Updated reply text' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['reviewId', 'reply']
|
required: ['reviewId', 'reply']
|
||||||
}
|
}
|
||||||
@ -73,7 +101,14 @@ export class ReputationTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
reviewId: { type: 'string', description: 'Review ID' },
|
reviewId: { type: 'string', description: 'Review ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['reviewId']
|
required: ['reviewId']
|
||||||
}
|
}
|
||||||
@ -91,6 +126,13 @@ export class ReputationTools {
|
|||||||
startDate: { type: 'string', description: 'Start date' },
|
startDate: { type: 'string', description: 'Start date' },
|
||||||
endDate: { type: 'string', description: 'End date' }
|
endDate: { type: 'string', description: 'End date' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -105,7 +147,14 @@ export class ReputationTools {
|
|||||||
contactId: { type: 'string', description: 'Contact ID to request review from' },
|
contactId: { type: 'string', description: 'Contact ID to request review from' },
|
||||||
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to request review on' },
|
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to request review on' },
|
||||||
method: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Delivery method' },
|
method: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Delivery method' },
|
||||||
message: { type: 'string', description: 'Custom message (optional)' }
|
message: { type: 'string', description: 'Custom message (optional)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['contactId', 'platform', 'method']
|
required: ['contactId', 'platform', 'method']
|
||||||
}
|
}
|
||||||
@ -122,6 +171,13 @@ export class ReputationTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -134,6 +190,13 @@ export class ReputationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -144,6 +207,13 @@ export class ReputationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -153,7 +223,14 @@ export class ReputationTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to disconnect' }
|
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to disconnect' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['platform']
|
required: ['platform']
|
||||||
}
|
}
|
||||||
@ -168,6 +245,13 @@ export class ReputationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -181,6 +265,13 @@ export class ReputationTools {
|
|||||||
facebookLink: { type: 'string', description: 'Custom Facebook review link' },
|
facebookLink: { type: 'string', description: 'Custom Facebook review link' },
|
||||||
yelpLink: { type: 'string', description: 'Custom Yelp review link' }
|
yelpLink: { type: 'string', description: 'Custom Yelp review link' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -193,6 +284,13 @@ export class ReputationTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "reputation",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -36,7 +36,14 @@ export class SaasTools {
|
|||||||
isActive: {
|
isActive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Filter by active status'
|
description: 'Filter by active status'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "saas",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId']
|
required: ['companyId']
|
||||||
}
|
}
|
||||||
@ -54,7 +61,14 @@ export class SaasTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID to retrieve'
|
description: 'Location ID to retrieve'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "saas",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId', 'locationId']
|
required: ['companyId', 'locationId']
|
||||||
}
|
}
|
||||||
@ -81,7 +95,14 @@ export class SaasTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['active', 'paused', 'cancelled'],
|
enum: ['active', 'paused', 'cancelled'],
|
||||||
description: 'Subscription status'
|
description: 'Subscription status'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "saas",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId', 'locationId']
|
required: ['companyId', 'locationId']
|
||||||
}
|
}
|
||||||
@ -103,7 +124,14 @@ export class SaasTools {
|
|||||||
paused: {
|
paused: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether to pause (true) or unpause (false)'
|
description: 'Whether to pause (true) or unpause (false)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "saas",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId', 'locationId', 'paused']
|
required: ['companyId', 'locationId', 'paused']
|
||||||
}
|
}
|
||||||
@ -125,7 +153,14 @@ export class SaasTools {
|
|||||||
enabled: {
|
enabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether to enable (true) or disable (false) SaaS'
|
description: 'Whether to enable (true) or disable (false) SaaS'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "saas",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId', 'locationId', 'enabled']
|
required: ['companyId', 'locationId', 'enabled']
|
||||||
}
|
}
|
||||||
@ -151,7 +186,14 @@ export class SaasTools {
|
|||||||
enabled: {
|
enabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether rebilling is enabled'
|
description: 'Whether rebilling is enabled'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "saas",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId']
|
required: ['companyId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,13 @@ export class SmartListsTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -29,7 +36,14 @@ export class SmartListsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
smartListId: { type: 'string', description: 'Smart List ID' },
|
smartListId: { type: 'string', description: 'Smart List ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['smartListId']
|
required: ['smartListId']
|
||||||
}
|
}
|
||||||
@ -50,7 +64,14 @@ export class SmartListsTools {
|
|||||||
field: { type: 'string', description: 'Field to filter on' },
|
field: { type: 'string', description: 'Field to filter on' },
|
||||||
operator: { type: 'string', description: 'Comparison operator (equals, contains, etc.)' },
|
operator: { type: 'string', description: 'Comparison operator (equals, contains, etc.)' },
|
||||||
value: { type: 'string', description: 'Filter value' }
|
value: { type: 'string', description: 'Filter value' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Filter conditions'
|
description: 'Filter conditions'
|
||||||
},
|
},
|
||||||
@ -69,7 +90,14 @@ export class SmartListsTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Smart list name' },
|
name: { type: 'string', description: 'Smart list name' },
|
||||||
filters: { type: 'array', description: 'Filter conditions' },
|
filters: { type: 'array', description: 'Filter conditions' },
|
||||||
filterOperator: { type: 'string', enum: ['AND', 'OR'], description: 'How to combine filters' }
|
filterOperator: { type: 'string', enum: ['AND', 'OR'], description: 'How to combine filters' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['smartListId']
|
required: ['smartListId']
|
||||||
}
|
}
|
||||||
@ -81,7 +109,14 @@ export class SmartListsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
smartListId: { type: 'string', description: 'Smart List ID' },
|
smartListId: { type: 'string', description: 'Smart List ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['smartListId']
|
required: ['smartListId']
|
||||||
}
|
}
|
||||||
@ -95,7 +130,14 @@ export class SmartListsTools {
|
|||||||
smartListId: { type: 'string', description: 'Smart List ID' },
|
smartListId: { type: 'string', description: 'Smart List ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['smartListId']
|
required: ['smartListId']
|
||||||
}
|
}
|
||||||
@ -107,7 +149,14 @@ export class SmartListsTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
smartListId: { type: 'string', description: 'Smart List ID' },
|
smartListId: { type: 'string', description: 'Smart List ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['smartListId']
|
required: ['smartListId']
|
||||||
}
|
}
|
||||||
@ -120,7 +169,14 @@ export class SmartListsTools {
|
|||||||
properties: {
|
properties: {
|
||||||
smartListId: { type: 'string', description: 'Smart List ID to duplicate' },
|
smartListId: { type: 'string', description: 'Smart List ID to duplicate' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Name for the duplicate' }
|
name: { type: 'string', description: 'Name for the duplicate' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "smartlists",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['smartListId']
|
required: ['smartListId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,14 @@ export class SnapshotsTools {
|
|||||||
limit: {
|
limit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Maximum number of snapshots to return'
|
description: 'Maximum number of snapshots to return'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "snapshots",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId']
|
required: ['companyId']
|
||||||
}
|
}
|
||||||
@ -45,7 +52,14 @@ export class SnapshotsTools {
|
|||||||
companyId: {
|
companyId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Company/Agency ID'
|
description: 'Company/Agency ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "snapshots",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['snapshotId', 'companyId']
|
required: ['snapshotId', 'companyId']
|
||||||
}
|
}
|
||||||
@ -71,7 +85,14 @@ export class SnapshotsTools {
|
|||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Description of the snapshot'
|
description: 'Description of the snapshot'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "snapshots",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['companyId', 'locationId', 'name']
|
required: ['companyId', 'locationId', 'name']
|
||||||
}
|
}
|
||||||
@ -93,7 +114,14 @@ export class SnapshotsTools {
|
|||||||
pushId: {
|
pushId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The push operation ID'
|
description: 'The push operation ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "snapshots",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['snapshotId', 'companyId']
|
required: ['snapshotId', 'companyId']
|
||||||
}
|
}
|
||||||
@ -115,7 +143,14 @@ export class SnapshotsTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Target location ID'
|
description: 'Target location ID'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "snapshots",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['snapshotId', 'companyId', 'locationId']
|
required: ['snapshotId', 'companyId', 'locationId']
|
||||||
}
|
}
|
||||||
@ -150,7 +185,14 @@ export class SnapshotsTools {
|
|||||||
surveys: { type: 'boolean', description: 'Override existing surveys' },
|
surveys: { type: 'boolean', description: 'Override existing surveys' },
|
||||||
calendars: { type: 'boolean', description: 'Override existing calendars' },
|
calendars: { type: 'boolean', description: 'Override existing calendars' },
|
||||||
automations: { type: 'boolean', description: 'Override existing automations' },
|
automations: { type: 'boolean', description: 'Override existing automations' },
|
||||||
triggers: { type: 'boolean', description: 'Override existing triggers' }
|
triggers: { type: 'boolean', description: 'Override existing triggers' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "snapshots",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'What to override vs skip'
|
description: 'What to override vs skip'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,14 @@ export class SocialMediaTools {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['post', 'story', 'reel'],
|
enum: ['post', 'story', 'reel'],
|
||||||
description: 'Type of post to search for'
|
description: 'Type of post to search for'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['fromDate', 'toDate']
|
required: ['fromDate', 'toDate']
|
||||||
}
|
}
|
||||||
@ -80,7 +87,14 @@ export class SocialMediaTools {
|
|||||||
properties: {
|
properties: {
|
||||||
url: { type: 'string', description: 'Media URL' },
|
url: { type: 'string', description: 'Media URL' },
|
||||||
caption: { type: 'string', description: 'Media caption' },
|
caption: { type: 'string', description: 'Media caption' },
|
||||||
type: { type: 'string', description: 'Media MIME type' }
|
type: { type: 'string', description: 'Media MIME type' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['url']
|
required: ['url']
|
||||||
},
|
},
|
||||||
@ -116,7 +130,14 @@ export class SocialMediaTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
postId: { type: 'string', description: 'Social media post ID' }
|
postId: { type: 'string', description: 'Social media post ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['postId']
|
required: ['postId']
|
||||||
}
|
}
|
||||||
@ -139,7 +160,14 @@ export class SocialMediaTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Updated tag IDs'
|
description: 'Updated tag IDs'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['postId']
|
required: ['postId']
|
||||||
}
|
}
|
||||||
@ -150,7 +178,14 @@ export class SocialMediaTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
postId: { type: 'string', description: 'Social media post ID to delete' }
|
postId: { type: 'string', description: 'Social media post ID to delete' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['postId']
|
required: ['postId']
|
||||||
}
|
}
|
||||||
@ -166,7 +201,14 @@ export class SocialMediaTools {
|
|||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Array of post IDs to delete',
|
description: 'Array of post IDs to delete',
|
||||||
maxItems: 50
|
maxItems: 50
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "batch"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['postIds']
|
required: ['postIds']
|
||||||
}
|
}
|
||||||
@ -180,6 +222,13 @@ export class SocialMediaTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {},
|
properties: {},
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -190,7 +239,14 @@ export class SocialMediaTools {
|
|||||||
properties: {
|
properties: {
|
||||||
accountId: { type: 'string', description: 'Account ID to delete' },
|
accountId: { type: 'string', description: 'Account ID to delete' },
|
||||||
companyId: { type: 'string', description: 'Company ID' },
|
companyId: { type: 'string', description: 'Company ID' },
|
||||||
userId: { type: 'string', description: 'User ID' }
|
userId: { type: 'string', description: 'User ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['accountId']
|
required: ['accountId']
|
||||||
}
|
}
|
||||||
@ -203,7 +259,14 @@ export class SocialMediaTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
file: { type: 'string', description: 'CSV file data (base64 or file path)' }
|
file: { type: 'string', description: 'CSV file data (base64 or file path)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['file']
|
required: ['file']
|
||||||
}
|
}
|
||||||
@ -219,6 +282,13 @@ export class SocialMediaTools {
|
|||||||
includeUsers: { type: 'boolean', description: 'Include user data' },
|
includeUsers: { type: 'boolean', description: 'Include user data' },
|
||||||
userId: { type: 'string', description: 'Filter by user ID' }
|
userId: { type: 'string', description: 'Filter by user ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -236,7 +306,14 @@ export class SocialMediaTools {
|
|||||||
rowsCount: { type: 'number', description: 'Number of rows to process' },
|
rowsCount: { type: 'number', description: 'Number of rows to process' },
|
||||||
fileName: { type: 'string', description: 'CSV file name' },
|
fileName: { type: 'string', description: 'CSV file name' },
|
||||||
approver: { type: 'string', description: 'Approver user ID' },
|
approver: { type: 'string', description: 'Approver user ID' },
|
||||||
userId: { type: 'string', description: 'User ID' }
|
userId: { type: 'string', description: 'User ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['accountIds', 'filePath', 'rowsCount', 'fileName']
|
required: ['accountIds', 'filePath', 'rowsCount', 'fileName']
|
||||||
}
|
}
|
||||||
@ -253,6 +330,13 @@ export class SocialMediaTools {
|
|||||||
limit: { type: 'number', description: 'Number to return', default: 10 },
|
limit: { type: 'number', description: 'Number to return', default: 10 },
|
||||||
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -261,7 +345,14 @@ export class SocialMediaTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
categoryId: { type: 'string', description: 'Category ID' }
|
categoryId: { type: 'string', description: 'Category ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['categoryId']
|
required: ['categoryId']
|
||||||
}
|
}
|
||||||
@ -276,6 +367,13 @@ export class SocialMediaTools {
|
|||||||
limit: { type: 'number', description: 'Number to return', default: 10 },
|
limit: { type: 'number', description: 'Number to return', default: 10 },
|
||||||
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -288,7 +386,14 @@ export class SocialMediaTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Array of tag IDs'
|
description: 'Array of tag IDs'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['tagIds']
|
required: ['tagIds']
|
||||||
}
|
}
|
||||||
@ -308,7 +413,14 @@ export class SocialMediaTools {
|
|||||||
},
|
},
|
||||||
userId: { type: 'string', description: 'User ID initiating OAuth' },
|
userId: { type: 'string', description: 'User ID initiating OAuth' },
|
||||||
page: { type: 'string', description: 'Page context' },
|
page: { type: 'string', description: 'Page context' },
|
||||||
reconnect: { type: 'boolean', description: 'Whether this is a reconnection' }
|
reconnect: { type: 'boolean', description: 'Whether this is a reconnection' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['platform', 'userId']
|
required: ['platform', 'userId']
|
||||||
}
|
}
|
||||||
@ -324,7 +436,14 @@ export class SocialMediaTools {
|
|||||||
enum: ['google', 'facebook', 'instagram', 'linkedin', 'twitter', 'tiktok', 'tiktok-business'],
|
enum: ['google', 'facebook', 'instagram', 'linkedin', 'twitter', 'tiktok', 'tiktok-business'],
|
||||||
description: 'Social media platform'
|
description: 'Social media platform'
|
||||||
},
|
},
|
||||||
accountId: { type: 'string', description: 'OAuth account ID' }
|
accountId: { type: 'string', description: 'OAuth account ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "social-media",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['platform', 'accountId']
|
required: ['platform', 'accountId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1068,7 +1068,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
code: { type: 'string', description: 'State code (e.g., CA, NY)' }
|
code: { type: 'string', description: 'State code (e.g., CA, NY)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['code']
|
required: ['code']
|
||||||
}
|
}
|
||||||
@ -1092,6 +1099,13 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
offset: { type: 'number', description: 'Number of zones to skip (optional)' },
|
offset: { type: 'number', description: 'Number of zones to skip (optional)' },
|
||||||
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' }
|
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1102,7 +1116,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to retrieve' },
|
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to retrieve' },
|
||||||
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' }
|
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId']
|
required: ['shippingZoneId']
|
||||||
}
|
}
|
||||||
@ -1129,7 +1150,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
code: { type: 'string', description: 'State code (e.g., CA, NY)' }
|
code: { type: 'string', description: 'State code (e.g., CA, NY)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['code']
|
required: ['code']
|
||||||
}
|
}
|
||||||
@ -1149,7 +1177,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to delete' }
|
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to delete' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId']
|
required: ['shippingZoneId']
|
||||||
}
|
}
|
||||||
@ -1170,7 +1205,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
properties: {
|
properties: {
|
||||||
street1: { type: 'string', description: 'Street address line 1' },
|
street1: { type: 'string', description: 'Street address line 1' },
|
||||||
city: { type: 'string', description: 'City' },
|
city: { type: 'string', description: 'City' },
|
||||||
country: { type: 'string', description: 'Country code' }
|
country: { type: 'string', description: 'Country code' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['street1', 'city', 'country']
|
required: ['street1', 'city', 'country']
|
||||||
},
|
},
|
||||||
@ -1203,7 +1245,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
name: { type: 'string', description: 'Name of the shipping rate' },
|
name: { type: 'string', description: 'Name of the shipping rate' },
|
||||||
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
||||||
amount: { type: 'number', description: 'Shipping rate amount' },
|
amount: { type: 'number', description: 'Shipping rate amount' },
|
||||||
conditionType: { type: 'string', description: 'Condition type for rate calculation' }
|
conditionType: { type: 'string', description: 'Condition type for rate calculation' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId', 'name', 'currency', 'amount', 'conditionType']
|
required: ['shippingZoneId', 'name', 'currency', 'amount', 'conditionType']
|
||||||
}
|
}
|
||||||
@ -1215,7 +1264,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' }
|
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId']
|
required: ['shippingZoneId']
|
||||||
}
|
}
|
||||||
@ -1228,7 +1284,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
||||||
shippingRateId: { type: 'string', description: 'ID of the shipping rate to retrieve' }
|
shippingRateId: { type: 'string', description: 'ID of the shipping rate to retrieve' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId', 'shippingRateId']
|
required: ['shippingZoneId', 'shippingRateId']
|
||||||
}
|
}
|
||||||
@ -1241,7 +1304,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
||||||
shippingRateId: { type: 'string', description: 'ID of the shipping rate to update' }
|
shippingRateId: { type: 'string', description: 'ID of the shipping rate to update' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId', 'shippingRateId']
|
required: ['shippingZoneId', 'shippingRateId']
|
||||||
}
|
}
|
||||||
@ -1254,7 +1324,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
|
||||||
shippingRateId: { type: 'string', description: 'ID of the shipping rate to delete' }
|
shippingRateId: { type: 'string', description: 'ID of the shipping rate to delete' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingZoneId', 'shippingRateId']
|
required: ['shippingZoneId', 'shippingRateId']
|
||||||
}
|
}
|
||||||
@ -1277,7 +1354,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: 'string', description: 'Service name' },
|
name: { type: 'string', description: 'Service name' },
|
||||||
value: { type: 'string', description: 'Service value' }
|
value: { type: 'string', description: 'Service value' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'value']
|
required: ['name', 'value']
|
||||||
}
|
}
|
||||||
@ -1294,6 +1378,13 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1303,7 +1394,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to retrieve' }
|
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to retrieve' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingCarrierId']
|
required: ['shippingCarrierId']
|
||||||
}
|
}
|
||||||
@ -1315,7 +1413,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to update' }
|
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to update' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingCarrierId']
|
required: ['shippingCarrierId']
|
||||||
}
|
}
|
||||||
@ -1327,7 +1432,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||||
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to delete' }
|
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to delete' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['shippingCarrierId']
|
required: ['shippingCarrierId']
|
||||||
}
|
}
|
||||||
@ -1349,7 +1461,14 @@ These settings control your store's shipping origin and email notification prefe
|
|||||||
street1: { type: 'string', description: 'Street address line 1' },
|
street1: { type: 'string', description: 'Street address line 1' },
|
||||||
city: { type: 'string', description: 'City' },
|
city: { type: 'string', description: 'City' },
|
||||||
zip: { type: 'string', description: 'Postal/ZIP code' },
|
zip: { type: 'string', description: 'Postal/ZIP code' },
|
||||||
country: { type: 'string', description: 'Country code' }
|
country: { type: 'string', description: 'Country code' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "stores",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'street1', 'city', 'zip', 'country']
|
required: ['name', 'street1', 'city', 'zip', 'country']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,14 @@ export class SurveyTools {
|
|||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Filter surveys by type (e.g., "folder")'
|
description: 'Filter surveys by type (e.g., "folder")'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "surveys",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
@ -69,7 +76,14 @@ export class SurveyTools {
|
|||||||
endAt: {
|
endAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'End date for filtering submissions (YYYY-MM-DD format)'
|
description: 'End date for filtering submissions (YYYY-MM-DD format)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "surveys",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,13 @@ export class TemplatesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -30,7 +37,14 @@ export class TemplatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'SMS Template ID' },
|
templateId: { type: 'string', description: 'SMS Template ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -43,7 +57,14 @@ export class TemplatesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Template name' },
|
name: { type: 'string', description: 'Template name' },
|
||||||
body: { type: 'string', description: 'SMS message body (can include merge fields like {{contact.first_name}})' }
|
body: { type: 'string', description: 'SMS message body (can include merge fields like {{contact.first_name}})' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'body']
|
required: ['name', 'body']
|
||||||
}
|
}
|
||||||
@ -57,7 +78,14 @@ export class TemplatesTools {
|
|||||||
templateId: { type: 'string', description: 'SMS Template ID' },
|
templateId: { type: 'string', description: 'SMS Template ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Template name' },
|
name: { type: 'string', description: 'Template name' },
|
||||||
body: { type: 'string', description: 'SMS message body' }
|
body: { type: 'string', description: 'SMS message body' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -69,7 +97,14 @@ export class TemplatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'SMS Template ID' },
|
templateId: { type: 'string', description: 'SMS Template ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -84,6 +119,13 @@ export class TemplatesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,7 +136,14 @@ export class TemplatesTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Template name' },
|
name: { type: 'string', description: 'Template name' },
|
||||||
audioUrl: { type: 'string', description: 'URL to audio file' }
|
audioUrl: { type: 'string', description: 'URL to audio file' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'audioUrl']
|
required: ['name', 'audioUrl']
|
||||||
}
|
}
|
||||||
@ -106,7 +155,14 @@ export class TemplatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -123,6 +179,13 @@ export class TemplatesTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -135,7 +198,14 @@ export class TemplatesTools {
|
|||||||
name: { type: 'string', description: 'Template name' },
|
name: { type: 'string', description: 'Template name' },
|
||||||
content: { type: 'string', description: 'Post content' },
|
content: { type: 'string', description: 'Post content' },
|
||||||
mediaUrls: { type: 'array', items: { type: 'string' }, description: 'Media URLs' },
|
mediaUrls: { type: 'array', items: { type: 'string' }, description: 'Media URLs' },
|
||||||
platforms: { type: 'array', items: { type: 'string' }, description: 'Target platforms' }
|
platforms: { type: 'array', items: { type: 'string' }, description: 'Target platforms' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'content']
|
required: ['name', 'content']
|
||||||
}
|
}
|
||||||
@ -147,7 +217,14 @@ export class TemplatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -163,6 +240,13 @@ export class TemplatesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
status: { type: 'string', enum: ['approved', 'pending', 'rejected', 'all'], description: 'Template status' }
|
status: { type: 'string', enum: ['approved', 'pending', 'rejected', 'all'], description: 'Template status' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -175,7 +259,14 @@ export class TemplatesTools {
|
|||||||
name: { type: 'string', description: 'Template name' },
|
name: { type: 'string', description: 'Template name' },
|
||||||
category: { type: 'string', enum: ['marketing', 'utility', 'authentication'], description: 'Template category' },
|
category: { type: 'string', enum: ['marketing', 'utility', 'authentication'], description: 'Template category' },
|
||||||
language: { type: 'string', description: 'Language code (e.g., en_US)' },
|
language: { type: 'string', description: 'Language code (e.g., en_US)' },
|
||||||
components: { type: 'array', description: 'Template components (header, body, footer, buttons)' }
|
components: { type: 'array', description: 'Template components (header, body, footer, buttons)' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'category', 'language', 'components']
|
required: ['name', 'category', 'language', 'components']
|
||||||
}
|
}
|
||||||
@ -187,7 +278,14 @@ export class TemplatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
templateId: { type: 'string', description: 'Template ID' },
|
templateId: { type: 'string', description: 'Template ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['templateId']
|
required: ['templateId']
|
||||||
}
|
}
|
||||||
@ -203,6 +301,13 @@ export class TemplatesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
type: { type: 'string', enum: ['sms', 'email', 'all'], description: 'Snippet type' }
|
type: { type: 'string', enum: ['sms', 'email', 'all'], description: 'Snippet type' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -215,7 +320,14 @@ export class TemplatesTools {
|
|||||||
name: { type: 'string', description: 'Snippet name' },
|
name: { type: 'string', description: 'Snippet name' },
|
||||||
shortcut: { type: 'string', description: 'Keyboard shortcut (e.g., /thanks)' },
|
shortcut: { type: 'string', description: 'Keyboard shortcut (e.g., /thanks)' },
|
||||||
content: { type: 'string', description: 'Snippet content' },
|
content: { type: 'string', description: 'Snippet content' },
|
||||||
type: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Snippet type' }
|
type: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Snippet type' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'content']
|
required: ['name', 'content']
|
||||||
}
|
}
|
||||||
@ -230,7 +342,14 @@ export class TemplatesTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Snippet name' },
|
name: { type: 'string', description: 'Snippet name' },
|
||||||
shortcut: { type: 'string', description: 'Keyboard shortcut' },
|
shortcut: { type: 'string', description: 'Keyboard shortcut' },
|
||||||
content: { type: 'string', description: 'Snippet content' }
|
content: { type: 'string', description: 'Snippet content' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['snippetId']
|
required: ['snippetId']
|
||||||
}
|
}
|
||||||
@ -242,7 +361,14 @@ export class TemplatesTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
snippetId: { type: 'string', description: 'Snippet ID' },
|
snippetId: { type: 'string', description: 'Snippet ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "templates",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['snippetId']
|
required: ['snippetId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,13 @@ export class TriggersTools {
|
|||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -31,7 +38,14 @@ export class TriggersTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
triggerId: { type: 'string', description: 'Trigger ID' },
|
triggerId: { type: 'string', description: 'Trigger ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -63,7 +77,14 @@ export class TriggersTools {
|
|||||||
field: { type: 'string', description: 'Field to filter' },
|
field: { type: 'string', description: 'Field to filter' },
|
||||||
operator: { type: 'string', description: 'Comparison operator' },
|
operator: { type: 'string', description: 'Comparison operator' },
|
||||||
value: { type: 'string', description: 'Filter value' }
|
value: { type: 'string', description: 'Filter value' }
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: 'Conditions that must be met'
|
description: 'Conditions that must be met'
|
||||||
},
|
},
|
||||||
@ -93,7 +114,14 @@ export class TriggersTools {
|
|||||||
name: { type: 'string', description: 'Trigger name' },
|
name: { type: 'string', description: 'Trigger name' },
|
||||||
filters: { type: 'array', description: 'Filter conditions' },
|
filters: { type: 'array', description: 'Filter conditions' },
|
||||||
actions: { type: 'array', description: 'Actions to perform' },
|
actions: { type: 'array', description: 'Actions to perform' },
|
||||||
status: { type: 'string', enum: ['active', 'inactive'], description: 'Trigger status' }
|
status: { type: 'string', enum: ['active', 'inactive'], description: 'Trigger status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -105,7 +133,14 @@ export class TriggersTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
triggerId: { type: 'string', description: 'Trigger ID' },
|
triggerId: { type: 'string', description: 'Trigger ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -117,7 +152,14 @@ export class TriggersTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
triggerId: { type: 'string', description: 'Trigger ID' },
|
triggerId: { type: 'string', description: 'Trigger ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -129,7 +171,14 @@ export class TriggersTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
triggerId: { type: 'string', description: 'Trigger ID' },
|
triggerId: { type: 'string', description: 'Trigger ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -142,6 +191,13 @@ export class TriggersTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,7 +212,14 @@ export class TriggersTools {
|
|||||||
startDate: { type: 'string', description: 'Start date' },
|
startDate: { type: 'string', description: 'Start date' },
|
||||||
endDate: { type: 'string', description: 'End date' },
|
endDate: { type: 'string', description: 'End date' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' }
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -169,7 +232,14 @@ export class TriggersTools {
|
|||||||
properties: {
|
properties: {
|
||||||
triggerId: { type: 'string', description: 'Trigger ID' },
|
triggerId: { type: 'string', description: 'Trigger ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
testData: { type: 'object', description: 'Sample data to test with' }
|
testData: { type: 'object', description: 'Sample data to test with' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
@ -182,7 +252,14 @@ export class TriggersTools {
|
|||||||
properties: {
|
properties: {
|
||||||
triggerId: { type: 'string', description: 'Trigger ID to duplicate' },
|
triggerId: { type: 'string', description: 'Trigger ID to duplicate' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
name: { type: 'string', description: 'Name for the duplicate' }
|
name: { type: 'string', description: 'Name for the duplicate' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "triggers",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['triggerId']
|
required: ['triggerId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,13 @@ export class UsersTools {
|
|||||||
description: 'Sort direction'
|
description: 'Sort direction'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "users",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -65,7 +72,14 @@ export class UsersTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "users",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['userId']
|
required: ['userId']
|
||||||
}
|
}
|
||||||
@ -117,7 +131,14 @@ export class UsersTools {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Scopes only assigned to this user'
|
description: 'Scopes only assigned to this user'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "users",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['firstName', 'lastName', 'email']
|
required: ['firstName', 'lastName', 'email']
|
||||||
}
|
}
|
||||||
@ -163,7 +184,14 @@ export class UsersTools {
|
|||||||
permissions: {
|
permissions: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'User permissions object'
|
description: 'User permissions object'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "users",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['userId']
|
required: ['userId']
|
||||||
}
|
}
|
||||||
@ -181,7 +209,14 @@ export class UsersTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Location ID (uses default if not provided)'
|
description: 'Location ID (uses default if not provided)'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "users",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['userId']
|
required: ['userId']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,13 @@ export class WebhooksTools {
|
|||||||
properties: {
|
properties: {
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' }
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -27,7 +34,14 @@ export class WebhooksTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
webhookId: { type: 'string', description: 'Webhook ID' },
|
webhookId: { type: 'string', description: 'Webhook ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['webhookId']
|
required: ['webhookId']
|
||||||
}
|
}
|
||||||
@ -46,7 +60,14 @@ export class WebhooksTools {
|
|||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Events to subscribe to (e.g., contact.created, opportunity.updated)'
|
description: 'Events to subscribe to (e.g., contact.created, opportunity.updated)'
|
||||||
},
|
},
|
||||||
secret: { type: 'string', description: 'Secret key for webhook signature verification' }
|
secret: { type: 'string', description: 'Secret key for webhook signature verification' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'url', 'events']
|
required: ['name', 'url', 'events']
|
||||||
}
|
}
|
||||||
@ -66,7 +87,14 @@ export class WebhooksTools {
|
|||||||
items: { type: 'string' },
|
items: { type: 'string' },
|
||||||
description: 'Events to subscribe to'
|
description: 'Events to subscribe to'
|
||||||
},
|
},
|
||||||
active: { type: 'boolean', description: 'Whether webhook is active' }
|
active: { type: 'boolean', description: 'Whether webhook is active' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "write",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['webhookId']
|
required: ['webhookId']
|
||||||
}
|
}
|
||||||
@ -78,7 +106,14 @@ export class WebhooksTools {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
webhookId: { type: 'string', description: 'Webhook ID' },
|
webhookId: { type: 'string', description: 'Webhook ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "delete",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['webhookId']
|
required: ['webhookId']
|
||||||
}
|
}
|
||||||
@ -89,6 +124,13 @@ export class WebhooksTools {
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {}
|
properties: {}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -101,7 +143,14 @@ export class WebhooksTools {
|
|||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
limit: { type: 'number', description: 'Max results' },
|
limit: { type: 'number', description: 'Max results' },
|
||||||
offset: { type: 'number', description: 'Pagination offset' },
|
offset: { type: 'number', description: 'Pagination offset' },
|
||||||
status: { type: 'string', enum: ['success', 'failed', 'pending'], description: 'Filter by delivery status' }
|
status: { type: 'string', enum: ['success', 'failed', 'pending'], description: 'Filter by delivery status' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['webhookId']
|
required: ['webhookId']
|
||||||
}
|
}
|
||||||
@ -114,7 +163,14 @@ export class WebhooksTools {
|
|||||||
properties: {
|
properties: {
|
||||||
webhookId: { type: 'string', description: 'Webhook ID' },
|
webhookId: { type: 'string', description: 'Webhook ID' },
|
||||||
logId: { type: 'string', description: 'Webhook log entry ID to retry' },
|
logId: { type: 'string', description: 'Webhook log entry ID to retry' },
|
||||||
locationId: { type: 'string', description: 'Location ID' }
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['webhookId', 'logId']
|
required: ['webhookId', 'logId']
|
||||||
}
|
}
|
||||||
@ -127,7 +183,14 @@ export class WebhooksTools {
|
|||||||
properties: {
|
properties: {
|
||||||
webhookId: { type: 'string', description: 'Webhook ID' },
|
webhookId: { type: 'string', description: 'Webhook ID' },
|
||||||
locationId: { type: 'string', description: 'Location ID' },
|
locationId: { type: 'string', description: 'Location ID' },
|
||||||
eventType: { type: 'string', description: 'Event type to test' }
|
eventType: { type: 'string', description: 'Event type to test' },
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "webhooks",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ['webhookId', 'eventType']
|
required: ['webhookId', 'eventType']
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,14 @@ export class WorkflowTools {
|
|||||||
locationId: {
|
locationId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The location ID to get workflows for. If not provided, uses the default location from configuration.'
|
description: 'The location ID to get workflows for. If not provided, uses the default location from configuration.'
|
||||||
}
|
},
|
||||||
|
_meta: {
|
||||||
|
labels: {
|
||||||
|
category: "workflows",
|
||||||
|
access: "read",
|
||||||
|
complexity: "simple"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/ui/json-render-app/index.html
Normal file
12
src/ui/json-render-app/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>GHL Dynamic View</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1637
src/ui/json-render-app/package-lock.json
generated
Normal file
1637
src/ui/json-render-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
src/ui/json-render-app/package.json
Normal file
16
src/ui/json-render-app/package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "ghl-json-render-app",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-singlefile": "^2.0.3",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.39.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
250
src/ui/json-render-app/src/charts.ts
Normal file
250
src/ui/json-render-app/src/charts.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* Chart Components for GHL Dynamic View
|
||||||
|
* Pure CSS/SVG charts — no external libraries
|
||||||
|
*/
|
||||||
|
|
||||||
|
type ChartFn = (props: any, children: string) => string;
|
||||||
|
|
||||||
|
function esc(s: unknown): string {
|
||||||
|
if (s === null || s === undefined) return '';
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartPalette = ['#4f46e5', '#7c3aed', '#16a34a', '#3b82f6', '#eab308', '#ef4444', '#ec4899', '#f97316'];
|
||||||
|
|
||||||
|
export const BarChart: ChartFn = (props) => {
|
||||||
|
const {
|
||||||
|
bars = [], orientation = 'vertical', maxValue, showValues = true, title,
|
||||||
|
} = props;
|
||||||
|
const max = maxValue || Math.max(...bars.map((b: any) => b.value), 1);
|
||||||
|
|
||||||
|
if (orientation === 'horizontal') {
|
||||||
|
return `
|
||||||
|
<div class="chart-container">
|
||||||
|
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
|
||||||
|
<div class="bar-chart-h">
|
||||||
|
${bars.map((b: any, i: number) => {
|
||||||
|
const pct = Math.min(100, (b.value / max) * 100);
|
||||||
|
const color = b.color || chartPalette[i % chartPalette.length];
|
||||||
|
return `
|
||||||
|
<div class="bar-h-row">
|
||||||
|
<span class="bar-h-label">${esc(b.label)}</span>
|
||||||
|
<div class="bar-h-track">
|
||||||
|
<div class="bar-h-fill" style="width:${pct}%;background:${color}"></div>
|
||||||
|
</div>
|
||||||
|
${showValues ? `<span class="bar-h-value">${Number(b.value).toLocaleString()}</span>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical bars via SVG
|
||||||
|
const svgW = Math.max(bars.length * 60, 200);
|
||||||
|
const svgH = 180;
|
||||||
|
const padTop = 10;
|
||||||
|
const padBot = 30;
|
||||||
|
const barW = Math.min(36, (svgW / bars.length) * 0.6);
|
||||||
|
const gap = svgW / bars.length;
|
||||||
|
const plotH = svgH - padTop - padBot;
|
||||||
|
|
||||||
|
const barsSvg = bars.map((b: any, i: number) => {
|
||||||
|
const pct = Math.min(1, b.value / max);
|
||||||
|
const h = pct * plotH;
|
||||||
|
const x = gap * i + (gap - barW) / 2;
|
||||||
|
const y = padTop + plotH - h;
|
||||||
|
const color = b.color || chartPalette[i % chartPalette.length];
|
||||||
|
return `
|
||||||
|
<rect x="${x}" y="${y}" width="${barW}" height="${h}" rx="4" fill="${color}" class="bar-v-rect"/>
|
||||||
|
${showValues ? `<text x="${x + barW / 2}" y="${y - 4}" text-anchor="middle" class="bar-v-val">${Number(b.value).toLocaleString()}</text>` : ''}
|
||||||
|
<text x="${x + barW / 2}" y="${svgH - 6}" text-anchor="middle" class="bar-v-label">${esc(b.label)}</text>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="chart-container">
|
||||||
|
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
|
||||||
|
<div class="chart-scroll">
|
||||||
|
<svg viewBox="0 0 ${svgW} ${svgH}" class="bar-chart-svg" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<line x1="0" y1="${padTop + plotH}" x2="${svgW}" y2="${padTop + plotH}" stroke="#e5e7eb" stroke-width="1"/>
|
||||||
|
${barsSvg}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LineChart: ChartFn = (props) => {
|
||||||
|
const {
|
||||||
|
points = [], color = '#4f46e5', showPoints = true, showArea = false, title, yAxisLabel,
|
||||||
|
} = props;
|
||||||
|
if (points.length === 0) return '<div class="chart-container chart-empty">No data</div>';
|
||||||
|
|
||||||
|
const vals = points.map((p: any) => p.value);
|
||||||
|
const minV = Math.min(...vals);
|
||||||
|
const maxV = Math.max(...vals);
|
||||||
|
const range = maxV - minV || 1;
|
||||||
|
|
||||||
|
const svgW = Math.max(points.length * 60, 200);
|
||||||
|
const svgH = 180;
|
||||||
|
const padL = 40;
|
||||||
|
const padR = 10;
|
||||||
|
const padT = 16;
|
||||||
|
const padB = 30;
|
||||||
|
const plotW = svgW - padL - padR;
|
||||||
|
const plotH = svgH - padT - padB;
|
||||||
|
|
||||||
|
const pts = points.map((p: any, i: number) => {
|
||||||
|
const x = padL + (plotW / Math.max(points.length - 1, 1)) * i;
|
||||||
|
const y = padT + plotH - ((p.value - minV) / range) * plotH;
|
||||||
|
return { x, y, label: p.label, value: p.value };
|
||||||
|
});
|
||||||
|
|
||||||
|
const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
|
||||||
|
const areaPath = showArea
|
||||||
|
? `${linePath} L${pts[pts.length - 1].x},${padT + plotH} L${pts[0].x},${padT + plotH} Z`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Y-axis ticks (5 ticks)
|
||||||
|
const ticks = Array.from({ length: 5 }, (_, i) => {
|
||||||
|
const val = minV + (range * i) / 4;
|
||||||
|
const y = padT + plotH - (plotH * i) / 4;
|
||||||
|
return { val, y };
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="chart-container">
|
||||||
|
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
|
||||||
|
<div class="chart-scroll">
|
||||||
|
<svg viewBox="0 0 ${svgW} ${svgH}" class="line-chart-svg" preserveAspectRatio="xMinYMid meet">
|
||||||
|
${ticks.map(t => `
|
||||||
|
<line x1="${padL}" y1="${t.y}" x2="${svgW - padR}" y2="${t.y}" stroke="#f3f4f6" stroke-width="1"/>
|
||||||
|
<text x="${padL - 6}" y="${t.y + 3}" text-anchor="end" class="chart-axis-text">${Math.round(t.val).toLocaleString()}</text>
|
||||||
|
`).join('')}
|
||||||
|
${yAxisLabel ? `<text x="10" y="${padT + plotH / 2}" transform="rotate(-90,10,${padT + plotH / 2})" class="chart-axis-label">${esc(yAxisLabel)}</text>` : ''}
|
||||||
|
${showArea ? `<path d="${areaPath}" fill="${color}" opacity="0.1"/>` : ''}
|
||||||
|
<path d="${linePath}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
${showPoints ? pts.map(p => `<circle cx="${p.x}" cy="${p.y}" r="4" fill="white" stroke="${color}" stroke-width="2.5"/>`).join('') : ''}
|
||||||
|
${pts.map((p, i) => `<text x="${p.x}" y="${svgH - 6}" text-anchor="middle" class="chart-axis-text">${esc(points[i].label)}</text>`).join('')}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PieChart: ChartFn = (props) => {
|
||||||
|
const {
|
||||||
|
segments = [], donut = false, title, showLegend = true,
|
||||||
|
} = props;
|
||||||
|
const total = segments.reduce((s: number, seg: any) => s + seg.value, 0) || 1;
|
||||||
|
const r = 70;
|
||||||
|
const cx = 90;
|
||||||
|
const cy = 90;
|
||||||
|
const svgSize = 180;
|
||||||
|
|
||||||
|
let cumAngle = -Math.PI / 2; // start at 12 o'clock
|
||||||
|
const arcs = segments.map((seg: any, i: number) => {
|
||||||
|
const frac = seg.value / total;
|
||||||
|
const angle = frac * Math.PI * 2;
|
||||||
|
const startAngle = cumAngle;
|
||||||
|
const endAngle = cumAngle + angle;
|
||||||
|
cumAngle = endAngle;
|
||||||
|
|
||||||
|
const x1 = cx + r * Math.cos(startAngle);
|
||||||
|
const y1 = cy + r * Math.sin(startAngle);
|
||||||
|
const x2 = cx + r * Math.cos(endAngle);
|
||||||
|
const y2 = cy + r * Math.sin(endAngle);
|
||||||
|
const largeArc = angle > Math.PI ? 1 : 0;
|
||||||
|
const color = seg.color || chartPalette[i % chartPalette.length];
|
||||||
|
|
||||||
|
// Single full segment
|
||||||
|
if (frac >= 0.9999) {
|
||||||
|
return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<path d="M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc},1 ${x2},${y2} Z" fill="${color}"/>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const donutHole = donut
|
||||||
|
? `<circle cx="${cx}" cy="${cy}" r="${r * 0.55}" fill="white"/>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const legendHtml = showLegend ? `
|
||||||
|
<div class="pie-legend">
|
||||||
|
${segments.map((seg: any, i: number) => {
|
||||||
|
const color = seg.color || chartPalette[i % chartPalette.length];
|
||||||
|
const pct = ((seg.value / total) * 100).toFixed(1);
|
||||||
|
return `<div class="pie-legend-item"><span class="pie-legend-dot" style="background:${color}"></span><span class="pie-legend-label">${esc(seg.label)}</span><span class="pie-legend-value">${pct}%</span></div>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="chart-container">
|
||||||
|
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
|
||||||
|
<div class="pie-chart-layout">
|
||||||
|
<svg viewBox="0 0 ${svgSize} ${svgSize}" class="pie-chart-svg">
|
||||||
|
${arcs.join('')}
|
||||||
|
${donutHole}
|
||||||
|
${donut ? `<text x="${cx}" y="${cy + 5}" text-anchor="middle" class="pie-center-text">${total.toLocaleString()}</text>` : ''}
|
||||||
|
</svg>
|
||||||
|
${legendHtml}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FunnelChart: ChartFn = (props) => {
|
||||||
|
const {
|
||||||
|
stages = [], showDropoff = true, title,
|
||||||
|
} = props;
|
||||||
|
if (stages.length === 0) return '<div class="chart-container chart-empty">No data</div>';
|
||||||
|
|
||||||
|
const maxVal = stages[0]?.value || 1;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="chart-container">
|
||||||
|
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
|
||||||
|
<div class="funnel-chart">
|
||||||
|
${stages.map((s: any, i: number) => {
|
||||||
|
const pct = Math.max(20, (s.value / maxVal) * 100);
|
||||||
|
const color = s.color || chartPalette[i % chartPalette.length];
|
||||||
|
const dropoff = i > 0
|
||||||
|
? (((stages[i - 1].value - s.value) / stages[i - 1].value) * 100).toFixed(1)
|
||||||
|
: null;
|
||||||
|
return `
|
||||||
|
<div class="funnel-stage">
|
||||||
|
<div class="funnel-label-col">
|
||||||
|
<span class="funnel-label">${esc(s.label)}</span>
|
||||||
|
<span class="funnel-value">${Number(s.value).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="funnel-bar-col">
|
||||||
|
<div class="funnel-bar" style="width:${pct}%;background:${color}"></div>
|
||||||
|
</div>
|
||||||
|
${showDropoff && dropoff !== null ? `<span class="funnel-dropoff">-${dropoff}%</span>` : '<span class="funnel-dropoff"></span>'}
|
||||||
|
</div>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SparklineChart: ChartFn = (props) => {
|
||||||
|
const {
|
||||||
|
values = [], color = '#4f46e5', height = 24, width = 80,
|
||||||
|
} = props;
|
||||||
|
if (values.length < 2) {
|
||||||
|
return `<span class="sparkline-empty" style="width:${width}px;height:${height}px">\u2014</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minV = Math.min(...values);
|
||||||
|
const maxV = Math.max(...values);
|
||||||
|
const range = maxV - minV || 1;
|
||||||
|
const pad = 2;
|
||||||
|
|
||||||
|
const pts = values.map((v: number, i: number) => {
|
||||||
|
const x = pad + ((width - pad * 2) / (values.length - 1)) * i;
|
||||||
|
const y = pad + (height - pad * 2) - ((v - minV) / range) * (height - pad * 2);
|
||||||
|
return `${x},${y}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<svg class="sparkline-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><polyline points="${pts.join(' ')}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||||
|
};
|
||||||
1224
src/ui/json-render-app/src/components.ts
Normal file
1224
src/ui/json-render-app/src/components.ts
Normal file
File diff suppressed because it is too large
Load Diff
479
src/ui/json-render-app/src/main.ts
Normal file
479
src/ui/json-render-app/src/main.ts
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
/**
|
||||||
|
* GHL Dynamic View - MCP App Entry Point
|
||||||
|
* Receives UI trees from the generate_ghl_view tool via MCP ext-apps protocol
|
||||||
|
* and renders them using the component library.
|
||||||
|
*
|
||||||
|
* Implements the MCP ext-apps handshake:
|
||||||
|
* 1. View sends ui/initialize → Host responds
|
||||||
|
* 2. View sends ui/notifications/initialized → Host acks
|
||||||
|
* 3. Host sends ui/notifications/tool-result → View renders
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { COMPONENTS } from './components';
|
||||||
|
import { STYLES } from './styles';
|
||||||
|
|
||||||
|
// ─── Inject Styles ──────────────────────────────────────────
|
||||||
|
const styleEl = document.createElement('style');
|
||||||
|
styleEl.textContent = STYLES;
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
|
||||||
|
// ─── Tree Renderer ──────────────────────────────────────────
|
||||||
|
interface UIElement {
|
||||||
|
key: string;
|
||||||
|
type: string;
|
||||||
|
props: Record<string, any>;
|
||||||
|
children?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UITree {
|
||||||
|
root: string;
|
||||||
|
elements: Record<string, UIElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderElement(el: UIElement, elements: Record<string, UIElement>): string {
|
||||||
|
const component = COMPONENTS[el.type];
|
||||||
|
if (!component) {
|
||||||
|
return `<div class="error-state"><p>Unknown component: ${el.type}</p></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childrenHtml = (el.children || [])
|
||||||
|
.map(key => {
|
||||||
|
const childEl = elements[key];
|
||||||
|
if (!childEl) return `<div class="text-muted">Missing element: ${key}</div>`;
|
||||||
|
return renderElement(childEl, elements);
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return component(el.props || {}, childrenHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTree(tree: UITree): void {
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
if (!appEl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rootEl = tree.elements[tree.root];
|
||||||
|
if (!rootEl) {
|
||||||
|
appEl.innerHTML = `<div class="error-state"><h3>Render Error</h3><p>Root element "${tree.root}" not found in tree</p></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appEl.innerHTML = renderElement(rootEl, tree.elements);
|
||||||
|
|
||||||
|
// After rendering, report actual content size to host
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
reportSize();
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
appEl.innerHTML = `<div class="error-state"><h3>Render Error</h3><p>${err.message || 'Unknown error'}</p></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Measure actual content and notify host of preferred size */
|
||||||
|
function reportSize(): void {
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
if (!appEl) return;
|
||||||
|
|
||||||
|
const height = Math.min(appEl.scrollHeight + 16, 600); // Cap at 600px
|
||||||
|
const width = appEl.scrollWidth + 8;
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.parent.postMessage({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'ui/notifications/size-changed',
|
||||||
|
params: { width, height },
|
||||||
|
}, '*');
|
||||||
|
} catch {
|
||||||
|
// Not in iframe
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also set body height so the iframe can auto-size if the host supports it
|
||||||
|
document.body.style.height = height + 'px';
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Loading State ──────────────────────────────────────────
|
||||||
|
function showLoading(): void {
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
if (appEl) {
|
||||||
|
appEl.innerHTML = `
|
||||||
|
<div class="loading-state">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>Generating view...</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message: string): void {
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
if (appEl) {
|
||||||
|
appEl.innerHTML = `<div class="error-state"><h3>Error</h3><p>${message}</p></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Try to extract a uiTree from any data shape and render it */
|
||||||
|
function tryRenderFromData(data: any): boolean {
|
||||||
|
if (!data || typeof data !== 'object') return false;
|
||||||
|
|
||||||
|
// structuredContent.uiTree
|
||||||
|
if (data.structuredContent?.uiTree) {
|
||||||
|
renderTree(data.structuredContent.uiTree);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Direct uiTree
|
||||||
|
if (data.uiTree) {
|
||||||
|
renderTree(data.uiTree);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Direct tree shape
|
||||||
|
if (data.root && data.elements) {
|
||||||
|
renderTree(data as UITree);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Nested in content array
|
||||||
|
if (Array.isArray(data.content)) {
|
||||||
|
for (const item of data.content) {
|
||||||
|
if (item.structuredContent?.uiTree) {
|
||||||
|
renderTree(item.structuredContent.uiTree);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── MCP ext-apps Protocol (JSON-RPC over postMessage) ──────
|
||||||
|
let rpcId = 1;
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
function sendToHost(message: Record<string, any>): void {
|
||||||
|
try {
|
||||||
|
window.parent.postMessage(message, '*');
|
||||||
|
} catch {
|
||||||
|
// Not in an iframe or parent unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJsonRpcRequest(method: string, params: Record<string, any> = {}): number {
|
||||||
|
const id = rpcId++;
|
||||||
|
sendToHost({ jsonrpc: '2.0', id, method, params });
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJsonRpcNotification(method: string, params: Record<string, any> = {}): void {
|
||||||
|
sendToHost({ jsonrpc: '2.0', method, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Message Handler ────────────────────────────────────────
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
window.addEventListener('message', (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const data = event.data;
|
||||||
|
if (!data || typeof data !== 'object') return;
|
||||||
|
|
||||||
|
// ── JSON-RPC protocol (ext-apps standard) ──
|
||||||
|
if (data.jsonrpc === '2.0') {
|
||||||
|
|
||||||
|
// JSON-RPC response (to our requests)
|
||||||
|
if ('id' in data && (data.result !== undefined || data.error !== undefined)) {
|
||||||
|
const id = data.id as number;
|
||||||
|
// Check if it's a response to a tool call
|
||||||
|
const pending = pendingToolCalls.get(id);
|
||||||
|
if (pending) {
|
||||||
|
pendingToolCalls.delete(id);
|
||||||
|
if (data.error) {
|
||||||
|
pending.reject(new Error(data.error.message || 'Tool call failed'));
|
||||||
|
} else {
|
||||||
|
pending.resolve(data.result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise it's likely the initialize response
|
||||||
|
if (!initialized) {
|
||||||
|
initialized = true;
|
||||||
|
sendJsonRpcNotification('ui/notifications/initialized', {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host sends tool result
|
||||||
|
if (data.method === 'ui/notifications/tool-result') {
|
||||||
|
if (tryRenderFromData(data.params)) return;
|
||||||
|
// Try params directly as structured content
|
||||||
|
if (data.params && tryRenderFromData(data.params)) return;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host sends tool input (args before result — could render partial)
|
||||||
|
if (data.method === 'ui/notifications/tool-input') {
|
||||||
|
// Tool is still executing; keep showing loading
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host sends partial input (streaming)
|
||||||
|
if (data.method === 'ui/notifications/tool-input-partial') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host sends tool cancelled
|
||||||
|
if (data.method === 'ui/notifications/tool-cancelled') {
|
||||||
|
showError('View generation was cancelled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host context changed (theme, etc.)
|
||||||
|
if (data.method === 'ui/notifications/host-context-changed') {
|
||||||
|
// Could apply theme here in the future
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host sends teardown request
|
||||||
|
if (data.method === 'ui/teardown' && data.id) {
|
||||||
|
sendToHost({ jsonrpc: '2.0', id: data.id, result: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping
|
||||||
|
if (data.method === 'ping' && data.id) {
|
||||||
|
sendToHost({ jsonrpc: '2.0', id: data.id, result: {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Legacy / non-JSON-RPC fallbacks ──
|
||||||
|
|
||||||
|
// Some hosts may send custom formats
|
||||||
|
if (data.type === 'tool-result' || data.type === 'mcp-tool-result') {
|
||||||
|
if (tryRenderFromData(data)) return;
|
||||||
|
const content = data.structuredContent || data.content || data.data || data;
|
||||||
|
if (tryRenderFromData(content)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'mcp-app-init' && data.data) {
|
||||||
|
if (tryRenderFromData(data.data)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct data passthrough
|
||||||
|
if (tryRenderFromData(data)) return;
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
showError(err.message || 'Failed to process message');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Check pre-injected data ────────────────────────────────
|
||||||
|
const preInjected = (window as any).__MCP_APP_DATA__;
|
||||||
|
if (preInjected) {
|
||||||
|
tryRenderFromData(preInjected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── MCP Tool Calling ───────────────────────────────────────
|
||||||
|
const pendingToolCalls = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
|
||||||
|
|
||||||
|
function callTool(toolName: string, args: Record<string, any>): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = rpcId++;
|
||||||
|
pendingToolCalls.set(id, { resolve, reject });
|
||||||
|
sendToHost({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: toolName, arguments: args } });
|
||||||
|
// Timeout after 30s
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingToolCalls.has(id)) {
|
||||||
|
pendingToolCalls.delete(id);
|
||||||
|
reject(new Error('Tool call timed out'));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Toast Notifications ────────────────────────────────────
|
||||||
|
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info'): void {
|
||||||
|
const existing = document.getElementById('mcp-toast');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.id = 'mcp-toast';
|
||||||
|
toast.className = `mcp-toast mcp-toast-${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
requestAnimationFrame(() => toast.classList.add('mcp-toast-show'));
|
||||||
|
setTimeout(() => { toast.classList.remove('mcp-toast-show'); setTimeout(() => toast.remove(), 300); }, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Edit Modal ─────────────────────────────────────────────
|
||||||
|
function showEditModal(title: string, fields: Array<{key: string, label: string, value: string, type?: string}>, onSave: (values: Record<string, string>) => void): void {
|
||||||
|
const existing = document.getElementById('mcp-modal');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.id = 'mcp-modal';
|
||||||
|
modal.className = 'mcp-modal-overlay';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="mcp-modal">
|
||||||
|
<div class="mcp-modal-header">
|
||||||
|
<span class="mcp-modal-title">${title}</span>
|
||||||
|
<button class="mcp-modal-close" data-modal-close>×</button>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-modal-body">
|
||||||
|
${fields.map(f => `
|
||||||
|
<div class="mcp-field">
|
||||||
|
<label class="mcp-field-label">${f.label}</label>
|
||||||
|
<input class="mcp-field-input" data-field="${f.key}" type="${f.type || 'text'}" value="${f.value}" />
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
<div class="mcp-modal-footer">
|
||||||
|
<button class="btn btn-secondary btn-sm" data-modal-close>Cancel</button>
|
||||||
|
<button class="btn btn-primary btn-sm" data-modal-save>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
modal.querySelectorAll('[data-modal-close]').forEach(el => el.addEventListener('click', () => modal.remove()));
|
||||||
|
modal.querySelector('[data-modal-save]')?.addEventListener('click', () => {
|
||||||
|
const values: Record<string, string> = {};
|
||||||
|
modal.querySelectorAll<HTMLInputElement>('[data-field]').forEach(inp => {
|
||||||
|
values[inp.dataset.field!] = inp.value;
|
||||||
|
});
|
||||||
|
onSave(values);
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Drag & Drop (Kanban) ───────────────────────────────────
|
||||||
|
let draggedCardId: string | null = null;
|
||||||
|
let draggedFromStage: string | null = null;
|
||||||
|
|
||||||
|
document.addEventListener('dragstart', (e) => {
|
||||||
|
const card = (e.target as HTMLElement).closest?.('.kanban-card[draggable]') as HTMLElement | null;
|
||||||
|
if (!card) return;
|
||||||
|
draggedCardId = card.dataset.cardId || null;
|
||||||
|
draggedFromStage = card.dataset.stageId || null;
|
||||||
|
card.classList.add('kanban-card-dragging');
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', draggedCardId || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragend', (e) => {
|
||||||
|
const card = (e.target as HTMLElement).closest?.('.kanban-card') as HTMLElement | null;
|
||||||
|
if (card) card.classList.remove('kanban-card-dragging');
|
||||||
|
document.querySelectorAll('.kanban-col-body').forEach(el => el.classList.remove('kanban-drop-target'));
|
||||||
|
draggedCardId = null;
|
||||||
|
draggedFromStage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragover', (e) => {
|
||||||
|
const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body') as HTMLElement | null;
|
||||||
|
if (colBody) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
||||||
|
colBody.classList.add('kanban-drop-target');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragleave', (e) => {
|
||||||
|
const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body') as HTMLElement | null;
|
||||||
|
if (colBody) colBody.classList.remove('kanban-drop-target');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body[data-stage-id]') as HTMLElement | null;
|
||||||
|
if (!colBody || !draggedCardId) return;
|
||||||
|
|
||||||
|
const newStageId = colBody.dataset.stageId;
|
||||||
|
if (!newStageId || newStageId === draggedFromStage) return;
|
||||||
|
|
||||||
|
// Move the card in the DOM immediately for snappy feel
|
||||||
|
const draggedEl = document.querySelector(`.kanban-card[data-card-id="${draggedCardId}"]`) as HTMLElement;
|
||||||
|
if (draggedEl) {
|
||||||
|
draggedEl.classList.remove('kanban-card-dragging');
|
||||||
|
draggedEl.dataset.stageId = newStageId;
|
||||||
|
colBody.appendChild(draggedEl);
|
||||||
|
showToast('Moving deal...', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the MCP server to persist the change
|
||||||
|
try {
|
||||||
|
await callTool('update_opportunity', {
|
||||||
|
opportunityId: draggedCardId,
|
||||||
|
pipelineStageId: newStageId,
|
||||||
|
});
|
||||||
|
showToast('Deal moved!', 'success');
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast(`Failed: ${err.message}`, 'error');
|
||||||
|
// TODO: revert DOM on failure
|
||||||
|
}
|
||||||
|
colBody.classList.remove('kanban-drop-target');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Double-click to Edit (Kanban cards) ────────────────────
|
||||||
|
document.addEventListener('dblclick', (e) => {
|
||||||
|
const card = (e.target as HTMLElement).closest?.('.kanban-card[data-card-id]') as HTMLElement | null;
|
||||||
|
if (card && card.dataset.cardId) {
|
||||||
|
const titleEl = card.querySelector('.kanban-card-title');
|
||||||
|
const valueEl = card.querySelector('.kanban-card-value');
|
||||||
|
showEditModal('Edit Opportunity', [
|
||||||
|
{ key: 'name', label: 'Name', value: titleEl?.textContent || '' },
|
||||||
|
{ key: 'monetaryValue', label: 'Value ($)', value: (valueEl?.textContent || '').replace(/[^0-9.]/g, ''), type: 'number' },
|
||||||
|
{ key: 'status', label: 'Status', value: 'open' },
|
||||||
|
], async (values) => {
|
||||||
|
showToast('Updating...', 'info');
|
||||||
|
try {
|
||||||
|
const args: Record<string, any> = { opportunityId: card.dataset.cardId! };
|
||||||
|
if (values.name) args.name = values.name;
|
||||||
|
if (values.monetaryValue) args.monetaryValue = parseFloat(values.monetaryValue);
|
||||||
|
if (values.status) args.status = values.status;
|
||||||
|
await callTool('update_opportunity', args);
|
||||||
|
// Update DOM
|
||||||
|
if (titleEl && values.name) titleEl.textContent = values.name;
|
||||||
|
if (valueEl && values.monetaryValue) valueEl.textContent = `$${parseFloat(values.monetaryValue).toLocaleString()}`;
|
||||||
|
showToast('Updated!', 'success');
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast(`Failed: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-click on table row
|
||||||
|
const row = (e.target as HTMLElement).closest?.('tr[data-row-id]') as HTMLElement | null;
|
||||||
|
if (row && row.dataset.rowId) {
|
||||||
|
const cells = row.querySelectorAll('td');
|
||||||
|
const headers = row.closest('table')?.querySelectorAll('th');
|
||||||
|
const fields: Array<{key: string, label: string, value: string}> = [];
|
||||||
|
cells.forEach((cell, i) => {
|
||||||
|
if (cell.classList.contains('checkbox-col')) return;
|
||||||
|
const label = headers?.[i]?.textContent?.trim() || `Field ${i}`;
|
||||||
|
fields.push({ key: `field_${i}`, label, value: cell.textContent?.trim() || '' });
|
||||||
|
});
|
||||||
|
showEditModal('Row Details', fields, () => {
|
||||||
|
showToast('Edit via table is read-only for now', 'info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Initiate ext-apps handshake ────────────────────────────
|
||||||
|
// Send ui/initialize to the host to start the protocol
|
||||||
|
// The host will respond, then we send initialized, then it sends tool data
|
||||||
|
try {
|
||||||
|
if (window.parent && window.parent !== window) {
|
||||||
|
sendJsonRpcRequest('ui/initialize', {
|
||||||
|
protocolVersion: '2026-01-26',
|
||||||
|
appInfo: {
|
||||||
|
name: 'GHL Dynamic View',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
appCapabilities: {
|
||||||
|
tools: { listChanged: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not in an iframe context — rely on __MCP_APP_DATA__
|
||||||
|
}
|
||||||
973
src/ui/json-render-app/src/styles.ts
Normal file
973
src/ui/json-render-app/src/styles.ts
Normal file
@ -0,0 +1,973 @@
|
|||||||
|
/**
|
||||||
|
* Inlined CSS styles for the GHL Dynamic View app.
|
||||||
|
* Matches the polish of existing MCP App UIs.
|
||||||
|
*/
|
||||||
|
export const STYLES = `
|
||||||
|
/* ─── Reset & Base ────────────────────────────────────── */
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: white;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 12px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app { padding: 4px; }
|
||||||
|
|
||||||
|
/* ─── Compact Chat Overrides ──────────────────────────── */
|
||||||
|
/* Scale everything down for inline chat MCP App display */
|
||||||
|
|
||||||
|
a { color: #4f46e5; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.font-medium { font-weight: 500; }
|
||||||
|
.font-mono { font-family: 'SF Mono', 'Fira Code', monospace; }
|
||||||
|
.text-sm { font-size: 13px; }
|
||||||
|
.text-muted { color: #9ca3af; }
|
||||||
|
.text-secondary { color: #6b7280; }
|
||||||
|
.text-left { text-align: left; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
|
||||||
|
/* ─── Loading & Error States ──────────────────────────── */
|
||||||
|
.loading-state {
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
min-height: 120px; color: #6b7280;
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
width: 24px; height: 24px; border: 2px solid #e5e7eb; border-top-color: #4f46e5;
|
||||||
|
border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.loading-state p { font-size: 12px; }
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
padding: 12px; margin: 8px; background: #fef2f2; border: 1px solid #fecaca;
|
||||||
|
border-radius: 8px; color: #dc2626;
|
||||||
|
}
|
||||||
|
.error-state h3 { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
|
||||||
|
.error-state p { font-size: 12px; color: #991b1b; }
|
||||||
|
|
||||||
|
/* ─── Status Badges ───────────────────────────────────── */
|
||||||
|
.status-badge, .status-badge-sm {
|
||||||
|
display: inline-flex; align-items: center; border-radius: 9999px;
|
||||||
|
font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.status-badge { padding: 4px 12px; font-size: 11px; }
|
||||||
|
.status-badge-sm { padding: 2px 6px; font-size: 9px; }
|
||||||
|
|
||||||
|
.status-active { background: #dbeafe; color: #1e40af; }
|
||||||
|
.status-complete { background: #dcfce7; color: #166534; }
|
||||||
|
.status-paused { background: #fef3c7; color: #92400e; }
|
||||||
|
.status-draft { background: #e5e7eb; color: #4b5563; }
|
||||||
|
.status-error { background: #fee2e2; color: #991b1b; }
|
||||||
|
.status-sent { background: #dbeafe; color: #1e40af; }
|
||||||
|
.status-paid { background: #dcfce7; color: #166534; }
|
||||||
|
.status-pending { background: #fef3c7; color: #92400e; }
|
||||||
|
.status-open { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.status-won { background: #dcfce7; color: #15803d; }
|
||||||
|
.status-lost { background: #fee2e2; color: #b91c1c; }
|
||||||
|
|
||||||
|
/* ─── Page Header ─────────────────────────────────────── */
|
||||||
|
.page-header {
|
||||||
|
background: white; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.page-header-gradient {
|
||||||
|
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||||
|
color: white; padding: 8px 12px; position: sticky; top: 0; z-index: 50;
|
||||||
|
border-radius: 8px; margin-bottom: 8px;
|
||||||
|
box-shadow: 0 1px 4px rgba(79,70,229,0.3);
|
||||||
|
}
|
||||||
|
.page-header-top { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.page-header-title { font-size: 14px; font-weight: 700; color: #111827; }
|
||||||
|
.page-header-title-light { font-size: 13px; font-weight: 600; }
|
||||||
|
.page-header-subtitle { font-size: 11px; color: #6b7280; margin-top: 2px; }
|
||||||
|
.page-header-subtitle-light { font-size: 10px; opacity: 0.8; margin-top: 1px; }
|
||||||
|
.badge-light { padding: 2px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.2); }
|
||||||
|
.page-header-stats { display: flex; gap: 12px; margin-top: 6px; flex-wrap: wrap; }
|
||||||
|
.page-header-stats-light { display: flex; gap: 10px; margin-top: 4px; }
|
||||||
|
.stat-item { font-size: 11px; color: #6b7280; }
|
||||||
|
.stat-value { font-weight: 500; color: #374151; }
|
||||||
|
.stat-item-light { font-size: 10px; display: flex; align-items: center; gap: 4px; }
|
||||||
|
.stat-value-light { font-weight: 600; }
|
||||||
|
.stat-label-light { opacity: 0.7; }
|
||||||
|
|
||||||
|
/* ─── Card ────────────────────────────────────────────── */
|
||||||
|
.card {
|
||||||
|
background: white; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||||
|
overflow: hidden; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.card.no-border { box-shadow: none; }
|
||||||
|
.card-header { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; }
|
||||||
|
.card-title { font-size: 13px; font-weight: 600; color: #111827; }
|
||||||
|
.card-subtitle { font-size: 11px; color: #6b7280; margin-top: 1px; }
|
||||||
|
.card-body.p-0 { padding: 0; }
|
||||||
|
.card-body.p-sm { padding: 8px; }
|
||||||
|
.card-body.p-md { padding: 10px; }
|
||||||
|
.card-body.p-lg { padding: 12px; }
|
||||||
|
|
||||||
|
/* ─── Stats Grid ──────────────────────────────────────── */
|
||||||
|
.stats-grid { display: grid; gap: 8px; margin-bottom: 8px; }
|
||||||
|
.stats-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.stats-grid-3 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.stats-grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.stats-grid-6 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.stats-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
.stats-grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||||
|
.stats-grid-6 { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.stats-grid-6 { grid-template-columns: repeat(6, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Split Layout ────────────────────────────────────── */
|
||||||
|
.split-layout { display: grid; }
|
||||||
|
.split-50-50 { grid-template-columns: 1fr; }
|
||||||
|
.split-33-67 { grid-template-columns: 1fr; }
|
||||||
|
.split-67-33 { grid-template-columns: 1fr; }
|
||||||
|
.gap-sm { gap: 6px; }
|
||||||
|
.gap-md { gap: 10px; }
|
||||||
|
.gap-lg { gap: 14px; }
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.split-50-50 { grid-template-columns: 1fr 1fr; }
|
||||||
|
.split-33-67 { grid-template-columns: 1fr 2fr; }
|
||||||
|
.split-67-33 { grid-template-columns: 2fr 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Section ─────────────────────────────────────────── */
|
||||||
|
.section { margin-bottom: 8px; }
|
||||||
|
.section-header { margin-bottom: 6px; }
|
||||||
|
.section-title { font-size: 13px; font-weight: 600; color: #111827; }
|
||||||
|
.section-desc { font-size: 11px; color: #6b7280; margin-top: 1px; }
|
||||||
|
|
||||||
|
/* ─── Metric Card ─────────────────────────────────────── */
|
||||||
|
.metric-card {
|
||||||
|
background: rgba(255,255,255,0.95); border-radius: 8px; padding: 10px;
|
||||||
|
text-align: center; box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.metric-value { font-size: 18px; font-weight: 700; color: #111827; }
|
||||||
|
.metric-green { color: #059669; }
|
||||||
|
.metric-blue { color: #2563eb; }
|
||||||
|
.metric-purple { color: #7c3aed; }
|
||||||
|
.metric-yellow { color: #d97706; }
|
||||||
|
.metric-red { color: #dc2626; }
|
||||||
|
.metric-label { font-size: 10px; color: #6b7280; margin-top: 2px; }
|
||||||
|
.metric-trend { font-size: 11px; font-weight: 600; margin-top: 4px; }
|
||||||
|
.trend-up { color: #059669; }
|
||||||
|
.trend-down { color: #dc2626; }
|
||||||
|
.trend-flat { color: #6b7280; }
|
||||||
|
|
||||||
|
/* ─── Data Table ──────────────────────────────────────── */
|
||||||
|
.data-table-wrap {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-container { overflow-x: auto; }
|
||||||
|
.data-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||||
|
.data-table th {
|
||||||
|
text-align: left; padding: 6px 8px; background: #f9fafb; color: #6b7280;
|
||||||
|
font-weight: 500; font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px;
|
||||||
|
border-bottom: 1px solid #e5e7eb; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.data-table th.sortable { cursor: pointer; user-select: none; }
|
||||||
|
.data-table th.sortable:hover { background: #f3f4f6; }
|
||||||
|
.data-table td { padding: 5px 8px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; }
|
||||||
|
.data-table tr:hover { background: #f9fafb; }
|
||||||
|
.data-table .checkbox-col { width: 40px; }
|
||||||
|
.data-table input[type="checkbox"] { width: 16px; height: 16px; accent-color: #4f46e5; cursor: pointer; }
|
||||||
|
|
||||||
|
.avatar-cell { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.avatar {
|
||||||
|
width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center;
|
||||||
|
justify-content: center; color: white; font-weight: 600; font-size: 12px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tags { display: flex; flex-wrap: wrap; gap: 4px; max-width: 200px; }
|
||||||
|
.tag {
|
||||||
|
display: inline-block; padding: 2px 8px; background: #eef2ff; color: #4f46e5;
|
||||||
|
border-radius: 12px; font-size: 11px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.tag-more { background: #f3f4f6; color: #6b7280; }
|
||||||
|
.link { color: #4f46e5; text-decoration: none; }
|
||||||
|
.link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.table-pagination {
|
||||||
|
padding: 12px 20px; border-top: 1px solid #e5e7eb; display: flex;
|
||||||
|
justify-content: space-between; align-items: center;
|
||||||
|
}
|
||||||
|
.pagination-info { font-size: 13px; color: #6b7280; }
|
||||||
|
.pagination-buttons { display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
.empty-state { padding: 60px 20px; text-align: center; }
|
||||||
|
.empty-icon { font-size: 48px; margin-bottom: 16px; }
|
||||||
|
.empty-state p { color: #6b7280; font-size: 15px; }
|
||||||
|
|
||||||
|
/* ─── Kanban Board ────────────────────────────────────── */
|
||||||
|
.kanban-wrap { padding: 8px; overflow-x: auto; overflow-y: auto; max-height: calc(100vh - 80px); }
|
||||||
|
.kanban-cols { display: flex; gap: 8px; min-width: max-content; }
|
||||||
|
.kanban-col {
|
||||||
|
background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
display: flex; flex-direction: column; width: 210px; min-width: 210px;
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
.kanban-col-header {
|
||||||
|
padding: 10px 12px; border-bottom: 1px solid #e5e7eb;
|
||||||
|
position: sticky; top: 0; background: white; border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
.kanban-col-title-row { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.kanban-col-title { font-weight: 600; font-size: 12px; color: #1f2937; }
|
||||||
|
.kanban-col-count {
|
||||||
|
background: #e5e7eb; color: #4b5563; padding: 1px 6px; border-radius: 9999px;
|
||||||
|
font-size: 10px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.kanban-col-value { font-size: 10px; color: #6b7280; margin-top: 2px; }
|
||||||
|
.kanban-col-body { padding: 6px; overflow-y: auto; flex: 1; min-height: 60px; max-height: 320px; }
|
||||||
|
.kanban-empty { text-align: center; padding: 16px; color: #9ca3af; font-size: 10px; }
|
||||||
|
.kanban-card {
|
||||||
|
background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px;
|
||||||
|
padding: 8px 10px; margin-bottom: 6px; cursor: pointer;
|
||||||
|
transition: box-shadow 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.kanban-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-color: #4f46e5; }
|
||||||
|
.kanban-card-title { font-weight: 600; font-size: 11px; color: #111827; margin-bottom: 4px; line-height: 1.3; }
|
||||||
|
.kanban-card-subtitle { display: flex; align-items: center; gap: 4px; font-size: 10px; color: #6b7280; margin-bottom: 4px; }
|
||||||
|
.kanban-avatar {
|
||||||
|
width: 16px; height: 16px; background: #e5e7eb; border-radius: 50%;
|
||||||
|
display: flex; align-items: center; justify-content: center; font-size: 7px;
|
||||||
|
font-weight: 700; color: #4b5563;
|
||||||
|
}
|
||||||
|
.kanban-card-value { font-size: 12px; font-weight: 700; color: #059669; margin-bottom: 4px; }
|
||||||
|
.kanban-card-footer { display: flex; align-items: center; justify-content: space-between; font-size: 9px; color: #9ca3af; }
|
||||||
|
|
||||||
|
/* ─── Timeline ────────────────────────────────────────── */
|
||||||
|
.timeline { position: relative; padding-left: 24px; }
|
||||||
|
.timeline-line { position: absolute; left: 8px; top: 8px; bottom: 8px; width: 2px; background: #e5e7eb; }
|
||||||
|
.timeline-item { position: relative; margin-bottom: 16px; }
|
||||||
|
.timeline-item:last-child { margin-bottom: 0; }
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute; left: -16px; top: 4px; width: 16px; height: 16px;
|
||||||
|
border-radius: 50%; border: 2px solid; background: white;
|
||||||
|
display: flex; align-items: center; justify-content: center; font-size: 8px;
|
||||||
|
}
|
||||||
|
.tl-border-default { border-color: #d1d5db; }
|
||||||
|
.tl-border-success { border-color: #4ade80; }
|
||||||
|
.tl-border-warning { border-color: #facc15; }
|
||||||
|
.tl-border-error { border-color: #f87171; }
|
||||||
|
.timeline-content { margin-left: 8px; }
|
||||||
|
.timeline-title { font-weight: 500; font-size: 14px; color: #111827; }
|
||||||
|
.timeline-desc { font-size: 14px; color: #6b7280; margin-top: 2px; }
|
||||||
|
.timeline-time { font-size: 12px; color: #9ca3af; margin-top: 2px; }
|
||||||
|
|
||||||
|
/* ─── Progress Bar ────────────────────────────────────── */
|
||||||
|
.progress-wrap { margin-bottom: 20px; }
|
||||||
|
.progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||||
|
.progress-label { font-size: 14px; font-weight: 500; color: #374151; }
|
||||||
|
.progress-value { font-size: 14px; color: #6b7280; }
|
||||||
|
.progress-value strong { color: #111827; }
|
||||||
|
.progress-track { position: relative; height: 24px; background: #e5e7eb; border-radius: 12px; overflow: hidden; }
|
||||||
|
.progress-bar { height: 100%; border-radius: 12px; transition: width 0.5s; }
|
||||||
|
.bar-green { background: linear-gradient(to right, #10b981, #059669); }
|
||||||
|
.bar-blue { background: linear-gradient(to right, #3b82f6, #2563eb); }
|
||||||
|
.bar-purple { background: linear-gradient(to right, #8b5cf6, #7c3aed); }
|
||||||
|
.bar-yellow { background: linear-gradient(to right, #f59e0b, #d97706); }
|
||||||
|
.bar-red { background: linear-gradient(to right, #ef4444, #dc2626); }
|
||||||
|
.progress-benchmark { position: absolute; top: 0; height: 100%; width: 2px; background: rgba(0,0,0,0.35); }
|
||||||
|
.progress-benchmark-label { position: absolute; top: -16px; font-size: 10px; color: #6b7280; transform: translateX(-50%); }
|
||||||
|
|
||||||
|
/* ─── Detail Header ───────────────────────────────────── */
|
||||||
|
.detail-header {
|
||||||
|
background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
.detail-header-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 4px; }
|
||||||
|
.detail-title { font-size: 24px; font-weight: 700; color: #111827; }
|
||||||
|
.detail-entity-id { font-size: 14px; color: #9ca3af; margin-top: 2px; }
|
||||||
|
.detail-subtitle { font-size: 14px; color: #6b7280; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* ─── Key-Value List ──────────────────────────────────── */
|
||||||
|
.kv-list { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; }
|
||||||
|
.kv-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; border-bottom: 1px solid #f3f4f6; }
|
||||||
|
.kv-row:last-child { border-bottom: none; }
|
||||||
|
.kv-compact { padding: 8px 24px; }
|
||||||
|
.kv-total { background: #1e3a5f; color: white; padding: 16px 24px; border-bottom: none; }
|
||||||
|
.kv-success { background: #f0fdf4; }
|
||||||
|
.kv-highlight { background: #f9fafb; font-weight: 600; }
|
||||||
|
.kv-muted .kv-label, .kv-muted .kv-value { color: #9ca3af; }
|
||||||
|
.kv-label { font-size: 14px; color: #6b7280; }
|
||||||
|
.kv-total .kv-label { font-size: 16px; font-weight: 600; color: white; }
|
||||||
|
.kv-value { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #111827; }
|
||||||
|
.kv-value-bold { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 600; color: #111827; }
|
||||||
|
.kv-value-total { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 20px; font-weight: 700; color: white; }
|
||||||
|
.kv-value-success { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #059669; }
|
||||||
|
.kv-value-danger { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #dc2626; }
|
||||||
|
.kv-success .kv-label { color: #15803d; }
|
||||||
|
|
||||||
|
/* ─── Line Items Table ────────────────────────────────── */
|
||||||
|
.line-items-wrap {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
overflow: hidden; margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.line-items-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.line-items-table th {
|
||||||
|
background: #f9fafb; padding: 12px 16px; font-size: 12px; font-weight: 600;
|
||||||
|
color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.line-items-table td { padding: 16px; border-bottom: 1px solid #f3f4f6; }
|
||||||
|
.line-items-table tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
/* ─── Info Block ──────────────────────────────────────── */
|
||||||
|
.info-block { margin-bottom: 16px; }
|
||||||
|
.info-block-label { font-size: 11px; text-transform: uppercase; color: #9ca3af; letter-spacing: 0.05em; font-weight: 500; margin-bottom: 8px; }
|
||||||
|
.info-block-name { font-size: 16px; font-weight: 600; color: #111827; }
|
||||||
|
.info-block-lines { font-size: 14px; color: #6b7280; line-height: 1.6; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* ─── Search Bar ──────────────────────────────────────── */
|
||||||
|
.search-bar { padding: 12px 20px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
|
||||||
|
.search-input {
|
||||||
|
width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px;
|
||||||
|
font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.search-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
|
||||||
|
|
||||||
|
/* ─── Filter Chips ────────────────────────────────────── */
|
||||||
|
.filter-chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 20px; border-bottom: 1px solid #e5e7eb; }
|
||||||
|
.chip {
|
||||||
|
padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 500;
|
||||||
|
background: #f3f4f6; color: #4b5563; border: none; cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.chip:hover { background: #e5e7eb; }
|
||||||
|
.chip-active { background: #4f46e5; color: white; }
|
||||||
|
.chip-active:hover { background: #4338ca; }
|
||||||
|
|
||||||
|
/* ─── Tab Group ───────────────────────────────────────── */
|
||||||
|
.tab-group { display: flex; border-bottom: 1px solid #e5e7eb; margin-bottom: 16px; }
|
||||||
|
.tab {
|
||||||
|
padding: 8px 16px; font-size: 14px; font-weight: 500; border: none; background: none;
|
||||||
|
border-bottom: 2px solid transparent; color: #6b7280; cursor: pointer;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.tab:hover { color: #374151; border-bottom-color: #d1d5db; }
|
||||||
|
.tab-active { color: #4f46e5; border-bottom-color: #4f46e5; }
|
||||||
|
.tab-count {
|
||||||
|
margin-left: 6px; padding: 1px 6px; background: #f3f4f6; color: #6b7280;
|
||||||
|
border-radius: 9999px; font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Buttons ─────────────────────────────────────────── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; border-radius: 8px; font-weight: 500;
|
||||||
|
cursor: pointer; transition: background 0.15s, box-shadow 0.15s; border: none;
|
||||||
|
}
|
||||||
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.btn-primary { background: #4f46e5; color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
||||||
|
.btn-primary:hover:not(:disabled) { background: #4338ca; }
|
||||||
|
.btn-secondary { background: white; color: #374151; border: 1px solid #d1d5db; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
||||||
|
.btn-secondary:hover:not(:disabled) { background: #f9fafb; }
|
||||||
|
.btn-danger { background: #dc2626; color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
||||||
|
.btn-danger:hover:not(:disabled) { background: #b91c1c; }
|
||||||
|
.btn-ghost { background: none; color: #6b7280; }
|
||||||
|
.btn-ghost:hover:not(:disabled) { background: #f3f4f6; color: #111827; }
|
||||||
|
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
||||||
|
.btn-md { padding: 8px 16px; font-size: 14px; }
|
||||||
|
.btn-lg { padding: 10px 24px; font-size: 16px; }
|
||||||
|
|
||||||
|
/* ─── Action Bar ──────────────────────────────────────── */
|
||||||
|
.action-bar { display: flex; gap: 12px; margin-top: 20px; }
|
||||||
|
.align-left { justify-content: flex-start; }
|
||||||
|
.align-center { justify-content: center; }
|
||||||
|
.align-right { justify-content: flex-end; }
|
||||||
|
|
||||||
|
/* ─── Currency Display ────────────────────────────────── */
|
||||||
|
.currency-display { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 600; }
|
||||||
|
.currency-sm { font-size: 14px; }
|
||||||
|
.currency-md { font-size: 20px; }
|
||||||
|
.currency-lg { font-size: 32px; }
|
||||||
|
.currency-positive { color: #059669; }
|
||||||
|
.currency-negative { color: #dc2626; }
|
||||||
|
|
||||||
|
/* ─── Tag List ────────────────────────────────────────── */
|
||||||
|
.tag-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||||
|
.tag-list-sm .tag-pill { padding: 2px 8px; font-size: 11px; }
|
||||||
|
.tag-list-md .tag-pill { padding: 4px 10px; font-size: 12px; }
|
||||||
|
.tag-pill {
|
||||||
|
display: inline-flex; align-items: center; border-radius: 9999px;
|
||||||
|
font-weight: 500; transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.tag-pill-blue { background: #dbeafe; color: #1e40af; }
|
||||||
|
.tag-pill-green { background: #dcfce7; color: #166534; }
|
||||||
|
.tag-pill-red { background: #fee2e2; color: #991b1b; }
|
||||||
|
.tag-pill-yellow { background: #fef3c7; color: #92400e; }
|
||||||
|
.tag-pill-purple { background: #ede9fe; color: #5b21b6; }
|
||||||
|
.tag-pill-gray { background: #e5e7eb; color: #4b5563; }
|
||||||
|
.tag-pill-indigo { background: #e0e7ff; color: #3730a3; }
|
||||||
|
.tag-pill-pink { background: #fce7f3; color: #9d174d; }
|
||||||
|
.tag-pill-outlined { background: transparent; border: 1.5px solid currentColor; }
|
||||||
|
.tag-pill-more { background: #f3f4f6; color: #6b7280; }
|
||||||
|
|
||||||
|
/* ─── Card Grid ───────────────────────────────────────── */
|
||||||
|
.card-grid { display: grid; gap: 16px; }
|
||||||
|
.card-grid-1 { grid-template-columns: 1fr; }
|
||||||
|
.card-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.card-grid-3 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.card-grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.card-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
.card-grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||||
|
}
|
||||||
|
.card-grid-item {
|
||||||
|
background: white; border-radius: 10px; overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08); transition: box-shadow 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.card-grid-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.12); transform: translateY(-2px); }
|
||||||
|
.card-grid-image {
|
||||||
|
height: 140px; background-size: cover; background-position: center; background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
.card-grid-image-placeholder {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #eef2ff, #e0e7ff); font-size: 32px;
|
||||||
|
}
|
||||||
|
.card-grid-body { padding: 12px 14px; }
|
||||||
|
.card-grid-title { font-weight: 600; font-size: 14px; color: #111827; margin-bottom: 2px; }
|
||||||
|
.card-grid-subtitle { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
|
||||||
|
.card-grid-desc { font-size: 13px; color: #6b7280; line-height: 1.4; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
.card-grid-footer { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.cg-status-active { background: #dbeafe; color: #1e40af; }
|
||||||
|
.cg-status-complete { background: #dcfce7; color: #166534; }
|
||||||
|
.cg-status-draft { background: #e5e7eb; color: #4b5563; }
|
||||||
|
.cg-status-error { background: #fee2e2; color: #991b1b; }
|
||||||
|
.cg-status-pending { background: #fef3c7; color: #92400e; }
|
||||||
|
|
||||||
|
/* ─── Avatar Group ────────────────────────────────────── */
|
||||||
|
.avatar-group { display: flex; align-items: center; }
|
||||||
|
.ag-avatar {
|
||||||
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||||
|
color: white; font-weight: 600; border: 2px solid white; margin-left: -8px;
|
||||||
|
overflow: hidden; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ag-avatar:first-child { margin-left: 0; }
|
||||||
|
.ag-sm .ag-avatar { width: 28px; height: 28px; font-size: 10px; }
|
||||||
|
.ag-md .ag-avatar { width: 36px; height: 36px; font-size: 12px; }
|
||||||
|
.ag-lg .ag-avatar { width: 44px; height: 44px; font-size: 14px; }
|
||||||
|
.ag-img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.ag-initials { line-height: 1; }
|
||||||
|
.ag-overflow { background: #e5e7eb; color: #4b5563; }
|
||||||
|
|
||||||
|
/* ─── Star Rating ─────────────────────────────────────── */
|
||||||
|
.star-rating-wrap { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.star-rating-summary { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.star-rating-value { font-size: 24px; font-weight: 700; color: #111827; }
|
||||||
|
.star-rating-stars { font-size: 18px; color: #f59e0b; letter-spacing: 2px; }
|
||||||
|
.star-rating-count { font-size: 13px; color: #6b7280; }
|
||||||
|
.star-rating-distribution { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.star-dist-row { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.star-dist-label { font-size: 12px; color: #6b7280; width: 24px; text-align: right; flex-shrink: 0; }
|
||||||
|
.star-dist-track { flex: 1; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
|
||||||
|
.star-dist-bar { height: 100%; background: #f59e0b; border-radius: 4px; transition: width 0.3s; }
|
||||||
|
.star-dist-count { font-size: 12px; color: #6b7280; width: 32px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* ─── Stock Indicator ─────────────────────────────────── */
|
||||||
|
.stock-indicator {
|
||||||
|
display: inline-flex; align-items: center; gap: 10px; padding: 10px 16px;
|
||||||
|
border-radius: 10px; border: 1px solid #e5e7eb; background: white;
|
||||||
|
}
|
||||||
|
.stock-icon { font-size: 20px; }
|
||||||
|
.stock-info { display: flex; flex-direction: column; }
|
||||||
|
.stock-label { font-size: 13px; font-weight: 600; color: #111827; }
|
||||||
|
.stock-qty { font-size: 14px; font-weight: 500; color: #374151; }
|
||||||
|
.stock-level { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.stock-ok .stock-level { color: #059669; }
|
||||||
|
.stock-ok { border-color: #bbf7d0; background: #f0fdf4; }
|
||||||
|
.stock-low .stock-level { color: #d97706; }
|
||||||
|
.stock-low { border-color: #fde68a; background: #fffbeb; }
|
||||||
|
.stock-critical .stock-level { color: #dc2626; }
|
||||||
|
.stock-critical { border-color: #fecaca; background: #fef2f2; }
|
||||||
|
|
||||||
|
/* ─── Chat Thread ─────────────────────────────────────── */
|
||||||
|
.chat-thread {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
|
||||||
|
}
|
||||||
|
.chat-thread-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb;
|
||||||
|
}
|
||||||
|
.chat-thread-title { font-size: 16px; font-weight: 600; color: #111827; }
|
||||||
|
.chat-thread-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 9999px; }
|
||||||
|
.chat-thread-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; max-height: 600px; overflow-y: auto; }
|
||||||
|
.chat-msg { display: flex; align-items: flex-end; gap: 8px; }
|
||||||
|
.chat-msg-outbound { flex-direction: row; justify-content: flex-end; }
|
||||||
|
.chat-msg-inbound { flex-direction: row; }
|
||||||
|
.chat-avatar {
|
||||||
|
width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center;
|
||||||
|
justify-content: center; color: white; font-weight: 600; font-size: 11px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.chat-avatar-outbound { order: 2; }
|
||||||
|
.chat-avatar-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
|
||||||
|
.chat-bubble-wrap { display: flex; flex-direction: column; max-width: 70%; }
|
||||||
|
.chat-bubble-wrap-right { align-items: flex-end; }
|
||||||
|
.chat-sender { font-size: 11px; font-weight: 600; color: #6b7280; margin-bottom: 2px; padding: 0 4px; }
|
||||||
|
.chat-bubble {
|
||||||
|
padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.45;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.chat-bubble-inbound {
|
||||||
|
background: #f3f4f6; color: #1f2937;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
.chat-bubble-outbound {
|
||||||
|
background: #4f46e5; color: white;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.chat-meta { font-size: 11px; color: #9ca3af; margin-top: 3px; padding: 0 4px; }
|
||||||
|
|
||||||
|
/* ─── Email Preview ───────────────────────────────────── */
|
||||||
|
.email-preview {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
|
||||||
|
}
|
||||||
|
.email-header {
|
||||||
|
padding: 16px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb;
|
||||||
|
}
|
||||||
|
.email-subject { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 10px; }
|
||||||
|
.email-header-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 13px; }
|
||||||
|
.email-header-row:last-child { margin-bottom: 0; }
|
||||||
|
.email-label {
|
||||||
|
font-weight: 600; color: #6b7280; min-width: 40px; text-transform: uppercase;
|
||||||
|
font-size: 10px; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.email-value { color: #374151; }
|
||||||
|
.email-attachments {
|
||||||
|
padding: 10px 20px; border-bottom: 1px solid #e5e7eb; display: flex; flex-wrap: wrap; gap: 8px;
|
||||||
|
}
|
||||||
|
.email-attachment {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px;
|
||||||
|
background: #f3f4f6; border-radius: 6px; font-size: 12px; color: #374151;
|
||||||
|
}
|
||||||
|
.email-body { padding: 20px; font-size: 14px; line-height: 1.6; color: #374151; }
|
||||||
|
.email-body img { max-width: 100%; height: auto; }
|
||||||
|
|
||||||
|
/* ─── Content Preview ─────────────────────────────────── */
|
||||||
|
.content-preview {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
|
||||||
|
}
|
||||||
|
.content-preview-header {
|
||||||
|
padding: 14px 20px; border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.content-preview-title { font-size: 16px; font-weight: 600; color: #111827; }
|
||||||
|
.content-preview-body {
|
||||||
|
padding: 20px; font-size: 14px; line-height: 1.7; color: #374151;
|
||||||
|
}
|
||||||
|
.content-preview-body h1 { font-size: 24px; font-weight: 700; margin: 16px 0 8px; color: #111827; }
|
||||||
|
.content-preview-body h2 { font-size: 20px; font-weight: 600; margin: 14px 0 6px; color: #111827; }
|
||||||
|
.content-preview-body h3 { font-size: 16px; font-weight: 600; margin: 12px 0 4px; color: #111827; }
|
||||||
|
.content-preview-body a { color: #4f46e5; }
|
||||||
|
.content-preview-body a:hover { text-decoration: underline; }
|
||||||
|
.content-preview-body img { max-width: 100%; height: auto; border-radius: 8px; margin: 8px 0; }
|
||||||
|
.content-preview-pre {
|
||||||
|
background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px;
|
||||||
|
padding: 16px; font-family: 'SF Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 13px; white-space: pre-wrap; word-break: break-word; overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Transcript View ─────────────────────────────────── */
|
||||||
|
.transcript-view {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
|
||||||
|
}
|
||||||
|
.transcript-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb;
|
||||||
|
}
|
||||||
|
.transcript-title { font-size: 16px; font-weight: 600; color: #111827; }
|
||||||
|
.transcript-meta { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.transcript-duration { font-size: 13px; color: #4f46e5; font-weight: 500; }
|
||||||
|
.transcript-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 9999px; }
|
||||||
|
.transcript-body { padding: 8px 0; }
|
||||||
|
.transcript-entry {
|
||||||
|
display: flex; gap: 16px; padding: 10px 20px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.transcript-entry:hover { background: #f9fafb; }
|
||||||
|
.transcript-timestamp {
|
||||||
|
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #9ca3af;
|
||||||
|
min-width: 60px; flex-shrink: 0; padding-top: 2px;
|
||||||
|
}
|
||||||
|
.transcript-content { flex: 1; min-width: 0; }
|
||||||
|
.transcript-speaker { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
||||||
|
.transcript-speaker-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.transcript-speaker-name { font-size: 13px; font-weight: 600; color: #374151; }
|
||||||
|
.transcript-role-badge {
|
||||||
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
.transcript-text { font-size: 14px; color: #4b5563; line-height: 1.5; }
|
||||||
|
|
||||||
|
/* ─── Audio Player ────────────────────────────────────── */
|
||||||
|
.audio-player {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
padding: 16px 20px; display: flex; flex-direction: column; gap: 12px;
|
||||||
|
}
|
||||||
|
.audio-player-info { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.audio-player-icon { font-size: 24px; }
|
||||||
|
.audio-player-title { font-size: 14px; font-weight: 600; color: #111827; }
|
||||||
|
.audio-player-type { font-size: 12px; color: #6b7280; }
|
||||||
|
.audio-player-controls { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.audio-play-btn {
|
||||||
|
width: 40px; height: 40px; border-radius: 50%; border: none;
|
||||||
|
background: #4f46e5; color: white; display: flex; align-items: center;
|
||||||
|
justify-content: center; cursor: pointer; flex-shrink: 0;
|
||||||
|
transition: background 0.15s, transform 0.1s;
|
||||||
|
box-shadow: 0 2px 6px rgba(79,70,229,0.3);
|
||||||
|
}
|
||||||
|
.audio-play-btn:hover { background: #4338ca; transform: scale(1.05); }
|
||||||
|
.audio-play-btn:active { transform: scale(0.97); }
|
||||||
|
.audio-waveform {
|
||||||
|
flex: 1; display: flex; align-items: flex-end; gap: 2px; height: 36px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.audio-bar {
|
||||||
|
flex: 1; min-width: 2px; border-radius: 1px;
|
||||||
|
background: #d1d5db; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.audio-bar-played { background: #4f46e5; }
|
||||||
|
.audio-duration {
|
||||||
|
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px;
|
||||||
|
color: #6b7280; min-width: 40px; text-align: right; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Checklist View ──────────────────────────────────── */
|
||||||
|
.checklist-view {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
|
||||||
|
}
|
||||||
|
.checklist-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 14px 20px; border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.checklist-title { font-size: 16px; font-weight: 600; color: #111827; }
|
||||||
|
.checklist-progress-wrap { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.checklist-progress-text { font-size: 12px; color: #6b7280; white-space: nowrap; }
|
||||||
|
.checklist-progress-track { width: 80px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; }
|
||||||
|
.checklist-progress-bar { height: 100%; background: #4f46e5; border-radius: 3px; transition: width 0.3s; }
|
||||||
|
.checklist-body { padding: 4px 0; }
|
||||||
|
.checklist-item {
|
||||||
|
display: flex; align-items: flex-start; gap: 12px; padding: 12px 20px;
|
||||||
|
border-bottom: 1px solid #f3f4f6; transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.checklist-item:last-child { border-bottom: none; }
|
||||||
|
.checklist-item:hover { background: #f9fafb; }
|
||||||
|
.checklist-item-done { opacity: 0.7; }
|
||||||
|
.checklist-checkbox {
|
||||||
|
width: 20px; height: 20px; border-radius: 6px; border: 2px solid #d1d5db;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0; margin-top: 1px; transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.checklist-checkbox-checked {
|
||||||
|
background: #4f46e5; border-color: #4f46e5; color: white;
|
||||||
|
}
|
||||||
|
.checklist-item-content { flex: 1; min-width: 0; }
|
||||||
|
.checklist-item-title { font-size: 14px; font-weight: 500; color: #111827; }
|
||||||
|
.checklist-item-title-done { text-decoration: line-through; color: #9ca3af; }
|
||||||
|
.checklist-item-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
|
||||||
|
.checklist-due, .checklist-assignee { font-size: 12px; color: #6b7280; }
|
||||||
|
.checklist-priority {
|
||||||
|
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
padding: 1px 6px; border: 1px solid; border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── CalendarView ────────────────────────────────────── */
|
||||||
|
.cal-view {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
padding: 20px; margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.cal-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 4px; }
|
||||||
|
.cal-header-month { font-size: 15px; font-weight: 500; color: #4f46e5; margin-bottom: 12px; }
|
||||||
|
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: #e5e7eb; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
|
||||||
|
.cal-day-header {
|
||||||
|
background: #f9fafb; padding: 8px 4px; text-align: center; font-size: 11px;
|
||||||
|
font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.cal-cell {
|
||||||
|
background: white; min-height: 80px; padding: 4px 6px; position: relative;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.cal-cell-empty { background: #f9fafb; }
|
||||||
|
.cal-today { background: #eef2ff; }
|
||||||
|
.cal-day-num { font-size: 12px; font-weight: 500; color: #374151; margin-bottom: 2px; }
|
||||||
|
.cal-day-today {
|
||||||
|
background: #4f46e5; color: white; border-radius: 50%; width: 22px; height: 22px;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; font-weight: 600;
|
||||||
|
}
|
||||||
|
.cal-evts { display: flex; flex-direction: column; gap: 2px; flex: 1; overflow: hidden; }
|
||||||
|
.cal-evt {
|
||||||
|
padding: 1px 4px; border-radius: 3px; font-size: 10px; color: white;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4;
|
||||||
|
}
|
||||||
|
.cal-evt-more { font-size: 9px; color: #6b7280; padding-left: 2px; }
|
||||||
|
|
||||||
|
/* ─── FlowDiagram ─────────────────────────────────────── */
|
||||||
|
.flow-diagram {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
padding: 20px; margin-bottom: 16px; overflow-x: auto;
|
||||||
|
}
|
||||||
|
.flow-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 16px; }
|
||||||
|
.flow-container { display: flex; align-items: center; gap: 0; padding: 12px 0; }
|
||||||
|
.flow-horizontal { flex-direction: row; }
|
||||||
|
.flow-vertical { flex-direction: column; }
|
||||||
|
.flow-node {
|
||||||
|
padding: 12px 20px; border-radius: 10px; border: 2px solid; text-align: center;
|
||||||
|
min-width: 120px; flex-shrink: 0; position: relative;
|
||||||
|
}
|
||||||
|
.flow-node-start { border-color: #059669; background: #ecfdf5; }
|
||||||
|
.flow-node-action { border-color: #3b82f6; background: #eff6ff; }
|
||||||
|
.flow-node-condition { border-color: #d97706; background: #fffbeb; }
|
||||||
|
.flow-diamond { border-radius: 4px; border-style: dashed; }
|
||||||
|
.flow-node-end { border-color: #6b7280; background: #f3f4f6; }
|
||||||
|
.flow-node-label { font-size: 13px; font-weight: 600; color: #111827; }
|
||||||
|
.flow-node-desc { font-size: 11px; color: #6b7280; margin-top: 4px; }
|
||||||
|
.flow-arrow {
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
font-size: 20px; color: #9ca3af; padding: 0 8px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.flow-arrow-vert { padding: 8px 0; }
|
||||||
|
.flow-edge-label { font-size: 10px; color: #6b7280; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* ─── TreeView ────────────────────────────────────────── */
|
||||||
|
.tree-view {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
padding: 16px 20px; margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.tree-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 12px; }
|
||||||
|
.tree-list { display: flex; flex-direction: column; }
|
||||||
|
.tree-item {
|
||||||
|
display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 6px;
|
||||||
|
cursor: default; transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.tree-item:hover { background: #f9fafb; }
|
||||||
|
.tree-chevron { width: 16px; font-size: 12px; color: #9ca3af; flex-shrink: 0; text-align: center; }
|
||||||
|
.tree-icon { font-size: 14px; flex-shrink: 0; }
|
||||||
|
.tree-label { font-size: 14px; color: #111827; font-weight: 500; }
|
||||||
|
.tree-badge {
|
||||||
|
padding: 1px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600;
|
||||||
|
background: #eef2ff; color: #4f46e5; margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── MediaGallery ────────────────────────────────────── */
|
||||||
|
.mg-gallery {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
padding: 20px; margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.mg-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 12px; }
|
||||||
|
.mg-grid { display: grid; gap: 12px; }
|
||||||
|
.mg-card {
|
||||||
|
border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;
|
||||||
|
transition: box-shadow 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.mg-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-color: #4f46e5; }
|
||||||
|
.mg-thumb {
|
||||||
|
width: 100%; aspect-ratio: 4/3; overflow: hidden; background: #f3f4f6; position: relative;
|
||||||
|
}
|
||||||
|
.mg-img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.mg-placeholder {
|
||||||
|
width: 100%; height: 100%; display: flex; flex-direction: column;
|
||||||
|
align-items: center; justify-content: center; gap: 6px; color: #9ca3af;
|
||||||
|
}
|
||||||
|
.mg-placeholder-icon { font-size: 32px; }
|
||||||
|
.mg-type-badge {
|
||||||
|
padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700;
|
||||||
|
background: #e5e7eb; color: #4b5563;
|
||||||
|
}
|
||||||
|
.mg-info { padding: 10px 12px; }
|
||||||
|
.mg-name {
|
||||||
|
font-size: 13px; font-weight: 600; color: #111827; white-space: nowrap;
|
||||||
|
overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.mg-meta { display: flex; gap: 12px; margin-top: 4px; font-size: 11px; color: #9ca3af; }
|
||||||
|
|
||||||
|
/* ─── DuplicateCompare ────────────────────────────────── */
|
||||||
|
.dc-compare {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
overflow: hidden; margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.dc-title { font-size: 18px; font-weight: 600; color: #111827; padding: 16px 20px 0; }
|
||||||
|
.dc-header-row {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px;
|
||||||
|
background: #f9fafb; border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.dc-header-label {
|
||||||
|
padding: 10px 16px; font-size: 12px; font-weight: 600; color: #6b7280;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.dc-body { }
|
||||||
|
.dc-row {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.dc-row:last-child { border-bottom: none; }
|
||||||
|
.dc-field { padding: 10px 16px; font-size: 13px; color: #6b7280; font-weight: 500; background: #f9fafb; }
|
||||||
|
.dc-val { padding: 10px 16px; font-size: 13px; color: #111827; }
|
||||||
|
.dc-diff { background: #fefce8; }
|
||||||
|
|
||||||
|
/* ─── Chart Components ────────────────────────────────── */
|
||||||
|
.chart-container {
|
||||||
|
background: white; border-radius: 12px; padding: 16px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.chart-title {
|
||||||
|
font-size: 15px; font-weight: 600; color: #111827; margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.chart-scroll { overflow-x: auto; }
|
||||||
|
.chart-empty { text-align: center; color: #9ca3af; padding: 32px; font-size: 14px; }
|
||||||
|
|
||||||
|
/* Bar Chart - Horizontal */
|
||||||
|
.bar-chart-h { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.bar-h-row { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.bar-h-label { width: 90px; font-size: 13px; color: #374151; font-weight: 500; text-align: right; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.bar-h-track { flex: 1; height: 24px; background: #f3f4f6; border-radius: 6px; overflow: hidden; }
|
||||||
|
.bar-h-fill { height: 100%; border-radius: 6px; transition: width 0.4s ease; min-width: 2px; }
|
||||||
|
.bar-h-value { width: 60px; font-size: 13px; font-weight: 600; color: #111827; font-family: 'SF Mono', 'Fira Code', monospace; }
|
||||||
|
|
||||||
|
/* Bar Chart - Vertical SVG */
|
||||||
|
.bar-chart-svg { width: 100%; height: auto; max-height: 200px; }
|
||||||
|
.bar-v-rect { transition: opacity 0.2s; }
|
||||||
|
.bar-v-rect:hover { opacity: 0.8; }
|
||||||
|
.bar-v-val { font-size: 11px; fill: #374151; font-weight: 600; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||||
|
.bar-v-label { font-size: 10px; fill: #6b7280; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||||
|
|
||||||
|
/* Line Chart SVG */
|
||||||
|
.line-chart-svg { width: 100%; height: auto; max-height: 200px; }
|
||||||
|
.chart-axis-text { font-size: 10px; fill: #9ca3af; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||||
|
.chart-axis-label { font-size: 10px; fill: #6b7280; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||||
|
|
||||||
|
/* Pie Chart */
|
||||||
|
.pie-chart-layout { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; justify-content: center; }
|
||||||
|
.pie-chart-svg { width: 160px; height: 160px; flex-shrink: 0; }
|
||||||
|
.pie-center-text { font-size: 18px; font-weight: 700; fill: #111827; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||||
|
.pie-legend { display: flex; flex-direction: column; gap: 6px; min-width: 120px; }
|
||||||
|
.pie-legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
||||||
|
.pie-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.pie-legend-label { color: #374151; flex: 1; }
|
||||||
|
.pie-legend-value { color: #6b7280; font-weight: 500; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
||||||
|
|
||||||
|
/* Funnel Chart */
|
||||||
|
.funnel-chart { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.funnel-stage { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.funnel-label-col { width: 100px; flex-shrink: 0; text-align: right; }
|
||||||
|
.funnel-label { display: block; font-size: 13px; font-weight: 500; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.funnel-value { display: block; font-size: 11px; color: #6b7280; font-family: 'SF Mono', 'Fira Code', monospace; }
|
||||||
|
.funnel-bar-col { flex: 1; }
|
||||||
|
.funnel-bar { height: 28px; border-radius: 6px; transition: width 0.4s ease; min-width: 4px; }
|
||||||
|
.funnel-dropoff { width: 50px; font-size: 11px; color: #ef4444; font-weight: 600; text-align: left; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* Sparkline */
|
||||||
|
.sparkline-svg { display: inline-block; vertical-align: middle; }
|
||||||
|
.sparkline-empty { display: inline-flex; align-items: center; justify-content: center; color: #9ca3af; font-size: 12px; vertical-align: middle; }
|
||||||
|
|
||||||
|
/* ─── Interactive: Drag & Drop ────────────────────────── */
|
||||||
|
.kanban-card[draggable] { cursor: grab; transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s; }
|
||||||
|
.kanban-card[draggable]:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.12); transform: translateY(-1px); }
|
||||||
|
.kanban-card-dragging { opacity: 0.4; transform: scale(0.95); }
|
||||||
|
.kanban-drop-target { background: #eef2ff; border: 2px dashed #818cf8; border-radius: 6px; min-height: 40px; transition: background 0.15s; }
|
||||||
|
|
||||||
|
/* ─── Interactive: Clickable Rows ─────────────────────── */
|
||||||
|
.clickable-row { cursor: pointer; transition: background 0.1s; }
|
||||||
|
.clickable-row:hover { background: #f0f4ff !important; }
|
||||||
|
|
||||||
|
/* ─── Modal ───────────────────────────────────────────── */
|
||||||
|
.mcp-modal-overlay {
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex;
|
||||||
|
align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
.mcp-modal {
|
||||||
|
background: white; border-radius: 10px; width: 90%; max-width: 360px;
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,0.2); overflow: hidden;
|
||||||
|
}
|
||||||
|
.mcp-modal-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 10px 14px; border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.mcp-modal-title { font-size: 13px; font-weight: 600; color: #111827; }
|
||||||
|
.mcp-modal-close { background: none; border: none; font-size: 18px; cursor: pointer; color: #6b7280; padding: 0 4px; }
|
||||||
|
.mcp-modal-close:hover { color: #111827; }
|
||||||
|
.mcp-modal-body { padding: 12px 14px; }
|
||||||
|
.mcp-modal-footer { padding: 8px 14px; border-top: 1px solid #e5e7eb; display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
|
|
||||||
|
.mcp-field { margin-bottom: 10px; }
|
||||||
|
.mcp-field-label { display: block; font-size: 11px; font-weight: 500; color: #374151; margin-bottom: 3px; }
|
||||||
|
.mcp-field-input {
|
||||||
|
width: 100%; padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 6px;
|
||||||
|
font-size: 12px; outline: none; transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.mcp-field-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 2px rgba(79,70,229,0.15); }
|
||||||
|
|
||||||
|
/* ─── Toast ───────────────────────────────────────────── */
|
||||||
|
.mcp-toast {
|
||||||
|
position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%) translateY(20px);
|
||||||
|
padding: 6px 16px; border-radius: 8px; font-size: 11px; font-weight: 500;
|
||||||
|
z-index: 2000; opacity: 0; transition: opacity 0.2s, transform 0.2s;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: none;
|
||||||
|
}
|
||||||
|
.mcp-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||||
|
.mcp-toast-success { background: #059669; color: white; }
|
||||||
|
.mcp-toast-error { background: #dc2626; color: white; }
|
||||||
|
.mcp-toast-info { background: #1f2937; color: white; }
|
||||||
|
|
||||||
|
/* ─── Interactive Components ──────────────────────────── */
|
||||||
|
.interactive-wrap {
|
||||||
|
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
overflow: hidden; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact Picker */
|
||||||
|
.contact-picker { padding: 12px; }
|
||||||
|
.cp-search-wrap { position: relative; }
|
||||||
|
.cp-input { width: 100%; }
|
||||||
|
.cp-results {
|
||||||
|
position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
|
||||||
|
background: white; border: 1px solid #e5e7eb; border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto; margin-top: 4px;
|
||||||
|
}
|
||||||
|
.cp-result-item { padding: 8px 12px; cursor: pointer; font-size: 13px; border-bottom: 1px solid #f3f4f6; }
|
||||||
|
.cp-result-item:hover { background: #f0f4ff; }
|
||||||
|
.cp-selected {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 6px 10px; background: #eef2ff; border-radius: 6px; margin-bottom: 8px;
|
||||||
|
font-size: 13px; font-weight: 500; color: #4f46e5;
|
||||||
|
}
|
||||||
|
.cp-clear { background: none; border: none; cursor: pointer; font-size: 16px; color: #6b7280; padding: 0 4px; }
|
||||||
|
.cp-clear:hover { color: #dc2626; }
|
||||||
|
|
||||||
|
/* Invoice Builder */
|
||||||
|
.invoice-builder .ib-items td { padding: 6px 4px; }
|
||||||
|
.invoice-builder .ib-items input { font-size: 12px; }
|
||||||
|
.invoice-builder .ib-line-total { font-family: 'SF Mono', monospace; font-weight: 500; text-align: right; padding-right: 8px; white-space: nowrap; }
|
||||||
|
.invoice-builder .ib-grand-total { font-size: 14px; }
|
||||||
|
.invoice-builder .ib-remove-row { color: #dc2626; }
|
||||||
|
.invoice-builder .ib-add-row { color: #4f46e5; }
|
||||||
|
|
||||||
|
/* Opportunity Editor */
|
||||||
|
.opportunity-editor select.mcp-field-input {
|
||||||
|
appearance: auto; padding: 6px 8px; background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editable Field */
|
||||||
|
.editable-field {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
|
||||||
|
padding: 2px 4px; border-radius: 4px; transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.editable-field:hover { background: #f3f4f6; }
|
||||||
|
.ef-edit-icon { font-size: 12px; opacity: 0; transition: opacity 0.15s; }
|
||||||
|
.editable-field:hover .ef-edit-icon { opacity: 0.6; }
|
||||||
|
.ef-input { font-size: inherit; }
|
||||||
|
|
||||||
|
/* Amount Input */
|
||||||
|
.amount-input { display: inline-flex; align-items: center; cursor: pointer; }
|
||||||
|
.ai-display { cursor: pointer; }
|
||||||
|
.ai-raw { font-size: 16px; width: 120px; }
|
||||||
|
|
||||||
|
/* Select Dropdown */
|
||||||
|
.select-dropdown { appearance: auto; background: white; }
|
||||||
|
`;
|
||||||
13
src/ui/json-render-app/tsconfig.json
Normal file
13
src/ui/json-render-app/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "*.ts"]
|
||||||
|
}
|
||||||
32
src/ui/json-render-app/vite.config.ts
Normal file
32
src/ui/json-render-app/vite.config.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
viteSingleFile(),
|
||||||
|
{
|
||||||
|
name: 'copy-output',
|
||||||
|
closeBundle() {
|
||||||
|
const outDir = path.resolve(__dirname, "../../../dist/app-ui");
|
||||||
|
const src = path.join(outDir, "index.html");
|
||||||
|
const dstDynamic = path.join(outDir, "dynamic-view.html");
|
||||||
|
const dstUniversal = path.join(outDir, "universal-renderer.html");
|
||||||
|
if (fs.existsSync(src)) {
|
||||||
|
// Copy to both names — universal-renderer is the canonical one
|
||||||
|
fs.copyFileSync(src, dstUniversal);
|
||||||
|
fs.renameSync(src, dstDynamic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
root: path.resolve(__dirname),
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, "../../../dist/app-ui"),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: path.resolve(__dirname, "index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
12
src/ui/react-app/index.html
Normal file
12
src/ui/react-app/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>MCP UI Kit</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3190
src/ui/react-app/package-lock.json
generated
Normal file
3190
src/ui/react-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
src/ui/react-app/package.json
Normal file
25
src/ui/react-app/package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "mcp-ui-kit-react",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "MCP UI Kit — Generic React component library for MCP App UIs",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --noEmit && vite build",
|
||||||
|
"dev": "vite build --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/ext-apps": "^1.0.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-singlefile": "^2.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/ui/react-app/src/App.tsx
Normal file
211
src/ui/react-app/src/App.tsx
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* App.tsx — Root component for the MCP UI Kit React app.
|
||||||
|
*
|
||||||
|
* Uses useApp from ext-apps/react to connect to the MCP host.
|
||||||
|
* Receives UI trees via ontoolresult and renders them via UITreeRenderer.
|
||||||
|
*
|
||||||
|
* HYBRID INTERACTIVITY: Uses mergeUITrees to preserve local component state
|
||||||
|
* across tool result updates. Wraps with ChangeTrackerProvider for shared
|
||||||
|
* change tracking across all interactive components.
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
||||||
|
import { useApp } from "@modelcontextprotocol/ext-apps/react";
|
||||||
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { MCPAppProvider } from "./context/MCPAppContext.js";
|
||||||
|
import { ChangeTrackerProvider } from "./context/ChangeTrackerContext.js";
|
||||||
|
import { UITreeRenderer } from "./renderer/UITreeRenderer.js";
|
||||||
|
import { ToastProvider } from "./components/shared/Toast.js";
|
||||||
|
import { SaveIndicator } from "./components/shared/SaveIndicator.js";
|
||||||
|
import { mergeUITrees } from "./utils/mergeUITrees.js";
|
||||||
|
import type { UITree } from "./types.js";
|
||||||
|
import "./styles/base.css";
|
||||||
|
import "./styles/interactive.css";
|
||||||
|
|
||||||
|
// ─── Parse UI Tree from tool result ─────────────────────────
|
||||||
|
|
||||||
|
function extractUITree(result: CallToolResult): UITree | null {
|
||||||
|
// 1. Check structuredContent first — this is where generateDynamicView puts the uiTree
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) {
|
||||||
|
if (sc.uiTree && sc.uiTree.root && sc.uiTree.elements) {
|
||||||
|
return sc.uiTree as UITree;
|
||||||
|
}
|
||||||
|
// structuredContent might BE the tree directly
|
||||||
|
if (sc.root && sc.elements) {
|
||||||
|
return sc as UITree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check content array for JSON text containing a UI tree
|
||||||
|
if (result.content) {
|
||||||
|
for (const item of result.content) {
|
||||||
|
if (item.type === "text") {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(item.text);
|
||||||
|
if (parsed && parsed.root && parsed.elements) {
|
||||||
|
return parsed as UITree;
|
||||||
|
}
|
||||||
|
// Might be wrapped: { uiTree: { root, elements } }
|
||||||
|
if (parsed?.uiTree?.root && parsed?.uiTree?.elements) {
|
||||||
|
return parsed.uiTree as UITree;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON — skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for server-injected data via window.__MCP_APP_DATA__ */
|
||||||
|
function getPreInjectedTree(): UITree | null {
|
||||||
|
try {
|
||||||
|
const data = (window as any).__MCP_APP_DATA__;
|
||||||
|
if (!data) return null;
|
||||||
|
if (data.uiTree?.root && data.uiTree?.elements) return data.uiTree as UITree;
|
||||||
|
if (data.root && data.elements) return data as UITree;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main App ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [uiTree, setUITree] = useState<UITree | null>(null);
|
||||||
|
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
|
||||||
|
const [toolInput, setToolInput] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Check for pre-injected data (server injects via window.__MCP_APP_DATA__)
|
||||||
|
useEffect(() => {
|
||||||
|
const preInjected = getPreInjectedTree();
|
||||||
|
if (preInjected && !uiTree) {
|
||||||
|
console.info("[MCPApp] Found pre-injected UI tree");
|
||||||
|
setUITree(preInjected);
|
||||||
|
}
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const { app, isConnected, error } = useApp({
|
||||||
|
appInfo: { name: "MCP UI Kit", version: "1.0.0" },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
console.info("[MCPApp] Tool result received:", result);
|
||||||
|
const newTree = extractUITree(result);
|
||||||
|
if (newTree) {
|
||||||
|
// CRITICAL FIX: Merge trees instead of replacing.
|
||||||
|
// This preserves React component state (form inputs, drag state, etc.)
|
||||||
|
// by keeping exact old references for unchanged elements.
|
||||||
|
setUITree((prevTree) => {
|
||||||
|
if (!prevTree) return newTree;
|
||||||
|
return mergeUITrees(prevTree, newTree);
|
||||||
|
});
|
||||||
|
setToolInput(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.ontoolinput = async (input) => {
|
||||||
|
console.info("[MCPApp] Tool input received:", input);
|
||||||
|
setToolInput("Loading view...");
|
||||||
|
};
|
||||||
|
|
||||||
|
app.ontoolcancelled = (params) => {
|
||||||
|
console.info("[MCPApp] Tool cancelled:", params.reason);
|
||||||
|
setToolInput(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.onerror = (err) => {
|
||||||
|
console.error("[MCPApp] Error:", err);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.onhostcontextchanged = (params) => {
|
||||||
|
setHostContext((prev) => ({ ...prev, ...params }));
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (app) {
|
||||||
|
setHostContext(app.getHostContext());
|
||||||
|
}
|
||||||
|
}, [app]);
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="error-state">
|
||||||
|
<h3>Connection Error</h3>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connecting state
|
||||||
|
if (!isConnected || !app) {
|
||||||
|
return (
|
||||||
|
<div className="loading-state">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<p>Connecting to host...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeAreaStyle = {
|
||||||
|
paddingTop: hostContext?.safeAreaInsets?.top,
|
||||||
|
paddingRight: hostContext?.safeAreaInsets?.right,
|
||||||
|
paddingBottom: hostContext?.safeAreaInsets?.bottom,
|
||||||
|
paddingLeft: hostContext?.safeAreaInsets?.left,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tool call in progress (no tree yet)
|
||||||
|
if (toolInput && !uiTree) {
|
||||||
|
return (
|
||||||
|
<MCPAppProvider app={app}>
|
||||||
|
<ChangeTrackerProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<div id="app" style={safeAreaStyle}>
|
||||||
|
<div className="loading-state">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<p>{toolInput}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ToastProvider>
|
||||||
|
</ChangeTrackerProvider>
|
||||||
|
</MCPAppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waiting for first tool result
|
||||||
|
if (!uiTree) {
|
||||||
|
return (
|
||||||
|
<MCPAppProvider app={app}>
|
||||||
|
<ChangeTrackerProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<div id="app" style={safeAreaStyle}>
|
||||||
|
<div className="loading-state">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<p>Waiting for data...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ToastProvider>
|
||||||
|
</ChangeTrackerProvider>
|
||||||
|
</MCPAppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the UI tree
|
||||||
|
return (
|
||||||
|
<MCPAppProvider app={app}>
|
||||||
|
<ChangeTrackerProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<div id="app" style={safeAreaStyle}>
|
||||||
|
<UITreeRenderer tree={uiTree} />
|
||||||
|
<SaveIndicator />
|
||||||
|
</div>
|
||||||
|
</ToastProvider>
|
||||||
|
</ChangeTrackerProvider>
|
||||||
|
</MCPAppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
149
src/ui/react-app/src/apps/affiliate-dashboard/App.tsx
Normal file
149
src/ui/react-app/src/apps/affiliate-dashboard/App.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { PageHeader } from '../../components/layout/PageHeader';
|
||||||
|
import { StatsGrid } from '../../components/layout/StatsGrid';
|
||||||
|
import { MetricCard } from '../../components/data/MetricCard';
|
||||||
|
import { DataTable } from '../../components/data/DataTable';
|
||||||
|
import { StatusBadge } from '../../components/data/StatusBadge';
|
||||||
|
import { CurrencyDisplay } from '../../components/data/CurrencyDisplay';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
import '../../styles/interactive.css';
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): any {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc;
|
||||||
|
for (const item of result.content || []) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCurrency(amount: number | string | undefined): string {
|
||||||
|
if (amount === undefined || amount === null) return '$0.00';
|
||||||
|
const n = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||||
|
if (isNaN(n)) return '$0.00';
|
||||||
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(val: number | string | undefined): string {
|
||||||
|
if (val === undefined || val === null) return '0%';
|
||||||
|
const n = typeof val === 'string' ? parseFloat(val) : val;
|
||||||
|
if (isNaN(n)) return '0%';
|
||||||
|
return `${n.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusVariantMap: Record<string, string> = {
|
||||||
|
active: 'active',
|
||||||
|
paused: 'paused',
|
||||||
|
draft: 'draft',
|
||||||
|
completed: 'complete',
|
||||||
|
ended: 'complete',
|
||||||
|
inactive: 'draft',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'Affiliate Dashboard', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const parsed = extractData(result);
|
||||||
|
if (parsed) setData(parsed);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const s = data?.stats || data?.summary || {};
|
||||||
|
return {
|
||||||
|
totalAffiliates: s.totalAffiliates || s.affiliateCount || 0,
|
||||||
|
activeCampaigns: s.activeCampaigns || s.campaignCount || 0,
|
||||||
|
totalCommissions: s.totalCommissions || s.commissions || 0,
|
||||||
|
conversionRate: s.conversionRate || s.conversion || 0,
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const campaigns = useMemo(() => {
|
||||||
|
const items: any[] = data?.campaigns || [];
|
||||||
|
return items
|
||||||
|
.map((c) => {
|
||||||
|
const status = (c.status || 'active').toLowerCase();
|
||||||
|
return {
|
||||||
|
id: c.id || '',
|
||||||
|
name: c.name || c.title || 'Untitled Campaign',
|
||||||
|
status: status.charAt(0).toUpperCase() + status.slice(1),
|
||||||
|
statusVariant: statusVariantMap[status] || 'draft',
|
||||||
|
commissionType: c.commissionType || c.type || 'Percentage',
|
||||||
|
earnings: formatCurrency(c.earnings || c.totalEarnings || c.revenue || 0),
|
||||||
|
affiliates: c.affiliateCount || c.affiliates || 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((c) => {
|
||||||
|
if (!search) return true;
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
return c.name.toLowerCase().includes(q) || c.commissionType.toLowerCase().includes(q);
|
||||||
|
});
|
||||||
|
}, [data, search]);
|
||||||
|
|
||||||
|
if (error) return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
|
||||||
|
if (!isConnected) return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
if (!data) return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for data...</p></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageHeader title="Affiliate Dashboard" subtitle="Manage your affiliate program">
|
||||||
|
<StatsGrid columns={4}>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Affiliates"
|
||||||
|
value={stats.totalAffiliates.toLocaleString()}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Active Campaigns"
|
||||||
|
value={stats.activeCampaigns.toLocaleString()}
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Commissions"
|
||||||
|
value={formatCurrency(stats.totalCommissions)}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Conversion Rate"
|
||||||
|
value={formatPercent(stats.conversionRate)}
|
||||||
|
color="yellow"
|
||||||
|
/>
|
||||||
|
</StatsGrid>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600, color: '#1f2937' }}>Campaigns</h3>
|
||||||
|
<div className="search-bar" style={{ marginBottom: 12 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="search-input"
|
||||||
|
placeholder="Search campaigns…"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
columns={[
|
||||||
|
{ key: 'name', label: 'Campaign Name', sortable: true },
|
||||||
|
{ key: 'status', label: 'Status', format: 'status', sortable: true },
|
||||||
|
{ key: 'commissionType', label: 'Commission Type', sortable: true },
|
||||||
|
{ key: 'earnings', label: 'Earnings', format: 'currency', sortable: true },
|
||||||
|
{ key: 'affiliates', label: 'Affiliates', sortable: true },
|
||||||
|
]}
|
||||||
|
rows={campaigns}
|
||||||
|
pageSize={20}
|
||||||
|
emptyMessage="No campaigns found"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/ui/react-app/src/apps/affiliate-dashboard/index.html
Normal file
5
src/ui/react-app/src/apps/affiliate-dashboard/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Affiliate Dashboard</title></head>
|
||||||
|
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
|
||||||
|
</html>
|
||||||
4
src/ui/react-app/src/apps/affiliate-dashboard/main.tsx
Normal file
4
src/ui/react-app/src/apps/affiliate-dashboard/main.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App';
|
||||||
|
createRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);
|
||||||
22
src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/affiliate-dashboard'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
226
src/ui/react-app/src/apps/agent-stats/App.tsx
Normal file
226
src/ui/react-app/src/apps/agent-stats/App.tsx
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Agent Stats — User/agent performance metrics dashboard.
|
||||||
|
* Shows calls made, emails sent, tasks completed, appointments booked.
|
||||||
|
* Line chart: activity over time. Bar chart: performance by metric.
|
||||||
|
* Table: detailed activity log.
|
||||||
|
*/
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useApp } from "@modelcontextprotocol/ext-apps/react";
|
||||||
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { PageHeader } from "../../components/layout/PageHeader.js";
|
||||||
|
import { StatsGrid } from "../../components/layout/StatsGrid.js";
|
||||||
|
import { Section } from "../../components/layout/Section.js";
|
||||||
|
import { MetricCard } from "../../components/data/MetricCard.js";
|
||||||
|
import { LineChart } from "../../components/charts/LineChart.js";
|
||||||
|
import { BarChart } from "../../components/charts/BarChart.js";
|
||||||
|
import { DataTable } from "../../components/data/DataTable.js";
|
||||||
|
import { SparklineChart } from "../../components/charts/SparklineChart.js";
|
||||||
|
import "../../styles/base.css";
|
||||||
|
import "../../styles/interactive.css";
|
||||||
|
|
||||||
|
// ─── Types ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ActivityEntry {
|
||||||
|
date: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
contact?: string;
|
||||||
|
outcome?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocationData {
|
||||||
|
name?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentStatsData {
|
||||||
|
userId?: string;
|
||||||
|
location: LocationData;
|
||||||
|
dateRange: string;
|
||||||
|
callsMade?: number;
|
||||||
|
emailsSent?: number;
|
||||||
|
tasksCompleted?: number;
|
||||||
|
appointmentsBooked?: number;
|
||||||
|
activityLog?: ActivityEntry[];
|
||||||
|
activityOverTime?: { label: string; value: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Data Extraction ────────────────────────────────────────
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): AgentStatsData | null {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc as AgentStatsData;
|
||||||
|
if (result.content) {
|
||||||
|
for (const item of result.content) {
|
||||||
|
if (item.type === "text") {
|
||||||
|
try {
|
||||||
|
return JSON.parse(item.text) as AgentStatsData;
|
||||||
|
} catch {
|
||||||
|
/* skip */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── App ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = React.useState<AgentStatsData | null>(null);
|
||||||
|
|
||||||
|
const { app, isConnected, error } = useApp({
|
||||||
|
appInfo: { name: "agent-stats", version: "1.0.0" },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const extracted = extractData(result);
|
||||||
|
if (extracted) setData(extracted);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for pre-injected data
|
||||||
|
React.useEffect(() => {
|
||||||
|
const preInjected = (window as any).__MCP_APP_DATA__;
|
||||||
|
if (preInjected && !data) setData(preInjected as AgentStatsData);
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Derived data
|
||||||
|
const callsMade = data?.callsMade ?? 0;
|
||||||
|
const emailsSent = data?.emailsSent ?? 0;
|
||||||
|
const tasksCompleted = data?.tasksCompleted ?? 0;
|
||||||
|
const appointmentsBooked = data?.appointmentsBooked ?? 0;
|
||||||
|
|
||||||
|
const performanceBars = useMemo(() => [
|
||||||
|
{ label: "Calls", value: callsMade, color: "#4f46e5" },
|
||||||
|
{ label: "Emails", value: emailsSent, color: "#7c3aed" },
|
||||||
|
{ label: "Tasks", value: tasksCompleted, color: "#16a34a" },
|
||||||
|
{ label: "Appts", value: appointmentsBooked, color: "#3b82f6" },
|
||||||
|
], [callsMade, emailsSent, tasksCompleted, appointmentsBooked]);
|
||||||
|
|
||||||
|
const activityPoints = useMemo(
|
||||||
|
() => data?.activityOverTime ?? [],
|
||||||
|
[data?.activityOverTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const activityLog = useMemo(() => data?.activityLog ?? [], [data?.activityLog]);
|
||||||
|
|
||||||
|
const tableColumns = useMemo(() => [
|
||||||
|
{ key: "date", label: "Date", sortable: true, format: "date" },
|
||||||
|
{ key: "type", label: "Type", sortable: true },
|
||||||
|
{ key: "description", label: "Description" },
|
||||||
|
{ key: "contact", label: "Contact" },
|
||||||
|
{ key: "outcome", label: "Outcome" },
|
||||||
|
], []);
|
||||||
|
|
||||||
|
// Sparkline values from activity over time
|
||||||
|
const sparklineValues = useMemo(
|
||||||
|
() => activityPoints.map((p) => p.value),
|
||||||
|
[activityPoints],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="error-state">
|
||||||
|
<h3>Connection Error</h3>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="loading-state">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<p>{isConnected ? "Waiting for data..." : "Connecting..."}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalActivities = callsMade + emailsSent + tasksCompleted + appointmentsBooked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="app" style={{ padding: 4 }}>
|
||||||
|
<PageHeader
|
||||||
|
title="Agent Performance"
|
||||||
|
subtitle={
|
||||||
|
data.userId
|
||||||
|
? `Agent: ${data.userId} · ${data.dateRange}`
|
||||||
|
: `All Agents · ${data.dateRange}`
|
||||||
|
}
|
||||||
|
stats={[
|
||||||
|
{ label: "Location", value: data.location?.name ?? "—" },
|
||||||
|
{ label: "Total Activities", value: totalActivities.toLocaleString() },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsGrid columns={4}>
|
||||||
|
<MetricCard
|
||||||
|
label="Calls Made"
|
||||||
|
value={callsMade.toLocaleString()}
|
||||||
|
color="blue"
|
||||||
|
trend={callsMade > 0 ? "up" : "flat"}
|
||||||
|
trendValue={`${callsMade}`}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Emails Sent"
|
||||||
|
value={emailsSent.toLocaleString()}
|
||||||
|
color="purple"
|
||||||
|
trend={emailsSent > 0 ? "up" : "flat"}
|
||||||
|
trendValue={`${emailsSent}`}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Tasks Completed"
|
||||||
|
value={tasksCompleted.toLocaleString()}
|
||||||
|
color="green"
|
||||||
|
trend={tasksCompleted > 0 ? "up" : "flat"}
|
||||||
|
trendValue={`${tasksCompleted}`}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Appointments Booked"
|
||||||
|
value={appointmentsBooked.toLocaleString()}
|
||||||
|
color="blue"
|
||||||
|
trend={appointmentsBooked > 0 ? "up" : "flat"}
|
||||||
|
trendValue={`${appointmentsBooked}`}
|
||||||
|
/>
|
||||||
|
</StatsGrid>
|
||||||
|
|
||||||
|
{sparklineValues.length > 1 && (
|
||||||
|
<div style={{ margin: "12px 0", textAlign: "right" }}>
|
||||||
|
<span style={{ fontSize: 11, color: "#6b7280", marginRight: 4 }}>Trend:</span>
|
||||||
|
<SparklineChart values={sparklineValues} color="#4f46e5" height={20} width={100} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Section title="Activity Over Time">
|
||||||
|
<LineChart
|
||||||
|
points={activityPoints}
|
||||||
|
color="#4f46e5"
|
||||||
|
showArea
|
||||||
|
showPoints
|
||||||
|
title="Activities"
|
||||||
|
yAxisLabel="Count"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Performance by Metric">
|
||||||
|
<BarChart
|
||||||
|
bars={performanceBars}
|
||||||
|
orientation="vertical"
|
||||||
|
showValues
|
||||||
|
title="Metric Breakdown"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Activity Log">
|
||||||
|
<DataTable
|
||||||
|
columns={tableColumns}
|
||||||
|
rows={activityLog}
|
||||||
|
pageSize={10}
|
||||||
|
emptyMessage="No activities recorded"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/ui/react-app/src/apps/agent-stats/index.html
Normal file
12
src/ui/react-app/src/apps/agent-stats/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Agent Stats</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
src/ui/react-app/src/apps/agent-stats/main.tsx
Normal file
9
src/ui/react-app/src/apps/agent-stats/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App.js";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
22
src/ui/react-app/src/apps/agent-stats/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/agent-stats/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/agent-stats'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
93
src/ui/react-app/src/apps/appointment-booker/App.tsx
Normal file
93
src/ui/react-app/src/apps/appointment-booker/App.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { PageHeader } from '../../components/layout/PageHeader';
|
||||||
|
import { Card } from '../../components/layout/Card';
|
||||||
|
import { AppointmentBooker } from '../../components/interactive/AppointmentBooker';
|
||||||
|
import { MCPAppProvider } from '../../context/MCPAppContext';
|
||||||
|
import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
import '../../styles/interactive.css';
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): any {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc;
|
||||||
|
for (const item of result.content || []) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
|
||||||
|
const [appInstance, setAppInstance] = useState<any>(null);
|
||||||
|
|
||||||
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'Appointment Booker', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
setAppInstance(app);
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const d = extractData(result);
|
||||||
|
if (d) setData(d);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendar = data?.calendar;
|
||||||
|
const contact = data?.contact;
|
||||||
|
const locationId = data?.locationId;
|
||||||
|
const slots = data?.slots;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
|
||||||
|
}
|
||||||
|
if (!isConnected && !data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for calendar data...</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChangeTrackerProvider>
|
||||||
|
<MCPAppProvider app={appInstance}>
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Book Appointment"
|
||||||
|
subtitle={calendar?.name || 'Select a date and time'}
|
||||||
|
stats={[
|
||||||
|
...(calendar?.name ? [{ label: 'Calendar', value: calendar.name }] : []),
|
||||||
|
...(contact ? [{ label: 'Contact', value: contact.name || contact.firstName || contact.email || 'Selected' }] : []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Contact info card if pre-selected */}
|
||||||
|
{contact && (
|
||||||
|
<Card title="Contact" padding="sm">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 }}>
|
||||||
|
<div style={{ fontWeight: 600 }}>
|
||||||
|
{contact.name || [contact.firstName, contact.lastName].filter(Boolean).join(' ')}
|
||||||
|
</div>
|
||||||
|
{contact.email && <div style={{ color: '#6b7280' }}>📧 {contact.email}</div>}
|
||||||
|
{contact.phone && <div style={{ color: '#6b7280' }}>📞 {contact.phone}</div>}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card padding="sm">
|
||||||
|
<AppointmentBooker
|
||||||
|
slots={slots}
|
||||||
|
calendarId={calendar?.id || locationId}
|
||||||
|
calendarTool="get_calendar_free_slots"
|
||||||
|
bookTool="create_appointment"
|
||||||
|
contactSearchTool="search_contacts"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</MCPAppProvider>
|
||||||
|
</ChangeTrackerProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/ui/react-app/src/apps/appointment-booker/index.html
Normal file
5
src/ui/react-app/src/apps/appointment-booker/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
|
||||||
|
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
|
||||||
|
</html>
|
||||||
9
src/ui/react-app/src/apps/appointment-booker/main.tsx
Normal file
9
src/ui/react-app/src/apps/appointment-booker/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App.js';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
22
src/ui/react-app/src/apps/appointment-booker/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/appointment-booker/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/appointment-booker'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
177
src/ui/react-app/src/apps/appointment-detail/App.tsx
Normal file
177
src/ui/react-app/src/apps/appointment-detail/App.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { DetailHeader } from '../../components/data/DetailHeader';
|
||||||
|
import { KeyValueList } from '../../components/data/KeyValueList';
|
||||||
|
import { Timeline } from '../../components/data/Timeline';
|
||||||
|
import { Card } from '../../components/layout/Card';
|
||||||
|
import { ActionBar } from '../../components/shared/ActionBar';
|
||||||
|
import { ActionButton } from '../../components/shared/ActionButton';
|
||||||
|
import { MCPAppProvider } from '../../context/MCPAppContext';
|
||||||
|
import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
|
||||||
|
import type { KeyValueItem, TimelineEvent, StatusVariant } from '../../types';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
import '../../styles/interactive.css';
|
||||||
|
|
||||||
|
function formatDate(d?: string): string {
|
||||||
|
if (!d) return '—';
|
||||||
|
try {
|
||||||
|
return new Date(d).toLocaleDateString('en-US', {
|
||||||
|
month: 'short', day: 'numeric', year: 'numeric',
|
||||||
|
});
|
||||||
|
} catch { return d; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): any {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc;
|
||||||
|
for (const item of result.content || []) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap: Record<string, StatusVariant> = {
|
||||||
|
confirmed: 'active',
|
||||||
|
booked: 'active',
|
||||||
|
completed: 'complete',
|
||||||
|
cancelled: 'error',
|
||||||
|
no_show: 'error',
|
||||||
|
pending: 'pending',
|
||||||
|
rescheduled: 'paused',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
|
||||||
|
const [appInstance, setAppInstance] = useState<any>(null);
|
||||||
|
|
||||||
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'Appointment Detail', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
setAppInstance(app);
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const d = extractData(result);
|
||||||
|
if (d) setData(d);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const appt = data?.appointment;
|
||||||
|
const notes: any[] = data?.notes || [];
|
||||||
|
|
||||||
|
const kvItems: KeyValueItem[] = useMemo(() => {
|
||||||
|
if (!appt) return [];
|
||||||
|
const items: KeyValueItem[] = [];
|
||||||
|
|
||||||
|
if (appt.title || appt.name) items.push({ label: 'Title', value: appt.title || appt.name });
|
||||||
|
|
||||||
|
// Date/time
|
||||||
|
if (appt.startTime) {
|
||||||
|
const start = new Date(appt.startTime);
|
||||||
|
items.push({ label: 'Date', value: start.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) });
|
||||||
|
items.push({ label: 'Time', value: start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appt.endTime && appt.startTime) {
|
||||||
|
const start = new Date(appt.startTime).getTime();
|
||||||
|
const end = new Date(appt.endTime).getTime();
|
||||||
|
const mins = Math.round((end - start) / 60000);
|
||||||
|
items.push({ label: 'Duration', value: mins >= 60 ? `${Math.floor(mins / 60)}h ${mins % 60}m` : `${mins}m` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact
|
||||||
|
const contactName = appt.contactName || appt.contact?.name ||
|
||||||
|
[appt.contact?.firstName, appt.contact?.lastName].filter(Boolean).join(' ');
|
||||||
|
if (contactName) items.push({ label: 'Contact', value: contactName, bold: true });
|
||||||
|
if (appt.contact?.email || appt.email) items.push({ label: 'Email', value: appt.contact?.email || appt.email });
|
||||||
|
if (appt.contact?.phone || appt.phone) items.push({ label: 'Phone', value: appt.contact?.phone || appt.phone });
|
||||||
|
|
||||||
|
// Status
|
||||||
|
if (appt.status || appt.appointmentStatus) {
|
||||||
|
const s = appt.status || appt.appointmentStatus;
|
||||||
|
items.push({ label: 'Status', value: s.charAt(0).toUpperCase() + s.slice(1), variant: statusMap[s.toLowerCase()] === 'error' ? 'danger' : statusMap[s.toLowerCase()] === 'active' ? 'success' : undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar / location
|
||||||
|
if (appt.calendarName || appt.calendar?.name) items.push({ label: 'Calendar', value: appt.calendarName || appt.calendar?.name });
|
||||||
|
if (appt.locationName || appt.location) items.push({ label: 'Location', value: appt.locationName || appt.location });
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [appt]);
|
||||||
|
|
||||||
|
const timelineEvents: TimelineEvent[] = useMemo(() => {
|
||||||
|
return notes.map((note) => ({
|
||||||
|
title: note.title || 'Note',
|
||||||
|
description: note.body || note.content || note.text || '',
|
||||||
|
timestamp: note.dateAdded || note.createdAt || note.date || '',
|
||||||
|
icon: 'note' as const,
|
||||||
|
variant: 'default' as const,
|
||||||
|
}));
|
||||||
|
}, [notes]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
|
||||||
|
}
|
||||||
|
if (!isConnected && !data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for appointment data...</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = appt?.status || appt?.appointmentStatus || '';
|
||||||
|
const statusVariant = statusMap[status.toLowerCase()] || 'active';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChangeTrackerProvider>
|
||||||
|
<MCPAppProvider app={appInstance}>
|
||||||
|
<div>
|
||||||
|
<DetailHeader
|
||||||
|
title={appt?.title || appt?.name || 'Appointment'}
|
||||||
|
subtitle={formatDate(appt?.startTime)}
|
||||||
|
entityId={appt?.id}
|
||||||
|
status={status ? status.charAt(0).toUpperCase() + status.slice(1) : undefined}
|
||||||
|
statusVariant={statusVariant}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card title="Details" padding="sm">
|
||||||
|
<KeyValueList items={kvItems} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{notes.length > 0 && (
|
||||||
|
<Card title="Notes" padding="sm">
|
||||||
|
<Timeline events={timelineEvents} />
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ActionBar align="right">
|
||||||
|
<ActionButton
|
||||||
|
label="Edit"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
toolName="update_appointment"
|
||||||
|
toolArgs={{ appointmentId: appt?.id }}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
label="Cancel Appointment"
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
toolName="update_appointment"
|
||||||
|
toolArgs={{ appointmentId: appt?.id, status: 'cancelled' }}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
label="Add Note"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
toolName="create_note"
|
||||||
|
toolArgs={{ contactId: appt?.contactId, body: '' }}
|
||||||
|
/>
|
||||||
|
</ActionBar>
|
||||||
|
</div>
|
||||||
|
</MCPAppProvider>
|
||||||
|
</ChangeTrackerProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/ui/react-app/src/apps/appointment-detail/index.html
Normal file
5
src/ui/react-app/src/apps/appointment-detail/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
|
||||||
|
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
|
||||||
|
</html>
|
||||||
9
src/ui/react-app/src/apps/appointment-detail/main.tsx
Normal file
9
src/ui/react-app/src/apps/appointment-detail/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App.js';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
22
src/ui/react-app/src/apps/appointment-detail/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/appointment-detail/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/appointment-detail'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
320
src/ui/react-app/src/apps/blog-manager/App.tsx
Normal file
320
src/ui/react-app/src/apps/blog-manager/App.tsx
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
/**
|
||||||
|
* blog-manager — Blog posts table with search and filters.
|
||||||
|
* Shows posts with title, author, status, site, dates. Client-side filtering and search.
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { MCPAppProvider } from '../../context/MCPAppContext';
|
||||||
|
import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
|
||||||
|
import { PageHeader } from '../../components/layout/PageHeader';
|
||||||
|
import { DataTable } from '../../components/data/DataTable';
|
||||||
|
import { StatusBadge } from '../../components/data/StatusBadge';
|
||||||
|
import { SearchBar } from '../../components/shared/SearchBar';
|
||||||
|
import { FilterChips } from '../../components/shared/FilterChips';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
import '../../styles/interactive.css';
|
||||||
|
|
||||||
|
// ─── Types ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface BlogPost {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
status?: string;
|
||||||
|
site?: string;
|
||||||
|
siteId?: string;
|
||||||
|
publishedAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogSite {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogData {
|
||||||
|
posts: BlogPost[];
|
||||||
|
sites?: BlogSite[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Extract data from tool result ──────────────────────────
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): BlogData | null {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc as BlogData;
|
||||||
|
if (result.content) {
|
||||||
|
for (const item of result.content) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text) as BlogData; } catch { /* skip */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── App ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<BlogData | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const d = (window as any).__MCP_APP_DATA__;
|
||||||
|
if (d && !data) setData(d as BlogData);
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const { app, isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'blog-manager', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (createdApp) => {
|
||||||
|
createdApp.ontoolresult = async (result) => {
|
||||||
|
const d = extractData(result);
|
||||||
|
if (d) setData(d);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error-state"><h3>Connection Error</h3><p>{error.message}</p></div>;
|
||||||
|
}
|
||||||
|
if (!isConnected || !app) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for data...</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MCPAppProvider app={app}>
|
||||||
|
<ChangeTrackerProvider>
|
||||||
|
<div id="app">
|
||||||
|
<BlogManagerView data={data} app={app} />
|
||||||
|
</div>
|
||||||
|
</ChangeTrackerProvider>
|
||||||
|
</MCPAppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── View ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function BlogManagerView({ data, app }: { data: BlogData; app: any }) {
|
||||||
|
const { posts, sites = [] } = data;
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<Set<string>>(new Set());
|
||||||
|
const [siteFilter, setSiteFilter] = useState<Set<string>>(new Set());
|
||||||
|
const [authorFilter, setAuthorFilter] = useState<Set<string>>(new Set());
|
||||||
|
const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
|
||||||
|
const [isActing, setIsActing] = useState(false);
|
||||||
|
|
||||||
|
const handleBlogAction = useCallback(async (action: string, blogData: Record<string, any>) => {
|
||||||
|
if (!app) return;
|
||||||
|
setIsActing(true);
|
||||||
|
setActionResult(null);
|
||||||
|
try {
|
||||||
|
await app.updateModelContext({
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({ action, data: blogData }),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
setActionResult({ type: 'success', msg: `✓ ${action.replace('_', ' ')} request sent` });
|
||||||
|
setTimeout(() => setActionResult(null), 3000);
|
||||||
|
} catch {
|
||||||
|
setActionResult({ type: 'error', msg: '✗ Failed to send request' });
|
||||||
|
} finally {
|
||||||
|
setIsActing(false);
|
||||||
|
}
|
||||||
|
}, [app]);
|
||||||
|
|
||||||
|
// Derive unique values for filters
|
||||||
|
const uniqueStatuses = useMemo(() => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
posts.forEach(p => { if (p.status) set.add(p.status); });
|
||||||
|
return Array.from(set);
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
|
const uniqueSites = useMemo(() => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
posts.forEach(p => { if (p.site) set.add(p.site); });
|
||||||
|
return Array.from(set);
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
|
const uniqueAuthors = useMemo(() => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
posts.forEach(p => { if (p.author) set.add(p.author); });
|
||||||
|
return Array.from(set);
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
|
// Apply client-side filters and search
|
||||||
|
const filteredPosts = useMemo(() => {
|
||||||
|
return posts.filter(p => {
|
||||||
|
// Status filter
|
||||||
|
if (statusFilter.size > 0 && p.status && !statusFilter.has(p.status)) return false;
|
||||||
|
// Site filter
|
||||||
|
if (siteFilter.size > 0 && p.site && !siteFilter.has(p.site)) return false;
|
||||||
|
// Author filter
|
||||||
|
if (authorFilter.size > 0 && p.author && !authorFilter.has(p.author)) return false;
|
||||||
|
// Search query
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
const matches = [p.title, p.author, p.site, p.slug]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some(v => v!.toLowerCase().includes(q));
|
||||||
|
if (!matches) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [posts, statusFilter, siteFilter, authorFilter, searchQuery]);
|
||||||
|
|
||||||
|
const columns = useMemo(() => [
|
||||||
|
{ key: 'title', label: 'Title', sortable: true },
|
||||||
|
{ key: 'author', label: 'Author', sortable: true },
|
||||||
|
{ key: 'status', label: 'Status', format: 'status' as const, sortable: true },
|
||||||
|
{ key: 'site', label: 'Site', sortable: true },
|
||||||
|
{ key: 'publishedAt', label: 'Published', format: 'date' as const, sortable: true },
|
||||||
|
{ key: 'updatedAt', label: 'Updated', format: 'date' as const, sortable: true },
|
||||||
|
], []);
|
||||||
|
|
||||||
|
const rows = filteredPosts.map((p, i) => ({
|
||||||
|
id: p.id || String(i),
|
||||||
|
title: p.title || 'Untitled',
|
||||||
|
author: p.author || '—',
|
||||||
|
status: p.status || 'draft',
|
||||||
|
site: p.site || '—',
|
||||||
|
publishedAt: p.publishedAt || '—',
|
||||||
|
updatedAt: p.updatedAt || '—',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const publishedCount = posts.filter(p => p.status === 'published').length;
|
||||||
|
const draftCount = posts.filter(p => p.status === 'draft').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Blog Manager"
|
||||||
|
subtitle={`${posts.length} post${posts.length !== 1 ? 's' : ''}`}
|
||||||
|
stats={[
|
||||||
|
{ label: 'Published', value: String(publishedCount) },
|
||||||
|
{ label: 'Drafts', value: String(draftCount) },
|
||||||
|
{ label: 'Sites', value: String(sites.length || uniqueSites.length) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="search-bar" style={{ marginBottom: 12 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="search-input"
|
||||||
|
placeholder="Search posts..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
|
||||||
|
{uniqueStatuses.length > 0 && (
|
||||||
|
<div className="filter-chips">
|
||||||
|
{uniqueStatuses.map(s => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
className={`chip ${statusFilter.has(s) ? 'chip-active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setStatusFilter(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(s)) next.delete(s); else next.add(s);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.charAt(0).toUpperCase() + s.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uniqueSites.length > 1 && (
|
||||||
|
<div className="filter-chips">
|
||||||
|
{uniqueSites.map(s => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
className={`chip ${siteFilter.has(s) ? 'chip-active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSiteFilter(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(s)) next.delete(s); else next.add(s);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uniqueAuthors.length > 1 && (
|
||||||
|
<div className="filter-chips">
|
||||||
|
{uniqueAuthors.map(a => (
|
||||||
|
<button
|
||||||
|
key={a}
|
||||||
|
className={`chip ${authorFilter.has(a) ? 'chip-active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setAuthorFilter(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(a)) next.delete(a); else next.add(a);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{a}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
rows={rows}
|
||||||
|
pageSize={10}
|
||||||
|
emptyMessage="No blog posts found"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Blog post actions */}
|
||||||
|
{filteredPosts.length > 0 && (
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
{actionResult && (
|
||||||
|
<div style={{ color: actionResult.type === 'success' ? '#059669' : '#dc2626', fontSize: 13, marginBottom: 8 }}>
|
||||||
|
{actionResult.msg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||||
|
<span style={{ fontSize: 12, color: '#6b7280', lineHeight: '28px' }}>Actions:</span>
|
||||||
|
{filteredPosts.slice(0, 5).map((p, i) => (
|
||||||
|
<span key={p.id || i} style={{ display: 'inline-flex', gap: 2 }}>
|
||||||
|
{p.status === 'draft' && (
|
||||||
|
<button
|
||||||
|
className="chip"
|
||||||
|
onClick={() => handleBlogAction('publish_blog_post', { postId: p.id, title: p.title })}
|
||||||
|
disabled={isActing}
|
||||||
|
title={`Publish "${p.title}"`}
|
||||||
|
style={{ color: '#059669' }}
|
||||||
|
>
|
||||||
|
📢 Publish {(p.title || '').length > 15 ? (p.title || '').slice(0, 15) + '…' : p.title}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="chip"
|
||||||
|
onClick={() => handleBlogAction('delete_blog_post', { postId: p.id, title: p.title })}
|
||||||
|
disabled={isActing}
|
||||||
|
title={`Delete "${p.title}"`}
|
||||||
|
style={{ color: '#dc2626' }}
|
||||||
|
>
|
||||||
|
🗑 {(p.title || '').length > 15 ? (p.title || '').slice(0, 15) + '…' : p.title}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/ui/react-app/src/apps/blog-manager/index.html
Normal file
5
src/ui/react-app/src/apps/blog-manager/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
|
||||||
|
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
|
||||||
|
</html>
|
||||||
9
src/ui/react-app/src/apps/blog-manager/main.tsx
Normal file
9
src/ui/react-app/src/apps/blog-manager/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App.js';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
22
src/ui/react-app/src/apps/blog-manager/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/blog-manager/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/blog-manager'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
91
src/ui/react-app/src/apps/calendar-resources/App.tsx
Normal file
91
src/ui/react-app/src/apps/calendar-resources/App.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { PageHeader } from '../../components/layout/PageHeader';
|
||||||
|
import { DataTable } from '../../components/data/DataTable';
|
||||||
|
import type { TableColumn, TableRow } from '../../types';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): any {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc;
|
||||||
|
for (const item of result.content || []) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLUMNS: TableColumn[] = [
|
||||||
|
{ key: 'name', label: 'Name', sortable: true },
|
||||||
|
{ key: 'type', label: 'Type', sortable: true },
|
||||||
|
{ key: 'description', label: 'Description' },
|
||||||
|
{ key: 'availability', label: 'Availability', sortable: true, format: 'status' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
|
||||||
|
|
||||||
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'Calendar Resources', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const d = extractData(result);
|
||||||
|
if (d) setData(d);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const resources: any[] = data?.resources || [];
|
||||||
|
|
||||||
|
const rows: TableRow[] = useMemo(() => {
|
||||||
|
return resources.map((r) => ({
|
||||||
|
id: r.id || r.resourceId || '',
|
||||||
|
name: r.name || r.title || 'Unnamed',
|
||||||
|
type: r.type || r.resourceType || r.category || '—',
|
||||||
|
description: r.description || r.details || '—',
|
||||||
|
availability: r.isAvailable !== undefined
|
||||||
|
? (r.isAvailable ? 'Available' : 'Unavailable')
|
||||||
|
: r.availability || r.status || 'Unknown',
|
||||||
|
}));
|
||||||
|
}, [resources]);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const available = rows.filter((r) => String(r.availability).toLowerCase() === 'available').length;
|
||||||
|
const unavailable = rows.filter((r) => String(r.availability).toLowerCase() === 'unavailable').length;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
|
||||||
|
}
|
||||||
|
if (!isConnected && !data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for resource data...</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Calendar Resources"
|
||||||
|
subtitle="Rooms, equipment, and shared resources"
|
||||||
|
stats={[
|
||||||
|
{ label: 'Total', value: String(rows.length) },
|
||||||
|
{ label: 'Available', value: String(available) },
|
||||||
|
{ label: 'Unavailable', value: String(unavailable) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<DataTable
|
||||||
|
columns={COLUMNS}
|
||||||
|
rows={rows}
|
||||||
|
emptyMessage="No resources found"
|
||||||
|
pageSize={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/ui/react-app/src/apps/calendar-resources/index.html
Normal file
5
src/ui/react-app/src/apps/calendar-resources/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
|
||||||
|
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
|
||||||
|
</html>
|
||||||
9
src/ui/react-app/src/apps/calendar-resources/main.tsx
Normal file
9
src/ui/react-app/src/apps/calendar-resources/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App.js';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
22
src/ui/react-app/src/apps/calendar-resources/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/calendar-resources/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/calendar-resources'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
208
src/ui/react-app/src/apps/calendar-view/App.tsx
Normal file
208
src/ui/react-app/src/apps/calendar-view/App.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { PageHeader } from '../../components/layout/PageHeader';
|
||||||
|
import { Card } from '../../components/layout/Card';
|
||||||
|
import { CalendarView } from '../../components/viz/CalendarView';
|
||||||
|
import type { CalendarEvent } from '../../types';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
import '../../styles/interactive.css';
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): any {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc;
|
||||||
|
for (const item of result.content || []) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVENT_TYPE_COLORS: Record<string, string> = {
|
||||||
|
meeting: '#4f46e5',
|
||||||
|
call: '#059669',
|
||||||
|
task: '#d97706',
|
||||||
|
deadline: '#dc2626',
|
||||||
|
event: '#7c3aed',
|
||||||
|
appointment: '#0891b2',
|
||||||
|
reminder: '#ec4899',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_TYPES = Object.keys(EVENT_TYPE_COLORS);
|
||||||
|
|
||||||
|
function formatDate(d?: string): string {
|
||||||
|
if (!d) return '\u2014';
|
||||||
|
try {
|
||||||
|
return new Date(d).toLocaleDateString('en-US', {
|
||||||
|
month: 'short', day: 'numeric', year: 'numeric',
|
||||||
|
});
|
||||||
|
} catch { return d; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
|
||||||
|
const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set());
|
||||||
|
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'Calendar View', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const d = extractData(result);
|
||||||
|
if (d) setData(d);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendar = data?.calendar;
|
||||||
|
const events: any[] = data?.events || [];
|
||||||
|
|
||||||
|
// Map events to CalendarEvent format with color coding
|
||||||
|
const calendarEvents: CalendarEvent[] = useMemo(() => {
|
||||||
|
return events.map((evt) => {
|
||||||
|
const eventType = (evt.type || evt.appointmentStatus || 'event').toLowerCase();
|
||||||
|
return {
|
||||||
|
title: evt.title || evt.name || evt.subject || 'Untitled',
|
||||||
|
date: evt.startTime || evt.date || evt.dateAdded || '',
|
||||||
|
time: evt.startTime
|
||||||
|
? new Date(evt.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
|
||||||
|
: evt.time || '',
|
||||||
|
type: eventType,
|
||||||
|
color: EVENT_TYPE_COLORS[eventType] || '#4f46e5',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
// Filter events by active types
|
||||||
|
const filteredEvents = useMemo(() => {
|
||||||
|
if (activeFilters.size === 0) return calendarEvents;
|
||||||
|
return calendarEvents.filter((e) => activeFilters.has(e.type || 'event'));
|
||||||
|
}, [calendarEvents, activeFilters]);
|
||||||
|
|
||||||
|
// Determine present event types
|
||||||
|
const presentTypes = useMemo(() => {
|
||||||
|
const types = new Set<string>();
|
||||||
|
for (const e of calendarEvents) {
|
||||||
|
types.add(e.type || 'event');
|
||||||
|
}
|
||||||
|
return Array.from(types);
|
||||||
|
}, [calendarEvents]);
|
||||||
|
|
||||||
|
// Events for selected date
|
||||||
|
const selectedDateEvents = useMemo(() => {
|
||||||
|
if (!selectedDate) return [];
|
||||||
|
return filteredEvents.filter((e) => {
|
||||||
|
try {
|
||||||
|
const eDate = new Date(e.date).toISOString().split('T')[0];
|
||||||
|
return eDate === selectedDate;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [filteredEvents, selectedDate]);
|
||||||
|
|
||||||
|
const toggleFilter = (type: string) => {
|
||||||
|
setActiveFilters((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(type)) next.delete(type);
|
||||||
|
else next.add(type);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
|
||||||
|
}
|
||||||
|
if (!isConnected && !data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for calendar data...</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title={calendar?.name || 'Calendar'}
|
||||||
|
subtitle={calendar?.description || `${events.length} events`}
|
||||||
|
stats={[
|
||||||
|
{ label: 'Events', value: String(filteredEvents.length) },
|
||||||
|
...(data.startDate ? [{ label: 'From', value: formatDate(data.startDate) }] : []),
|
||||||
|
...(data.endDate ? [{ label: 'To', value: formatDate(data.endDate) }] : []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Filter chips */}
|
||||||
|
{presentTypes.length > 1 && (
|
||||||
|
<div style={{ margin: '8px 0 12px' }}>
|
||||||
|
<div className="filter-chips">
|
||||||
|
{presentTypes.map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
className={`chip ${activeFilters.size === 0 || activeFilters.has(type) ? 'chip-active' : ''}`}
|
||||||
|
onClick={() => toggleFilter(type)}
|
||||||
|
style={{
|
||||||
|
borderColor: EVENT_TYPE_COLORS[type] || '#4f46e5',
|
||||||
|
...(activeFilters.size === 0 || activeFilters.has(type)
|
||||||
|
? { background: EVENT_TYPE_COLORS[type] || '#4f46e5', color: '#fff' }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card padding="sm">
|
||||||
|
<CalendarView
|
||||||
|
events={filteredEvents}
|
||||||
|
highlightToday
|
||||||
|
onDateClick={(date) => setSelectedDate(date === selectedDate ? null : date)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Selected date detail */}
|
||||||
|
{selectedDate && (
|
||||||
|
<Card title={`Events on ${selectedDate}`} padding="sm">
|
||||||
|
{selectedDateEvents.length === 0 ? (
|
||||||
|
<div className="empty-state"><div className="empty-icon">📅</div><p>No events on this date</p></div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{selectedDateEvents.map((evt, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: '#f9fafb',
|
||||||
|
borderRadius: 6,
|
||||||
|
borderLeft: `3px solid ${evt.color || '#4f46e5'}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 500, fontSize: 13 }}>{evt.time || '—'}</span>
|
||||||
|
<span style={{ fontSize: 13 }}>{evt.title}</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
fontSize: 11,
|
||||||
|
color: evt.color || '#6b7280',
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{evt.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/ui/react-app/src/apps/calendar-view/index.html
Normal file
5
src/ui/react-app/src/apps/calendar-view/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
|
||||||
|
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
|
||||||
|
</html>
|
||||||
9
src/ui/react-app/src/apps/calendar-view/main.tsx
Normal file
9
src/ui/react-app/src/apps/calendar-view/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { App } from './App.js';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
22
src/ui/react-app/src/apps/calendar-view/vite.config.ts
Normal file
22
src/ui/react-app/src/apps/calendar-view/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
root: __dirname,
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, '../../../../dist/apps/calendar-view'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@components': path.resolve(__dirname, '../../components'),
|
||||||
|
'@hooks': path.resolve(__dirname, '../../hooks'),
|
||||||
|
'@styles': path.resolve(__dirname, '../../styles'),
|
||||||
|
'@context': path.resolve(__dirname, '../../context'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
141
src/ui/react-app/src/apps/call-detail/App.tsx
Normal file
141
src/ui/react-app/src/apps/call-detail/App.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||||
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { DetailHeader } from '../../components/data/DetailHeader';
|
||||||
|
import { KeyValueList } from '../../components/data/KeyValueList';
|
||||||
|
import { Card } from '../../components/layout/Card';
|
||||||
|
import { AudioPlayer } from '../../components/data/AudioPlayer';
|
||||||
|
import { TranscriptView } from '../../components/comms/TranscriptView';
|
||||||
|
import '../../styles/base.css';
|
||||||
|
import '../../styles/interactive.css';
|
||||||
|
|
||||||
|
function extractData(result: CallToolResult): any {
|
||||||
|
const sc = (result as any).structuredContent;
|
||||||
|
if (sc) return sc;
|
||||||
|
for (const item of result.content || []) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
try { return JSON.parse(item.text); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds: number | string | undefined): string {
|
||||||
|
if (!seconds) return '0:00';
|
||||||
|
const s = typeof seconds === 'string' ? parseInt(seconds, 10) : seconds;
|
||||||
|
if (isNaN(s)) return '0:00';
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const sec = s % 60;
|
||||||
|
return `${m}:${sec.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(d: string | undefined): string {
|
||||||
|
if (!d) return '-';
|
||||||
|
try {
|
||||||
|
return new Date(d).toLocaleDateString('en-US', {
|
||||||
|
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
|
||||||
|
hour: 'numeric', minute: '2-digit',
|
||||||
|
});
|
||||||
|
} catch { return d; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusVariantMap: Record<string, string> = {
|
||||||
|
completed: 'complete',
|
||||||
|
missed: 'error',
|
||||||
|
voicemail: 'pending',
|
||||||
|
busy: 'paused',
|
||||||
|
'no-answer': 'draft',
|
||||||
|
failed: 'error',
|
||||||
|
active: 'active',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
|
||||||
|
|
||||||
|
const { isConnected, error } = useApp({
|
||||||
|
appInfo: { name: 'Call Detail', version: '1.0.0' },
|
||||||
|
capabilities: {},
|
||||||
|
onAppCreated: (app) => {
|
||||||
|
app.ontoolresult = async (result) => {
|
||||||
|
const parsed = extractData(result);
|
||||||
|
if (parsed) setData(parsed);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
|
||||||
|
if (!isConnected) return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
|
||||||
|
if (!data) return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for data...</p></div>;
|
||||||
|
|
||||||
|
const call = data?.call || data || {};
|
||||||
|
const contactName = call.contactName || call.contact?.name || 'Unknown Contact';
|
||||||
|
const status = (call.status || 'completed').toLowerCase();
|
||||||
|
const direction = (call.direction || 'inbound').toLowerCase();
|
||||||
|
const variant = statusVariantMap[status] || 'draft';
|
||||||
|
|
||||||
|
const metadataItems = [
|
||||||
|
{ label: 'Direction', value: direction.charAt(0).toUpperCase() + direction.slice(1), bold: true },
|
||||||
|
{ label: 'From', value: call.from || '-' },
|
||||||
|
{ label: 'To', value: call.to || '-' },
|
||||||
|
{ label: 'Duration', value: formatDuration(call.duration), bold: true },
|
||||||
|
{ label: 'Date', value: formatDate(call.date || call.createdAt || call.startedAt) },
|
||||||
|
{ label: 'Status', value: status.charAt(0).toUpperCase() + status.slice(1) },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (call.source) metadataItems.push({ label: 'Source', value: call.source, bold: false });
|
||||||
|
if (call.assignedTo) metadataItems.push({ label: 'Assigned To', value: call.assignedTo, bold: false });
|
||||||
|
|
||||||
|
const hasRecording = !!(call.recordingUrl || call.recording);
|
||||||
|
const transcript = call.transcript || call.transcription || [];
|
||||||
|
const transcriptEntries = Array.isArray(transcript) ? transcript : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DetailHeader
|
||||||
|
title={contactName}
|
||||||
|
subtitle={call.phone || call.from || call.to || ''}
|
||||||
|
entityId={call.id ? `Call #${call.id}` : undefined}
|
||||||
|
status={status.charAt(0).toUpperCase() + status.slice(1)}
|
||||||
|
statusVariant={variant as any}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gap: 16, marginTop: 16 }}>
|
||||||
|
<Card title="Call Details">
|
||||||
|
<KeyValueList items={metadataItems} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{hasRecording && (
|
||||||
|
<Card title="Recording">
|
||||||
|
<AudioPlayer
|
||||||
|
title={`Call with ${contactName}`}
|
||||||
|
duration={formatDuration(call.duration)}
|
||||||
|
type="recording"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transcriptEntries.length > 0 ? (
|
||||||
|
<Card title="Transcript">
|
||||||
|
<TranscriptView
|
||||||
|
entries={transcriptEntries.map((e: any) => ({
|
||||||
|
speaker: e.speaker || e.speakerName || 'Unknown',
|
||||||
|
speakerRole: e.speakerRole || e.role || 'customer',
|
||||||
|
text: e.text || e.content || '',
|
||||||
|
timestamp: e.timestamp || e.time || '',
|
||||||
|
}))}
|
||||||
|
title={`Call Transcript`}
|
||||||
|
duration={formatDuration(call.duration)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card title="Transcript">
|
||||||
|
<div className="empty-state">
|
||||||
|
<div className="empty-icon">📝</div>
|
||||||
|
<p>No transcript available for this call</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user