holon shape integration testing

This commit is contained in:
Jeff Emmett 2025-07-29 13:25:31 -04:00
parent 7e3cca656e
commit 2480d16e70
10 changed files with 1114 additions and 3 deletions

122
HOLON_SHAPE_README.md Normal file
View File

@ -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

204
package-lock.json generated
View File

@ -25,6 +25,7 @@
"cherry-markdown": "^0.8.57", "cherry-markdown": "^0.8.57",
"cloudflare-workers-unfurl": "^0.0.7", "cloudflare-workers-unfurl": "^0.0.7",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"holosphere": "^1.1.17",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"itty-router": "^5.0.17", "itty-router": "^5.0.17",
"jotai": "^2.6.0", "jotai": "^2.6.0",
@ -1847,6 +1848,48 @@
"node": ">=8.0.0" "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": { "node_modules/@radix-ui/number": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
@ -4286,6 +4329,21 @@
"printable-characters": "^1.0.42" "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": { "node_modules/async-listen": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz", "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz",
@ -6317,6 +6375,22 @@
"license": "MIT", "license": "MIT",
"optional": true "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": { "node_modules/fastq": {
"version": "1.18.0", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
@ -6626,6 +6700,53 @@
"node": ">=6.0" "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": { "node_modules/hamt_plus": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
@ -6936,6 +7057,49 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/hotkeys-js": {
"version": "3.13.9", "version": "3.13.9",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz", "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz",
@ -8938,9 +9102,9 @@
} }
}, },
"node_modules/openai": { "node_modules/openai": {
"version": "4.79.3", "version": "4.104.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.79.3.tgz", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
"integrity": "sha512-0yAnr6oxXAyVrYwLC1jA0KboyU7DjEmrfTXQX+jSpE+P4i72AI/Lxx5pvR3r9i5X7G33835lL+ZrnQ+MDvyuUg==", "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
@ -9226,6 +9390,26 @@
"node": ">=6" "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": { "node_modules/querystringify": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@ -11328,6 +11512,20 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true "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": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",

View File

@ -32,6 +32,7 @@
"cherry-markdown": "^0.8.57", "cherry-markdown": "^0.8.57",
"cloudflare-workers-unfurl": "^0.0.7", "cloudflare-workers-unfurl": "^0.0.7",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"holosphere": "^1.1.17",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"itty-router": "^5.0.17", "itty-router": "^5.0.17",
"jotai": "^2.6.0", "jotai": "^2.6.0",

View File

@ -29,6 +29,8 @@ import { SlideShape } from "@/shapes/SlideShapeUtil"
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings" import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
import { PromptShapeTool } from "@/tools/PromptShapeTool" import { PromptShapeTool } from "@/tools/PromptShapeTool"
import { PromptShape } from "@/shapes/PromptShapeUtil" import { PromptShape } from "@/shapes/PromptShapeUtil"
import { HolonShapeTool } from "@/tools/HolonShapeTool"
import { HolonShape } from "@/shapes/HolonShapeUtil"
import { llm } from "@/utils/llmUtils" import { llm } from "@/utils/llmUtils"
import { import {
lockElement, lockElement,
@ -49,6 +51,7 @@ const customShapeUtils = [
MycrozineTemplateShape, MycrozineTemplateShape,
MarkdownShape, MarkdownShape,
PromptShape, PromptShape,
HolonShape,
] ]
const customTools = [ const customTools = [
ChatBoxTool, ChatBoxTool,
@ -58,6 +61,7 @@ const customTools = [
MycrozineTemplateTool, MycrozineTemplateTool,
MarkdownTool, MarkdownTool,
PromptShapeTool, PromptShapeTool,
HolonShapeTool,
] ]
export function Board() { export function Board() {

View File

@ -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')
}
}

View File

@ -0,0 +1,7 @@
import { BaseBoxShapeTool } from 'tldraw'
export class HolonShapeTool extends BaseBoxShapeTool {
static override id = 'Holon'
static override initial = 'idle'
override shapeType = 'Holon'
}

View File

@ -111,6 +111,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} /> <TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} /> <TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} /> <TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.Holon} disabled={hasSelection} />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>

View File

@ -168,6 +168,14 @@ export function CustomToolbar() {
isSelected={tools["Prompt"].id === editor.getCurrentToolId()} isSelected={tools["Prompt"].id === editor.getCurrentToolId()}
/> />
)} )}
{tools["Holon"] && (
<TldrawUiMenuItem
{...tools["Holon"]}
icon="star"
label="Holon"
isSelected={tools["Holon"].id === editor.getCurrentToolId()}
/>
)}
</DefaultToolbar> </DefaultToolbar>
</div> </div>
) )

View File

@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = {
readonlyOk: true, readonlyOk: true,
onSelect: () => editor.setCurrentTool("Prompt"), onSelect: () => editor.setCurrentTool("Prompt"),
}, },
Holon: {
id: "Holon",
icon: "star",
label: "Holon",
type: "Holon",
kbd: "alt+h",
readonlyOk: true,
onSelect: () => editor.setCurrentTool("Holon"),
},
hand: { hand: {
...tools.hand, ...tools.hand,
onDoubleClick: (info: any) => { onDoubleClick: (info: any) => {

View File

@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil" import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
import { SlideShape } from "@/shapes/SlideShapeUtil" import { SlideShape } from "@/shapes/SlideShapeUtil"
import { PromptShape } from "@/shapes/PromptShapeUtil" import { PromptShape } from "@/shapes/PromptShapeUtil"
import { HolonShape } from "@/shapes/HolonShapeUtil"
// add custom shapes and bindings here if needed: // add custom shapes and bindings here if needed:
export const customSchema = createTLSchema({ export const customSchema = createTLSchema({
@ -52,6 +53,10 @@ export const customSchema = createTLSchema({
props: PromptShape.props, props: PromptShape.props,
migrations: PromptShape.migrations, migrations: PromptShape.migrations,
}, },
Holon: {
props: HolonShape.props,
migrations: HolonShape.migrations,
},
}, },
bindings: defaultBindingSchemas, bindings: defaultBindingSchemas,
}) })