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",
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
<<<<<<< HEAD
|
||||
"name": "ghl-mcp",
|
||||
=======
|
||||
"name": "@mastanley13/ghl-mcp-server",
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.72.1",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
@ -25,12 +18,9 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"bin": {
|
||||
"ghl-mcp-server": "dist/server.js"
|
||||
},
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.29",
|
||||
@ -39,12 +29,9 @@
|
||||
"ts-jest": "^29.3.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3"
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@ -60,6 +47,26 @@
|
||||
"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": {
|
||||
"version": "7.27.1",
|
||||
"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",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"dev": true,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@ -465,6 +469,15 @@
|
||||
"@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": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@ -1102,10 +1115,7 @@
|
||||
"version": "22.15.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
||||
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@ -1487,10 +1497,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
@ -2155,10 +2162,7 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
@ -2878,10 +2882,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@ -3481,6 +3482,19 @@
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@ -4613,6 +4627,12 @@
|
||||
"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": {
|
||||
"version": "29.3.4",
|
||||
"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",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@ -4772,10 +4789,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -4994,10 +5008,7 @@
|
||||
"version": "3.25.51",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz",
|
||||
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"peer": true,
|
||||
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"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",
|
||||
"start": "node dist/http-server.js",
|
||||
"start:stdio": "node dist/server.js",
|
||||
@ -45,6 +46,7 @@
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.72.1",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
|
||||
1154
src/apps/index.ts
1154
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' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "affiliates",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -31,7 +38,14 @@ export class AffiliatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -48,7 +62,14 @@ export class AffiliatesTools {
|
||||
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
|
||||
commissionValue: { type: 'number', description: 'Commission value (percentage or fixed amount)' },
|
||||
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']
|
||||
}
|
||||
@ -65,7 +86,14 @@ export class AffiliatesTools {
|
||||
description: { type: 'string', description: 'Campaign description' },
|
||||
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
|
||||
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']
|
||||
}
|
||||
@ -77,7 +105,14 @@ export class AffiliatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -96,6 +131,13 @@ export class AffiliatesTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "affiliates",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -105,7 +147,14 @@ export class AffiliatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -120,7 +169,14 @@ export class AffiliatesTools {
|
||||
contactId: { type: 'string', description: 'Contact ID to make affiliate' },
|
||||
campaignId: { type: 'string', description: 'Campaign to assign to' },
|
||||
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']
|
||||
}
|
||||
@ -134,7 +190,14 @@ export class AffiliatesTools {
|
||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -146,7 +209,14 @@ export class AffiliatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -159,7 +229,14 @@ export class AffiliatesTools {
|
||||
properties: {
|
||||
affiliateId: { type: 'string', description: 'Affiliate 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']
|
||||
}
|
||||
@ -171,7 +248,14 @@ export class AffiliatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -190,7 +274,14 @@ export class AffiliatesTools {
|
||||
startDate: { type: 'string', description: 'Start date' },
|
||||
endDate: { type: 'string', description: 'End date' },
|
||||
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']
|
||||
}
|
||||
@ -204,7 +295,14 @@ export class AffiliatesTools {
|
||||
affiliateId: { type: 'string', description: 'Affiliate ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -219,7 +317,14 @@ export class AffiliatesTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
amount: { type: 'number', description: 'Payout amount' },
|
||||
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']
|
||||
}
|
||||
@ -236,6 +341,13 @@ export class AffiliatesTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "affiliates",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -40,6 +40,13 @@ export class AssociationTools {
|
||||
default: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "read",
|
||||
complexity: "batch"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -67,6 +74,13 @@ export class AssociationTools {
|
||||
},
|
||||
secondObjectKey: {
|
||||
description: 'Key for the second object (e.g., "contact")'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['key', 'firstObjectLabel', 'firstObjectKey', 'secondObjectLabel', 'secondObjectKey']
|
||||
@ -81,6 +95,13 @@ export class AssociationTools {
|
||||
associationId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the association to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['associationId']
|
||||
@ -101,6 +122,13 @@ export class AssociationTools {
|
||||
},
|
||||
secondObjectLabel: {
|
||||
description: 'New label for the second object in the association'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['associationId', 'firstObjectLabel', 'secondObjectLabel']
|
||||
@ -115,6 +143,13 @@ export class AssociationTools {
|
||||
associationId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the association to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['associationId']
|
||||
@ -133,6 +168,13 @@ export class AssociationTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['keyName']
|
||||
@ -151,6 +193,13 @@ export class AssociationTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (optional)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['objectKey']
|
||||
@ -178,6 +227,13 @@ export class AssociationTools {
|
||||
secondRecordId: {
|
||||
type: 'string',
|
||||
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']
|
||||
@ -213,6 +269,13 @@ export class AssociationTools {
|
||||
type: 'string'
|
||||
},
|
||||
description: 'Optional array of association IDs to filter relations'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['recordId']
|
||||
@ -231,6 +294,13 @@ export class AssociationTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "associations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['relationId']
|
||||
|
||||
@ -94,6 +94,13 @@ export class BlogTools {
|
||||
publishedAt: {
|
||||
type: 'string',
|
||||
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']
|
||||
@ -165,6 +172,13 @@ export class BlogTools {
|
||||
publishedAt: {
|
||||
type: 'string',
|
||||
description: 'Updated ISO timestamp for publication date'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "blogs",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['postId', 'blogId']
|
||||
@ -200,6 +214,13 @@ export class BlogTools {
|
||||
type: 'string',
|
||||
enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'],
|
||||
description: 'Optional filter by publication status'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "blogs",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['blogId']
|
||||
@ -228,6 +249,13 @@ export class BlogTools {
|
||||
description: 'Optional search term to filter blogs by name'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "blogs",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -249,6 +277,13 @@ export class BlogTools {
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "blogs",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -270,6 +305,13 @@ export class BlogTools {
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "blogs",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -287,6 +329,13 @@ export class BlogTools {
|
||||
postId: {
|
||||
type: 'string',
|
||||
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']
|
||||
|
||||
@ -21,6 +21,13 @@ export class BusinessesTools {
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "businesses",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -36,6 +43,13 @@ export class BusinessesTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "businesses",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['businessId']
|
||||
@ -94,6 +108,13 @@ export class BusinessesTools {
|
||||
logoUrl: {
|
||||
type: 'string',
|
||||
description: 'URL to business logo image'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "businesses",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
@ -156,6 +177,13 @@ export class BusinessesTools {
|
||||
logoUrl: {
|
||||
type: 'string',
|
||||
description: 'URL to business logo image'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "businesses",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['businessId']
|
||||
@ -174,6 +202,13 @@ export class BusinessesTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "businesses",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['businessId']
|
||||
|
||||
@ -62,6 +62,13 @@ export class CalendarTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -80,6 +87,13 @@ export class CalendarTools {
|
||||
default: true
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -136,6 +150,13 @@ export class CalendarTools {
|
||||
type: 'boolean',
|
||||
description: 'Make calendar active immediately (default: true)',
|
||||
default: true
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name', 'calendarType']
|
||||
@ -150,6 +171,13 @@ export class CalendarTools {
|
||||
calendarId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the calendar to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['calendarId']
|
||||
@ -192,6 +220,13 @@ export class CalendarTools {
|
||||
isActive: {
|
||||
type: 'boolean',
|
||||
description: 'Updated active status'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['calendarId']
|
||||
@ -206,6 +241,13 @@ export class CalendarTools {
|
||||
calendarId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the calendar to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['calendarId']
|
||||
@ -236,6 +278,13 @@ export class CalendarTools {
|
||||
groupId: {
|
||||
type: 'string',
|
||||
description: 'Filter events by calendar group ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['startTime', 'endTime']
|
||||
@ -266,6 +315,13 @@ export class CalendarTools {
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'Specific user ID to check availability for'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['calendarId', 'startDate', 'endDate']
|
||||
@ -326,6 +382,13 @@ export class CalendarTools {
|
||||
type: 'boolean',
|
||||
description: 'Send notifications for this appointment',
|
||||
default: true
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['calendarId', 'contactId', 'startTime']
|
||||
@ -340,6 +403,13 @@ export class CalendarTools {
|
||||
appointmentId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the appointment to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['appointmentId']
|
||||
@ -384,6 +454,13 @@ export class CalendarTools {
|
||||
type: 'boolean',
|
||||
description: 'Send notifications for this update',
|
||||
default: true
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['appointmentId']
|
||||
@ -398,6 +475,13 @@ export class CalendarTools {
|
||||
appointmentId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the appointment to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['appointmentId']
|
||||
@ -428,6 +512,13 @@ export class CalendarTools {
|
||||
assignedUserId: {
|
||||
type: 'string',
|
||||
description: 'User ID to apply the block for'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['startTime', 'endTime']
|
||||
@ -462,6 +553,13 @@ export class CalendarTools {
|
||||
assignedUserId: {
|
||||
type: 'string',
|
||||
description: 'Updated assigned user ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "calendar",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['blockSlotId']
|
||||
@ -476,7 +574,14 @@ export class CalendarTools {
|
||||
name: { type: 'string', description: 'Group name' },
|
||||
description: { type: 'string', description: 'Group description' },
|
||||
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']
|
||||
}
|
||||
@ -488,7 +593,14 @@ export class CalendarTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -502,7 +614,14 @@ export class CalendarTools {
|
||||
groupId: { type: 'string', description: 'Calendar group ID' },
|
||||
name: { type: 'string', description: 'Group name' },
|
||||
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']
|
||||
}
|
||||
@ -513,7 +632,14 @@ export class CalendarTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -525,7 +651,14 @@ export class CalendarTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -538,7 +671,14 @@ export class CalendarTools {
|
||||
properties: {
|
||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||
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']
|
||||
}
|
||||
@ -551,7 +691,14 @@ export class CalendarTools {
|
||||
properties: {
|
||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||
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']
|
||||
}
|
||||
@ -565,7 +712,14 @@ export class CalendarTools {
|
||||
appointmentId: { type: 'string', description: 'Appointment ID' },
|
||||
noteId: { type: 'string', description: 'Note ID' },
|
||||
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']
|
||||
}
|
||||
@ -577,7 +731,14 @@ export class CalendarTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -591,6 +752,13 @@ export class CalendarTools {
|
||||
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
|
||||
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' },
|
||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||
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']
|
||||
}
|
||||
@ -615,7 +790,14 @@ export class CalendarTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -633,7 +815,14 @@ export class CalendarTools {
|
||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||
capacity: { type: 'number', description: 'Capacity per unit' },
|
||||
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']
|
||||
}
|
||||
@ -644,7 +833,14 @@ export class CalendarTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -658,6 +854,13 @@ export class CalendarTools {
|
||||
limit: { type: 'number', description: 'Maximum number to return', default: 20 },
|
||||
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' },
|
||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||
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']
|
||||
}
|
||||
@ -682,7 +892,14 @@ export class CalendarTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -700,7 +917,14 @@ export class CalendarTools {
|
||||
outOfService: { type: 'number', description: 'Number currently out of service' },
|
||||
capacity: { type: 'number', description: 'Room capacity' },
|
||||
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']
|
||||
}
|
||||
@ -711,7 +935,14 @@ export class CalendarTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -726,7 +957,14 @@ export class CalendarTools {
|
||||
isActive: { type: 'boolean', description: 'Filter by active status' },
|
||||
deleted: { type: 'boolean', description: 'Include deleted notifications' },
|
||||
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']
|
||||
}
|
||||
@ -749,7 +987,14 @@ export class CalendarTools {
|
||||
isActive: { type: 'boolean', description: 'Whether notification is active' },
|
||||
templateId: { type: 'string', description: 'Template ID' },
|
||||
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']
|
||||
},
|
||||
@ -766,7 +1011,14 @@ export class CalendarTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -786,7 +1038,14 @@ export class CalendarTools {
|
||||
deleted: { type: 'boolean', description: 'Whether notification is deleted' },
|
||||
templateId: { type: 'string', description: 'Template ID' },
|
||||
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']
|
||||
}
|
||||
@ -798,7 +1057,14 @@ export class CalendarTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -813,7 +1079,14 @@ export class CalendarTools {
|
||||
calendarId: { type: 'string', description: 'Filter by calendar ID' },
|
||||
groupId: { type: 'string', description: 'Filter by group ID' },
|
||||
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']
|
||||
}
|
||||
|
||||
@ -22,6 +22,13 @@ export class CampaignsTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "campaigns",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -31,7 +38,14 @@ export class CampaignsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -45,7 +59,14 @@ export class CampaignsTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
name: { type: 'string', description: 'Campaign name' },
|
||||
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']
|
||||
}
|
||||
@ -59,7 +80,14 @@ export class CampaignsTools {
|
||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -71,7 +99,14 @@ export class CampaignsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -85,7 +120,14 @@ export class CampaignsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -97,7 +139,14 @@ export class CampaignsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -109,7 +158,14 @@ export class CampaignsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -123,7 +179,14 @@ export class CampaignsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -138,7 +201,14 @@ export class CampaignsTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
status: { type: 'string', enum: ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'unsubscribed'], description: 'Filter by recipient status' },
|
||||
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']
|
||||
}
|
||||
@ -155,6 +225,13 @@ export class CampaignsTools {
|
||||
contactId: { type: 'string', description: 'Filter by contact 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',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
|
||||
@ -33,6 +33,13 @@ export class CompaniesTools {
|
||||
description: 'Search query to filter companies'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "general",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -48,6 +55,13 @@ export class CompaniesTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "general",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId']
|
||||
@ -127,6 +141,13 @@ export class CompaniesTools {
|
||||
id: { type: 'string' },
|
||||
key: { type: 'string' },
|
||||
value: { type: 'string' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "general",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
description: 'Custom field values'
|
||||
@ -214,6 +235,13 @@ export class CompaniesTools {
|
||||
id: { type: 'string' },
|
||||
key: { type: 'string' },
|
||||
value: { type: 'string' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "general",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
description: 'Custom field values'
|
||||
@ -240,6 +268,13 @@ export class CompaniesTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "general",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId']
|
||||
|
||||
@ -81,6 +81,13 @@ export class ContactTools {
|
||||
source: { type: 'string', description: 'Source of the contact' }
|
||||
},
|
||||
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' },
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
required: ['contactId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -132,6 +160,13 @@ export class ContactTools {
|
||||
contactId: { type: 'string', description: 'Contact ID' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
required: ['contactId', 'tags']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -169,6 +218,13 @@ export class ContactTools {
|
||||
contactId: { type: 'string', description: 'Contact ID' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
required: ['contactId', 'taskId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -226,6 +303,13 @@ export class ContactTools {
|
||||
taskId: { type: 'string', description: 'Task ID' }
|
||||
},
|
||||
required: ['contactId', 'taskId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -239,6 +323,13 @@ export class ContactTools {
|
||||
completed: { type: 'boolean', description: 'Completion status' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
required: ['contactId', 'body']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -277,6 +382,13 @@ export class ContactTools {
|
||||
noteId: { type: 'string', description: 'Note ID' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' },
|
||||
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' },
|
||||
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' }
|
||||
},
|
||||
required: ['businessId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -357,6 +504,13 @@ export class ContactTools {
|
||||
contactId: { type: 'string', description: 'Contact ID' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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)' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
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' }
|
||||
},
|
||||
required: ['contactId', 'followers']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -425,6 +607,13 @@ export class ContactTools {
|
||||
campaignId: { type: 'string', description: 'Campaign ID' }
|
||||
},
|
||||
required: ['contactId', 'campaignId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -437,6 +626,13 @@ export class ContactTools {
|
||||
campaignId: { type: 'string', description: 'Campaign ID' }
|
||||
},
|
||||
required: ['contactId', 'campaignId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -448,6 +644,13 @@ export class ContactTools {
|
||||
contactId: { type: 'string', description: 'Contact ID' }
|
||||
},
|
||||
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)' }
|
||||
},
|
||||
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)' }
|
||||
},
|
||||
required: ['contactId', 'workflowId']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "contacts",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@ -69,6 +69,13 @@ export class ConversationTools {
|
||||
fromNumber: {
|
||||
type: 'string',
|
||||
description: 'Optional: Phone number to send from (must be configured in GHL)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['contactId', 'message']
|
||||
@ -115,6 +122,13 @@ export class ConversationTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: Array of BCC email addresses'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['contactId', 'subject']
|
||||
@ -152,6 +166,13 @@ export class ConversationTools {
|
||||
description: 'Filter by user ID assigned to conversations'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -181,6 +202,13 @@ export class ConversationTools {
|
||||
]
|
||||
},
|
||||
description: 'Filter messages by type (optional)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['conversationId']
|
||||
@ -195,6 +223,13 @@ export class ConversationTools {
|
||||
contactId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the contact to create conversation with'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['contactId']
|
||||
@ -218,6 +253,13 @@ export class ConversationTools {
|
||||
type: 'number',
|
||||
description: 'Set the unread message count (0 to mark as read)',
|
||||
minimum: 0
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['conversationId']
|
||||
@ -243,6 +285,13 @@ export class ConversationTools {
|
||||
default: 'unread'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -254,6 +303,13 @@ export class ConversationTools {
|
||||
conversationId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the conversation to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['conversationId']
|
||||
@ -270,6 +326,13 @@ export class ConversationTools {
|
||||
emailMessageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the email message to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['emailMessageId']
|
||||
@ -284,6 +347,13 @@ export class ConversationTools {
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the message to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['messageId']
|
||||
@ -303,6 +373,13 @@ export class ConversationTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of file URLs to upload as attachments'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['conversationId', 'attachmentUrls']
|
||||
@ -330,6 +407,13 @@ export class ConversationTools {
|
||||
code: { type: 'string' },
|
||||
type: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
emailMessageId: {
|
||||
@ -425,6 +509,13 @@ export class ConversationTools {
|
||||
description: 'Call status'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['type', 'conversationId', 'conversationProviderId']
|
||||
@ -469,6 +560,13 @@ export class ConversationTools {
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Date of the call (ISO format)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status']
|
||||
@ -485,6 +583,13 @@ export class ConversationTools {
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the call message to get recording for'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['messageId']
|
||||
@ -499,6 +604,13 @@ export class ConversationTools {
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the call message to get transcription for'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['messageId']
|
||||
@ -513,6 +625,13 @@ export class ConversationTools {
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the call message to download transcription for'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['messageId']
|
||||
@ -529,6 +648,13 @@ export class ConversationTools {
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the scheduled message to cancel'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['messageId']
|
||||
@ -543,6 +669,13 @@ export class ConversationTools {
|
||||
emailMessageId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the scheduled email to cancel'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['emailMessageId']
|
||||
@ -567,6 +700,13 @@ export class ConversationTools {
|
||||
isTyping: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the agent is currently typing'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "conversations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['visitorId', 'conversationId', 'isTyping']
|
||||
|
||||
@ -21,6 +21,13 @@ export class CoursesTools {
|
||||
limit: { type: 'number', description: 'Max results to return' },
|
||||
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' },
|
||||
name: { type: 'string', description: 'Import job name' },
|
||||
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']
|
||||
}
|
||||
@ -49,6 +63,13 @@ export class CoursesTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "courses",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -58,7 +79,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -73,7 +101,14 @@ export class CoursesTools {
|
||||
title: { type: 'string', description: 'Product title' },
|
||||
description: { type: 'string', description: 'Product description' },
|
||||
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']
|
||||
}
|
||||
@ -88,7 +123,14 @@ export class CoursesTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
title: { type: 'string', description: 'Product title' },
|
||||
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']
|
||||
}
|
||||
@ -100,7 +142,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -117,6 +166,13 @@ export class CoursesTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "courses",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -126,7 +182,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -139,7 +202,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
categoryId: { type: 'string', description: 'Category 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']
|
||||
}
|
||||
@ -151,7 +221,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -169,6 +246,13 @@ export class CoursesTools {
|
||||
offset: { type: 'number', description: 'Pagination offset' },
|
||||
categoryId: { type: 'string', description: 'Filter by category' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "courses",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -178,7 +262,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -194,7 +285,14 @@ export class CoursesTools {
|
||||
description: { type: 'string', description: 'Course description' },
|
||||
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
|
||||
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']
|
||||
}
|
||||
@ -210,7 +308,14 @@ export class CoursesTools {
|
||||
title: { type: 'string', description: 'Course title' },
|
||||
description: { type: 'string', description: 'Course description' },
|
||||
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']
|
||||
}
|
||||
@ -222,7 +327,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -236,7 +348,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -251,7 +370,14 @@ export class CoursesTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
userId: { type: 'string', description: 'User ID of instructor' },
|
||||
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']
|
||||
}
|
||||
@ -267,7 +393,14 @@ export class CoursesTools {
|
||||
courseId: { type: 'string', description: 'Course ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -280,7 +413,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
courseId: { type: 'string', description: 'Course 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']
|
||||
}
|
||||
@ -297,7 +437,14 @@ export class CoursesTools {
|
||||
contentType: { type: 'string', enum: ['video', 'text', 'quiz', 'assignment'], description: 'Content type' },
|
||||
content: { type: 'string', description: 'Post content (text/HTML)' },
|
||||
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']
|
||||
}
|
||||
@ -314,7 +461,14 @@ export class CoursesTools {
|
||||
title: { type: 'string', description: 'Post/lesson title' },
|
||||
content: { type: 'string', description: 'Post content' },
|
||||
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']
|
||||
}
|
||||
@ -327,7 +481,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
courseId: { type: 'string', description: 'Course 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']
|
||||
}
|
||||
@ -341,7 +502,14 @@ export class CoursesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -358,7 +526,14 @@ export class CoursesTools {
|
||||
price: { type: 'number', description: 'Price in cents' },
|
||||
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
||||
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']
|
||||
}
|
||||
@ -373,7 +548,14 @@ export class CoursesTools {
|
||||
offerId: { type: 'string', description: 'Offer ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -386,7 +568,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
productId: { type: 'string', description: 'Course product 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']
|
||||
}
|
||||
@ -402,7 +591,14 @@ export class CoursesTools {
|
||||
courseId: { type: 'string', description: 'Course ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -415,7 +611,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
courseId: { type: 'string', description: 'Course ID' },
|
||||
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']
|
||||
}
|
||||
@ -428,7 +631,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
courseId: { type: 'string', description: 'Course 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']
|
||||
}
|
||||
@ -443,7 +653,14 @@ export class CoursesTools {
|
||||
properties: {
|
||||
courseId: { type: 'string', description: 'Course 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']
|
||||
}
|
||||
@ -458,7 +675,14 @@ export class CoursesTools {
|
||||
postId: { type: 'string', description: 'Post/Lesson ID' },
|
||||
contactId: { type: 'string', description: 'Contact/Student 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']
|
||||
}
|
||||
|
||||
@ -26,6 +26,13 @@ export class CustomFieldV2Tools {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The ID of the custom field or folder to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['id']
|
||||
@ -74,6 +81,13 @@ export class CustomFieldV2Tools {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL associated with the option (only for RADIO type)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['key', 'label']
|
||||
@ -160,6 +174,13 @@ export class CustomFieldV2Tools {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL associated with the option (only for RADIO type)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['key', 'label']
|
||||
@ -188,6 +209,13 @@ export class CustomFieldV2Tools {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The ID of the custom field to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['id']
|
||||
@ -206,6 +234,13 @@ export class CustomFieldV2Tools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['objectKey']
|
||||
@ -229,6 +264,13 @@ export class CustomFieldV2Tools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['objectKey', 'name']
|
||||
@ -251,6 +293,13 @@ export class CustomFieldV2Tools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['id', 'name']
|
||||
@ -269,6 +318,13 @@ export class CustomFieldV2Tools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'GoHighLevel location ID (will use default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "custom-fields",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['id']
|
||||
|
||||
@ -40,6 +40,13 @@ export class EmailISVTools {
|
||||
verify: {
|
||||
type: 'string',
|
||||
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']
|
||||
|
||||
@ -50,6 +50,13 @@ export class EmailTools {
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "email",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -70,6 +77,13 @@ export class EmailTools {
|
||||
type: 'boolean',
|
||||
description: 'Whether the template is plain text.',
|
||||
default: false
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "email",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['title', 'html']
|
||||
@ -92,6 +106,13 @@ export class EmailTools {
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "email",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -111,6 +132,13 @@ export class EmailTools {
|
||||
previewText: {
|
||||
type: 'string',
|
||||
description: 'The updated preview text for the template.'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "email",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['templateId', 'html']
|
||||
@ -125,6 +153,13 @@ export class EmailTools {
|
||||
templateId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the template to delete.'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "email",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['templateId']
|
||||
|
||||
@ -33,6 +33,13 @@ export class FormsTools {
|
||||
description: 'Filter by form type (e.g., "form", "survey")'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "forms",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -68,6 +75,13 @@ export class FormsTools {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "forms",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['formId']
|
||||
@ -86,6 +100,13 @@ export class FormsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "forms",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['formId']
|
||||
|
||||
@ -46,6 +46,13 @@ export class FunnelsTools {
|
||||
description: 'Filter by type (funnel or website)'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -61,6 +68,13 @@ export class FunnelsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId']
|
||||
@ -87,6 +101,13 @@ export class FunnelsTools {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of pages to return'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId']
|
||||
@ -105,6 +126,13 @@ export class FunnelsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId']
|
||||
@ -136,6 +164,13 @@ export class FunnelsTools {
|
||||
pathName: {
|
||||
type: 'string',
|
||||
description: 'Source path for the redirect'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId', 'target', 'action']
|
||||
@ -171,6 +206,13 @@ export class FunnelsTools {
|
||||
pathName: {
|
||||
type: 'string',
|
||||
description: 'Source path for the redirect'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId', 'redirectId']
|
||||
@ -193,6 +235,13 @@ export class FunnelsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId', 'redirectId']
|
||||
@ -219,6 +268,13 @@ export class FunnelsTools {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of redirects to return'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "funnels",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['funnelId']
|
||||
|
||||
@ -85,7 +85,14 @@ export class InvoicesTools {
|
||||
title: { type: 'string', description: 'Invoice title' },
|
||||
currency: { type: 'string', description: 'Currency code' },
|
||||
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']
|
||||
}
|
||||
@ -101,7 +108,14 @@ export class InvoicesTools {
|
||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||
status: { type: 'string', description: 'Filter by status' },
|
||||
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']
|
||||
}
|
||||
@ -113,7 +127,14 @@ export class InvoicesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -128,7 +149,14 @@ export class InvoicesTools {
|
||||
altId: { type: 'string', description: 'Location ID' },
|
||||
name: { type: 'string', description: 'Template name' },
|
||||
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']
|
||||
}
|
||||
@ -140,7 +168,14 @@ export class InvoicesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -157,7 +192,14 @@ export class InvoicesTools {
|
||||
name: { type: 'string', description: 'Schedule name' },
|
||||
templateId: { type: 'string', description: 'Template 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']
|
||||
}
|
||||
@ -172,7 +214,14 @@ export class InvoicesTools {
|
||||
limit: { type: 'string', description: 'Number of results per page', default: '10' },
|
||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||
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']
|
||||
}
|
||||
@ -184,7 +233,14 @@ export class InvoicesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -203,7 +259,14 @@ export class InvoicesTools {
|
||||
currency: { type: 'string', description: 'Currency code' },
|
||||
issueDate: { type: 'string', description: 'Issue 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']
|
||||
}
|
||||
@ -219,7 +282,14 @@ export class InvoicesTools {
|
||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||
status: { type: 'string', description: 'Filter by status' },
|
||||
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']
|
||||
}
|
||||
@ -231,7 +301,14 @@ export class InvoicesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -246,7 +323,14 @@ export class InvoicesTools {
|
||||
altId: { type: 'string', description: 'Location ID' },
|
||||
emailTo: { type: 'string', description: 'Email address to send to' },
|
||||
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']
|
||||
}
|
||||
@ -264,7 +348,14 @@ export class InvoicesTools {
|
||||
title: { type: 'string', description: 'Estimate title' },
|
||||
currency: { type: 'string', description: 'Currency code' },
|
||||
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']
|
||||
}
|
||||
@ -280,7 +371,14 @@ export class InvoicesTools {
|
||||
offset: { type: 'string', description: 'Offset for pagination', default: '0' },
|
||||
status: { type: 'string', enum: ['all', 'draft', 'sent', 'accepted', 'declined', 'invoiced', 'viewed'], description: 'Filter by status' },
|
||||
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']
|
||||
}
|
||||
@ -295,7 +393,14 @@ export class InvoicesTools {
|
||||
altId: { type: 'string', description: 'Location ID' },
|
||||
emailTo: { type: 'string', description: 'Email address to send to' },
|
||||
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']
|
||||
}
|
||||
@ -309,7 +414,14 @@ export class InvoicesTools {
|
||||
estimateId: { type: 'string', description: 'Estimate ID' },
|
||||
altId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -324,6 +436,13 @@ export class InvoicesTools {
|
||||
properties: {
|
||||
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'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "links",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -44,6 +51,13 @@ export class LinksTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "links",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['linkId']
|
||||
@ -74,6 +88,13 @@ export class LinksTools {
|
||||
fieldValue: {
|
||||
type: 'string',
|
||||
description: 'Value to set for the custom field'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "links",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name', 'redirectTo']
|
||||
@ -108,6 +129,13 @@ export class LinksTools {
|
||||
fieldValue: {
|
||||
type: 'string',
|
||||
description: 'Value to set for the custom field'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "links",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['linkId']
|
||||
@ -126,6 +154,13 @@ export class LinksTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "links",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['linkId']
|
||||
|
||||
@ -81,6 +81,13 @@ export class LocationTools {
|
||||
format: 'email'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -92,6 +99,13 @@ export class LocationTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the location to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -148,7 +162,14 @@ export class LocationTools {
|
||||
properties: {
|
||||
firstName: { type: 'string', description: 'Prospect first 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'],
|
||||
description: 'Prospect information for the location'
|
||||
@ -210,6 +231,13 @@ export class LocationTools {
|
||||
timezone: {
|
||||
type: 'string',
|
||||
description: 'Updated timezone'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'companyId']
|
||||
@ -229,6 +257,13 @@ export class LocationTools {
|
||||
type: 'boolean',
|
||||
description: 'Whether to delete associated Twilio account',
|
||||
default: false
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'deleteTwilioAccount']
|
||||
@ -245,6 +280,13 @@ export class LocationTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'The location ID to get tags from'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -263,6 +305,13 @@ export class LocationTools {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the tag to create'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'name']
|
||||
@ -281,6 +330,13 @@ export class LocationTools {
|
||||
tagId: {
|
||||
type: 'string',
|
||||
description: 'The tag ID to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'tagId']
|
||||
@ -303,6 +359,13 @@ export class LocationTools {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Updated name for the tag'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'tagId', 'name']
|
||||
@ -321,6 +384,13 @@ export class LocationTools {
|
||||
tagId: {
|
||||
type: 'string',
|
||||
description: 'The tag ID to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'tagId']
|
||||
@ -369,6 +439,13 @@ export class LocationTools {
|
||||
businessId: {
|
||||
type: 'string',
|
||||
description: 'Business ID filter'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -391,6 +468,13 @@ export class LocationTools {
|
||||
enum: ['contact', 'opportunity', 'all'],
|
||||
description: 'Filter by model type (default: all)',
|
||||
default: 'all'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -428,6 +512,13 @@ export class LocationTools {
|
||||
type: 'number',
|
||||
description: 'Position/order of the field (default: 0)',
|
||||
default: 0
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'name', 'dataType']
|
||||
@ -446,6 +537,13 @@ export class LocationTools {
|
||||
customFieldId: {
|
||||
type: 'string',
|
||||
description: 'The custom field ID to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'customFieldId']
|
||||
@ -476,6 +574,13 @@ export class LocationTools {
|
||||
position: {
|
||||
type: 'number',
|
||||
description: 'Updated position/order'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'customFieldId', 'name']
|
||||
@ -494,6 +599,13 @@ export class LocationTools {
|
||||
customFieldId: {
|
||||
type: 'string',
|
||||
description: 'The custom field ID to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'customFieldId']
|
||||
@ -510,6 +622,13 @@ export class LocationTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'The location ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -532,6 +651,13 @@ export class LocationTools {
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Value to assign'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'name', 'value']
|
||||
@ -550,6 +676,13 @@ export class LocationTools {
|
||||
customValueId: {
|
||||
type: 'string',
|
||||
description: 'The custom value ID to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'customValueId']
|
||||
@ -576,6 +709,13 @@ export class LocationTools {
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Updated value'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'customValueId', 'name', 'value']
|
||||
@ -594,6 +734,13 @@ export class LocationTools {
|
||||
customValueId: {
|
||||
type: 'string',
|
||||
description: 'The custom value ID to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'customValueId']
|
||||
@ -634,6 +781,13 @@ export class LocationTools {
|
||||
type: 'string',
|
||||
enum: ['sms', 'email', 'whatsapp'],
|
||||
description: 'Filter by template type'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'originId']
|
||||
@ -652,6 +806,13 @@ export class LocationTools {
|
||||
templateId: {
|
||||
type: 'string',
|
||||
description: 'The template ID to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "locations",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'templateId']
|
||||
|
||||
@ -80,6 +80,13 @@ export class MediaTools {
|
||||
parentId: {
|
||||
type: 'string',
|
||||
description: 'Parent folder ID to list files within a specific folder'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "media",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: []
|
||||
@ -121,6 +128,13 @@ export class MediaTools {
|
||||
altId: {
|
||||
type: 'string',
|
||||
description: 'Location or Agency ID (uses default location if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "media",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: []
|
||||
@ -145,6 +159,13 @@ export class MediaTools {
|
||||
altId: {
|
||||
type: 'string',
|
||||
description: 'Location or Agency ID (uses default location if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "media",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['id']
|
||||
|
||||
@ -20,6 +20,13 @@ export class OAuthTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -45,7 +59,14 @@ export class OAuthTools {
|
||||
skip: { type: 'number', description: 'Records to skip' },
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
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']
|
||||
}
|
||||
@ -58,6 +79,13 @@ export class OAuthTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "oauth",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -67,7 +95,14 @@ export class OAuthTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -82,6 +117,13 @@ export class OAuthTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "oauth",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -91,7 +133,14 @@ export class OAuthTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -106,6 +155,13 @@ export class OAuthTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "oauth",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -120,6 +176,13 @@ export class OAuthTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Permission scopes for the key'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "oauth",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
@ -132,7 +195,14 @@ export class OAuthTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
|
||||
@ -48,6 +48,13 @@ export class ObjectTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "read",
|
||||
complexity: "batch"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: []
|
||||
@ -64,7 +71,14 @@ export class ObjectTools {
|
||||
description: 'Singular and plural names for the custom object',
|
||||
properties: {
|
||||
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']
|
||||
},
|
||||
@ -112,6 +126,13 @@ export class ObjectTools {
|
||||
type: 'boolean',
|
||||
description: 'Whether to fetch all standard/custom fields of the object',
|
||||
default: true
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['key']
|
||||
@ -133,6 +154,13 @@ export class ObjectTools {
|
||||
properties: {
|
||||
singular: { type: 'string', description: 'Updated singular name' },
|
||||
plural: { type: 'string', description: 'Updated plural name' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
description: {
|
||||
@ -181,6 +209,13 @@ export class ObjectTools {
|
||||
description: 'Array of user IDs who follow this record (limited to 10)',
|
||||
items: { type: 'string' },
|
||||
maxItems: 10
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['schemaKey', 'properties']
|
||||
@ -199,6 +234,13 @@ export class ObjectTools {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'ID of the record to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['schemaKey', 'recordId']
|
||||
@ -237,6 +279,13 @@ export class ObjectTools {
|
||||
description: 'Updated array of user IDs who follow this record',
|
||||
items: { type: 'string' },
|
||||
maxItems: 10
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['schemaKey', 'recordId']
|
||||
@ -255,6 +304,13 @@ export class ObjectTools {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'ID of the record to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['schemaKey', 'recordId']
|
||||
@ -295,6 +351,13 @@ export class ObjectTools {
|
||||
type: 'array',
|
||||
description: 'Cursor for pagination (returned from previous search)',
|
||||
items: { type: 'string' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "objects",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['schemaKey', 'query']
|
||||
|
||||
@ -69,6 +69,13 @@ export class OpportunityTools {
|
||||
default: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -77,6 +84,13 @@ export class OpportunityTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -88,6 +102,13 @@ export class OpportunityTools {
|
||||
opportunityId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the opportunity to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['opportunityId']
|
||||
@ -124,6 +145,13 @@ export class OpportunityTools {
|
||||
assignedTo: {
|
||||
type: 'string',
|
||||
description: 'User ID to assign this opportunity to'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name', 'pipelineId', 'contactId']
|
||||
@ -143,6 +171,13 @@ export class OpportunityTools {
|
||||
type: 'string',
|
||||
description: 'New status for the opportunity',
|
||||
enum: ['open', 'won', 'lost', 'abandoned']
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['opportunityId', 'status']
|
||||
@ -157,6 +192,13 @@ export class OpportunityTools {
|
||||
opportunityId: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the opportunity to delete'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['opportunityId']
|
||||
@ -196,6 +238,13 @@ export class OpportunityTools {
|
||||
assignedTo: {
|
||||
type: 'string',
|
||||
description: 'Updated assigned user ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['opportunityId']
|
||||
@ -236,6 +285,13 @@ export class OpportunityTools {
|
||||
assignedTo: {
|
||||
type: 'string',
|
||||
description: 'User ID to assign this opportunity to'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "write",
|
||||
complexity: "complex"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['pipelineId', 'contactId']
|
||||
@ -255,6 +311,13 @@ export class OpportunityTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of user IDs to add as followers'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['opportunityId', 'followers']
|
||||
@ -274,6 +337,13 @@ export class OpportunityTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of user IDs to remove as followers'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "deals",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['opportunityId', 'followers']
|
||||
|
||||
@ -69,6 +69,13 @@ export class PaymentsTools {
|
||||
imageUrl: {
|
||||
type: 'string',
|
||||
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']
|
||||
@ -98,6 +105,13 @@ export class PaymentsTools {
|
||||
type: 'number',
|
||||
description: 'Starting index for pagination',
|
||||
default: 0
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType']
|
||||
@ -160,6 +174,13 @@ export class PaymentsTools {
|
||||
type: 'number',
|
||||
description: 'Starting index for pagination',
|
||||
default: 0
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType']
|
||||
@ -186,6 +207,13 @@ export class PaymentsTools {
|
||||
altType: {
|
||||
type: 'string',
|
||||
description: 'Alt Type (type of identifier)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['orderId', 'altId', 'altType']
|
||||
@ -231,6 +259,13 @@ export class PaymentsTools {
|
||||
description: 'Tracking URL'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
items: {
|
||||
@ -277,6 +312,13 @@ export class PaymentsTools {
|
||||
type: 'string',
|
||||
enum: ['location'],
|
||||
description: 'Alt Type'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['orderId', 'altId', 'altType']
|
||||
@ -347,6 +389,13 @@ export class PaymentsTools {
|
||||
type: 'number',
|
||||
description: 'Starting index for pagination',
|
||||
default: 0
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType']
|
||||
@ -373,6 +422,13 @@ export class PaymentsTools {
|
||||
altType: {
|
||||
type: 'string',
|
||||
description: 'Alt Type (type of identifier)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['transactionId', 'altId', 'altType']
|
||||
@ -436,6 +492,13 @@ export class PaymentsTools {
|
||||
type: 'number',
|
||||
description: 'Starting index for pagination',
|
||||
default: 0
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType']
|
||||
@ -459,6 +522,13 @@ export class PaymentsTools {
|
||||
type: 'string',
|
||||
enum: ['location'],
|
||||
description: 'Alt Type'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['subscriptionId', 'altId', 'altType']
|
||||
@ -499,6 +569,13 @@ export class PaymentsTools {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search term to filter coupons by name or code'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType']
|
||||
@ -553,6 +630,13 @@ export class PaymentsTools {
|
||||
description: 'Product IDs that the coupon applies to',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
applyToFuturePayments: {
|
||||
@ -643,6 +727,13 @@ export class PaymentsTools {
|
||||
description: 'Product IDs that the coupon applies to',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
applyToFuturePayments: {
|
||||
@ -696,6 +787,13 @@ export class PaymentsTools {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Coupon ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType', 'id']
|
||||
@ -723,6 +821,13 @@ export class PaymentsTools {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'Coupon code'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['altId', 'altType', 'id', 'code']
|
||||
@ -759,6 +864,13 @@ export class PaymentsTools {
|
||||
imageUrl: {
|
||||
type: 'string',
|
||||
description: 'Public image URL for the payment gateway logo'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'name', 'description', 'paymentsUrl', 'queryUrl', 'imageUrl']
|
||||
@ -773,6 +885,13 @@ export class PaymentsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -787,6 +906,13 @@ export class PaymentsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId']
|
||||
@ -813,6 +939,13 @@ export class PaymentsTools {
|
||||
publishableKey: {
|
||||
type: 'string',
|
||||
description: 'Publishable key for live payments'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['apiKey', 'publishableKey']
|
||||
@ -849,6 +982,13 @@ export class PaymentsTools {
|
||||
liveMode: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to disconnect live or test mode config'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "payments",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['locationId', 'liveMode']
|
||||
|
||||
@ -19,6 +19,13 @@ export class PhoneTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "phone-numbers",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -28,7 +35,14 @@ export class PhoneTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -43,7 +57,14 @@ export class PhoneTools {
|
||||
country: { type: 'string', description: 'Country code (e.g., US, CA)' },
|
||||
areaCode: { type: 'string', description: 'Area code to search' },
|
||||
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']
|
||||
}
|
||||
@ -56,7 +77,14 @@ export class PhoneTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -72,7 +100,14 @@ export class PhoneTools {
|
||||
name: { type: 'string', description: 'Friendly name' },
|
||||
forwardingNumber: { type: 'string', description: 'Number to forward calls to' },
|
||||
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']
|
||||
}
|
||||
@ -84,7 +119,14 @@ export class PhoneTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -98,7 +140,14 @@ export class PhoneTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -114,7 +163,14 @@ export class PhoneTools {
|
||||
enabled: { type: 'boolean', description: 'Enable forwarding' },
|
||||
forwardTo: { type: 'string', description: 'Number to forward to' },
|
||||
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']
|
||||
}
|
||||
@ -129,6 +185,13 @@ export class PhoneTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "phone-numbers",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -148,6 +211,13 @@ export class PhoneTools {
|
||||
digit: { type: 'string', description: 'Digit to press (0-9, *, #)' },
|
||||
action: { type: 'string', description: 'Action type' },
|
||||
destination: { type: 'string', description: 'Action destination' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "phone-numbers",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
description: 'Menu options'
|
||||
@ -166,7 +236,14 @@ export class PhoneTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
name: { type: 'string', description: 'Menu name' },
|
||||
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']
|
||||
}
|
||||
@ -178,7 +255,14 @@ export class PhoneTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -193,6 +277,13 @@ export class PhoneTools {
|
||||
properties: {
|
||||
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' },
|
||||
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' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "phone-numbers",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -230,7 +335,14 @@ export class PhoneTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -245,6 +357,13 @@ export class PhoneTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "phone-numbers",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -255,7 +374,14 @@ export class PhoneTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -268,7 +394,14 @@ export class PhoneTools {
|
||||
properties: {
|
||||
callerIdId: { type: 'string', description: 'Caller ID record 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']
|
||||
}
|
||||
@ -280,7 +413,14 @@ export class PhoneTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
|
||||
@ -184,7 +184,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
description: { type: 'string', description: 'Product description' },
|
||||
image: { type: 'string', description: 'Product image URL' },
|
||||
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']
|
||||
}
|
||||
@ -201,7 +208,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
search: { type: 'string', description: 'Search term for product names' },
|
||||
storeId: { type: 'string', description: 'Filter by store ID' },
|
||||
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: []
|
||||
}
|
||||
@ -213,7 +227,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -234,7 +255,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
},
|
||||
description: { type: 'string', description: 'Product description' },
|
||||
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']
|
||||
}
|
||||
@ -246,7 +274,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -269,7 +304,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
||||
amount: { type: 'number', description: 'Price amount in cents' },
|
||||
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']
|
||||
}
|
||||
@ -283,7 +325,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
productId: { type: 'string', description: 'Product ID to list prices for' },
|
||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||
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']
|
||||
}
|
||||
@ -299,7 +348,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||
limit: { type: 'number', description: 'Maximum number of items to return' },
|
||||
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: []
|
||||
}
|
||||
@ -322,6 +378,13 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
|
||||
title: { type: 'string', description: 'SEO title' },
|
||||
description: { type: 'string', description: 'SEO description' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "products",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
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)' },
|
||||
limit: { type: 'number', description: 'Maximum number of collections to return' },
|
||||
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: []
|
||||
}
|
||||
|
||||
@ -19,7 +19,14 @@ export class ReportingTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -36,7 +43,14 @@ export class ReportingTools {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
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']
|
||||
}
|
||||
@ -53,7 +67,14 @@ export class ReportingTools {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
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']
|
||||
}
|
||||
@ -70,7 +91,14 @@ export class ReportingTools {
|
||||
pipelineId: { type: 'string', description: 'Filter by pipeline ID' },
|
||||
startDate: { type: 'string', description: 'Start 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']
|
||||
}
|
||||
@ -85,7 +113,14 @@ export class ReportingTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -98,7 +133,14 @@ export class ReportingTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -114,7 +156,14 @@ export class ReportingTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
funnelId: { type: 'string', description: 'Filter by funnel ID' },
|
||||
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']
|
||||
}
|
||||
@ -130,7 +179,14 @@ export class ReportingTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
platform: { type: 'string', enum: ['google', 'facebook', 'all'], description: 'Ad platform' },
|
||||
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']
|
||||
}
|
||||
@ -146,7 +202,14 @@ export class ReportingTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
userId: { type: 'string', description: 'Filter by user ID' },
|
||||
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']
|
||||
}
|
||||
@ -164,6 +227,13 @@ export class ReportingTools {
|
||||
startDate: { type: 'string', description: 'Start 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' },
|
||||
startDate: { type: 'string', description: 'Start 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']
|
||||
}
|
||||
@ -193,7 +270,14 @@ export class ReportingTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
startDate: { type: 'string', description: 'Start 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']
|
||||
}
|
||||
|
||||
@ -26,6 +26,13 @@ export class ReputationTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "reputation",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -35,7 +42,14 @@ export class ReputationTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -48,7 +62,14 @@ export class ReputationTools {
|
||||
properties: {
|
||||
reviewId: { type: 'string', description: 'Review 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']
|
||||
}
|
||||
@ -61,7 +82,14 @@ export class ReputationTools {
|
||||
properties: {
|
||||
reviewId: { type: 'string', description: 'Review 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']
|
||||
}
|
||||
@ -73,7 +101,14 @@ export class ReputationTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -91,6 +126,13 @@ export class ReputationTools {
|
||||
startDate: { type: 'string', description: 'Start 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' },
|
||||
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to request review on' },
|
||||
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']
|
||||
}
|
||||
@ -122,6 +171,13 @@ export class ReputationTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "reputation",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -134,6 +190,13 @@ export class ReputationTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "reputation",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -144,6 +207,13 @@ export class ReputationTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "reputation",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -153,7 +223,14 @@ export class ReputationTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -168,6 +245,13 @@ export class ReputationTools {
|
||||
properties: {
|
||||
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' },
|
||||
yelpLink: { type: 'string', description: 'Custom Yelp review link' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "reputation",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -193,6 +284,13 @@ export class ReputationTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "reputation",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -36,6 +36,13 @@ export class SaasTools {
|
||||
isActive: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by active status'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "saas",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId']
|
||||
@ -54,6 +61,13 @@ export class SaasTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID to retrieve'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "saas",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId', 'locationId']
|
||||
@ -81,6 +95,13 @@ export class SaasTools {
|
||||
type: 'string',
|
||||
enum: ['active', 'paused', 'cancelled'],
|
||||
description: 'Subscription status'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "saas",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId', 'locationId']
|
||||
@ -103,6 +124,13 @@ export class SaasTools {
|
||||
paused: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to pause (true) or unpause (false)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "saas",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId', 'locationId', 'paused']
|
||||
@ -125,6 +153,13 @@ export class SaasTools {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to enable (true) or disable (false) SaaS'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "saas",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId', 'locationId', 'enabled']
|
||||
@ -151,6 +186,13 @@ export class SaasTools {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether rebilling is enabled'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "saas",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId']
|
||||
|
||||
@ -20,6 +20,13 @@ export class SmartListsTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "smartlists",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -29,7 +36,14 @@ export class SmartListsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -50,6 +64,13 @@ export class SmartListsTools {
|
||||
field: { type: 'string', description: 'Field to filter on' },
|
||||
operator: { type: 'string', description: 'Comparison operator (equals, contains, etc.)' },
|
||||
value: { type: 'string', description: 'Filter value' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "smartlists",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
description: 'Filter conditions'
|
||||
@ -69,7 +90,14 @@ export class SmartListsTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
name: { type: 'string', description: 'Smart list name' },
|
||||
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']
|
||||
}
|
||||
@ -81,7 +109,14 @@ export class SmartListsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -95,7 +130,14 @@ export class SmartListsTools {
|
||||
smartListId: { type: 'string', description: 'Smart List ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -107,7 +149,14 @@ export class SmartListsTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -120,7 +169,14 @@ export class SmartListsTools {
|
||||
properties: {
|
||||
smartListId: { type: 'string', description: 'Smart List ID to duplicate' },
|
||||
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']
|
||||
}
|
||||
|
||||
@ -27,6 +27,13 @@ export class SnapshotsTools {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of snapshots to return'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "snapshots",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId']
|
||||
@ -45,6 +52,13 @@ export class SnapshotsTools {
|
||||
companyId: {
|
||||
type: 'string',
|
||||
description: 'Company/Agency ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "snapshots",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['snapshotId', 'companyId']
|
||||
@ -71,6 +85,13 @@ export class SnapshotsTools {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of the snapshot'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "snapshots",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['companyId', 'locationId', 'name']
|
||||
@ -93,6 +114,13 @@ export class SnapshotsTools {
|
||||
pushId: {
|
||||
type: 'string',
|
||||
description: 'The push operation ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "snapshots",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['snapshotId', 'companyId']
|
||||
@ -115,6 +143,13 @@ export class SnapshotsTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Target location ID'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "snapshots",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['snapshotId', 'companyId', 'locationId']
|
||||
@ -150,7 +185,14 @@ export class SnapshotsTools {
|
||||
surveys: { type: 'boolean', description: 'Override existing surveys' },
|
||||
calendars: { type: 'boolean', description: 'Override existing calendars' },
|
||||
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'
|
||||
}
|
||||
|
||||
@ -56,6 +56,13 @@ export class SocialMediaTools {
|
||||
type: 'string',
|
||||
enum: ['post', 'story', 'reel'],
|
||||
description: 'Type of post to search for'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['fromDate', 'toDate']
|
||||
@ -80,7 +87,14 @@ export class SocialMediaTools {
|
||||
properties: {
|
||||
url: { type: 'string', description: 'Media URL' },
|
||||
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']
|
||||
},
|
||||
@ -116,7 +130,14 @@ export class SocialMediaTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -139,6 +160,13 @@ export class SocialMediaTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Updated tag IDs'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['postId']
|
||||
@ -150,7 +178,14 @@ export class SocialMediaTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -166,6 +201,13 @@ export class SocialMediaTools {
|
||||
items: { type: 'string' },
|
||||
description: 'Array of post IDs to delete',
|
||||
maxItems: 50
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "delete",
|
||||
complexity: "batch"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['postIds']
|
||||
@ -180,6 +222,13 @@ export class SocialMediaTools {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -190,7 +239,14 @@ export class SocialMediaTools {
|
||||
properties: {
|
||||
accountId: { type: 'string', description: 'Account ID to delete' },
|
||||
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']
|
||||
}
|
||||
@ -203,7 +259,14 @@ export class SocialMediaTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -219,6 +282,13 @@ export class SocialMediaTools {
|
||||
includeUsers: { type: 'boolean', description: 'Include user data' },
|
||||
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' },
|
||||
fileName: { type: 'string', description: 'CSV file name' },
|
||||
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']
|
||||
}
|
||||
@ -253,6 +330,13 @@ export class SocialMediaTools {
|
||||
limit: { type: 'number', description: 'Number to return', default: 10 },
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
categoryId: { type: 'string', description: 'Category ID' }
|
||||
categoryId: { type: 'string', description: 'Category ID' },
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['categoryId']
|
||||
}
|
||||
@ -276,6 +367,13 @@ export class SocialMediaTools {
|
||||
limit: { type: 'number', description: 'Number to return', default: 10 },
|
||||
skip: { type: 'number', description: 'Number to skip', default: 0 }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -288,6 +386,13 @@ export class SocialMediaTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tag IDs'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "social-media",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['tagIds']
|
||||
@ -308,7 +413,14 @@ export class SocialMediaTools {
|
||||
},
|
||||
userId: { type: 'string', description: 'User ID initiating OAuth' },
|
||||
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']
|
||||
}
|
||||
@ -324,7 +436,14 @@ export class SocialMediaTools {
|
||||
enum: ['google', 'facebook', 'instagram', 'linkedin', 'twitter', 'tiktok', 'tiktok-business'],
|
||||
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']
|
||||
}
|
||||
|
||||
@ -1068,7 +1068,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
items: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -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)' },
|
||||
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: {
|
||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||
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']
|
||||
}
|
||||
@ -1129,7 +1150,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
items: {
|
||||
type: 'object',
|
||||
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']
|
||||
}
|
||||
@ -1149,7 +1177,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -1170,7 +1205,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
properties: {
|
||||
street1: { type: 'string', description: 'Street address line 1' },
|
||||
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']
|
||||
},
|
||||
@ -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' },
|
||||
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
|
||||
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']
|
||||
}
|
||||
@ -1215,7 +1264,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -1228,7 +1284,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||
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']
|
||||
}
|
||||
@ -1241,7 +1304,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||
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']
|
||||
}
|
||||
@ -1254,7 +1324,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
|
||||
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']
|
||||
}
|
||||
@ -1277,7 +1354,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -1294,6 +1378,13 @@ These settings control your store's shipping origin and email notification prefe
|
||||
properties: {
|
||||
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',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -1315,7 +1413,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -1327,7 +1432,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -1349,7 +1461,14 @@ These settings control your store's shipping origin and email notification prefe
|
||||
street1: { type: 'string', description: 'Street address line 1' },
|
||||
city: { type: 'string', description: 'City' },
|
||||
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']
|
||||
}
|
||||
|
||||
@ -31,6 +31,13 @@ export class SurveyTools {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Filter surveys by type (e.g., "folder")'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "surveys",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
@ -69,6 +76,13 @@ export class SurveyTools {
|
||||
endAt: {
|
||||
type: 'string',
|
||||
description: 'End date for filtering submissions (YYYY-MM-DD format)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "surveys",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
|
||||
@ -21,6 +21,13 @@ export class TemplatesTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "templates",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -30,7 +37,14 @@ export class TemplatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -43,7 +57,14 @@ export class TemplatesTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -57,7 +78,14 @@ export class TemplatesTools {
|
||||
templateId: { type: 'string', description: 'SMS Template ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -69,7 +97,14 @@ export class TemplatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -84,6 +119,13 @@ export class TemplatesTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "templates",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -94,7 +136,14 @@ export class TemplatesTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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']
|
||||
}
|
||||
@ -106,7 +155,14 @@ export class TemplatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -123,6 +179,13 @@ export class TemplatesTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
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' },
|
||||
content: { type: 'string', description: 'Post content' },
|
||||
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']
|
||||
}
|
||||
@ -147,7 +217,14 @@ export class TemplatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -163,6 +240,13 @@ export class TemplatesTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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' },
|
||||
category: { type: 'string', enum: ['marketing', 'utility', 'authentication'], description: 'Template category' },
|
||||
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']
|
||||
}
|
||||
@ -187,7 +278,14 @@ export class TemplatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -203,6 +301,13 @@ export class TemplatesTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
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' },
|
||||
shortcut: { type: 'string', description: 'Keyboard shortcut (e.g., /thanks)' },
|
||||
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']
|
||||
}
|
||||
@ -230,7 +342,14 @@ export class TemplatesTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
name: { type: 'string', description: 'Snippet name' },
|
||||
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']
|
||||
}
|
||||
@ -242,7 +361,14 @@ export class TemplatesTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
|
||||
@ -22,6 +22,13 @@ export class TriggersTools {
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
offset: { type: 'number', description: 'Pagination offset' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "triggers",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -31,7 +38,14 @@ export class TriggersTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -63,6 +77,13 @@ export class TriggersTools {
|
||||
field: { type: 'string', description: 'Field to filter' },
|
||||
operator: { type: 'string', description: 'Comparison operator' },
|
||||
value: { type: 'string', description: 'Filter value' }
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "triggers",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
description: 'Conditions that must be met'
|
||||
@ -93,7 +114,14 @@ export class TriggersTools {
|
||||
name: { type: 'string', description: 'Trigger name' },
|
||||
filters: { type: 'array', description: 'Filter conditions' },
|
||||
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']
|
||||
}
|
||||
@ -105,7 +133,14 @@ export class TriggersTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -117,7 +152,14 @@ export class TriggersTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -129,7 +171,14 @@ export class TriggersTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -142,6 +191,13 @@ export class TriggersTools {
|
||||
properties: {
|
||||
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' },
|
||||
endDate: { type: 'string', description: 'End date' },
|
||||
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']
|
||||
}
|
||||
@ -169,7 +232,14 @@ export class TriggersTools {
|
||||
properties: {
|
||||
triggerId: { type: 'string', description: 'Trigger 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']
|
||||
}
|
||||
@ -182,7 +252,14 @@ export class TriggersTools {
|
||||
properties: {
|
||||
triggerId: { type: 'string', description: 'Trigger ID to duplicate' },
|
||||
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']
|
||||
}
|
||||
|
||||
@ -50,6 +50,13 @@ export class UsersTools {
|
||||
description: 'Sort direction'
|
||||
}
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "users",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -65,6 +72,13 @@ export class UsersTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "users",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['userId']
|
||||
@ -117,6 +131,13 @@ export class UsersTools {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Scopes only assigned to this user'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "users",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['firstName', 'lastName', 'email']
|
||||
@ -163,6 +184,13 @@ export class UsersTools {
|
||||
permissions: {
|
||||
type: 'object',
|
||||
description: 'User permissions object'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "users",
|
||||
access: "write",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['userId']
|
||||
@ -181,6 +209,13 @@ export class UsersTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
description: 'Location ID (uses default if not provided)'
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "users",
|
||||
access: "delete",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['userId']
|
||||
|
||||
@ -18,6 +18,13 @@ export class WebhooksTools {
|
||||
properties: {
|
||||
locationId: { type: 'string', description: 'Location ID' }
|
||||
}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "webhooks",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -27,7 +34,14 @@ export class WebhooksTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -46,7 +60,14 @@ export class WebhooksTools {
|
||||
items: { type: 'string' },
|
||||
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']
|
||||
}
|
||||
@ -66,7 +87,14 @@ export class WebhooksTools {
|
||||
items: { type: 'string' },
|
||||
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']
|
||||
}
|
||||
@ -78,7 +106,14 @@ export class WebhooksTools {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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']
|
||||
}
|
||||
@ -89,6 +124,13 @@ export class WebhooksTools {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
_meta: {
|
||||
labels: {
|
||||
category: "webhooks",
|
||||
access: "read",
|
||||
complexity: "simple"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -101,7 +143,14 @@ export class WebhooksTools {
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
limit: { type: 'number', description: 'Max results' },
|
||||
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']
|
||||
}
|
||||
@ -114,7 +163,14 @@ export class WebhooksTools {
|
||||
properties: {
|
||||
webhookId: { type: 'string', description: 'Webhook ID' },
|
||||
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']
|
||||
}
|
||||
@ -127,7 +183,14 @@ export class WebhooksTools {
|
||||
properties: {
|
||||
webhookId: { type: 'string', description: 'Webhook 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']
|
||||
}
|
||||
|
||||
@ -18,6 +18,13 @@ export class WorkflowTools {
|
||||
locationId: {
|
||||
type: 'string',
|
||||
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
|
||||
|
||||
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