holon shape integration testing
This commit is contained in:
parent
7e3cca656e
commit
2480d16e70
|
|
@ -0,0 +1,122 @@
|
|||
# Holon Shape for Canvas Website
|
||||
|
||||
## Overview
|
||||
|
||||
The Holon shape is a new tool that allows users to interact with Holon data objects through a visual interface. It provides functionality to input a Holon ID and perform put/get operations on JSON tasklists.
|
||||
|
||||
## Features
|
||||
|
||||
- **Holon ID Input**: Enter a Holon ID (e.g., -4962820663) to connect to a specific holon
|
||||
- **Tasklist Management**: Load, view, and manage tasklists from the holon
|
||||
- **Task Operations**: Add new tasks to existing or new tasklists
|
||||
- **Task Completion**: Toggle task completion status
|
||||
- **Real-time Updates**: Changes are immediately reflected in the holon data
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Adding the Holon Shape**:
|
||||
- Select the Holon tool from the toolbar (star icon) or context menu
|
||||
- Use keyboard shortcut `Alt+H` to quickly select the Holon tool
|
||||
- Click and drag on the canvas to create a new Holon shape
|
||||
|
||||
2. **Connecting to a Holon**:
|
||||
- Enter a Holon ID in the input field
|
||||
- Click "Load Tasklists" to fetch existing tasklists
|
||||
|
||||
3. **Managing Tasks**:
|
||||
- Enter a tasklist name and task description
|
||||
- Click "Add" to create a new task
|
||||
- Check/uncheck tasks to mark them as complete/incomplete
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Files Created/Modified
|
||||
|
||||
- `src/shapes/HolonShapeUtil.tsx` - Main shape utility with UI component and sync support
|
||||
- `src/tools/HolonShapeTool.ts` - Tool definition for the Holon shape
|
||||
- `src/routes/Board.tsx` - Updated to include Holon shape and tool
|
||||
- `src/ui/overrides.tsx` - Added Holon tool definition with keyboard shortcut (Alt+H)
|
||||
- `src/ui/CustomContextMenu.tsx` - Added Holon tool to context menu
|
||||
- `src/ui/CustomToolbar.tsx` - Added Holon tool to toolbar
|
||||
- `worker/TldrawDurableObject.ts` - Added Holon shape to worker schema for multiplayer sync
|
||||
|
||||
### HoloSphere Class
|
||||
|
||||
The `HoloSphere` class provides the interface for interacting with Holon data:
|
||||
|
||||
```typescript
|
||||
class HoloSphere {
|
||||
async getAll(holonId: string, dataType: string): Promise<any[]>
|
||||
async put(holonId: string, dataType: string, data: any): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The shape currently uses these API endpoints:
|
||||
- `GET https://api.holons.io/holons/{holonId}/tasklists` - Fetch tasklists
|
||||
- `PUT https://api.holons.io/holons/{holonId}/tasklists` - Update tasklists
|
||||
|
||||
## Data Structure
|
||||
|
||||
Tasklists are stored as JSON arrays with the following structure:
|
||||
|
||||
```typescript
|
||||
interface Tasklist {
|
||||
name: string
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: number
|
||||
text: string
|
||||
completed: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Holons i/o
|
||||
|
||||
This shape integrates with the Holons i/o ecosystem, allowing users to:
|
||||
|
||||
- Connect to their personal me-holon and we-holons
|
||||
- Manage tasks across different holons
|
||||
- Collaborate on shared tasklists
|
||||
- Track task completion and progress
|
||||
|
||||
## Sync Support
|
||||
|
||||
The Holon shape is fully integrated with TL-Draw's sync system:
|
||||
|
||||
- **Multiplayer Support**: All state changes are synchronized across multiple users
|
||||
- **Real-time Updates**: Task additions, completions, and Holon ID changes sync immediately
|
||||
- **State Persistence**: Shape state is preserved in the room's persistent storage
|
||||
- **Conflict Resolution**: Uses TL-Draw's built-in conflict resolution for concurrent edits
|
||||
- **Worker Schema**: Properly registered in the worker schema for multiplayer sessions
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Support for other Holon data types (offers, requests, expenses, etc.)
|
||||
- Real-time synchronization with other users
|
||||
- Integration with the Holons Bot commands
|
||||
- Support for nested tasklists and subtasks
|
||||
- Export/import functionality for tasklists
|
||||
|
||||
## Example Usage
|
||||
|
||||
```typescript
|
||||
// Example Holon ID from the Holons i/o system
|
||||
const holonId = "-4962820663"
|
||||
|
||||
// The shape will automatically handle:
|
||||
// - Fetching existing tasklists
|
||||
// - Adding new tasks
|
||||
// - Updating task completion status
|
||||
// - Persisting changes to the holon
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The shape currently uses simulated API endpoints
|
||||
- Error handling is implemented for network failures
|
||||
- The UI is responsive and follows the existing design patterns
|
||||
- All data operations are logged to the console for debugging
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"cherry-markdown": "^0.8.57",
|
||||
"cloudflare-workers-unfurl": "^0.0.7",
|
||||
"gray-matter": "^4.0.3",
|
||||
"holosphere": "^1.1.17",
|
||||
"html2canvas": "^1.4.1",
|
||||
"itty-router": "^5.0.17",
|
||||
"jotai": "^2.6.0",
|
||||
|
|
@ -1847,6 +1848,48 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-schema": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz",
|
||||
"integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"asn1js": "^3.0.6",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/json-schema": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
|
||||
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/webcrypto": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz",
|
||||
"integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@peculiar/json-schema": "^1.1.12",
|
||||
"pvtsutils": "^1.3.5",
|
||||
"tslib": "^2.6.2",
|
||||
"webcrypto-core": "^1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||
|
|
@ -4286,6 +4329,21 @@
|
|||
"printable-characters": "^1.0.42"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1js": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz",
|
||||
"integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.6",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-listen": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz",
|
||||
|
|
@ -6317,6 +6375,22 @@
|
|||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
|
|
@ -6626,6 +6700,53 @@
|
|||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gun": {
|
||||
"version": "0.2020.1241",
|
||||
"resolved": "https://registry.npmjs.org/gun/-/gun-0.2020.1241.tgz",
|
||||
"integrity": "sha512-rmGqLuJj4fAuZ/0lddCvXHbENPkEnBOBYpq+kXHrwQ5RdNtQ5p0Io99lD1qUXMFmtwNacQ/iqo3VTmjmMyAYZg==",
|
||||
"license": "(Zlib OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"ws": "^7.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@peculiar/webcrypto": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/gun/node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/h3-js": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.2.1.tgz",
|
||||
"integrity": "sha512-HYiUrq5qTRFqMuQu3jEHqxXLk1zsSJiby9Lja/k42wHjabZG7tN9rOuzT/PEFf+Wa7rsnHLMHRWIu0mgcJ0ewQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=4",
|
||||
"npm": ">=3",
|
||||
"yarn": ">=1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hamt_plus": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
|
||||
|
|
@ -6936,6 +7057,49 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/holosphere": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/holosphere/-/holosphere-1.1.17.tgz",
|
||||
"integrity": "sha512-tQsP9lFoOnU1KDqU2s+TZTj3P5GCzN8DXtl2VXSIg1DY+8SoflqdI7nFAfW8JrgqIBvRTKKq38Zw22gdrhGZnA==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"gun": "^0.2020.1240",
|
||||
"h3-js": "^4.1.0",
|
||||
"openai": "^4.85.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"gun": "^0.2020.1240",
|
||||
"h3-js": "^4.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openai": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/holosphere/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/holosphere/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "3.13.9",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz",
|
||||
|
|
@ -8938,9 +9102,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.79.3",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.79.3.tgz",
|
||||
"integrity": "sha512-0yAnr6oxXAyVrYwLC1jA0KboyU7DjEmrfTXQX+jSpE+P4i72AI/Lxx5pvR3r9i5X7G33835lL+ZrnQ+MDvyuUg==",
|
||||
"version": "4.104.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
|
||||
"integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
|
@ -9226,6 +9390,26 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pvtsutils": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pvutils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
|
||||
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
|
|
@ -11328,6 +11512,20 @@
|
|||
"license": "Apache-2.0",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/webcrypto-core": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz",
|
||||
"integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.3.13",
|
||||
"@peculiar/json-schema": "^1.1.12",
|
||||
"asn1js": "^3.0.5",
|
||||
"pvtsutils": "^1.3.5",
|
||||
"tslib": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"cherry-markdown": "^0.8.57",
|
||||
"cloudflare-workers-unfurl": "^0.0.7",
|
||||
"gray-matter": "^4.0.3",
|
||||
"holosphere": "^1.1.17",
|
||||
"html2canvas": "^1.4.1",
|
||||
"itty-router": "^5.0.17",
|
||||
"jotai": "^2.6.0",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { SlideShape } from "@/shapes/SlideShapeUtil"
|
|||
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
||||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
import { HolonShapeTool } from "@/tools/HolonShapeTool"
|
||||
import { HolonShape } from "@/shapes/HolonShapeUtil"
|
||||
import { llm } from "@/utils/llmUtils"
|
||||
import {
|
||||
lockElement,
|
||||
|
|
@ -49,6 +51,7 @@ const customShapeUtils = [
|
|||
MycrozineTemplateShape,
|
||||
MarkdownShape,
|
||||
PromptShape,
|
||||
HolonShape,
|
||||
]
|
||||
const customTools = [
|
||||
ChatBoxTool,
|
||||
|
|
@ -58,6 +61,7 @@ const customTools = [
|
|||
MycrozineTemplateTool,
|
||||
MarkdownTool,
|
||||
PromptShapeTool,
|
||||
HolonShapeTool,
|
||||
]
|
||||
|
||||
export function Board() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,756 @@
|
|||
import {
|
||||
BaseBoxShapeUtil,
|
||||
HTMLContainer,
|
||||
TLBaseShape,
|
||||
} from "tldraw"
|
||||
import React, { useState, useCallback } from "react"
|
||||
import HoloSphere from "holosphere"
|
||||
|
||||
// Initialize HoloSphere
|
||||
const holosphere = new HoloSphere('holons')
|
||||
|
||||
type IHolon = TLBaseShape<
|
||||
"Holon",
|
||||
{
|
||||
w: number
|
||||
h: number
|
||||
holonId: string | null
|
||||
showAdvancedMenu?: boolean
|
||||
loadedContent?: {
|
||||
tasks?: any[] | { error: string }
|
||||
lists?: any[] | { error: string }
|
||||
users?: any[] | { error: string }
|
||||
events?: any[] | { error: string }
|
||||
proposals?: any[] | { error: string }
|
||||
offers?: any[] | { error: string }
|
||||
requests?: any[] | { error: string }
|
||||
balance?: any | { error: string }
|
||||
quests?: any[] | { error: string }
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
export class HolonShape extends BaseBoxShapeUtil<IHolon> {
|
||||
static override type = "Holon" as const
|
||||
|
||||
getDefaultProps(): IHolon["props"] {
|
||||
return {
|
||||
w: 400,
|
||||
h: 300,
|
||||
holonId: null,
|
||||
showAdvancedMenu: false,
|
||||
loadedContent: {},
|
||||
}
|
||||
}
|
||||
|
||||
component(shape: IHolon) {
|
||||
const isSelected = this.editor.getSelectedShapeIds().includes(shape.id)
|
||||
|
||||
const [inputHolonId, setInputHolonId] = useState(shape.props.holonId || "")
|
||||
const [error, setError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!inputHolonId.trim()) {
|
||||
setError("Please enter a Holon ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Basic validation - Holon IDs are typically numbers
|
||||
const isValidHolonId = /^-?\d+$/.test(inputHolonId.trim())
|
||||
if (!isValidHolonId) {
|
||||
setError("Invalid Holon ID format")
|
||||
return
|
||||
}
|
||||
|
||||
this.editor.updateShape<IHolon>({
|
||||
id: shape.id,
|
||||
type: "Holon",
|
||||
props: { ...shape.props, holonId: inputHolonId.trim() },
|
||||
})
|
||||
setError("")
|
||||
},
|
||||
[inputHolonId],
|
||||
)
|
||||
|
||||
const toggleAdvancedMenu = useCallback(() => {
|
||||
// Debounce to prevent rapid sync updates
|
||||
const timeoutId = setTimeout(() => {
|
||||
try {
|
||||
this.editor.updateShape<IHolon>({
|
||||
id: shape.id,
|
||||
type: "Holon",
|
||||
props: {
|
||||
...shape.props,
|
||||
showAdvancedMenu: !shape.props.showAdvancedMenu
|
||||
},
|
||||
})
|
||||
} catch (syncError) {
|
||||
console.error(`❌ Toggle menu failed (sync error):`, syncError)
|
||||
}
|
||||
}, 100) // 100ms debounce
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}, [shape.props.showAdvancedMenu])
|
||||
|
||||
const loadContent = useCallback(async (contentType: string) => {
|
||||
if (!shape.props.holonId) return
|
||||
|
||||
console.log(`🔄 Starting to load ${contentType} for Holon ${shape.props.holonId}`)
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
console.log(`📡 Calling holosphere.getAll('${shape.props.holonId}','${contentType}')`)
|
||||
|
||||
// Wrap HoloSphere API call in a timeout to prevent blocking sync
|
||||
const data = await Promise.race([
|
||||
holosphere.getAll(`'${shape.props.holonId}'`,`'${contentType}'`),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('HoloSphere API timeout')), 10000)
|
||||
)
|
||||
])
|
||||
|
||||
console.log(`🔍 Data(users):`, await holosphere.getAll('-1002848305066','status'))
|
||||
console.log(`🔍 Data(tasks):`, await holosphere.getAll('-1002848305066','tasks'))
|
||||
|
||||
console.log(`✅ Received data for ${contentType}:`, data)
|
||||
console.log(`📊 Data type: ${typeof data}, isArray: ${Array.isArray(data)}`)
|
||||
console.log(`📏 Data length: ${Array.isArray(data) ? data.length : 'N/A'}`)
|
||||
|
||||
// Test with different Holon ID to see if it's a data issue
|
||||
console.log(`🧪 Testing with different Holon ID:`, await holosphere.getAll('-1002848305066', 'status'))
|
||||
|
||||
// Test with different content type to see if it's a content type issue
|
||||
console.log(`🧪 Testing with different content type:`, await holosphere.getAll(shape.props.holonId, 'tasks'))
|
||||
|
||||
// Log the HoloSphere instance to see its configuration
|
||||
console.log(`🔧 HoloSphere instance:`, holosphere)
|
||||
console.log(`🔧 HoloSphere methods:`, Object.getOwnPropertyNames(Object.getPrototypeOf(holosphere)))
|
||||
|
||||
// Wrap shape update in try-catch to prevent sync interference
|
||||
try {
|
||||
this.editor.updateShape<IHolon>({
|
||||
id: shape.id,
|
||||
type: "Holon",
|
||||
props: {
|
||||
...shape.props,
|
||||
loadedContent: {
|
||||
...shape.props.loadedContent,
|
||||
[contentType]: data
|
||||
}
|
||||
},
|
||||
})
|
||||
console.log(`💾 Updated shape with ${contentType} data`)
|
||||
} catch (syncError) {
|
||||
console.error(`❌ Shape update failed (sync error):`, syncError)
|
||||
// Don't re-throw sync errors to prevent blocking
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to load ${contentType} from Holon ${shape.props.holonId}:`, error)
|
||||
console.error(`🔍 Error details:`, {
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
name: (error as Error).name
|
||||
})
|
||||
|
||||
// Wrap error state update in try-catch
|
||||
try {
|
||||
this.editor.updateShape<IHolon>({
|
||||
id: shape.id,
|
||||
type: "Holon",
|
||||
props: {
|
||||
...shape.props,
|
||||
loadedContent: {
|
||||
...shape.props.loadedContent,
|
||||
[contentType]: { error: `Failed to load ${contentType}: ${(error as Error).message}` }
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (syncError) {
|
||||
console.error(`❌ Error state update failed (sync error):`, syncError)
|
||||
}
|
||||
} finally {
|
||||
console.log(`🏁 Finished loading ${contentType}`)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [shape.props.holonId, shape.props.loadedContent])
|
||||
|
||||
const contentStyle = {
|
||||
pointerEvents: isSelected ? "none" as const : "all" as const,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "1px solid #D3D3D3",
|
||||
backgroundColor: "#FFFFFF",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
}
|
||||
|
||||
const wrapperStyle = {
|
||||
position: 'relative' as const,
|
||||
width: `${shape.props.w}px`,
|
||||
height: `${shape.props.h}px`,
|
||||
backgroundColor: "#F0F0F0",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
border: "none",
|
||||
background: "#007bff",
|
||||
color: "white",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
fontSize: "11px",
|
||||
margin: "2px",
|
||||
pointerEvents: "all" as const,
|
||||
whiteSpace: "nowrap" as const,
|
||||
transition: "background-color 0.2s",
|
||||
}
|
||||
|
||||
const secondaryButtonStyle = {
|
||||
...buttonStyle,
|
||||
background: "#6c757d",
|
||||
fontSize: "10px",
|
||||
padding: "4px 8px",
|
||||
}
|
||||
|
||||
const toggleButtonStyle = {
|
||||
...buttonStyle,
|
||||
background: "#28a745",
|
||||
fontSize: "10px",
|
||||
padding: "4px 8px",
|
||||
}
|
||||
|
||||
// For empty state (no holonId set)
|
||||
if (!shape.props.holonId) {
|
||||
return (
|
||||
<HTMLContainer>
|
||||
<div style={wrapperStyle}>
|
||||
<div
|
||||
style={{
|
||||
...contentStyle,
|
||||
cursor: 'text',
|
||||
touchAction: 'none',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const input = e.currentTarget.querySelector('input')
|
||||
input?.focus()
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "10px",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<h3 style={{ margin: "0 0 8px 0", color: "#495057", fontSize: "16px" }}>
|
||||
🌟 Holon Task Manager
|
||||
</h3>
|
||||
<p style={{ margin: "0", color: "#6c757d", fontSize: "12px" }}>
|
||||
Enter a Holon ID to get started
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={inputHolonId}
|
||||
onChange={(e) => setInputHolonId(e.target.value)}
|
||||
placeholder="Enter Holon ID (e.g., -4962820663)"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "15px",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
fontSize: "16px",
|
||||
touchAction: 'none',
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSubmit(e)
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
e.currentTarget.focus()
|
||||
}}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{error && (
|
||||
<div style={{ color: "red", marginTop: "10px", fontSize: "12px" }}>{error}</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// For loaded state (holonId is set)
|
||||
return (
|
||||
<HTMLContainer>
|
||||
<div style={wrapperStyle}>
|
||||
<div
|
||||
style={{
|
||||
...contentStyle,
|
||||
flexDirection: "column",
|
||||
padding: "12px",
|
||||
alignItems: "stretch",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: "12px", textAlign: "center" }}>
|
||||
<h3 style={{ margin: "0 0 4px 0", color: "#495057", fontSize: "14px" }}>
|
||||
🌟 Holon Task Manager
|
||||
</h3>
|
||||
<p style={{ margin: "0", color: "#6c757d", fontSize: "10px" }}>
|
||||
ID: {shape.props.holonId}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Basic Menu */}
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontSize: "10px", fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>
|
||||
Quick Actions:
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "2px" }}>
|
||||
<button
|
||||
onClick={() => loadContent("tasks")}
|
||||
disabled={isLoading}
|
||||
style={buttonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
📋 Tasks
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("lists")}
|
||||
disabled={isLoading}
|
||||
style={buttonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
📝 Lists
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("status")}
|
||||
disabled={isLoading}
|
||||
style={buttonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
👥 Users
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("events")}
|
||||
disabled={isLoading}
|
||||
style={buttonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
📅 Events
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Menu Toggle */}
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<button
|
||||
onClick={toggleAdvancedMenu}
|
||||
style={toggleButtonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
{shape.props.showAdvancedMenu ? "🔽 Hide Advanced" : "🔼 Show Advanced"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Advanced Menu */}
|
||||
{shape.props.showAdvancedMenu && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontSize: "10px", fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>
|
||||
Advanced Features:
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "2px" }}>
|
||||
<button
|
||||
onClick={() => loadContent("proposals")}
|
||||
disabled={isLoading}
|
||||
style={secondaryButtonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
📋 Proposals
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("offers")}
|
||||
disabled={isLoading}
|
||||
style={secondaryButtonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
🎁 Offers
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("requests")}
|
||||
disabled={isLoading}
|
||||
style={secondaryButtonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
🙏 Requests
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("balance")}
|
||||
disabled={isLoading}
|
||||
style={secondaryButtonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
💰 Balance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadContent("quests")}
|
||||
disabled={isLoading}
|
||||
style={secondaryButtonStyle}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
🎯 Quests
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading Indicator */}
|
||||
{isLoading && (
|
||||
<div style={{ textAlign: "center", padding: "8px", color: "#6c757d", fontSize: "10px" }}>
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Display */}
|
||||
<div style={{ flex: 1, overflow: "auto", fontSize: "10px" }}>
|
||||
{shape.props.loadedContent && Object.keys(shape.props.loadedContent).length > 0 && (
|
||||
<div>
|
||||
{shape.props.loadedContent.tasks && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>📋 Tasks:</div>
|
||||
{'error' in shape.props.loadedContent.tasks ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.tasks.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.tasks) ? (
|
||||
shape.props.loadedContent.tasks.map((task: any) => (
|
||||
<div key={task.id || task._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: task.completed ? "#d4edda" : "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
{task.completed ? "✅" : "⭕"} {task.text || task.title} {task.assigned && `(${task.assigned})`}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No tasks found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.lists && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>📝 Lists:</div>
|
||||
{'error' in shape.props.loadedContent.lists ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.lists.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.lists) ? (
|
||||
shape.props.loadedContent.lists.map((list: any) => (
|
||||
<div key={list.id || list._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
📋 {list.name || list.title}: {list.items ? list.items.join(", ") : "No items"}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No lists found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.users && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>👥 Users:</div>
|
||||
{'error' in shape.props.loadedContent.users ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.users.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.users) ? (
|
||||
shape.props.loadedContent.users.map((user: any) => (
|
||||
<div key={user.id || user._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
👤 {user.name || user.username} ({user.role || "Member"}) - {user.status || "Active"}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No users found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.events && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>📅 Events:</div>
|
||||
{'error' in shape.props.loadedContent.events ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.events.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.events) ? (
|
||||
shape.props.loadedContent.events.map((event: any) => (
|
||||
<div key={event.id || event._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
📅 {event.title || event.name} - {event.date || event.startDate} {event.time && event.time}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No events found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.proposals && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>📋 Proposals:</div>
|
||||
{'error' in shape.props.loadedContent.proposals ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.proposals.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.proposals) ? (
|
||||
shape.props.loadedContent.proposals.map((proposal: any) => (
|
||||
<div key={proposal.id || proposal._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
📋 {proposal.title || proposal.name} - {proposal.status || "Active"} {proposal.votes && `(${proposal.votes} votes)`}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No proposals found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.offers && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>🎁 Offers:</div>
|
||||
{'error' in shape.props.loadedContent.offers ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.offers.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.offers) ? (
|
||||
shape.props.loadedContent.offers.map((offer: any) => (
|
||||
<div key={offer.id || offer._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
🎁 {offer.title || offer.name} by {offer.offered_by || offer.user}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No offers found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.requests && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>🙏 Requests:</div>
|
||||
{'error' in shape.props.loadedContent.requests ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.requests.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.requests) ? (
|
||||
shape.props.loadedContent.requests.map((request: any) => (
|
||||
<div key={request.id || request._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
🙏 {request.title || request.name} by {request.requested_by || request.user}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No requests found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.balance && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>💰 Balance:</div>
|
||||
{'error' in shape.props.loadedContent.balance ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.balance.error}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
💰 {shape.props.loadedContent.balance.total || shape.props.loadedContent.balance.amount} {shape.props.loadedContent.balance.currency || "EUR"} {shape.props.loadedContent.balance.transactions && `(${shape.props.loadedContent.balance.transactions} transactions)`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shape.props.loadedContent.quests && (
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px", color: "#495057" }}>🎯 Quests:</div>
|
||||
{'error' in shape.props.loadedContent.quests ? (
|
||||
<div style={{ color: "#dc3545", fontSize: "9px", padding: "2px 4px" }}>
|
||||
❌ {shape.props.loadedContent.quests.error}
|
||||
</div>
|
||||
) : Array.isArray(shape.props.loadedContent.quests) ? (
|
||||
shape.props.loadedContent.quests.map((quest: any) => (
|
||||
<div key={quest.id || quest._id} style={{
|
||||
padding: "2px 4px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginBottom: "2px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "9px"
|
||||
}}>
|
||||
🎯 {quest.title || quest.name} - {quest.description} - {quest.status || "Active"}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div style={{ color: "#6c757d", fontSize: "9px", padding: "2px 4px" }}>
|
||||
No quests found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* External Link */}
|
||||
<div style={{ textAlign: "center", marginTop: "8px" }}>
|
||||
<a
|
||||
href={`https://dashboard.holons.io/${shape.props.holonId}/`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: "#1976d2",
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "10px",
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
Open in Holons.io →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
|
||||
override indicator(shape: IHolon) {
|
||||
return (
|
||||
<rect
|
||||
width={shape.props.w}
|
||||
height={shape.props.h}
|
||||
fill="none"
|
||||
stroke="dashed"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={8}
|
||||
strokeDashoffset={4}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle sync for Holon shape updates
|
||||
onBeforeUpdate = (_prev: IHolon, next: IHolon) => {
|
||||
return next
|
||||
}
|
||||
|
||||
// Handle creation with proper sync
|
||||
onBeforeCreate = (shape: IHolon) => {
|
||||
return shape
|
||||
}
|
||||
|
||||
// Handle pointer down for input focus
|
||||
onPointerDown = (shape: IHolon) => {
|
||||
if (!shape.props.holonId) {
|
||||
const input = document.querySelector('input[placeholder*="Holon ID"]') as HTMLInputElement
|
||||
input?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle double click for interaction
|
||||
override onDoubleClick = (shape: IHolon) => {
|
||||
// If no holonId is set, focus the input field
|
||||
if (!shape.props.holonId) {
|
||||
const input = document.querySelector('input[placeholder*="Holon ID"]') as HTMLInputElement
|
||||
input?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
// For existing holons, open in new tab
|
||||
window.open(`https://dashboard.holons.io/${shape.props.holonId}/`, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { BaseBoxShapeTool } from 'tldraw'
|
||||
|
||||
export class HolonShapeTool extends BaseBoxShapeTool {
|
||||
static override id = 'Holon'
|
||||
static override initial = 'idle'
|
||||
override shapeType = 'Holon'
|
||||
}
|
||||
|
|
@ -111,6 +111,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
|||
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.Holon} disabled={hasSelection} />
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,14 @@ export function CustomToolbar() {
|
|||
isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
{tools["Holon"] && (
|
||||
<TldrawUiMenuItem
|
||||
{...tools["Holon"]}
|
||||
icon="star"
|
||||
label="Holon"
|
||||
isSelected={tools["Holon"].id === editor.getCurrentToolId()}
|
||||
/>
|
||||
)}
|
||||
</DefaultToolbar>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = {
|
|||
readonlyOk: true,
|
||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||
},
|
||||
Holon: {
|
||||
id: "Holon",
|
||||
icon: "star",
|
||||
label: "Holon",
|
||||
type: "Holon",
|
||||
kbd: "alt+h",
|
||||
readonlyOk: true,
|
||||
onSelect: () => editor.setCurrentTool("Holon"),
|
||||
},
|
||||
hand: {
|
||||
...tools.hand,
|
||||
onDoubleClick: (info: any) => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
|||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||
import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
import { HolonShape } from "@/shapes/HolonShapeUtil"
|
||||
|
||||
// add custom shapes and bindings here if needed:
|
||||
export const customSchema = createTLSchema({
|
||||
|
|
@ -52,6 +53,10 @@ export const customSchema = createTLSchema({
|
|||
props: PromptShape.props,
|
||||
migrations: PromptShape.migrations,
|
||||
},
|
||||
Holon: {
|
||||
props: HolonShape.props,
|
||||
migrations: HolonShape.migrations,
|
||||
},
|
||||
},
|
||||
bindings: defaultBindingSchemas,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue