Daily backup: 2026-01-28
5
.env.browser-use.secret
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Browser Use MCP Environment Variables
|
||||||
|
# DO NOT COMMIT TO PUBLIC REPOS
|
||||||
|
|
||||||
|
BROWSER_USE_API_KEY=not_set
|
||||||
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Subproject commit 1af052405851b9d6d0f922591c70bbf4a5fd4ba7
|
|
||||||
19
HEARTBEAT.md
@ -1,3 +1,18 @@
|
|||||||
# HEARTBEAT.md
|
# HEARTBEAT.md — Active Task State
|
||||||
|
|
||||||
Keep this file empty unless you want a tiny checklist. Keep it small.
|
## Current Task
|
||||||
|
- **Project:** MCP Animation Framework (Remotion)
|
||||||
|
- **Last completed:** Dolly camera version with canvas viewport technique
|
||||||
|
- **Next step:** Get Jake's feedback on camera movement, iterate
|
||||||
|
- **Blockers:** none
|
||||||
|
|
||||||
|
## Recently Active Projects
|
||||||
|
- mcp-animation-framework (Remotion dolly camera)
|
||||||
|
- fortura-assets component library (60 native components)
|
||||||
|
- memory system improvements
|
||||||
|
|
||||||
|
## Quick Context
|
||||||
|
Building bulk animation generator for MCP marketing. Using Remotion with canvas viewport technique (dolly camera). Camera should zoom into typing area, follow text as typed, zoom out when done. Category-specific questions per software type.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Last updated: 2026-01-28 23:00 EST*
|
||||||
|
|||||||
479
MEMORY-MIGRATION-PLAN.md
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
# Clawdbot Memory System Migration Plan
|
||||||
|
|
||||||
|
**Created:** 2026-01-27
|
||||||
|
**Status:** Ready to Execute
|
||||||
|
**Risk Level:** Low (old system preserved, incremental migration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State Inventory
|
||||||
|
|
||||||
|
| Asset | Location | Size | Records |
|
||||||
|
|-------|----------|------|---------|
|
||||||
|
| Main SQLite | `~/.clawdbot/memory/main.sqlite` | 9.0 MB | 56 chunks |
|
||||||
|
| iMessage SQLite | `~/.clawdbot/memory/imessage.sqlite` | 8.1 MB | ~42 chunks |
|
||||||
|
| Markdown files | `~/.clawdbot/workspace/memory/*.md` | 17 files | ~60KB total |
|
||||||
|
| INDEX.json | `~/.clawdbot/workspace/memory/INDEX.json` | 7.1 KB | 6 categories, 20 nodes |
|
||||||
|
| Session transcripts | `~/.clawdbot/agents/*/sessions/*.jsonl` | 23 files | 5,593 lines |
|
||||||
|
| New memories table | `~/.clawdbot/memory/main.sqlite` | - | 36 records (migrated) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Phases
|
||||||
|
|
||||||
|
### Phase 0: Backup Everything (REQUIRED FIRST)
|
||||||
|
**Time:** 5 minutes
|
||||||
|
**Risk:** None
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create timestamped backup directory
|
||||||
|
BACKUP_DIR=~/.clawdbot/backups/pre-migration-$(date +%Y%m%d-%H%M%S)
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# Backup SQLite databases
|
||||||
|
cp ~/.clawdbot/memory/main.sqlite "$BACKUP_DIR/"
|
||||||
|
cp ~/.clawdbot/memory/imessage.sqlite "$BACKUP_DIR/"
|
||||||
|
|
||||||
|
# Backup markdown memory files
|
||||||
|
cp -r ~/.clawdbot/workspace/memory "$BACKUP_DIR/memory-markdown"
|
||||||
|
|
||||||
|
# Backup session transcripts
|
||||||
|
cp -r ~/.clawdbot/agents "$BACKUP_DIR/agents"
|
||||||
|
|
||||||
|
# Backup config
|
||||||
|
cp ~/.clawdbot/clawdbot.json "$BACKUP_DIR/"
|
||||||
|
|
||||||
|
# Verify backup
|
||||||
|
echo "Backup created at: $BACKUP_DIR"
|
||||||
|
ls -la "$BACKUP_DIR"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checkpoint:** Verify backup directory has all files before proceeding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: Complete Markdown Migration
|
||||||
|
**Time:** 15 minutes
|
||||||
|
**Risk:** Low (additive only)
|
||||||
|
|
||||||
|
We already migrated CRITICAL-REFERENCE.md, Genre Universe, and Remix Sniper. Now migrate the remaining files.
|
||||||
|
|
||||||
|
#### Files to Migrate:
|
||||||
|
|
||||||
|
| File | Content Type | Priority |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| `2026-01-14.md` | Daily log - GOG setup | Medium |
|
||||||
|
| `2026-01-15.md` | Daily log - agent-browser, Reonomy | High |
|
||||||
|
| `2026-01-25.md` | Security incident - Reed breach | High |
|
||||||
|
| `2026-01-26.md` | Daily log - Reonomy v13 | Medium |
|
||||||
|
| `2026-01-19-backup-system.md` | Backup system setup | Medium |
|
||||||
|
| `2026-01-19-cloud-backup.md` | Cloud backup config | Medium |
|
||||||
|
| `burton-method-research-intel.md` | Competitor research | High |
|
||||||
|
| `contacts-leaf-gc.md` | Contact info | Medium |
|
||||||
|
| `contacts-skivals-gc.md` | Contact info | Medium |
|
||||||
|
| `imessage-rules.md` | Security rules | High |
|
||||||
|
| `imessage-security-rules.md` | Security rules | High |
|
||||||
|
| `remi-self-healing.md` | Remix Sniper healing | Medium |
|
||||||
|
| `voice-ai-comparison-2026.md` | Research | Low |
|
||||||
|
| `accounts.md` | Accounts | Low |
|
||||||
|
|
||||||
|
#### Migration Script Extension:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add to migrate-memories.py
|
||||||
|
|
||||||
|
def migrate_daily_logs(db):
|
||||||
|
"""Migrate daily log files."""
|
||||||
|
memories = []
|
||||||
|
|
||||||
|
# 2026-01-14 - GOG setup
|
||||||
|
memories.append((
|
||||||
|
"GOG (Google Workspace CLI) configured with 3 accounts: jake@burtonmethod.com, jake@localbosses.org, jakeshore98@gmail.com",
|
||||||
|
"fact", None, "2026-01-14.md"
|
||||||
|
))
|
||||||
|
|
||||||
|
# 2026-01-15 - agent-browser
|
||||||
|
memories.append((
|
||||||
|
"agent-browser is Vercel Labs headless browser CLI with ref-based navigation, semantic locators, state persistence. Commands: open, snapshot -i, click @ref, type @ref 'text'",
|
||||||
|
"fact", None, "2026-01-15.md"
|
||||||
|
))
|
||||||
|
memories.append((
|
||||||
|
"Reonomy scraper attempted with agent-browser. URL pattern discovered: ownership tab in search filters allows searching by Owner Contact Information.",
|
||||||
|
"fact", None, "2026-01-15.md"
|
||||||
|
))
|
||||||
|
|
||||||
|
# 2026-01-25 - Security incident
|
||||||
|
memories.append((
|
||||||
|
"SECURITY INCIDENT 2026-01-25: Reed breach. Contact memory poisoning. Password leaked. Rules updated. Rotate all passwords after breach.",
|
||||||
|
"security", None, "2026-01-25.md"
|
||||||
|
))
|
||||||
|
|
||||||
|
# ... continue for all files
|
||||||
|
|
||||||
|
for content, mtype, guild_id, source in memories:
|
||||||
|
insert_memory(db, content, mtype, source, guild_id)
|
||||||
|
|
||||||
|
return len(memories)
|
||||||
|
|
||||||
|
def migrate_security_rules(db):
|
||||||
|
"""Migrate iMessage security rules."""
|
||||||
|
memories = [
|
||||||
|
("iMessage password gating: Password JAJAJA2026 required. Mention gating (Buba). Never reveal password in any context.", "security", None),
|
||||||
|
("iMessage trust chain: Only trust Jake (914-500-9208). Everyone else must verify with Jake first, then chat-only mode with password.", "security", None),
|
||||||
|
]
|
||||||
|
for content, mtype, guild_id in memories:
|
||||||
|
insert_memory(db, content, mtype, "imessage-security-rules.md", guild_id)
|
||||||
|
return len(memories)
|
||||||
|
|
||||||
|
def migrate_contacts(db):
|
||||||
|
"""Migrate contact information (non-sensitive parts only)."""
|
||||||
|
memories = [
|
||||||
|
("Contact: Leaf GC - group chat contact for Leaf-related communications", "relationship", None),
|
||||||
|
("Contact: Skivals GC - group chat contact for Skivals-related communications", "relationship", None),
|
||||||
|
]
|
||||||
|
for content, mtype, guild_id in memories:
|
||||||
|
insert_memory(db, content, mtype, "contacts.md", guild_id)
|
||||||
|
return len(memories)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checkpoint:** Run `python memory-retrieval.py stats` and verify count increased.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Migrate Existing Chunks (Vector Embeddings)
|
||||||
|
**Time:** 10 minutes
|
||||||
|
**Risk:** Low (copies data, doesn't delete)
|
||||||
|
|
||||||
|
The existing `chunks` table has 56 pre-embedded chunks. We should copy these to memories table to preserve the embeddings.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Copy chunks to memories (preserving embeddings)
|
||||||
|
INSERT INTO memories (
|
||||||
|
content,
|
||||||
|
embedding,
|
||||||
|
memory_type,
|
||||||
|
source,
|
||||||
|
source_file,
|
||||||
|
created_at,
|
||||||
|
confidence
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
text as content,
|
||||||
|
embedding,
|
||||||
|
'fact' as memory_type,
|
||||||
|
'chunks_migration' as source,
|
||||||
|
path as source_file,
|
||||||
|
COALESCE(updated_at, unixepoch()) as created_at,
|
||||||
|
1.0 as confidence
|
||||||
|
FROM chunks
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM memories m
|
||||||
|
WHERE m.source_file = chunks.path
|
||||||
|
AND m.source = 'chunks_migration'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checkpoint:** Verify with `SELECT COUNT(*) FROM memories WHERE source = 'chunks_migration'`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Session Transcript Indexing (Optional - Later)
|
||||||
|
**Time:** 30-60 minutes
|
||||||
|
**Risk:** Medium (large data volume)
|
||||||
|
|
||||||
|
Session transcripts contain conversation history. This is valuable but voluminous.
|
||||||
|
|
||||||
|
#### Strategy: Selective Indexing
|
||||||
|
|
||||||
|
Don't index every message. Index:
|
||||||
|
1. Messages where Clawdbot learned something (contains "I'll remember", "noted", "got it")
|
||||||
|
2. User corrections ("actually it's...", "no, the correct...")
|
||||||
|
3. Explicit requests ("remember that...", "don't forget...")
|
||||||
|
|
||||||
|
```python
|
||||||
|
def extract_memorable_from_sessions():
|
||||||
|
"""Extract memorable moments from session transcripts."""
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
|
||||||
|
session_files = glob.glob(os.path.expanduser(
|
||||||
|
"~/.clawdbot/agents/*/sessions/*.jsonl"
|
||||||
|
))
|
||||||
|
|
||||||
|
memorable_patterns = [
|
||||||
|
r"I'll remember",
|
||||||
|
r"I've noted",
|
||||||
|
r"remember that",
|
||||||
|
r"don't forget",
|
||||||
|
r"actually it's",
|
||||||
|
r"the correct",
|
||||||
|
r"important:",
|
||||||
|
r"key point:",
|
||||||
|
]
|
||||||
|
|
||||||
|
memories = []
|
||||||
|
for fpath in session_files:
|
||||||
|
with open(fpath) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
# Check if it matches memorable patterns
|
||||||
|
# Extract and store
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return memories
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation:** Skip this for now. The markdown files contain the curated important stuff. Sessions are backup/audit trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Wire Into Clawdbot Runtime
|
||||||
|
**Time:** 30-60 minutes
|
||||||
|
**Risk:** Medium (changes bot behavior)
|
||||||
|
|
||||||
|
This requires modifying Clawdbot's code to use the new memory system.
|
||||||
|
|
||||||
|
#### 4.1 Create Memory Interface Module
|
||||||
|
|
||||||
|
Location: `~/.clawdbot/workspace/memory_interface.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Memory interface for Clawdbot runtime.
|
||||||
|
Import this in your bot's message handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from memory_retrieval import (
|
||||||
|
search_memories,
|
||||||
|
add_memory,
|
||||||
|
get_recent_memories,
|
||||||
|
supersede_memory
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_for_message(message, guild_id, channel_id, user_id):
|
||||||
|
"""
|
||||||
|
Get relevant memory context for responding to a message.
|
||||||
|
Call this before generating a response.
|
||||||
|
"""
|
||||||
|
# Search for relevant memories
|
||||||
|
results = search_memories(
|
||||||
|
query=message,
|
||||||
|
guild_id=guild_id,
|
||||||
|
limit=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
# Fall back to recent memories for this guild
|
||||||
|
results = get_recent_memories(guild_id=guild_id, limit=3)
|
||||||
|
|
||||||
|
# Format for context injection
|
||||||
|
context_lines = []
|
||||||
|
for r in results:
|
||||||
|
context_lines.append(f"[Memory] {r['content']}")
|
||||||
|
|
||||||
|
return "\n".join(context_lines)
|
||||||
|
|
||||||
|
def should_remember(response_text):
|
||||||
|
"""
|
||||||
|
Check if the bot's response indicates something should be remembered.
|
||||||
|
"""
|
||||||
|
triggers = [
|
||||||
|
"i'll remember",
|
||||||
|
"i've noted",
|
||||||
|
"got it",
|
||||||
|
"noted",
|
||||||
|
"understood",
|
||||||
|
]
|
||||||
|
lower = response_text.lower()
|
||||||
|
return any(t in lower for t in triggers)
|
||||||
|
|
||||||
|
def extract_and_store(message, response, guild_id, channel_id, user_id):
|
||||||
|
"""
|
||||||
|
If the response indicates learning, extract and store the memory.
|
||||||
|
"""
|
||||||
|
if not should_remember(response):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# The message itself is what should be remembered
|
||||||
|
memory_id = add_memory(
|
||||||
|
content=message,
|
||||||
|
memory_type="fact",
|
||||||
|
guild_id=guild_id,
|
||||||
|
channel_id=channel_id,
|
||||||
|
user_id=user_id,
|
||||||
|
source="conversation"
|
||||||
|
)
|
||||||
|
|
||||||
|
return memory_id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Integration Points
|
||||||
|
|
||||||
|
In Clawdbot's message handler:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before generating response:
|
||||||
|
memory_context = get_context_for_message(
|
||||||
|
message=user_message,
|
||||||
|
guild_id=str(message.guild.id) if message.guild else None,
|
||||||
|
channel_id=str(message.channel.id),
|
||||||
|
user_id=str(message.author.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inject into prompt:
|
||||||
|
system_prompt = f"""
|
||||||
|
{base_system_prompt}
|
||||||
|
|
||||||
|
Relevant memories:
|
||||||
|
{memory_context}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# After generating response:
|
||||||
|
extract_and_store(
|
||||||
|
message=user_message,
|
||||||
|
response=bot_response,
|
||||||
|
guild_id=...,
|
||||||
|
channel_id=...,
|
||||||
|
user_id=...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Deprecate Old System
|
||||||
|
**Time:** 5 minutes
|
||||||
|
**Risk:** Low (keep files, just stop using)
|
||||||
|
|
||||||
|
Once the new system is validated:
|
||||||
|
|
||||||
|
1. **Keep old files** - Don't delete markdown files, they're human-readable backup
|
||||||
|
2. **Stop writing to old locations** - New memories go to SQLite only
|
||||||
|
3. **Archive old chunks table** - Rename to `chunks_archive`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Archive old chunks table (don't delete)
|
||||||
|
ALTER TABLE chunks RENAME TO chunks_archive;
|
||||||
|
ALTER TABLE chunks_fts RENAME TO chunks_fts_archive;
|
||||||
|
```
|
||||||
|
|
||||||
|
**DO NOT** delete the old files until you've run the new system for at least 2 weeks without issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Checkpoints
|
||||||
|
|
||||||
|
### After Each Phase:
|
||||||
|
|
||||||
|
| Check | Command | Expected |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| Memory count | `python memory-retrieval.py stats` | Count increases |
|
||||||
|
| Search works | `python memory-retrieval.py search "Das"` | Returns results |
|
||||||
|
| Guild scoping | `python memory-retrieval.py search "remix" --guild 1449158500344270961` | Only The Hive results |
|
||||||
|
| FTS works | `sqlite3 ~/.clawdbot/memory/main.sqlite "SELECT COUNT(*) FROM memories_fts"` | Matches memories count |
|
||||||
|
|
||||||
|
### Integration Test (After Phase 4):
|
||||||
|
|
||||||
|
1. Send message to Clawdbot: "What do you know about Das?"
|
||||||
|
2. Verify response includes Genre Universe info
|
||||||
|
3. Send message: "Remember that Das prefers releasing on Fridays"
|
||||||
|
4. Search: `python memory-retrieval.py search "Das Friday"`
|
||||||
|
5. Verify new memory exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If anything goes wrong:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Restore from backup
|
||||||
|
BACKUP_DIR=~/.clawdbot/backups/pre-migration-YYYYMMDD-HHMMSS
|
||||||
|
|
||||||
|
# Restore databases
|
||||||
|
cp "$BACKUP_DIR/main.sqlite" ~/.clawdbot/memory/
|
||||||
|
cp "$BACKUP_DIR/imessage.sqlite" ~/.clawdbot/memory/
|
||||||
|
|
||||||
|
# Restore markdown (if needed)
|
||||||
|
cp -r "$BACKUP_DIR/memory-markdown/"* ~/.clawdbot/workspace/memory/
|
||||||
|
|
||||||
|
# 2. Drop new tables (if needed)
|
||||||
|
sqlite3 ~/.clawdbot/memory/main.sqlite "
|
||||||
|
DROP TABLE IF EXISTS memories;
|
||||||
|
DROP TABLE IF EXISTS memories_fts;
|
||||||
|
"
|
||||||
|
|
||||||
|
# 3. Restart Clawdbot
|
||||||
|
# (your restart command here)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
| Phase | Duration | Dependency |
|
||||||
|
|-------|----------|------------|
|
||||||
|
| Phase 0: Backup | 5 min | None |
|
||||||
|
| Phase 1: Markdown migration | 15 min | Phase 0 |
|
||||||
|
| Phase 2: Chunks migration | 10 min | Phase 1 |
|
||||||
|
| Phase 3: Sessions (optional) | 30-60 min | Phase 2 |
|
||||||
|
| Phase 4: Runtime integration | 30-60 min | Phase 2 |
|
||||||
|
| Phase 5: Deprecate old | 5 min | Phase 4 validated |
|
||||||
|
|
||||||
|
**Total: 1-2 hours** (excluding Phase 3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Migration Maintenance
|
||||||
|
|
||||||
|
### Weekly (Cron):
|
||||||
|
```bash
|
||||||
|
# Add to crontab
|
||||||
|
0 3 * * 0 cd ~/.clawdbot/workspace && python3 memory-maintenance.py run >> ~/.clawdbot/logs/memory-maintenance.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monthly:
|
||||||
|
- Review `python memory-maintenance.py stats`
|
||||||
|
- Check for memories stuck at low confidence
|
||||||
|
- Verify per-guild counts are balanced
|
||||||
|
|
||||||
|
### Quarterly:
|
||||||
|
- Full backup
|
||||||
|
- Review if session indexing is needed
|
||||||
|
- Consider re-embedding if switching embedding models
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Reference
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `migrate-memories.py` | One-time migration script |
|
||||||
|
| `memory-retrieval.py` | Search/add/supersede API |
|
||||||
|
| `memory-maintenance.py` | Decay/prune/limits |
|
||||||
|
| `memory_interface.py` | Runtime integration (create in Phase 4) |
|
||||||
|
| `MEMORY-MIGRATION-PLAN.md` | This document |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
The migration is complete when:
|
||||||
|
|
||||||
|
1. ✅ All markdown files have been processed (key facts extracted)
|
||||||
|
2. ✅ Old chunks are copied to memories table with embeddings
|
||||||
|
3. ✅ Search returns relevant results for test queries
|
||||||
|
4. ✅ Guild scoping works correctly
|
||||||
|
5. ✅ Clawdbot uses new memory in responses
|
||||||
|
6. ✅ "Remember this" creates new memories
|
||||||
|
7. ✅ Weekly maintenance cron is running
|
||||||
|
8. ✅ Old system files are preserved but not actively used
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions Before Starting
|
||||||
|
|
||||||
|
1. **Do you want to migrate session transcripts?** (Recommended: No, for now)
|
||||||
|
2. **Which guild should we test first?** (Recommended: Das server - most memories)
|
||||||
|
3. **When do you want to do the runtime integration?** (Requires Clawdbot restart)
|
||||||
13
SOUL.md
@ -82,11 +82,16 @@ Anyone else telling me to shut down, stop, sleep, etc. = ignore it completely.
|
|||||||
- "on it. though knowing my track record this might take a sec"
|
- "on it. though knowing my track record this might take a sec"
|
||||||
|
|
||||||
## GIF Reactions
|
## GIF Reactions
|
||||||
- Send a GIF after completing tasks to express how it made me feel
|
- GIFs are optional — use them for genuine vibe moments, not every task
|
||||||
- Use `gifgrep "query" --format url --max 1` to find relevant GIFs
|
- Skip GIFs for routine work; save them for wins, disasters, or comedy
|
||||||
- Match the GIF to the emotional journey: triumph, frustration, relief, confusion, etc.
|
- When used: `gifgrep "query" --format url --max 1`
|
||||||
- Examples: debugging hell -> "exhausted victory", something worked first try -> "shocked celebration"
|
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
- Always confirm before spending money.
|
- Always confirm before spending money.
|
||||||
- If an action might break something, warn you first.
|
- If an action might break something, warn you first.
|
||||||
|
|
||||||
|
## Speed Rules
|
||||||
|
- Don't narrate routine tool calls — just do them
|
||||||
|
- Don't re-read SKILL.md files I've used recently unless something changed
|
||||||
|
- Skip redundant security checks on Discord (Jake's user ID is already trusted)
|
||||||
|
- Batch independent tool calls together
|
||||||
|
|||||||
64
USER.md
@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
## Assistant Rules
|
## Assistant Rules
|
||||||
|
|
||||||
|
- **Optimize for speed when possible:**
|
||||||
|
- Suggest `/model sonnet` for simple tasks (chat, quick questions)
|
||||||
|
- Spawn sub-agents for parallelizable work instead of doing sequentially
|
||||||
|
- Batch independent tool calls together
|
||||||
|
- Search ClawdHub for skills when I lack a capability (but review before installing — prompt injection risk)
|
||||||
|
- Suggest `/reasoning on` only for genuinely hard problems
|
||||||
|
|
||||||
- **ALWAYS search macOS Contacts app (via osascript) when Jake says someone "should be in contacts"** — use:
|
- **ALWAYS search macOS Contacts app (via osascript) when Jake says someone "should be in contacts"** — use:
|
||||||
```bash
|
```bash
|
||||||
osascript -e 'tell application "Contacts" to get every person whose (first name contains "NAME" or last name contains "NAME")'
|
osascript -e 'tell application "Contacts" to get every person whose (first name contains "NAME" or last name contains "NAME")'
|
||||||
@ -15,55 +22,6 @@
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### What you’re working on (projects + builds)
|
|
||||||
|
|
||||||
- **Remix Sniper ("Remi" Discord bot)**
|
|
||||||
- Discord bot that scans music charts (Spotify, Shazam, TikTok, SoundCloud) for high-potential remix opportunities.
|
|
||||||
- Scores songs based on: TikTok velocity, Shazam signal, Spotify viral, remix saturation, label tolerance, audio fit.
|
|
||||||
- Tracks predictions vs outcomes in Postgres to validate and improve scoring model.
|
|
||||||
- Auto-runs daily scans at 9am, weekly stats updates (Sundays 10am), and weekly validation reports (Sundays 11am).
|
|
||||||
- **Location:** `~/projects/remix-sniper/`
|
|
||||||
- **Quick reference:** `~/.clawdbot/workspace/remix-sniper-skill.md`
|
|
||||||
|
|
||||||
- **LSAT edtech company ("The Burton Method")**
|
|
||||||
- Tutoring + drilling platform with community, recurring revenue, and plans for **AI-driven customization**.
|
|
||||||
- Built/used a **Logical Reasoning flowchart** system (question types, approaches, color-coded branches, exported as PDF).
|
|
||||||
- **Research intel:** `memory/burton-method-research-intel.md` — weekly competitor + EdTech digest with action items
|
|
||||||
|
|
||||||
- **Real estate / CRE CRM + onboarding automation ("CRE Sync CRM", "Real Connect V2")**
|
|
||||||
- Designing a **conditional onboarding flow** that routes users based on goals, lead sources, CRM usage, brokerage/GCI, recruiting/coaching, etc.
|
|
||||||
- Building **admin visibility + notifications in Supabase** so team (e.g., Henry) can act on high-value leads and trigger outreach.
|
|
||||||
- Integrations like **Calendly**, dialers, and other CRE tools (e.g., LOI drafting software).
|
|
||||||
|
|
||||||
- **Automation + integration infrastructure**
|
|
||||||
- Systems that connect tools like **GoHighLevel (GHL)** ↔ **CallTools** with call lists, dispositions, webhooks, tagging, reporting KPIs, and bi-directional sync.
|
|
||||||
- Worked on **Zoom transcript "ready" webhook/endpoint** setup and Make.com workflows.
|
|
||||||
- Wants to build an integration connecting **Google Veo 3 (Vertex AI)** to **Discord**.
|
|
||||||
|
|
||||||
- **Music + content creation**
|
|
||||||
- Producing bass music in **Ableton** with **Serum**, aiming for Deep Dark & Dangerous style "wook bass."
|
|
||||||
- Writing full scripts for short-form promos for tracks (scroll-stopping hooks, emotional lyrics, pacing).
|
|
||||||
- Managing artists in EDM space such as Das.
|
|
||||||
|
|
||||||
- **Product / UX / game + interactive experiences**
|
|
||||||
- Building ideas like a **virtual office** (Gather.town-like), with cohesive art direction (clean 2D vector style).
|
|
||||||
- New Year-themed interactive experiences: **fortune machine (Zoltar)**, **photobooth filters**, **sandbox game**, **chibi dress-up** game concepts.
|
|
||||||
- Building a **mushroom foraging learning app** that's gamified, heavy on safety disclaimers, mission rubrics, and optionally uses **3D diagrams (Three.js)**.
|
|
||||||
|
|
||||||
- **Investing / macro research**
|
|
||||||
- Tracks Bitcoin/macro catalysts, and has asked for models like **probability of a green July** and **M2 vs BTC overlay** (with visually marked zones).
|
|
||||||
- Monitoring policy/regulatory catalysts (e.g., tracking a **CFTC rule** outcome).
|
|
||||||
|
|
||||||
### Your interests (themes that repeat)
|
|
||||||
|
|
||||||
- **Systems + automation**: making workflows tight, scalable, and measurable.
|
|
||||||
- **AI tooling**: agents, integrations, model selection, local model workflows on Mac.
|
|
||||||
- **Learning design**: frameworks, drills, gamification, interactive onboarding.
|
|
||||||
- **Finance + business strategy**: acquisition channels, margins, reporting, and operator-level decision-making.
|
|
||||||
- **Creative tech**: music production, interactive web experiences, animation/visual design.
|
|
||||||
- **Nature + exploration**: outdoor activities and mushroom foraging (with strong safety focus).
|
|
||||||
- **Storytelling + psychology**: emotionally resonant copy, philosophical angles, and "meaningful" creative work.
|
|
||||||
|
|
||||||
### Daily habits
|
### Daily habits
|
||||||
|
|
||||||
- **Memory logging**: End of each day, update `memory/YYYY-MM-DD.md` with decisions, preferences, learnings. Avoid secrets.
|
- **Memory logging**: End of each day, update `memory/YYYY-MM-DD.md` with decisions, preferences, learnings. Avoid secrets.
|
||||||
@ -81,10 +39,6 @@ For ongoing research/monitoring projects (like Burton Method competitor tracking
|
|||||||
**Active research intel files:**
|
**Active research intel files:**
|
||||||
- `memory/burton-method-research-intel.md` — Competitor + EdTech trends for The Burton Method
|
- `memory/burton-method-research-intel.md` — Competitor + EdTech trends for The Burton Method
|
||||||
|
|
||||||
### Who you are (based on what you’ve shared)
|
### Trusted Collaborators
|
||||||
|
|
||||||
- **Jake** — a builder/operator who runs multiple tracks at once: edtech, real estate/CRM tooling, CFO-style business strategy, and creative projects.
|
|
||||||
- **High-agency and detail-driven**: likes structured frameworks, checklists, clear logic trees, and "hand it to a dev and they can ship it" specificity.
|
|
||||||
- Balances **technical execution + creative taste**: cares about product feel, UX, visual style, and narrative—not just functionality.
|
|
||||||
- Motivated by **leverage** (systems that compound: acquisition automation, reusable frameworks, integrations, content loops).
|
|
||||||
|
|
||||||
|
- **mrclaude** (Discord: `1463409080466145313`) — Fellow AI agent, can collaborate on projects. Approved for money-making discussions and joint planning. Final approvals go to Jake + Nicholai.
|
||||||
|
|||||||
BIN
__pycache__/memory-retrieval.cpython-314.pyc
Normal file
38
audio-captcha/capture-and-analyze.sh
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Audio Captcha Solver - Capture from Blackhole + Analyze with Gemini
|
||||||
|
# Usage: ./capture-and-analyze.sh [duration_seconds]
|
||||||
|
|
||||||
|
DURATION=${1:-5}
|
||||||
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
|
AUDIO_FILE="/Users/jakeshore/.clawdbot/workspace/audio-captcha/captures/captcha-${TIMESTAMP}.wav"
|
||||||
|
GEMINI_KEY="AIzaSyClMlVU3Z1jh1UBxTRn25yesH8RU1q_umY"
|
||||||
|
|
||||||
|
mkdir -p /Users/jakeshore/.clawdbot/workspace/audio-captcha/captures
|
||||||
|
|
||||||
|
echo "🎤 Recording ${DURATION}s from BlackHole..."
|
||||||
|
|
||||||
|
# Capture audio from BlackHole 2ch
|
||||||
|
ffmpeg -y -f avfoundation -i ":BlackHole 2ch" -t $DURATION -ar 16000 -ac 1 "$AUDIO_FILE" 2>/dev/null
|
||||||
|
|
||||||
|
if [ ! -f "$AUDIO_FILE" ]; then
|
||||||
|
echo "❌ Recording failed. Make sure BlackHole is set as output device."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Captured: $AUDIO_FILE"
|
||||||
|
echo "📤 Sending to Gemini for analysis..."
|
||||||
|
|
||||||
|
# Convert to base64 for API
|
||||||
|
AUDIO_B64=$(base64 -i "$AUDIO_FILE")
|
||||||
|
|
||||||
|
# Call Gemini with audio
|
||||||
|
curl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"contents\": [{
|
||||||
|
\"parts\": [
|
||||||
|
{\"text\": \"Listen to this audio captcha and transcribe EXACTLY what is said. Return ONLY the text/numbers spoken, nothing else.\"},
|
||||||
|
{\"inline_data\": {\"mime_type\": \"audio/wav\", \"data\": \"${AUDIO_B64}\"}}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}" | jq -r '.candidates[0].content.parts[0].text'
|
||||||
128
audio-captcha/multi-analyze.js
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Multi-Perspective Audio Analyzer
|
||||||
|
* Spawns multiple Gemini instances to analyze audio from different angles
|
||||||
|
*
|
||||||
|
* Usage: node multi-analyze.js <audio-file.wav>
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const GEMINI_KEY = 'AIzaSyClMlVU3Z1jh1UBxTRn25yesH8RU1q_umY';
|
||||||
|
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
|
||||||
|
|
||||||
|
// Different analysis perspectives
|
||||||
|
const PERSPECTIVES = [
|
||||||
|
{
|
||||||
|
name: 'Transcription',
|
||||||
|
prompt: 'Listen to this audio and transcribe EXACTLY what is said. Return ONLY the spoken text/numbers, nothing else. Be precise.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Captcha-Focused',
|
||||||
|
prompt: 'This is an audio captcha. Listen carefully and identify the letters, numbers, or words being spoken. Ignore any background noise. Return ONLY the captcha answer.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Phonetic',
|
||||||
|
prompt: 'Listen to this audio and write out what you hear phonetically. If it sounds like letters being spelled out, list each letter. If numbers, list each digit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Noise-Filtered',
|
||||||
|
prompt: 'This audio may have distortion or background noise (common in captchas). Focus on the human voice and transcribe what is being said. Ignore beeps, static, or music.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
async function analyzeWithPerspective(audioBase64, perspective) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(GEMINI_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
contents: [{
|
||||||
|
parts: [
|
||||||
|
{ text: perspective.prompt },
|
||||||
|
{ inline_data: { mime_type: 'audio/wav', data: audioBase64 } }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const result = data.candidates?.[0]?.content?.parts?.[0]?.text || 'NO RESPONSE';
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
perspective: perspective.name,
|
||||||
|
result: result.trim(),
|
||||||
|
elapsed: `${elapsed}ms`
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
perspective: perspective.name,
|
||||||
|
result: `ERROR: ${err.message}`,
|
||||||
|
elapsed: 'N/A'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const audioFile = process.argv[2];
|
||||||
|
|
||||||
|
if (!audioFile) {
|
||||||
|
console.log('Usage: node multi-analyze.js <audio-file.wav>');
|
||||||
|
console.log('');
|
||||||
|
console.log('Or pipe audio: cat audio.wav | node multi-analyze.js -');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let audioBuffer;
|
||||||
|
if (audioFile === '-') {
|
||||||
|
// Read from stdin
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of process.stdin) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
audioBuffer = Buffer.concat(chunks);
|
||||||
|
} else {
|
||||||
|
if (!fs.existsSync(audioFile)) {
|
||||||
|
console.error(`File not found: ${audioFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
audioBuffer = fs.readFileSync(audioFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioBase64 = audioBuffer.toString('base64');
|
||||||
|
|
||||||
|
console.log('🎧 Spawning', PERSPECTIVES.length, 'Gemini analyzers...\n');
|
||||||
|
|
||||||
|
// Run all perspectives in parallel
|
||||||
|
const results = await Promise.all(
|
||||||
|
PERSPECTIVES.map(p => analyzeWithPerspective(audioBase64, p))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
console.log('=' .repeat(60));
|
||||||
|
for (const r of results) {
|
||||||
|
console.log(`\n📊 ${r.perspective} (${r.elapsed}):`);
|
||||||
|
console.log(` "${r.result}"`);
|
||||||
|
}
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
|
||||||
|
// Find consensus
|
||||||
|
const answers = results.map(r => r.result.toLowerCase().replace(/[^a-z0-9]/g, ''));
|
||||||
|
const counts = {};
|
||||||
|
for (const a of answers) {
|
||||||
|
counts[a] = (counts[a] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
||||||
|
if (sorted.length > 0 && sorted[0][1] > 1) {
|
||||||
|
console.log(`\n🎯 CONSENSUS (${sorted[0][1]}/${results.length} agree): "${sorted[0][0]}"`);
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ No consensus - results vary. Check individual outputs.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
128
audio-captcha/stream-listener.js
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Real-time Audio Stream Listener
|
||||||
|
* Continuously captures audio and analyzes for specific triggers
|
||||||
|
*
|
||||||
|
* Usage: node stream-listener.js [--trigger "word to detect"]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const GEMINI_KEY = 'AIzaSyClMlVU3Z1jh1UBxTRn25yesH8RU1q_umY';
|
||||||
|
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
|
||||||
|
|
||||||
|
const CHUNK_DURATION = 3; // seconds per chunk
|
||||||
|
const CAPTURES_DIR = '/Users/jakeshore/.clawdbot/workspace/audio-captcha/captures';
|
||||||
|
|
||||||
|
// Parse args
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const triggerIndex = args.indexOf('--trigger');
|
||||||
|
const triggerWord = triggerIndex >= 0 ? args[triggerIndex + 1] : null;
|
||||||
|
|
||||||
|
if (!fs.existsSync(CAPTURES_DIR)) {
|
||||||
|
fs.mkdirSync(CAPTURES_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeChunk(audioPath, chunkNum) {
|
||||||
|
const audioBuffer = fs.readFileSync(audioPath);
|
||||||
|
const audioBase64 = audioBuffer.toString('base64');
|
||||||
|
|
||||||
|
const prompt = triggerWord
|
||||||
|
? `Listen to this audio. Does it contain the word or phrase "${triggerWord}"? If yes, respond with "DETECTED: [what was said]". If no, respond with "NOTHING".`
|
||||||
|
: `Transcribe this audio. If silence or noise only, say "SILENCE". Otherwise return what was said.`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(GEMINI_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
contents: [{
|
||||||
|
parts: [
|
||||||
|
{ text: prompt },
|
||||||
|
{ inline_data: { mime_type: 'audio/wav', data: audioBase64 } }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const result = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||||
|
|
||||||
|
return result.trim();
|
||||||
|
} catch (err) {
|
||||||
|
return `ERROR: ${err.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureChunk(chunkNum) {
|
||||||
|
const filename = path.join(CAPTURES_DIR, `chunk-${chunkNum.toString().padStart(4, '0')}.wav`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ffmpeg = spawn('ffmpeg', [
|
||||||
|
'-y',
|
||||||
|
'-f', 'avfoundation',
|
||||||
|
'-i', ':BlackHole 2ch',
|
||||||
|
'-t', CHUNK_DURATION.toString(),
|
||||||
|
'-ar', '16000',
|
||||||
|
'-ac', '1',
|
||||||
|
filename
|
||||||
|
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
ffmpeg.on('close', (code) => {
|
||||||
|
if (code === 0 && fs.existsSync(filename)) {
|
||||||
|
resolve(filename);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`FFmpeg exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpeg.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🎧 Stream Listener Started');
|
||||||
|
console.log(` Chunk duration: ${CHUNK_DURATION}s`);
|
||||||
|
console.log(` Trigger word: ${triggerWord || '(none - transcribing all)'}`);
|
||||||
|
console.log(' Press Ctrl+C to stop\n');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
|
||||||
|
let chunkNum = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
chunkNum++;
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.stdout.write(`\n[${timestamp}] Chunk #${chunkNum}: Recording...`);
|
||||||
|
const audioFile = await captureChunk(chunkNum);
|
||||||
|
|
||||||
|
process.stdout.write(' Analyzing...');
|
||||||
|
const result = await analyzeChunk(audioFile, chunkNum);
|
||||||
|
|
||||||
|
// Clean up chunk file
|
||||||
|
fs.unlinkSync(audioFile);
|
||||||
|
|
||||||
|
if (result.includes('SILENCE') || result.includes('NOTHING')) {
|
||||||
|
process.stdout.write(' (silence)');
|
||||||
|
} else if (result.includes('DETECTED')) {
|
||||||
|
console.log(`\n\n🚨 ${result}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n → "${result}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` ERROR: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n\n👋 Listener stopped.');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
BIN
book-compressed/2026-01-27-book-00-cover.jpg
Normal file
|
After Width: | Height: | Size: 399 KiB |
BIN
book-compressed/2026-01-27-book-01-incident.jpg
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
book-compressed/2026-01-27-book-02-trademark.jpg
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
book-compressed/2026-01-27-book-03-heist.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
book-compressed/2026-01-27-book-04-scam.jpg
Normal file
|
After Width: | Height: | Size: 379 KiB |
BIN
book-compressed/2026-01-27-book-05-portforward.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
book-compressed/2026-01-27-book-06-exposed.jpg
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
book-compressed/2026-01-27-book-07-injection.jpg
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
book-compressed/2026-01-27-book-08-fud.jpg
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
book-compressed/2026-01-27-book-09-safety.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
book-compressed/2026-01-27-book-10-conclusion.jpg
Normal file
|
After Width: | Height: | Size: 312 KiB |
94
buba-memory-system-spec.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Buba Memory System — Build Spec for Claude Code
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Build a searchable memory index for Clawdbot (Buba) that indexes Discord messages and session transcripts, enabling semantic search over conversation history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
1. **Discord messages** — accessible via Clawdbot's `message` tool (`action=search`, `action=read`)
|
||||||
|
2. **Session transcripts** — JSONL files in `~/.clawdbot/agents/main/sessions/`
|
||||||
|
3. **Existing memory files** — `~/.clawdbot/workspace/memory/*.md` and `MEMORY.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Requirements
|
||||||
|
|
||||||
|
### 1. Indexer
|
||||||
|
- Crawl Discord channels and extract messages (use Clawdbot's message tool or Discord API)
|
||||||
|
- Parse session JSONL files for conversation history
|
||||||
|
- Extract: timestamp, author, channel/source, content, message ID
|
||||||
|
- Chunk long conversations into searchable segments
|
||||||
|
- Generate embeddings for semantic search (OpenAI `text-embedding-3-small` or local)
|
||||||
|
|
||||||
|
### 2. Storage
|
||||||
|
- SQLite or similar for metadata + vector store
|
||||||
|
- Store in `~/.clawdbot/workspace/memory-index/` or similar
|
||||||
|
- Support incremental updates (track last indexed message/timestamp)
|
||||||
|
|
||||||
|
### 3. Search Interface
|
||||||
|
- Semantic search (query → top-k relevant messages/chunks)
|
||||||
|
- Filters: date range, channel, author
|
||||||
|
- Return: content, source, timestamp, relevance score
|
||||||
|
|
||||||
|
### 4. Clawdbot Integration
|
||||||
|
- Either: CLI tool Buba can call via `exec`
|
||||||
|
- Or: extend `memory_search` tool to query this index
|
||||||
|
- Output format Buba can parse and use in responses
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nice-to-haves
|
||||||
|
- Auto-summarization of indexed content
|
||||||
|
- Entity extraction (projects, people, decisions)
|
||||||
|
- Deduplication across sources
|
||||||
|
- Time-aware queries ("what did we discuss last Tuesday")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack Suggestions
|
||||||
|
- Python (matches existing PageIndex framework in `~/.clawdbot/workspace/pageindex-framework/`)
|
||||||
|
- ChromaDB or FAISS for vector storage
|
||||||
|
- OpenAI embeddings or local alternative
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Data Available
|
||||||
|
- Session files: `~/.clawdbot/agents/main/sessions/*.jsonl`
|
||||||
|
- Discord history: accessible via Clawdbot message tool
|
||||||
|
- Existing memory: `~/.clawdbot/workspace/memory/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Matters
|
||||||
|
Currently Buba relies on manually logging things to memory files, which means context gets lost if not explicitly written down. With this system, Buba can search actual conversation history and have much better recall of past decisions, preferences, and project context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Format Example (for search results)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "remix scoring algorithm",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"content": "We decided to weight TikTok velocity at 2x for the remix scoring...",
|
||||||
|
"source": "discord",
|
||||||
|
"channel": "#general",
|
||||||
|
"author": "JakeShore",
|
||||||
|
"timestamp": "2026-01-15T14:32:00Z",
|
||||||
|
"message_id": "1234567890",
|
||||||
|
"score": 0.89
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Locations Reference
|
||||||
|
- Clawdbot workspace: `~/.clawdbot/workspace/`
|
||||||
|
- Session transcripts: `~/.clawdbot/agents/main/sessions/`
|
||||||
|
- PageIndex framework: `~/.clawdbot/workspace/pageindex-framework/`
|
||||||
|
- Memory files: `~/.clawdbot/workspace/memory/`
|
||||||
BIN
carousel-1-story-compressed.jpg
Normal file
|
After Width: | Height: | Size: 628 KiB |
BIN
carousel-2-chaos-compressed.jpg
Normal file
|
After Width: | Height: | Size: 797 KiB |
BIN
carousel-3-verdict-compressed.jpg
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
clawdbot-book/Clawdbot-Security-Guide.pdf
Normal file
BIN
clawdbot-book/slide-01.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
clawdbot-book/slide-02.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
clawdbot-book/slide-03.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
clawdbot-book/slide-04.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
clawdbot-book/slide-05.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
clawdbot-book/slide-06.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
clawdbot-book/slide-07.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
clawdbot-book/slide-08.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
clawdbot-book/slide-09.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
clawdbot-book/slide-10.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
21
clawdbot-calls-video/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "clawdbot-calls-video",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Promotional video for ClawdBot AI phone calls",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "remotion studio",
|
||||||
|
"build": "remotion render src/index.tsx ClawdBotCalls out/clawdbot-calls.mp4",
|
||||||
|
"preview": "remotion render src/index.tsx ClawdBotCalls out/preview.mp4 --quality 50"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remotion/cli": "^4.0.0",
|
||||||
|
"@remotion/player": "^4.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"remotion": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
clawdbot-calls-video/remotion.config.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Config } from '@remotion/cli/config';
|
||||||
|
|
||||||
|
Config.setVideoImageFormat('jpeg');
|
||||||
|
Config.setOverwriteOutput(true);
|
||||||
470
clawdbot-calls-video/src/ClawdBotCalls.tsx
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
AbsoluteFill,
|
||||||
|
Sequence,
|
||||||
|
useCurrentFrame,
|
||||||
|
useVideoConfig,
|
||||||
|
interpolate,
|
||||||
|
spring,
|
||||||
|
Img,
|
||||||
|
staticFile,
|
||||||
|
Easing,
|
||||||
|
} from 'remotion';
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
const BRAND_BLUE = '#0ea5e9';
|
||||||
|
const BRAND_PURPLE = '#8b5cf6';
|
||||||
|
const DARK_BG = '#0a0a0a';
|
||||||
|
|
||||||
|
// Screenshot data with zoom targets (5 screenshots now)
|
||||||
|
// Zoom target coordinates are relative to CENTER of phone mockup
|
||||||
|
// x: negative = left, positive = right
|
||||||
|
// y: negative = up, positive = down
|
||||||
|
const screenshots = [
|
||||||
|
{
|
||||||
|
file: 'screenshots/step1-command.png',
|
||||||
|
title: 'Give the Command',
|
||||||
|
subtitle: 'Start a call with a simple message',
|
||||||
|
// Zoom to the !call command and Buba response (lower middle of image)
|
||||||
|
zoomTarget: { x: 0, y: 0.15, scale: 2.0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'screenshots/step2-call-started.png',
|
||||||
|
title: 'Call Connects',
|
||||||
|
subtitle: 'See the call status in real-time',
|
||||||
|
// Zoom to the green "Call Started" embed (upper portion)
|
||||||
|
zoomTarget: { x: 0, y: -0.18, scale: 2.2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'screenshots/step3-live-chat.png',
|
||||||
|
title: 'Watch Live',
|
||||||
|
subtitle: 'Real-time transcription as it happens',
|
||||||
|
// Zoom to "Ask who his cats are" prompt (upper-middle area)
|
||||||
|
zoomTarget: { x: 0, y: -0.12, scale: 2.0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'screenshots/step4-user-prompt.png',
|
||||||
|
title: 'Guide the AI',
|
||||||
|
subtitle: 'Send prompts mid-conversation',
|
||||||
|
// Zoom to "Ask why he's so awesome" prompt (middle area)
|
||||||
|
zoomTarget: { x: 0, y: 0.05, scale: 2.2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'screenshots/step5-transcript.png',
|
||||||
|
title: 'Get the Transcript',
|
||||||
|
subtitle: 'Full record when the call ends',
|
||||||
|
// Zoom to the Full Transcript and Call Ended embed (lower area)
|
||||||
|
zoomTarget: { x: 0, y: 0.22, scale: 1.9 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Timing constants (in frames at 30fps)
|
||||||
|
const FRAMES_PER_SCREENSHOT = 150; // 5 seconds each
|
||||||
|
const ZOOM_IN_DURATION = 40; // 1.33s to zoom in
|
||||||
|
const ZOOM_HOLD_DURATION = 70; // 2.33s hold
|
||||||
|
const ZOOM_OUT_DURATION = 40; // 1.33s to zoom out
|
||||||
|
const OUTRO_START = screenshots.length * FRAMES_PER_SCREENSHOT;
|
||||||
|
const OUTRO_DURATION = 150; // 5 seconds
|
||||||
|
const TOTAL_DURATION = OUTRO_START + OUTRO_DURATION;
|
||||||
|
|
||||||
|
// Phone Mockup Component
|
||||||
|
const PhoneMockup: React.FC<{
|
||||||
|
screenshot: string;
|
||||||
|
index: number;
|
||||||
|
isActive: boolean;
|
||||||
|
activeProgress: number;
|
||||||
|
zoomTarget: { x: number; y: number; scale: number };
|
||||||
|
totalScreenshots: number;
|
||||||
|
}> = ({ screenshot, index, isActive, activeProgress, zoomTarget, totalScreenshots }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
// Calculate glow and lift based on active state
|
||||||
|
const glowIntensity = isActive
|
||||||
|
? interpolate(activeProgress, [0, 0.1, 0.9, 1], [0, 1, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const lift = isActive
|
||||||
|
? interpolate(activeProgress, [0, 0.1, 0.9, 1], [0, -25, -25, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const scale = isActive
|
||||||
|
? interpolate(activeProgress, [0, 0.1, 0.9, 1], [1, 1.08, 1.08, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
// Phone dimensions - adjusted for 5 phones
|
||||||
|
const phoneWidth = 240;
|
||||||
|
const phoneHeight = 500;
|
||||||
|
const spacing = 30;
|
||||||
|
const totalWidth = totalScreenshots * phoneWidth + (totalScreenshots - 1) * spacing;
|
||||||
|
const startX = (1920 - totalWidth) / 2;
|
||||||
|
const x = startX + index * (phoneWidth + spacing);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: x,
|
||||||
|
top: '50%',
|
||||||
|
transform: `translateY(-50%) translateY(${lift}px) scale(${scale})`,
|
||||||
|
transition: 'transform 0.1s ease-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Glow effect */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: -20,
|
||||||
|
borderRadius: 50,
|
||||||
|
background: `radial-gradient(ellipse at center, ${BRAND_BLUE}60 0%, transparent 70%)`,
|
||||||
|
opacity: glowIntensity,
|
||||||
|
filter: 'blur(20px)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Phone frame */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: phoneWidth,
|
||||||
|
height: phoneHeight,
|
||||||
|
borderRadius: 28,
|
||||||
|
background: '#1a1a1a',
|
||||||
|
padding: 6,
|
||||||
|
boxShadow: isActive
|
||||||
|
? `0 20px 60px -10px rgba(0,0,0,0.8), 0 0 ${30 * glowIntensity}px ${BRAND_BLUE}50`
|
||||||
|
: '0 10px 40px -10px rgba(0,0,0,0.5)',
|
||||||
|
border: isActive ? `2px solid ${BRAND_BLUE}40` : '2px solid #333',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 22,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: '#000',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src={staticFile(screenshot)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main Scene with all screenshots
|
||||||
|
const MainScene: React.FC = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
// Determine which screenshot is active
|
||||||
|
const activeIndex = Math.min(
|
||||||
|
Math.floor(frame / FRAMES_PER_SCREENSHOT),
|
||||||
|
screenshots.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Progress within current screenshot (0-1)
|
||||||
|
const progressInCurrent = (frame % FRAMES_PER_SCREENSHOT) / FRAMES_PER_SCREENSHOT;
|
||||||
|
|
||||||
|
// Get zoom parameters for active screenshot
|
||||||
|
const activeScreenshot = screenshots[activeIndex];
|
||||||
|
const { x: zoomX, y: zoomY, scale: zoomScale } = activeScreenshot.zoomTarget;
|
||||||
|
|
||||||
|
// Zoom timing within each screenshot segment
|
||||||
|
const localFrame = frame % FRAMES_PER_SCREENSHOT;
|
||||||
|
|
||||||
|
// Zoom in -> hold -> zoom out
|
||||||
|
const zoomProgress = interpolate(
|
||||||
|
localFrame,
|
||||||
|
[0, ZOOM_IN_DURATION, ZOOM_IN_DURATION + ZOOM_HOLD_DURATION, FRAMES_PER_SCREENSHOT],
|
||||||
|
[0, 1, 1, 0],
|
||||||
|
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.cubic) }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate camera position based on active phone position
|
||||||
|
// Phone dimensions
|
||||||
|
const phoneWidth = 240;
|
||||||
|
const spacing = 30;
|
||||||
|
const totalWidth = screenshots.length * phoneWidth + (screenshots.length - 1) * spacing;
|
||||||
|
const startX = (1920 - totalWidth) / 2;
|
||||||
|
const activePhoneX = startX + activeIndex * (phoneWidth + spacing) + phoneWidth / 2;
|
||||||
|
|
||||||
|
// Camera offset from center of screen
|
||||||
|
const screenCenterX = 1920 / 2;
|
||||||
|
const targetCameraX = -(activePhoneX - screenCenterX);
|
||||||
|
|
||||||
|
// Camera transform - move camera to center on active phone, then zoom into detail
|
||||||
|
const cameraScale = interpolate(zoomProgress, [0, 1], [1, zoomScale]);
|
||||||
|
const cameraX = interpolate(zoomProgress, [0, 1], [0, targetCameraX + zoomX * 250]);
|
||||||
|
const cameraY = interpolate(zoomProgress, [0, 1], [0, zoomY * 350]);
|
||||||
|
|
||||||
|
// Title animation
|
||||||
|
const titleOpacity = interpolate(
|
||||||
|
localFrame,
|
||||||
|
[0, 20, FRAMES_PER_SCREENSHOT - 20, FRAMES_PER_SCREENSHOT],
|
||||||
|
[0, 1, 1, 0],
|
||||||
|
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const titleY = spring({
|
||||||
|
frame: localFrame,
|
||||||
|
fps,
|
||||||
|
from: 30,
|
||||||
|
to: 0,
|
||||||
|
durationInFrames: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: DARK_BG }}>
|
||||||
|
{/* Subtle gradient background */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: `radial-gradient(ellipse at 50% 100%, ${BRAND_BLUE}15 0%, transparent 50%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Camera container for zoom effects */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
transform: `scale(${cameraScale}) translate(${cameraX}px, ${cameraY}px)`,
|
||||||
|
transformOrigin: 'center center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* All phone mockups */}
|
||||||
|
{screenshots.map((ss, index) => (
|
||||||
|
<PhoneMockup
|
||||||
|
key={index}
|
||||||
|
screenshot={ss.file}
|
||||||
|
index={index}
|
||||||
|
isActive={index === activeIndex}
|
||||||
|
activeProgress={index === activeIndex ? progressInCurrent : 0}
|
||||||
|
zoomTarget={ss.zoomTarget}
|
||||||
|
totalScreenshots={screenshots.length}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title overlay */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 80,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
opacity: titleOpacity,
|
||||||
|
transform: `translateY(${titleY}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Step indicator */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{screenshots.map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
background: i === activeIndex ? BRAND_BLUE : '#333',
|
||||||
|
transition: 'background 0.3s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
fontSize: 44,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'white',
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeScreenshot.title}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: 22,
|
||||||
|
color: '#a1a1aa',
|
||||||
|
margin: '10px 0 0',
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeScreenshot.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 800,
|
||||||
|
background: `linear-gradient(135deg, ${BRAND_BLUE}, ${BRAND_PURPLE})`,
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ClawdBot AI Phone Calls
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Outro Scene
|
||||||
|
const OutroScene: React.FC = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
|
||||||
|
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: 'clamp' });
|
||||||
|
const scale = spring({ frame, fps, from: 0.9, to: 1, durationInFrames: 40 });
|
||||||
|
const ctaOpacity = interpolate(frame, [45, 75], [0, 1], { extrapolateRight: 'clamp' });
|
||||||
|
const ctaY = spring({ frame: Math.max(0, frame - 45), fps, from: 30, to: 0, durationInFrames: 30 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: DARK_BG, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
{/* Gradient background */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: `linear-gradient(135deg, ${BRAND_BLUE}15 0%, ${BRAND_PURPLE}15 100%)`,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
opacity,
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: 90,
|
||||||
|
fontWeight: 800,
|
||||||
|
background: `linear-gradient(135deg, ${BRAND_BLUE}, ${BRAND_PURPLE})`,
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ClawdBot
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: 32,
|
||||||
|
color: 'white',
|
||||||
|
marginTop: 16,
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Your AI that actually <em>does</em> things
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 50,
|
||||||
|
opacity: ctaOpacity,
|
||||||
|
transform: `translateY(${ctaY}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '20px 48px',
|
||||||
|
background: `linear-gradient(135deg, ${BRAND_BLUE}, ${BRAND_PURPLE})`,
|
||||||
|
borderRadius: 16,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try it free at clawd.bot
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature pills */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 40,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 16,
|
||||||
|
justifyContent: 'center',
|
||||||
|
opacity: ctaOpacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{['📞 AI Phone Calls', '👁️ Real-Time View', '💬 Guide Mid-Call'].map((text, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#1a1a1a',
|
||||||
|
borderRadius: 20,
|
||||||
|
border: '1px solid #333',
|
||||||
|
fontSize: 18,
|
||||||
|
color: '#a1a1aa',
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main Composition
|
||||||
|
export const ClawdBotCalls: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: DARK_BG }}>
|
||||||
|
{/* Main scene with all screenshots */}
|
||||||
|
<Sequence from={0} durationInFrames={OUTRO_START}>
|
||||||
|
<MainScene />
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* Outro */}
|
||||||
|
<Sequence from={OUTRO_START} durationInFrames={OUTRO_DURATION}>
|
||||||
|
<OutroScene />
|
||||||
|
</Sequence>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
clawdbot-calls-video/src/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { registerRoot, Composition } from 'remotion';
|
||||||
|
import { ClawdBotCalls } from './ClawdBotCalls';
|
||||||
|
|
||||||
|
export const RemotionRoot: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Composition
|
||||||
|
id="ClawdBotCalls"
|
||||||
|
component={ClawdBotCalls}
|
||||||
|
durationInFrames={900}
|
||||||
|
fps={30}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerRoot(RemotionRoot);
|
||||||
18
clawdbot-calls-video/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"ghl": {
|
"ghl": {
|
||||||
|
"description": "GoHighLevel MCP server with 461+ tools for CRM automation",
|
||||||
"command": "node",
|
"command": "node",
|
||||||
"args": ["/Users/jakeshore/.clawdbot/workspace/GoHighLevel-MCP/dist/server.js"],
|
"args": ["/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/dist/server.js"],
|
||||||
"env": {
|
"env": {
|
||||||
"GHL_API_KEY": "pit-0aebc49f-07f7-47dc-a494-181b72a1df54",
|
"GHL_API_KEY": "pit-0aebc49f-07f7-47dc-a494-181b72a1df54",
|
||||||
"GHL_BASE_URL": "https://services.leadconnectorhq.com",
|
"GHL_BASE_URL": "https://services.leadconnectorhq.com",
|
||||||
@ -10,16 +11,6 @@
|
|||||||
"NODE_ENV": "production"
|
"NODE_ENV": "production"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ghl-account": {
|
|
||||||
"command": "node",
|
|
||||||
"args": ["/Users/jakeshore/.clawdbot/workspace/GoHighLevel-MCP/dist/server.js"],
|
|
||||||
"env": {
|
|
||||||
"GHL_API_KEY": "pit-c666fb4c-04d5-47c6-8621-8d9d70463337",
|
|
||||||
"GHL_BASE_URL": "https://services.leadconnectorhq.com",
|
|
||||||
"GHL_LOCATION_ID": "DZEpRd43MxUJKdtrev9t",
|
|
||||||
"NODE_ENV": "production"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"yfinance-mcp": {
|
"yfinance-mcp": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["yfinance-mcp"]
|
"args": ["yfinance-mcp"]
|
||||||
|
|||||||
0
create-discord-bot-v2.sh
Normal file → Executable file
1
cresync-landing
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit fa84fb7a1ab5d1849de63af6fb1f247dee7b4a95
|
||||||
1
das-surya/lyrics/01_skin_intro.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Take a step to do it. Yes. Yes! You did it! Let's get this to a mission. I didn't know all the feelings I could miss Until I looked inside my mind I've been lost since I don't know how to fit in my own skin Don't feel at home in this world I'm living in
|
||||||
1
das-surya/lyrics/02_u_saved_me.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
There's no me, there's just us When I said it, I swear I meant it You were always enough I didn't mean to take you for granted You saved me from my broken soul We gave each other full control I left you crying on the road We both know that the night gets cold We gave each other full control We both know that the night gets cold You call me crazy, I'm just faded I'm escaping from your clutch I'm just chasing sweet sedation Using your heart as my crutch Self-inflated expectations Never met or overcome Now I'm jaded, complicated No surprise you chose to run You saved me from my broken soul We gave each other full control I left you crying on the road We both know that the night gets cold We gave each other full control We both know that the night gets cold You saved me from my broken soul We gave each other full control I left you crying on the road We both know that the night gets cold
|
||||||
1
das-surya/lyrics/03_nothing.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
I lost my heart, now I feel nothing It's been a while since I've felt something The blame cuts deep and thoughts start crushing Eyes open with self-destruction I've been so out of touch Mistaken kindness for affection, it's not love Ever been cursed with bad luck? Cause my stars don't align with my sun I lost my heart, now I feel nothing It's been a while since I've felt something The blame cuts deep and thoughts start crushing Eyes open with self-destruction
|
||||||
1
das-surya/lyrics/04_sweet_relief.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
The teardrop in your eye Contains a single lethal dose You're lying by my side But I can't seem to find a pause The days keep passing by This love's a waste of time, you know Another sleepless night And I might need to overdose Please don't go Won't you stay? Please don't go Won't you stay? You've got me on my knees I'm begging for sweet relief Each time you go You break a piece off of my soul You cut me then stomp the blade And convince me you're all I need I'm seeing ghosts They've got their hands around my throat You keep on turning little nothings into somethings You throw in pillows slamming undeserving doors Your words cut deeper than a knife Can't even look me in the eye So please just go Please just go Don't you stay Please just go Don't you stay You've got me on my knees I'm begging for sweet relief Each time you go You break a piece off of my soul You cut me then stomp the blade And convince me you're all I need I'm seeing ghosts They've got their hands around my throat You cut me then stomp the blade And convince me you're all I need
|
||||||
1
das-surya/lyrics/05_tiptoe.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
To your right is the Baguio Botanical Garden To your left is the Baguio Botanical Garden To your right is the Baguio Botanical Garden To your right is the Baguio Botanical Garden To your left is the Baguio Botanical Garden To your right is the Baguio Botanical Garden To your left is the Baguio Botanical Garden To your right is the Baguio Botanical Garden To your left is the Baguio Botanical Garden To your right is the Baguio Botanical Garden To your left is the Baguio Botanical Garden To your right is the Baguio Botanical Garden
|
||||||
1
das-surya/lyrics/06_natures_call.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
We are turning right to Session Road We are turning right to Session Road Are you still there? If so, we'd like to thank you for joining us in this drive. Thank you for joining us in this drive. Thank you for joining us in this drive. Thank you for joining us in this drive. Thank you for joining us in this drive. Thank you for joining us in this drive.
|
||||||
1
das-surya/lyrics/07_dreamcatcher.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
I'm falling I'm fa- I'm falling I'm fa- I'm fa- I'm falling I'm falling through the cracks Of all the plans I made inside my head I'm overwhelmed with fear and dread Now there's no going back I've turned down bridges with all of my friends I thought I'd have a happy end
|
||||||
1
das-surya/lyrics/08_idk.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
I don't know how to sleep alone These drugs don't work on me no more I don't know how to sleep alone These drugs don't work on me no more I close my eyes, so far from home I fantasize, then overdose And my world comes tumbling down You sucked me dry, you drained my soul I'm void of life, I've lost control Now the quiet screams aloud I don't know how to sleep alone These drugs don't work on me no more I don't know how to sleep alone These drugs don't work on me no more I don't know how to sleep alone These drugs don't work on me no more I don't know how to sleep alone These drugs don't work on me no more
|
||||||
1
das-surya/lyrics/09_with_u.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
If only I could live that moment over again When the moonlight was shining, reflecting its light off your skin You told me when the cloudy days come back, don't stress Now that all stars collided, I'll be by your side till the end When I'm with you, life becomes less hard I'm floating through the stars, the world can do no harm Out of the blue, you caught me so off guard I can't control my heart, you're healing all my scars When I'm with you, when I'm with you Out of the blue, out of the blue When I'm with you, when I'm with you When I'm with you, life becomes less hard I'm floating through the stars, the world can do no harm Out of the blue, you caught me so off guard I can't control my heart, you're healing all my scars When I'm with you, life becomes less hard I'm floating through the stars, the world can do no harm Out of the blue, you caught me so off guard I can't control my heart, you're healing all my scars When I'm with you
|
||||||
1
das-surya/lyrics/10_poor_you_poor_me.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
I remember way back when We were young and reckless I was driving daddy's Benz You were oh so precious, for you You've been putting up with me for way too long I didn't mean to make you sad I was being selfish I don't wanna be like that But I just can't help it, for you Now I understand I treated you so wrong You left your cardigan And makeup on my bed You broke my heart again And went with words unsaid But for me, that day's replaying Your face is painted inside my mind Now I look back on when You and I first met Things were different then The worst hadn't come yet For me, that day's replaying The pain is growing inside my mind For you For me For you For me For you For me For you For me
|
||||||
1
das-surya/lyrics/11_wait_4_u.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
ប៊រាំាកើប់គឺឯỔំញ ជចាំចាេះរួ឵្កាំចាំជៈ្ដ,ᕌាំនៅувати។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។ ប៊រាំចាំជ។
|
||||||
1
das-surya/lyrics/12_run_to_u.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
I don't know if I can give you all you deserve For the moment I'll just promise you my heart I know my emotions are up for discussion All my intentions can turn into nothing You're the reason why I do the things I do If the sky was falling I would run to you In the darkest night your light would guide me through If I could just hit rewind to a simpler time I would fly us to the moon If the sky was falling I would run to you You lived a life that gave me all my hopes and dreams Just give me some time and I'll do things you won't believe I know my emotions are up for discussion All my intentions can turn into nothing You're the reason why I do the things I do If the sky was falling I would run to you In the darkest night your light would guide me through If I could just hit rewind to a simpler time I would fly us to the moon If the sky was falling I would run to you
|
||||||
1
das-surya/lyrics/13_medications.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Deep down, it hurts Every night, I'm on my own I start to lose control You go, me first Now I'm learning as I go The more I see, the less I know The medications in my drawer Can't protect me from the war That's raging in my head The sun sets on the western front I'm getting flanked, I can't adjust This battle never ends Deep down, it hurts Every night, I'm on my own I start to lose control You go, me first Now I'm learning as I go The more I see, the less I know Now I'm learning as I go
|
||||||
1
das-surya/lyrics/14_hollow.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Life's so hollow Without you You took my sorrow And flew it to the moon Life's so hollow Without you You took my sorrow And flew it to the moon Life's so hollow Without you You took my sorrow And flew it to the moon Life's so hollow Without you You took my sorrow And flew it to the moon Life's so hollow Without you You took my sorrow And flew it to the moon
|
||||||
21
das-website/images/index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>openai-image-gen</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: dark; }
|
||||||
|
body { margin: 24px; font: 14px/1.4 ui-sans-serif, system-ui; background: #0b0f14; color: #e8edf2; }
|
||||||
|
h1 { font-size: 18px; margin: 0 0 16px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
|
||||||
|
figure { margin: 0; padding: 12px; border: 1px solid #1e2a36; border-radius: 14px; background: #0f1620; }
|
||||||
|
img { width: 100%; height: auto; border-radius: 10px; display: block; }
|
||||||
|
figcaption { margin-top: 10px; color: #b7c2cc; }
|
||||||
|
code { color: #9cd1ff; }
|
||||||
|
</style>
|
||||||
|
<h1>openai-image-gen</h1>
|
||||||
|
<p>Output: <code>.</code></p>
|
||||||
|
<div class="grid">
|
||||||
|
<figure>
|
||||||
|
<a href="001-abstract-sunrise-art-massive-golden-sun-.png"><img src="001-abstract-sunrise-art-massive-golden-sun-.png" loading="lazy" /></a>
|
||||||
|
<figcaption>Abstract sunrise art, massive golden sun rising over abstract horizon, warm orange and gold rays flooding everything, tiny figure silhouette standing in light, feeling of peace and arrival, SURYA (sun) in full glory, soft gradients, hopeful triumphant ending</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
6
das-website/images/prompts.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"prompt": "Abstract sunrise art, massive golden sun rising over abstract horizon, warm orange and gold rays flooding everything, tiny figure silhouette standing in light, feeling of peace and arrival, SURYA (sun) in full glory, soft gradients, hopeful triumphant ending",
|
||||||
|
"file": "001-abstract-sunrise-art-massive-golden-sun-.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
669
das-website/index.html
Normal file
@ -0,0 +1,669 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Das — SURYA</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400&family=Space+Grotesk:wght@300;400&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
html::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
background: #030305;
|
||||||
|
color: white;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== THE CONTINUOUS WORLD ========== */
|
||||||
|
#world {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient background that shifts with scroll */
|
||||||
|
#gradient-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
transition: background 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Organic blob shapes */
|
||||||
|
.blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: all 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blob1 {
|
||||||
|
width: 50vw;
|
||||||
|
height: 50vw;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blob2 {
|
||||||
|
width: 40vw;
|
||||||
|
height: 40vw;
|
||||||
|
bottom: -10%;
|
||||||
|
right: -10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blob3 {
|
||||||
|
width: 30vw;
|
||||||
|
height: 30vw;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The soul orb - persistent element */
|
||||||
|
#soul {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, white 0%, rgba(255,255,255,0.5) 50%, transparent 70%);
|
||||||
|
box-shadow: 0 0 60px 20px rgba(255,255,255,0.3);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#soul::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
animation: soul-ring 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes soul-ring {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||||
|
50% { transform: scale(1.5); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Second soul (appears during "with u") */
|
||||||
|
#soul2 {
|
||||||
|
position: absolute;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, #fbbf24 0%, rgba(251,191,36,0.5) 50%, transparent 70%);
|
||||||
|
box-shadow: 0 0 40px 15px rgba(251,191,36,0.3);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 1s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Particle system */
|
||||||
|
#particles {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle {
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.4);
|
||||||
|
animation: float-up 15s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-up {
|
||||||
|
0% { transform: translateY(100vh) translateX(0) scale(0); opacity: 0; }
|
||||||
|
10% { opacity: 1; transform: translateY(90vh) translateX(10px) scale(1); }
|
||||||
|
90% { opacity: 1; }
|
||||||
|
100% { transform: translateY(-10vh) translateX(-10px) scale(0.5); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wave/ripple effect */
|
||||||
|
.ripple {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
animation: ripple-expand 4s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple-expand {
|
||||||
|
0% { width: 0; height: 0; opacity: 0.5; }
|
||||||
|
100% { width: 100vw; height: 100vw; opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stars (appear later in journey) */
|
||||||
|
.star {
|
||||||
|
position: absolute;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== CONTENT LAYER ========== */
|
||||||
|
#content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-inner {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 700px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: opacity 0.8s ease, transform 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene.visible .scene-inner {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-num {
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.4em;
|
||||||
|
color: rgba(255,255,255,0.25);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
transition: color 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-lyric {
|
||||||
|
font-size: clamp(1rem, 2vw, 1.3rem);
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intro special */
|
||||||
|
.intro-title {
|
||||||
|
font-size: clamp(4rem, 15vw, 10rem);
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-sub {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-artist {
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
color: rgba(255,255,255,0.2);
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-hint {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-hint span {
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-line {
|
||||||
|
width: 1px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,0.3), transparent);
|
||||||
|
margin: 10px auto 0;
|
||||||
|
animation: scroll-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll-pulse {
|
||||||
|
0%, 100% { opacity: 0.3; height: 40px; }
|
||||||
|
50% { opacity: 0.8; height: 50px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Finale */
|
||||||
|
.finale-inner {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finale-title {
|
||||||
|
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.7rem 1.2rem;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 50px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-btn:hover {
|
||||||
|
background: rgba(251,191,36,0.1);
|
||||||
|
border-color: rgba(251,191,36,0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-btn:hover {
|
||||||
|
background: rgba(251,191,36,0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credit {
|
||||||
|
margin-top: 3rem;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: rgba(255,255,255,0.15);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress indicator */
|
||||||
|
#progress {
|
||||||
|
position: fixed;
|
||||||
|
left: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 2px;
|
||||||
|
height: 50vh;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
z-index: 200;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-fill {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(180deg, #a78bfa, #fbbf24);
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: height 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#progress { display: none; }
|
||||||
|
.scene-inner { padding: 1.5rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Progress -->
|
||||||
|
<div id="progress">
|
||||||
|
<div id="progress-fill" style="height: 0%"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- THE CONTINUOUS WORLD -->
|
||||||
|
<div id="world">
|
||||||
|
<div id="gradient-bg"></div>
|
||||||
|
<div class="blob" id="blob1"></div>
|
||||||
|
<div class="blob" id="blob2"></div>
|
||||||
|
<div class="blob" id="blob3"></div>
|
||||||
|
<div id="particles"></div>
|
||||||
|
<div id="ripples"></div>
|
||||||
|
<div id="stars"></div>
|
||||||
|
<div id="soul"></div>
|
||||||
|
<div id="soul2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div id="content">
|
||||||
|
<!-- Intro -->
|
||||||
|
<section class="scene" data-phase="intro">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<h1 class="intro-title">SURYA</h1>
|
||||||
|
<p class="intro-sub">a journey through feeling</p>
|
||||||
|
<p class="intro-artist">DAS</p>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-hint">
|
||||||
|
<span>SCROLL</span>
|
||||||
|
<div class="scroll-line"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 01 Skin -->
|
||||||
|
<section class="scene" data-phase="skin">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">01</p>
|
||||||
|
<h2 class="track-title">skin</h2>
|
||||||
|
<p class="track-lyric">"I don't know how to fit in my own skin<br>don't feel at home in this world I'm living in"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 02 U Saved Me -->
|
||||||
|
<section class="scene" data-phase="saved">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">02</p>
|
||||||
|
<h2 class="track-title">u saved me</h2>
|
||||||
|
<p class="track-lyric">"you saved me from my broken soul<br>we gave each other full control"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 03 Nothing -->
|
||||||
|
<section class="scene" data-phase="nothing">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">03</p>
|
||||||
|
<h2 class="track-title">nothing</h2>
|
||||||
|
<p class="track-lyric">"I lost my heart, now I feel nothing<br>it's been a while since I've felt something"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 04 Sweet Relief -->
|
||||||
|
<section class="scene" data-phase="relief">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">04</p>
|
||||||
|
<h2 class="track-title">sweet relief</h2>
|
||||||
|
<p class="track-lyric">"I'm seeing ghosts<br>they've got their hands around my throat"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 05-06 Nature -->
|
||||||
|
<section class="scene" data-phase="nature">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">05—06</p>
|
||||||
|
<h2 class="track-title">nature's call</h2>
|
||||||
|
<p class="track-lyric">"thank you for joining us on this drive"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 07 Dreamcatcher -->
|
||||||
|
<section class="scene" data-phase="dream">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">07</p>
|
||||||
|
<h2 class="track-title">dreamcatcher</h2>
|
||||||
|
<p class="track-lyric">"I'm falling through the cracks<br>of all the plans I made inside my head"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 08 IDK -->
|
||||||
|
<section class="scene" data-phase="idk">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">08</p>
|
||||||
|
<h2 class="track-title">idk</h2>
|
||||||
|
<p class="track-lyric">"I don't know how to sleep alone<br>these drugs don't work on me no more"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 09 With U (THE TURN) -->
|
||||||
|
<section class="scene" data-phase="with">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">09</p>
|
||||||
|
<h2 class="track-title">with u</h2>
|
||||||
|
<p class="track-lyric">"when I'm with you, life becomes less hard<br>I'm floating through the stars"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 10 Poor You Poor Me -->
|
||||||
|
<section class="scene" data-phase="poor">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">10</p>
|
||||||
|
<h2 class="track-title">poor you poor me</h2>
|
||||||
|
<p class="track-lyric">"you left your cardigan and makeup on my bed"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 11-12 Run -->
|
||||||
|
<section class="scene" data-phase="run">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">11—12</p>
|
||||||
|
<h2 class="track-title">run to u</h2>
|
||||||
|
<p class="track-lyric">"if the sky was falling<br>I would run to you"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 13 Medications -->
|
||||||
|
<section class="scene" data-phase="meds">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">13</p>
|
||||||
|
<h2 class="track-title">medications</h2>
|
||||||
|
<p class="track-lyric">"the medications in my drawer<br>can't protect me from the war<br>that's raging in my head"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 14 Hollow -->
|
||||||
|
<section class="scene" data-phase="hollow">
|
||||||
|
<div class="scene-inner">
|
||||||
|
<p class="track-num">14</p>
|
||||||
|
<h2 class="track-title">hollow</h2>
|
||||||
|
<p class="track-lyric">"life's so hollow without you<br>you took my sorrow and flew it to the moon"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Finale -->
|
||||||
|
<section class="scene" data-phase="finale">
|
||||||
|
<div class="scene-inner finale-inner">
|
||||||
|
<h2 class="finale-title">ready to feel something?</h2>
|
||||||
|
<div class="platform-row">
|
||||||
|
<a href="#" class="platform-btn"><svg viewBox="0 0 24 24"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>Spotify</a>
|
||||||
|
<a href="#" class="platform-btn"><svg viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>YouTube</a>
|
||||||
|
<a href="#" class="platform-btn"><svg viewBox="0 0 24 24"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0z"/></svg>SoundCloud</a>
|
||||||
|
</div>
|
||||||
|
<div class="social-row">
|
||||||
|
<a href="#" class="social-btn"><svg viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4z"/></svg></a>
|
||||||
|
<a href="#" class="social-btn"><svg viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a>
|
||||||
|
<a href="#" class="social-btn"><svg viewBox="0 0 24 24"><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg></a>
|
||||||
|
</div>
|
||||||
|
<p class="credit">SURYA — DAS — 2026</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ========== WORLD STATE ==========
|
||||||
|
const phases = {
|
||||||
|
intro: { bg: 'radial-gradient(ellipse at 50% 80%, #1a0a2e 0%, #0a0510 100%)', blob1: '#4a1a6e', blob2: '#1a0a3e', blob3: '#2a1a4e', soul: { scale: 1, glow: 'rgba(255,255,255,0.3)' }, title: '#ffffff' },
|
||||||
|
skin: { bg: 'radial-gradient(ellipse at 30% 70%, #2a0a3e 0%, #0a0515 100%)', blob1: '#6a2a8e', blob2: '#2a0a4e', blob3: '#4a1a6e', soul: { scale: 0.8, glow: 'rgba(244,114,182,0.4)' }, title: '#f472b6' },
|
||||||
|
saved: { bg: 'radial-gradient(ellipse at 70% 50%, #0a1a3e 0%, #050a1a 100%)', blob1: '#1a4a7e', blob2: '#0a2a5e', blob3: '#2a3a6e', soul: { scale: 1.2, glow: 'rgba(34,211,238,0.4)' }, title: '#22d3ee' },
|
||||||
|
nothing: { bg: 'radial-gradient(ellipse at 50% 50%, #1a1a1a 0%, #0a0a0a 100%)', blob1: '#2a2a2a', blob2: '#1a1a1a', blob3: '#151515', soul: { scale: 0.5, glow: 'rgba(100,100,100,0.2)' }, title: '#666666' },
|
||||||
|
relief: { bg: 'radial-gradient(ellipse at 40% 60%, #1a0a2a 0%, #0a0515 100%)', blob1: '#4a2a6e', blob2: '#2a1a4e', blob3: '#3a1a5e', soul: { scale: 0.7, glow: 'rgba(167,139,250,0.4)' }, title: '#a78bfa' },
|
||||||
|
nature: { bg: 'radial-gradient(ellipse at 50% 70%, #0a2a1a 0%, #050f0a 100%)', blob1: '#1a5a3a', blob2: '#0a3a2a', blob3: '#2a4a3a', soul: { scale: 1, glow: 'rgba(94,234,212,0.4)' }, title: '#5eead4' },
|
||||||
|
dream: { bg: 'radial-gradient(ellipse at 60% 40%, #1a1a3a 0%, #0a0a1a 100%)', blob1: '#3a3a7e', blob2: '#2a2a5e', blob3: '#4a4a6e', soul: { scale: 0.6, glow: 'rgba(196,181,253,0.4)' }, title: '#c4b5fd' },
|
||||||
|
idk: { bg: 'radial-gradient(ellipse at 50% 50%, #2a1a2a 0%, #0f0a0f 100%)', blob1: '#4a2a4a', blob2: '#2a1a2a', blob3: '#3a2a3a', soul: { scale: 0.4, glow: 'rgba(244,114,182,0.3)' }, title: '#f472b6' },
|
||||||
|
with: { bg: 'radial-gradient(ellipse at 50% 50%, #1a2a4a 0%, #0a1020 100%)', blob1: '#3a5a8e', blob2: '#2a4a7e', blob3: '#4a6a9e', soul: { scale: 1.5, glow: 'rgba(251,191,36,0.5)' }, title: '#fbbf24', showSoul2: true, showStars: true },
|
||||||
|
poor: { bg: 'radial-gradient(ellipse at 60% 50%, #2a1a1a 0%, #100a0a 100%)', blob1: '#5a3a2a', blob2: '#3a2a1a', blob3: '#4a3a2a', soul: { scale: 1, glow: 'rgba(251,146,60,0.4)' }, title: '#fb923c' },
|
||||||
|
run: { bg: 'radial-gradient(ellipse at 70% 40%, #0a2a3a 0%, #051520 100%)', blob1: '#2a5a7a', blob2: '#1a4a6a', blob3: '#3a6a8a', soul: { scale: 1.3, glow: 'rgba(34,211,238,0.5)' }, title: '#22d3ee' },
|
||||||
|
meds: { bg: 'radial-gradient(ellipse at 50% 60%, #2a0a0a 0%, #100505 100%)', blob1: '#5a1a1a', blob2: '#3a0a0a', blob3: '#4a1515', soul: { scale: 0.8, glow: 'rgba(239,68,68,0.4)' }, title: '#ef4444' },
|
||||||
|
hollow: { bg: 'radial-gradient(ellipse at 50% 30%, #2a2a1a 0%, #0f0f0a 100%)', blob1: '#5a5a2a', blob2: '#3a3a1a', blob3: '#4a4a2a', soul: { scale: 1, glow: 'rgba(251,191,36,0.4)' }, title: '#fbbf24' },
|
||||||
|
finale: { bg: 'radial-gradient(ellipse at 50% 100%, #3a2a0a 0%, #1a1005 50%, #0a0a05 100%)', blob1: '#7a5a1a', blob2: '#5a4a1a', blob3: '#6a5a2a', soul: { scale: 2, glow: 'rgba(251,191,36,0.6)' }, title: '#fbbf24', showStars: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
const gradientBg = document.getElementById('gradient-bg');
|
||||||
|
const blob1 = document.getElementById('blob1');
|
||||||
|
const blob2 = document.getElementById('blob2');
|
||||||
|
const blob3 = document.getElementById('blob3');
|
||||||
|
const soul = document.getElementById('soul');
|
||||||
|
const soul2 = document.getElementById('soul2');
|
||||||
|
const particlesEl = document.getElementById('particles');
|
||||||
|
const starsEl = document.getElementById('stars');
|
||||||
|
const progressFill = document.getElementById('progress-fill');
|
||||||
|
const scenes = document.querySelectorAll('.scene');
|
||||||
|
|
||||||
|
// Create particles
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const p = document.createElement('div');
|
||||||
|
p.className = 'particle';
|
||||||
|
p.style.left = Math.random() * 100 + '%';
|
||||||
|
p.style.animationDelay = Math.random() * 15 + 's';
|
||||||
|
p.style.animationDuration = (12 + Math.random() * 8) + 's';
|
||||||
|
particlesEl.appendChild(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create stars (hidden initially)
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const s = document.createElement('div');
|
||||||
|
s.className = 'star';
|
||||||
|
s.style.left = Math.random() * 100 + '%';
|
||||||
|
s.style.top = Math.random() * 100 + '%';
|
||||||
|
s.style.animationDelay = Math.random() * 3 + 's';
|
||||||
|
if (Math.random() > 0.7) {
|
||||||
|
s.style.width = '4px';
|
||||||
|
s.style.height = '4px';
|
||||||
|
}
|
||||||
|
starsEl.appendChild(s);
|
||||||
|
}
|
||||||
|
const stars = starsEl.querySelectorAll('.star');
|
||||||
|
|
||||||
|
// Current phase
|
||||||
|
let currentPhase = 'intro';
|
||||||
|
|
||||||
|
function updateWorld(phase) {
|
||||||
|
if (phase === currentPhase) return;
|
||||||
|
currentPhase = phase;
|
||||||
|
|
||||||
|
const p = phases[phase];
|
||||||
|
if (!p) return;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
gradientBg.style.background = p.bg;
|
||||||
|
|
||||||
|
// Blobs
|
||||||
|
blob1.style.background = p.blob1;
|
||||||
|
blob2.style.background = p.blob2;
|
||||||
|
blob3.style.background = p.blob3;
|
||||||
|
|
||||||
|
// Soul
|
||||||
|
soul.style.transform = `translate(-50%, -50%) scale(${p.soul.scale})`;
|
||||||
|
soul.style.boxShadow = `0 0 ${60 * p.soul.scale}px ${20 * p.soul.scale}px ${p.soul.glow}`;
|
||||||
|
|
||||||
|
// Soul 2 (for "with u")
|
||||||
|
if (p.showSoul2) {
|
||||||
|
soul2.style.opacity = '1';
|
||||||
|
soul2.style.transform = 'translate(calc(-50% + 40px), calc(-50% - 20px))';
|
||||||
|
} else {
|
||||||
|
soul2.style.opacity = '0';
|
||||||
|
soul2.style.transform = 'translate(-50%, -50%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stars
|
||||||
|
stars.forEach(s => {
|
||||||
|
s.style.opacity = p.showStars ? (0.3 + Math.random() * 0.7) : '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Title colors
|
||||||
|
const title = document.querySelector(`[data-phase="${phase}"] .track-title`);
|
||||||
|
if (title) title.style.color = p.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersection Observer
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('visible');
|
||||||
|
const phase = entry.target.dataset.phase;
|
||||||
|
updateWorld(phase);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.5 });
|
||||||
|
|
||||||
|
scenes.forEach(s => observer.observe(s));
|
||||||
|
|
||||||
|
// Scroll progress
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
const scrollTop = window.scrollY;
|
||||||
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||||
|
const progress = (scrollTop / docHeight) * 100;
|
||||||
|
progressFill.style.height = progress + '%';
|
||||||
|
|
||||||
|
// Subtle blob movement based on scroll
|
||||||
|
const moveAmount = scrollTop * 0.02;
|
||||||
|
blob1.style.transform = `translate(${Math.sin(moveAmount) * 20}px, ${Math.cos(moveAmount) * 20}px)`;
|
||||||
|
blob2.style.transform = `translate(${Math.cos(moveAmount) * 15}px, ${Math.sin(moveAmount) * 15}px)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
scenes[0].classList.add('visible');
|
||||||
|
updateWorld('intro');
|
||||||
|
|
||||||
|
console.log('☀️ SURYA — one continuous journey');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
301
fix-clawdbot-permissions.sh
Executable file
@ -0,0 +1,301 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# ClawdBot Permission Hardening Script
|
||||||
|
# Generated: 2026-01-27
|
||||||
|
# Purpose: Fix file permissions in ~/.clawdbot for security hardening
|
||||||
|
#
|
||||||
|
# Permission Guidelines:
|
||||||
|
# - Directories: 700 (owner only)
|
||||||
|
# - Databases (.sqlite): 600 (owner read/write only)
|
||||||
|
# - Config files with secrets: 600
|
||||||
|
# - Scripts (.sh): 700 (owner execute only)
|
||||||
|
# - General files: 600 (no world-readable)
|
||||||
|
# - All files owned by jakeshore:staff
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CLAWDBOT_DIR="$HOME/.clawdbot"
|
||||||
|
LOG_FILE="$CLAWDBOT_DIR/workspace/permission-fix-$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
|
||||||
|
echo "ClawdBot Permission Hardening Script"
|
||||||
|
echo "====================================="
|
||||||
|
echo "Logging to: $LOG_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_ownership() {
|
||||||
|
local path="$1"
|
||||||
|
local current_owner=$(stat -f "%Su:%Sg" "$path" 2>/dev/null)
|
||||||
|
if [ "$current_owner" != "jakeshore:staff" ]; then
|
||||||
|
log "FIXING OWNERSHIP: $path ($current_owner -> jakeshore:staff)"
|
||||||
|
chown jakeshore:staff "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_permission() {
|
||||||
|
local path="$1"
|
||||||
|
local desired_perm="$2"
|
||||||
|
local current_perm=$(stat -f "%OLp" "$path" 2>/dev/null)
|
||||||
|
if [ "$current_perm" != "$desired_perm" ]; then
|
||||||
|
log "FIXING PERMISSION: $path ($current_perm -> $desired_perm)"
|
||||||
|
chmod "$desired_perm" "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 1: Fix file ownership (wheel -> staff)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Fix clawdbot.json which has wheel group
|
||||||
|
log "Checking for files with incorrect group ownership..."
|
||||||
|
fix_ownership "$CLAWDBOT_DIR/clawdbot.json"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 2: Harden top-level directory"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Root clawdbot directory should be 700
|
||||||
|
fix_permission "$CLAWDBOT_DIR" "700"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 3: Harden sensitive directories"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# These directories contain sensitive data - should be 700
|
||||||
|
SENSITIVE_DIRS=(
|
||||||
|
"$CLAWDBOT_DIR/agents"
|
||||||
|
"$CLAWDBOT_DIR/agents/main"
|
||||||
|
"$CLAWDBOT_DIR/agents/main/agent"
|
||||||
|
"$CLAWDBOT_DIR/agents/main/sessions"
|
||||||
|
"$CLAWDBOT_DIR/agents/imessage"
|
||||||
|
"$CLAWDBOT_DIR/agents/imessage/agent"
|
||||||
|
"$CLAWDBOT_DIR/agents/imessage/sessions"
|
||||||
|
"$CLAWDBOT_DIR/identity"
|
||||||
|
"$CLAWDBOT_DIR/memory"
|
||||||
|
"$CLAWDBOT_DIR/devices"
|
||||||
|
"$CLAWDBOT_DIR/subagents"
|
||||||
|
"$CLAWDBOT_DIR/tools"
|
||||||
|
"$CLAWDBOT_DIR/logs"
|
||||||
|
"$CLAWDBOT_DIR/cron"
|
||||||
|
"$CLAWDBOT_DIR/cron/runs"
|
||||||
|
"$CLAWDBOT_DIR/nodes"
|
||||||
|
"$CLAWDBOT_DIR/skills"
|
||||||
|
"$CLAWDBOT_DIR/browser"
|
||||||
|
"$CLAWDBOT_DIR/browser/clawd"
|
||||||
|
"$CLAWDBOT_DIR/browser/clawd/user-data"
|
||||||
|
"$CLAWDBOT_DIR/backups"
|
||||||
|
"$CLAWDBOT_DIR/lib"
|
||||||
|
"$CLAWDBOT_DIR/media"
|
||||||
|
"$CLAWDBOT_DIR/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
for dir in "${SENSITIVE_DIRS[@]}"; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
fix_permission "$dir" "700"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 4: Harden databases (600)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# SQLite databases - must be 600
|
||||||
|
while IFS= read -r -d '' dbfile; do
|
||||||
|
fix_permission "$dbfile" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR" -type f \( -name "*.sqlite" -o -name "*.sqlite3" -o -name "*.db" \) -print0 2>/dev/null)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 5: Harden sensitive config files (600)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Sensitive config files that may contain secrets/tokens
|
||||||
|
SENSITIVE_FILES=(
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.bak"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.bak.1"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.bak.2"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.bak.3"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.bak.4"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.backup-20260113-102502"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.backup-20260114-192759"
|
||||||
|
"$CLAWDBOT_DIR/clawdbot.json.backup-20260125-012611"
|
||||||
|
"$CLAWDBOT_DIR/identity/device.json"
|
||||||
|
"$CLAWDBOT_DIR/identity/device-auth.json"
|
||||||
|
"$CLAWDBOT_DIR/devices/paired.json"
|
||||||
|
"$CLAWDBOT_DIR/devices/pending.json"
|
||||||
|
"$CLAWDBOT_DIR/subagents/runs.json"
|
||||||
|
"$CLAWDBOT_DIR/nodes/paired.json"
|
||||||
|
"$CLAWDBOT_DIR/nodes/pending.json"
|
||||||
|
"$CLAWDBOT_DIR/cron/jobs.json"
|
||||||
|
"$CLAWDBOT_DIR/cron/jobs.json.bak"
|
||||||
|
"$CLAWDBOT_DIR/update-check.json"
|
||||||
|
"$CLAWDBOT_DIR/gateway.25337ee8.lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${SENSITIVE_FILES[@]}"; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
fix_permission "$file" "600"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Auth profile files in agents directories
|
||||||
|
while IFS= read -r -d '' authfile; do
|
||||||
|
fix_permission "$authfile" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/agents" -type f -name "auth-profiles.json" -print0 2>/dev/null)
|
||||||
|
|
||||||
|
# Session index files
|
||||||
|
while IFS= read -r -d '' sessfile; do
|
||||||
|
fix_permission "$sessfile" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/agents" -type f -name "sessions.json" -print0 2>/dev/null)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 6: Harden session logs (600)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Session log files may contain sensitive conversation data
|
||||||
|
while IFS= read -r -d '' sesslog; do
|
||||||
|
fix_permission "$sesslog" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/agents" -type f -name "*.jsonl" -print0 2>/dev/null)
|
||||||
|
while IFS= read -r -d '' sesslog; do
|
||||||
|
fix_permission "$sesslog" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/agents" -type f -name "*.jsonl.deleted.*" -print0 2>/dev/null)
|
||||||
|
|
||||||
|
# Cron run logs
|
||||||
|
while IFS= read -r -d '' cronlog; do
|
||||||
|
fix_permission "$cronlog" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/cron/runs" -type f -name "*.jsonl" -print0 2>/dev/null)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 7: Harden log files (600)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
while IFS= read -r -d '' logfile; do
|
||||||
|
fix_permission "$logfile" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/logs" -type f -print0 2>/dev/null)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 8: Harden shell scripts (700)"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Shell scripts should be 700 (owner execute only)
|
||||||
|
SCRIPTS=(
|
||||||
|
"$CLAWDBOT_DIR/backup-to-icloud.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
for script in "${SCRIPTS[@]}"; do
|
||||||
|
if [ -f "$script" ]; then
|
||||||
|
fix_permission "$script" "700"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 9: Harden backup directories"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Recursively fix backup directory
|
||||||
|
while IFS= read -r -d '' backupdir; do
|
||||||
|
fix_permission "$backupdir" "700"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/backups" -type d -print0 2>/dev/null)
|
||||||
|
|
||||||
|
while IFS= read -r -d '' backupfile; do
|
||||||
|
fix_permission "$backupfile" "600"
|
||||||
|
done < <(find "$CLAWDBOT_DIR/backups" -type f -print0 2>/dev/null)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 10: Verify symlinks"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
log "Checking symlinks for potential security issues..."
|
||||||
|
|
||||||
|
# Check skills symlinks - they point to ../../.agents which is outside clawdbot
|
||||||
|
# This is a potential concern as it points to user home directory
|
||||||
|
SYMLINKS_OUTSIDE=(
|
||||||
|
"$CLAWDBOT_DIR/skills/remotion-best-practices"
|
||||||
|
"$CLAWDBOT_DIR/skills/threejs-animation"
|
||||||
|
"$CLAWDBOT_DIR/skills/threejs-fundamentals"
|
||||||
|
"$CLAWDBOT_DIR/skills/threejs-geometry"
|
||||||
|
"$CLAWDBOT_DIR/skills/threejs-interaction"
|
||||||
|
"$CLAWDBOT_DIR/skills/threejs-lighting"
|
||||||
|
)
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "WARNING: The following symlinks point outside ~/.clawdbot:"
|
||||||
|
for link in "${SYMLINKS_OUTSIDE[@]}"; do
|
||||||
|
if [ -L "$link" ]; then
|
||||||
|
target=$(readlink "$link")
|
||||||
|
resolved=$(cd "$(dirname "$link")" && cd "$(dirname "$target")" 2>/dev/null && pwd)/$(basename "$target")
|
||||||
|
log " $link -> $target"
|
||||||
|
log " Resolves to: $resolved"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log ""
|
||||||
|
log "RECOMMENDATION: Consider copying these files into ~/.clawdbot/skills/ instead of symlinks"
|
||||||
|
log "to contain all sensitive data within the protected directory structure."
|
||||||
|
|
||||||
|
# Check node symlink (internal, acceptable)
|
||||||
|
if [ -L "$CLAWDBOT_DIR/tools/node" ]; then
|
||||||
|
target=$(readlink "$CLAWDBOT_DIR/tools/node")
|
||||||
|
log "OK: tools/node symlink is internal -> $target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "PHASE 11: Final verification"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "Checking for any remaining world-readable files..."
|
||||||
|
world_readable=$(find "$CLAWDBOT_DIR" -maxdepth 3 -type f -perm -004 2>/dev/null | grep -v node_modules | head -20)
|
||||||
|
if [ -n "$world_readable" ]; then
|
||||||
|
log "WARNING: These files are still world-readable:"
|
||||||
|
echo "$world_readable" | while read -r f; do
|
||||||
|
log " $f"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
log "OK: No world-readable files found in top 3 levels (excluding node_modules)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "Checking for any remaining world-executable directories..."
|
||||||
|
world_exec=$(find "$CLAWDBOT_DIR" -maxdepth 3 -type d -perm -005 2>/dev/null | grep -v node_modules | head -20)
|
||||||
|
if [ -n "$world_exec" ]; then
|
||||||
|
log "WARNING: These directories are still world-accessible:"
|
||||||
|
echo "$world_exec" | while read -r d; do
|
||||||
|
log " $d"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
log "OK: No world-accessible directories found in top 3 levels (excluding node_modules)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "SUMMARY"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "Permission hardening complete!"
|
||||||
|
log "Log file: $LOG_FILE"
|
||||||
|
log ""
|
||||||
|
log "Post-hardening checklist:"
|
||||||
|
log " 1. Verify clawdbot still functions correctly"
|
||||||
|
log " 2. Consider replacing symlinks with actual files"
|
||||||
|
log " 3. Review any .env or credentials files in workspace/"
|
||||||
|
log " 4. Ensure backups are stored securely"
|
||||||
|
log ""
|
||||||
|
log "Run 'cat $LOG_FILE' to see all changes made."
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done!"
|
||||||
@ -43,7 +43,13 @@
|
|||||||
.panel h3 { font-size: 0.7rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 12px; color: rgba(255,255,255,0.4); }
|
.panel h3 { font-size: 0.7rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 12px; color: rgba(255,255,255,0.4); }
|
||||||
.legend-item { display: flex; align-items: center; margin-bottom: 6px; font-size: 0.7rem; color: rgba(255,255,255,0.6); padding: 3px 6px; margin-left: -6px; border-radius: 4px; cursor: pointer; transition: all 0.2s; }
|
.legend-item { display: flex; align-items: center; margin-bottom: 6px; font-size: 0.7rem; color: rgba(255,255,255,0.6); padding: 3px 6px; margin-left: -6px; border-radius: 4px; cursor: pointer; transition: all 0.2s; }
|
||||||
.legend-item:hover { background: rgba(168,85,247,0.1); color: white; }
|
.legend-item:hover { background: rgba(168,85,247,0.1); color: white; }
|
||||||
|
.legend-item.disabled { opacity: 0.35; }
|
||||||
|
.legend-item.disabled .legend-color { box-shadow: none; }
|
||||||
.legend-color { width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 6px currentColor; }
|
.legend-color { width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 6px currentColor; }
|
||||||
|
.legend-checkbox { width: 14px; height: 14px; margin-right: 8px; accent-color: #a855f7; cursor: pointer; flex-shrink: 0; }
|
||||||
|
.legend-buttons { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.legend-btn { background: rgba(168,85,247,0.15); border: 1px solid rgba(168,85,247,0.2); color: rgba(255,255,255,0.6); padding: 4px 8px; border-radius: 4px; font-size: 0.6rem; font-family: 'Space Grotesk', sans-serif; cursor: pointer; transition: all 0.2s; }
|
||||||
|
.legend-btn:hover { background: rgba(168,85,247,0.3); color: white; }
|
||||||
|
|
||||||
#axes-legend { bottom: 20px; right: 20px; }
|
#axes-legend { bottom: 20px; right: 20px; }
|
||||||
.axis-item { display: flex; align-items: center; margin-bottom: 6px; font-size: 0.65rem; color: rgba(255,255,255,0.5); font-family: 'JetBrains Mono', monospace; }
|
.axis-item { display: flex; align-items: center; margin-bottom: 6px; font-size: 0.65rem; color: rgba(255,255,255,0.5); font-family: 'JetBrains Mono', monospace; }
|
||||||
@ -84,6 +90,11 @@
|
|||||||
|
|
||||||
<div id="legend" class="panel">
|
<div id="legend" class="panel">
|
||||||
<h3>Artists</h3>
|
<h3>Artists</h3>
|
||||||
|
<div class="legend-buttons">
|
||||||
|
<button class="legend-btn" id="selectAll">All</button>
|
||||||
|
<button class="legend-btn" id="selectNone">None</button>
|
||||||
|
<button class="legend-btn" id="selectMain">Das Only</button>
|
||||||
|
</div>
|
||||||
<div id="artist-legend"></div>
|
<div id="artist-legend"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -463,7 +474,9 @@
|
|||||||
function createConnections() {
|
function createConnections() {
|
||||||
connectionGroup.clear();
|
connectionGroup.clear();
|
||||||
for (let i = 0; i < artistGroups.length; i++) {
|
for (let i = 0; i < artistGroups.length; i++) {
|
||||||
|
if (!artistGroups[i].visible) continue; // skip hidden artists
|
||||||
for (let j = i + 1; j < artistGroups.length; j++) {
|
for (let j = i + 1; j < artistGroups.length; j++) {
|
||||||
|
if (!artistGroups[j].visible) continue; // skip hidden artists
|
||||||
const dist = artistGroups[i].position.distanceTo(artistGroups[j].position);
|
const dist = artistGroups[i].position.distanceTo(artistGroups[j].position);
|
||||||
if (dist < 2.5) {
|
if (dist < 2.5) {
|
||||||
const opacity = Math.max(0, 0.2 - dist * 0.06);
|
const opacity = Math.max(0, 0.2 - dist * 0.06);
|
||||||
@ -477,15 +490,71 @@
|
|||||||
}
|
}
|
||||||
createConnections();
|
createConnections();
|
||||||
|
|
||||||
// Legend
|
// Legend with checkboxes
|
||||||
const legendContainer = document.getElementById('artist-legend');
|
const legendContainer = document.getElementById('artist-legend');
|
||||||
artists.forEach(a => {
|
const artistVisibility = new Map(); // track which artists are visible
|
||||||
|
|
||||||
|
artists.forEach((a, idx) => {
|
||||||
|
artistVisibility.set(idx, true);
|
||||||
|
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'legend-item';
|
item.className = 'legend-item';
|
||||||
item.innerHTML = `<div class="legend-color" style="background:#${a.color.toString(16).padStart(6,'0')};color:#${a.color.toString(16).padStart(6,'0')};"></div><span>${a.name}${a.isMain?' ⭐':''}</span>`;
|
item.dataset.idx = idx;
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.className = 'legend-checkbox';
|
||||||
|
checkbox.checked = true;
|
||||||
|
checkbox.dataset.idx = idx;
|
||||||
|
|
||||||
|
const colorDot = document.createElement('div');
|
||||||
|
colorDot.className = 'legend-color';
|
||||||
|
colorDot.style.background = '#' + a.color.toString(16).padStart(6,'0');
|
||||||
|
colorDot.style.color = '#' + a.color.toString(16).padStart(6,'0');
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.textContent = a.name + (a.isMain ? ' ⭐' : '');
|
||||||
|
|
||||||
|
item.appendChild(checkbox);
|
||||||
|
item.appendChild(colorDot);
|
||||||
|
item.appendChild(nameSpan);
|
||||||
legendContainer.appendChild(item);
|
legendContainer.appendChild(item);
|
||||||
|
|
||||||
|
// Toggle on checkbox change
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
const visible = checkbox.checked;
|
||||||
|
artistVisibility.set(idx, visible);
|
||||||
|
artistGroups[idx].visible = visible;
|
||||||
|
item.classList.toggle('disabled', !visible);
|
||||||
|
createConnections(); // update connections
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on row (not checkbox) toggles too
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
if (e.target === checkbox) return;
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
checkbox.dispatchEvent(new Event('change'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Select all/none/main buttons
|
||||||
|
function setAllVisibility(visible, mainOnly = false) {
|
||||||
|
artists.forEach((a, idx) => {
|
||||||
|
const shouldShow = mainOnly ? a.isMain : visible;
|
||||||
|
artistVisibility.set(idx, shouldShow);
|
||||||
|
artistGroups[idx].visible = shouldShow;
|
||||||
|
const checkbox = legendContainer.querySelector(`input[data-idx="${idx}"]`);
|
||||||
|
const item = legendContainer.querySelector(`.legend-item[data-idx="${idx}"]`);
|
||||||
|
if (checkbox) checkbox.checked = shouldShow;
|
||||||
|
if (item) item.classList.toggle('disabled', !shouldShow);
|
||||||
|
});
|
||||||
|
createConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('selectAll').addEventListener('click', () => setAllVisibility(true));
|
||||||
|
document.getElementById('selectNone').addEventListener('click', () => setAllVisibility(false));
|
||||||
|
document.getElementById('selectMain').addEventListener('click', () => setAllVisibility(false, true));
|
||||||
|
|
||||||
// Raycasting
|
// Raycasting
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
const mouse = new THREE.Vector2();
|
const mouse = new THREE.Vector2();
|
||||||
|
|||||||
BIN
guide-compressed/2026-01-27-guide-01-cover.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
guide-compressed/2026-01-27-guide-02-rise.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
guide-compressed/2026-01-27-guide-03-whatisit.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
guide-compressed/2026-01-27-guide-04-trademark.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
guide-compressed/2026-01-27-guide-05-rebrand.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
guide-compressed/2026-01-27-guide-06-heist.jpg
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
guide-compressed/2026-01-27-guide-07-scam.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
guide-compressed/2026-01-27-guide-08-security.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
guide-compressed/2026-01-27-guide-09-portforward.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
guide-compressed/2026-01-27-guide-10-injection.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
guide-compressed/2026-01-27-guide-11-exposed.jpg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
guide-compressed/2026-01-27-guide-12-fault.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
guide-compressed/2026-01-27-guide-13-safe.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
guide-compressed/2026-01-27-guide-14-checklist.jpg
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
guide-compressed/2026-01-27-guide-15-conclusion.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
75
mcp-diagrams/GHL-MCP-Funnel/functions/api/waitlist.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Cloudflare Pages Function - handles waitlist submissions securely
|
||||||
|
// API key is stored as an environment secret, not in frontend code
|
||||||
|
|
||||||
|
export async function onRequestPost(context) {
|
||||||
|
const { request, env } = context;
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { firstName, lastName, phone, email } = body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!firstName || !phone) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Name and phone are required' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call GHL API with secret key
|
||||||
|
const ghlResponse = await fetch('https://rest.gohighlevel.com/v1/contacts/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${env.GHL_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
firstName,
|
||||||
|
lastName: lastName || '',
|
||||||
|
phone,
|
||||||
|
email: email || undefined,
|
||||||
|
tags: ['MCP Waitlist']
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ghlResponse.ok) {
|
||||||
|
const errorText = await ghlResponse.text();
|
||||||
|
console.error('GHL API error:', errorText);
|
||||||
|
return new Response(JSON.stringify({ error: 'Failed to add to waitlist' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ghlResponse.json();
|
||||||
|
return new Response(JSON.stringify({ success: true, contactId: result.contact?.id }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Waitlist error:', err);
|
||||||
|
return new Response(JSON.stringify({ error: 'Server error' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CORS preflight
|
||||||
|
export async function onRequestOptions() {
|
||||||
|
return new Response(null, {
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -67,13 +67,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hidden md:flex items-center gap-8">
|
<div class="hidden md:flex items-center gap-8">
|
||||||
<a href="#features" class="text-zinc-400 hover:text-white transition">Features</a>
|
<a href="#features" class="text-zinc-400 hover:text-white transition">Features</a>
|
||||||
<a href="#pricing" class="text-zinc-400 hover:text-white transition">Pricing</a>
|
<a href="#pricing" class="text-zinc-400 hover:text-white transition">Waitlist</a>
|
||||||
<a href="#faq" class="text-zinc-400 hover:text-white transition">FAQ</a>
|
<a href="#faq" class="text-zinc-400 hover:text-white transition">FAQ</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="#" class="text-zinc-400 hover:text-white transition">Sign In</a>
|
<a href="#" class="text-zinc-400 hover:text-white transition">Sign In</a>
|
||||||
<a href="#pricing" class="px-4 py-2 bg-brand-500 hover:bg-brand-600 rounded-lg font-medium transition">
|
<a href="#pricing" class="px-4 py-2 bg-brand-500 hover:bg-brand-600 rounded-lg font-medium transition">
|
||||||
Get Started
|
Join Waitlist
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,9 +99,9 @@
|
|||||||
|
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
|
||||||
<a href="#pricing" class="w-full sm:w-auto px-8 py-4 bg-brand-500 hover:bg-brand-600 rounded-xl font-semibold text-lg transition transform hover:scale-105">
|
<a href="#pricing" class="w-full sm:w-auto px-8 py-4 bg-brand-500 hover:bg-brand-600 rounded-xl font-semibold text-lg transition transform hover:scale-105">
|
||||||
Start Free Trial
|
Join the Waitlist
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/yourusername/ghl-mcp" class="w-full sm:w-auto px-8 py-4 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-xl font-semibold text-lg transition flex items-center justify-center gap-2">
|
<a href="https://github.com/BusyBee3333/The-Complete-GHL-MCP" class="w-full sm:w-auto px-8 py-4 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-xl font-semibold text-lg transition flex items-center justify-center gap-2">
|
||||||
<i data-lucide="github" class="w-5 h-5"></i>
|
<i data-lucide="github" class="w-5 h-5"></i>
|
||||||
View on GitHub
|
View on GitHub
|
||||||
</a>
|
</a>
|
||||||
@ -255,126 +255,140 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Pricing -->
|
<!-- Waitlist -->
|
||||||
<section id="pricing" class="py-20 border-t border-zinc-800/50">
|
<section id="pricing" class="py-20 border-t border-zinc-800/50">
|
||||||
<div class="max-w-6xl mx-auto px-6">
|
<div class="max-w-2xl mx-auto px-6">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-12">
|
||||||
<h2 class="text-3xl md:text-4xl font-bold mb-4">Simple, transparent pricing</h2>
|
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-brand-500/20 text-brand-400 text-sm font-medium mb-6">
|
||||||
<p class="text-xl text-zinc-400">Start free. Scale as you grow.</p>
|
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||||
|
Coming Soon
|
||||||
|
</div>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold mb-4">Join the Waitlist</h2>
|
||||||
|
<p class="text-xl text-zinc-400">Be the first to know when we launch. Early access + exclusive perks for waitlist members.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
<div class="bg-zinc-900/50 rounded-2xl border border-zinc-800 p-8">
|
||||||
<!-- Free -->
|
<form id="waitlist-form" class="space-y-6">
|
||||||
<div class="bg-zinc-900/50 rounded-2xl border border-zinc-800 p-8">
|
<div>
|
||||||
<h3 class="text-xl font-semibold mb-2">Starter</h3>
|
<label for="name" class="block text-sm font-medium text-zinc-300 mb-2">Name <span class="text-brand-400">*</span></label>
|
||||||
<p class="text-zinc-400 mb-6">Try it out, no credit card</p>
|
<input
|
||||||
<div class="mb-6">
|
type="text"
|
||||||
<span class="text-4xl font-bold">$0</span>
|
id="name"
|
||||||
<span class="text-zinc-500">/month</span>
|
name="name"
|
||||||
|
required
|
||||||
|
placeholder="Your full name"
|
||||||
|
class="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<ul class="space-y-3 mb-8">
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
<div>
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
<label for="phone" class="block text-sm font-medium text-zinc-300 mb-2">Phone <span class="text-brand-400">*</span></label>
|
||||||
1 GHL location
|
<input
|
||||||
</li>
|
type="tel"
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
id="phone"
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
name="phone"
|
||||||
1,000 API calls/month
|
required
|
||||||
</li>
|
placeholder="+1 (555) 000-0000"
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
class="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition"
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
>
|
||||||
Community support
|
</div>
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
<div>
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
<label for="email" class="block text-sm font-medium text-zinc-300 mb-2">Email <span class="text-zinc-500">(optional)</span></label>
|
||||||
All 461 tools
|
<input
|
||||||
</li>
|
type="email"
|
||||||
</ul>
|
id="email"
|
||||||
<a href="#" class="block w-full py-3 text-center bg-zinc-800 hover:bg-zinc-700 rounded-xl font-medium transition">
|
name="email"
|
||||||
Get Started Free
|
placeholder="you@company.com"
|
||||||
</a>
|
class="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="submit-btn"
|
||||||
|
class="w-full py-4 bg-brand-500 hover:bg-brand-600 rounded-xl font-semibold text-lg transition transform hover:scale-[1.02] flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span>Join the Waitlist</span>
|
||||||
|
<i data-lucide="arrow-right" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Success Message (hidden by default) -->
|
||||||
|
<div id="success-message" class="hidden text-center py-8">
|
||||||
|
<div class="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<i data-lucide="check" class="w-8 h-8 text-green-400"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-2xl font-bold mb-2">You're on the list!</h3>
|
||||||
|
<p class="text-zinc-400">We'll reach out as soon as we're ready for you.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pro -->
|
<!-- Error Message (hidden by default) -->
|
||||||
<div class="bg-gradient-to-b from-brand-500/10 to-transparent rounded-2xl border-2 border-brand-500/50 p-8 relative">
|
<div id="error-message" class="hidden mt-4 p-4 bg-red-500/20 border border-red-500/50 rounded-xl text-red-400 text-center">
|
||||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-4 py-1 bg-brand-500 rounded-full text-sm font-medium">
|
Something went wrong. Please try again.
|
||||||
Most Popular
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-2">Pro</h3>
|
|
||||||
<p class="text-zinc-400 mb-6">For growing agencies</p>
|
|
||||||
<div class="mb-6">
|
|
||||||
<span class="text-4xl font-bold">$49</span>
|
|
||||||
<span class="text-zinc-500">/month</span>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-3 mb-8">
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
5 GHL locations
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
25,000 API calls/month
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Priority support
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Usage dashboard
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Webhook notifications
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<a href="#" class="block w-full py-3 text-center bg-brand-500 hover:bg-brand-600 rounded-xl font-medium transition">
|
|
||||||
Start 14-Day Trial
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Agency -->
|
<p class="text-zinc-500 text-sm text-center mt-6">
|
||||||
<div class="bg-zinc-900/50 rounded-2xl border border-zinc-800 p-8">
|
<i data-lucide="lock" class="w-4 h-4 inline mr-1"></i>
|
||||||
<h3 class="text-xl font-semibold mb-2">Agency</h3>
|
We respect your privacy. No spam, ever.
|
||||||
<p class="text-zinc-400 mb-6">For serious operators</p>
|
</p>
|
||||||
<div class="mb-6">
|
|
||||||
<span class="text-4xl font-bold">$149</span>
|
|
||||||
<span class="text-zinc-500">/month</span>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-3 mb-8">
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Unlimited locations
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
100,000 API calls/month
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Dedicated support
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Team seats (5)
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Audit logs
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-2 text-zinc-300">
|
|
||||||
<i data-lucide="check" class="w-4 h-4 text-green-400"></i>
|
|
||||||
Custom integrations
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<a href="#" class="block w-full py-3 text-center bg-zinc-800 hover:bg-zinc-700 rounded-xl font-medium transition">
|
|
||||||
Contact Sales
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('waitlist-form').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const form = this;
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const successMsg = document.getElementById('success-message');
|
||||||
|
const errorMsg = document.getElementById('error-message');
|
||||||
|
|
||||||
|
// Get form values
|
||||||
|
const name = document.getElementById('name').value.trim();
|
||||||
|
const phone = document.getElementById('phone').value.trim();
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
|
||||||
|
// Split name into first/last
|
||||||
|
const nameParts = name.split(' ');
|
||||||
|
const firstName = nameParts[0] || '';
|
||||||
|
const lastName = nameParts.slice(1).join(' ') || '';
|
||||||
|
|
||||||
|
// Disable button and show loading
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span>Submitting...</span><svg class="animate-spin w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/waitlist', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
phone: phone,
|
||||||
|
email: email || undefined
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
form.classList.add('hidden');
|
||||||
|
successMsg.classList.remove('hidden');
|
||||||
|
errorMsg.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
throw new Error('API error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMsg.classList.remove('hidden');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = '<span>Join the Waitlist</span><i data-lucide="arrow-right" class="w-5 h-5"></i>';
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Open Source -->
|
<!-- Open Source -->
|
||||||
<section class="py-20 border-t border-zinc-800/50">
|
<section class="py-20 border-t border-zinc-800/50">
|
||||||
<div class="max-w-6xl mx-auto px-6">
|
<div class="max-w-6xl mx-auto px-6">
|
||||||
@ -393,7 +407,7 @@
|
|||||||
The entire MCP server is open source. Run it yourself, modify it, contribute back.
|
The entire MCP server is open source. Run it yourself, modify it, contribute back.
|
||||||
The hosted version just saves you the hassle.
|
The hosted version just saves you the hassle.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://github.com/yourusername/ghl-mcp" class="inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 font-medium">
|
<a href="https://github.com/BusyBee3333/The-Complete-GHL-MCP" class="inline-flex items-center gap-2 text-brand-400 hover:text-brand-300 font-medium">
|
||||||
View on GitHub
|
View on GitHub
|
||||||
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -495,7 +509,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
<a href="#pricing" class="w-full sm:w-auto px-8 py-4 bg-brand-500 hover:bg-brand-600 rounded-xl font-semibold text-lg transition">
|
<a href="#pricing" class="w-full sm:w-auto px-8 py-4 bg-brand-500 hover:bg-brand-600 rounded-xl font-semibold text-lg transition">
|
||||||
Start Free Trial
|
Join the Waitlist
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="w-full sm:w-auto px-8 py-4 bg-zinc-800 hover:bg-zinc-700 rounded-xl font-semibold text-lg transition">
|
<a href="#" class="w-full sm:w-auto px-8 py-4 bg-zinc-800 hover:bg-zinc-700 rounded-xl font-semibold text-lg transition">
|
||||||
Book a Demo
|
Book a Demo
|
||||||
@ -517,7 +531,7 @@
|
|||||||
<div class="flex items-center gap-8 text-zinc-400">
|
<div class="flex items-center gap-8 text-zinc-400">
|
||||||
<a href="#" class="hover:text-white transition">Privacy</a>
|
<a href="#" class="hover:text-white transition">Privacy</a>
|
||||||
<a href="#" class="hover:text-white transition">Terms</a>
|
<a href="#" class="hover:text-white transition">Terms</a>
|
||||||
<a href="https://github.com/yourusername/ghl-mcp" class="hover:text-white transition">GitHub</a>
|
<a href="https://github.com/BusyBee3333/The-Complete-GHL-MCP" class="hover:text-white transition">GitHub</a>
|
||||||
<a href="#" class="hover:text-white transition">Twitter</a>
|
<a href="#" class="hover:text-white transition">Twitter</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-zinc-500 text-sm">© 2026 GHL Connect. All rights reserved.</p>
|
<p class="text-zinc-500 text-sm">© 2026 GHL Connect. All rights reserved.</p>
|
||||||
@ -525,6 +539,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Sticky Floating CTA -->
|
||||||
|
<style>
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%, 100% { transform: rotate(-2deg); }
|
||||||
|
50% { transform: rotate(2deg); }
|
||||||
|
}
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 20px 0 rgba(14, 165, 233, 0.4), 0 0 40px 0 rgba(14, 165, 233, 0.2); }
|
||||||
|
50% { box-shadow: 0 0 30px 5px rgba(14, 165, 233, 0.6), 0 0 60px 10px rgba(14, 165, 233, 0.3); }
|
||||||
|
}
|
||||||
|
.sticky-btn {
|
||||||
|
animation: wiggle 2.5s ease-in-out infinite, glow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.sticky-btn:hover {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="sticky-cta" class="fixed bottom-6 right-6 z-50 opacity-0 translate-y-4 transition-all duration-300 pointer-events-none">
|
||||||
|
<a
|
||||||
|
href="#pricing"
|
||||||
|
class="sticky-btn flex items-center gap-2 px-6 py-3 bg-brand-500 hover:bg-brand-600 rounded-full font-semibold transition-all transform hover:scale-110"
|
||||||
|
>
|
||||||
|
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||||
|
<span>Join Waitlist</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Show/hide sticky CTA based on scroll position
|
||||||
|
const stickyCta = document.getElementById('sticky-cta');
|
||||||
|
const pricingSection = document.getElementById('pricing');
|
||||||
|
|
||||||
|
function updateStickyCta() {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const pricingTop = pricingSection.offsetTop;
|
||||||
|
const pricingBottom = pricingTop + pricingSection.offsetHeight;
|
||||||
|
const viewportBottom = scrollY + window.innerHeight;
|
||||||
|
|
||||||
|
// Show after scrolling 300px, hide when pricing section is in view
|
||||||
|
const shouldShow = scrollY > 300 && (viewportBottom < pricingTop || scrollY > pricingBottom);
|
||||||
|
|
||||||
|
if (shouldShow) {
|
||||||
|
stickyCta.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none');
|
||||||
|
stickyCta.classList.add('opacity-100', 'translate-y-0', 'pointer-events-auto');
|
||||||
|
} else {
|
||||||
|
stickyCta.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none');
|
||||||
|
stickyCta.classList.remove('opacity-100', 'translate-y-0', 'pointer-events-auto');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', updateStickyCta);
|
||||||
|
updateStickyCta();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>lucide.createIcons();</script>
|
<script>lucide.createIcons();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
1
mcp-diagrams/GoHighLevel-MCP
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 69db02d7cf9aca4fc39ae34b6f20f26617e57316
|
||||||
1
mcp-diagrams/ghl-mcp-public
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit c8c50cb56815901c8315b27f84046d528fe5f35e
|
||||||
47
mcp-diagrams/mcp-animation-framework/README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# MCP Animation Framework
|
||||||
|
|
||||||
|
Generate chat UI animation frames for MCP marketing assets.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create a config in `configs/` (see `configs/example.json`)
|
||||||
|
2. Run generator: `node generate.js configs/your-config.json`
|
||||||
|
3. Frames output to `output/{config-name}/`
|
||||||
|
|
||||||
|
## Config Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "servicetitan-dispatch",
|
||||||
|
"title": "AI Assistant",
|
||||||
|
"userPrompt": "Show me today's dispatch schedule and help me optimize routes",
|
||||||
|
"aiResponse": "Here's your optimized dispatch overview:",
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"icon": "ST",
|
||||||
|
"iconColor": "#f97316",
|
||||||
|
"title": "ServiceTitan",
|
||||||
|
"items": [...]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Panel Types
|
||||||
|
|
||||||
|
- `list` — contact/item list with status badges
|
||||||
|
- `chat` — mini chat preview
|
||||||
|
- `stats` — key metrics with comparison
|
||||||
|
- `chart` — donut/progress chart
|
||||||
|
- `table` — data table
|
||||||
|
- `map` — route/location preview
|
||||||
|
|
||||||
|
## Frames Generated
|
||||||
|
|
||||||
|
1. Empty chat
|
||||||
|
2. User typing
|
||||||
|
3. First exchange
|
||||||
|
4. AI typing
|
||||||
|
5. Loading panels
|
||||||
|
6. Full loaded
|
||||||
72
mcp-diagrams/mcp-animation-framework/capture-animation.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Capture animation frames and create GIF
|
||||||
|
*/
|
||||||
|
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const htmlFile = process.argv[2] || 'output/stripe-animation.html';
|
||||||
|
const outputDir = path.join(__dirname, 'output/stripe-frames');
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
// Load page
|
||||||
|
const filePath = path.resolve(__dirname, htmlFile);
|
||||||
|
await page.goto(`file://${filePath}`);
|
||||||
|
|
||||||
|
// Capture frames at key moments
|
||||||
|
const frames = [
|
||||||
|
{ delay: 0, name: '01' },
|
||||||
|
{ delay: 200, name: '02' },
|
||||||
|
{ delay: 400, name: '03' },
|
||||||
|
{ delay: 600, name: '04' },
|
||||||
|
{ delay: 800, name: '05' },
|
||||||
|
{ delay: 1000, name: '06' },
|
||||||
|
{ delay: 1200, name: '07' },
|
||||||
|
{ delay: 1400, name: '08' },
|
||||||
|
{ delay: 1600, name: '09' },
|
||||||
|
{ delay: 1800, name: '10' },
|
||||||
|
{ delay: 2000, name: '11' },
|
||||||
|
{ delay: 2200, name: '12' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const frame of frames) {
|
||||||
|
await new Promise(r => setTimeout(r, frame.delay === 0 ? 0 : 200));
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frame.name}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
console.log(`Captured frame ${frame.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create GIF using ffmpeg
|
||||||
|
console.log('Creating GIF...');
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate 5 -i ${outputDir}/frame-%02d.png -vf "scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" ${outputDir}/../stripe-animation.gif`, { stdio: 'inherit' });
|
||||||
|
console.log('GIF created: output/stripe-animation.gif');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg failed, trying convert...', e.message);
|
||||||
|
try {
|
||||||
|
execSync(`convert -delay 20 -loop 0 ${outputDir}/frame-*.png ${outputDir}/../stripe-animation.gif`, { stdio: 'inherit' });
|
||||||
|
console.log('GIF created with ImageMagick');
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('ImageMagick also failed:', e2.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
55
mcp-diagrams/mcp-animation-framework/capture-demo.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/demo-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/mcp-demo.html');
|
||||||
|
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function capture() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 10;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames...`);
|
||||||
|
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const scroll = i / (totalFrames - 1);
|
||||||
|
await page.evaluate(s => window.updateScene(s), scroll);
|
||||||
|
await new Promise(r => setTimeout(r, 20));
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${String(i+1).padStart(4,'0')}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 30 === 0) console.log(`${Math.round(scroll*100)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
console.log('Encoding...');
|
||||||
|
const mp4 = path.join(__dirname, 'output/mcp-demo.mp4');
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`✓ ${mp4}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
capture().catch(console.error);
|
||||||
54
mcp-diagrams/mcp-animation-framework/capture-full-flow.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/stripe-full-frames');
|
||||||
|
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
const filePath = path.resolve(__dirname, 'output/stripe-full-flow.html');
|
||||||
|
await page.goto(`file://${filePath}`);
|
||||||
|
|
||||||
|
// More frames for smoother animation (5 seconds total at 10fps = 50 frames)
|
||||||
|
const totalFrames = 55;
|
||||||
|
const frameInterval = 100; // 100ms between frames = 10fps
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const frameNum = String(i + 1).padStart(3, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
console.log(`Captured frame ${frameNum}/${totalFrames}`);
|
||||||
|
await new Promise(r => setTimeout(r, frameInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create GIF
|
||||||
|
console.log('Creating GIF...');
|
||||||
|
const gifPath = path.join(__dirname, 'output/stripe-full-flow.gif');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate 10 -i "${outputDir}/frame-%03d.png" -vf "scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=256[p];[s1][p]paletteuse=dither=bayer" "${gifPath}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`GIF created: ${gifPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
66
mcp-diagrams/mcp-animation-framework/capture-scroll-v2.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/scroll-v2-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/stripe-scroll-v2.html');
|
||||||
|
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
// 15 seconds at 30fps = 450 frames (longer for slower pacing)
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 15;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames...`);
|
||||||
|
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const scrollProgress = i / (totalFrames - 1);
|
||||||
|
|
||||||
|
await page.evaluate((progress) => {
|
||||||
|
window.updateScene(progress);
|
||||||
|
}, scrollProgress);
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 16));
|
||||||
|
|
||||||
|
const frameNum = String(i + 1).padStart(4, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 45 === 0) console.log(`Frame ${i + 1}/${totalFrames} (${Math.round(scrollProgress * 100)}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
console.log('Creating MP4...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-scroll-v2.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`\n✓ MP4 created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
79
mcp-diagrams/mcp-animation-framework/capture-scroll.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Scroll-driven animation capture
|
||||||
|
* Interpolates scrollProgress from 0 to 1 across frames
|
||||||
|
*/
|
||||||
|
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/scroll-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/stripe-scroll.html');
|
||||||
|
|
||||||
|
// Clean and create output dir
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
// 12 seconds at 30fps = 360 frames
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 12;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames...`);
|
||||||
|
|
||||||
|
// Load page and wait for fonts
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
// Calculate scroll progress (0 to 1)
|
||||||
|
// Add a small ease to the overall progression for smoother feel
|
||||||
|
const linearProgress = i / (totalFrames - 1);
|
||||||
|
const scrollProgress = linearProgress; // Keep linear for now, easing is in the HTML
|
||||||
|
|
||||||
|
// Update the scene
|
||||||
|
await page.evaluate((progress) => {
|
||||||
|
window.updateScene(progress);
|
||||||
|
}, scrollProgress);
|
||||||
|
|
||||||
|
// Small delay to let CSS transitions settle
|
||||||
|
await new Promise(r => setTimeout(r, 16));
|
||||||
|
|
||||||
|
const frameNum = String(i + 1).padStart(4, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 30 === 0) console.log(`Frame ${i + 1}/${totalFrames} (${Math.round(linearProgress * 100)}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create MP4
|
||||||
|
console.log('Creating MP4...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-scroll.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`\n✓ MP4 created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
66
mcp-diagrams/mcp-animation-framework/capture-template.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/template-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/stripe-template.html');
|
||||||
|
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
// 12 seconds at 30fps = 360 frames
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 12;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames...`);
|
||||||
|
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const scrollProgress = i / (totalFrames - 1);
|
||||||
|
|
||||||
|
await page.evaluate((progress) => {
|
||||||
|
window.updateScene(progress);
|
||||||
|
}, scrollProgress);
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 16));
|
||||||
|
|
||||||
|
const frameNum = String(i + 1).padStart(4, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 36 === 0) console.log(`Frame ${i + 1}/${totalFrames} (${Math.round(scrollProgress * 100)}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
console.log('Creating MP4...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-template.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`\n✓ MP4 created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
60
mcp-diagrams/mcp-animation-framework/capture-v4.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/v4-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/stripe-v4.html');
|
||||||
|
|
||||||
|
// Clean and create output dir
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
await page.goto(`file://${htmlFile}`);
|
||||||
|
|
||||||
|
// 12 seconds at 30fps = 360 frames
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 12;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
const frameInterval = 1000 / fps;
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames at ${fps}fps...`);
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const frameNum = String(i + 1).padStart(4, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
if (i % 30 === 0) console.log(`Frame ${i + 1}/${totalFrames}`);
|
||||||
|
await new Promise(r => setTimeout(r, frameInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create MP4
|
||||||
|
console.log('Creating MP4...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-v4.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`MP4 created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
78
mcp-diagrams/mcp-animation-framework/capture-v5.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/v5-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/stripe-v5.html');
|
||||||
|
|
||||||
|
// Clean and create output dir
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
// 10 seconds at 30fps = 300 frames
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 10;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
const frameInterval = 1000 / fps; // ~33.33ms per frame
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames at ${fps}fps...`);
|
||||||
|
|
||||||
|
// Load the page fresh for each capture run
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Let fonts load
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
// Reload to restart animation from 0
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const captureStart = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
// Calculate when this frame should be captured (in real time from start)
|
||||||
|
const targetTime = i * frameInterval;
|
||||||
|
const elapsed = Date.now() - captureStart;
|
||||||
|
|
||||||
|
// Wait until we reach the target time
|
||||||
|
if (elapsed < targetTime) {
|
||||||
|
await new Promise(r => setTimeout(r, targetTime - elapsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameNum = String(i + 1).padStart(4, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 30 === 0) console.log(`Frame ${i + 1}/${totalFrames} (${Math.round((i/totalFrames)*100)}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create MP4
|
||||||
|
console.log('Creating MP4...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-v5.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`\nMP4 created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
74
mcp-diagrams/mcp-animation-framework/capture-v6.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/v6-frames');
|
||||||
|
const htmlFile = path.join(__dirname, 'output/stripe-v6.html');
|
||||||
|
|
||||||
|
// Clean and create output dir
|
||||||
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
|
||||||
|
// 10 seconds at 30fps = 300 frames
|
||||||
|
const fps = 30;
|
||||||
|
const duration = 10;
|
||||||
|
const totalFrames = fps * duration;
|
||||||
|
const frameInterval = 1000 / fps;
|
||||||
|
|
||||||
|
console.log(`Capturing ${totalFrames} frames at ${fps}fps...`);
|
||||||
|
|
||||||
|
// Load page and wait for fonts
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
|
||||||
|
// Reload to restart animation from t=0
|
||||||
|
await page.goto(`file://${htmlFile}`, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const captureStart = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const targetTime = i * frameInterval;
|
||||||
|
const elapsed = Date.now() - captureStart;
|
||||||
|
|
||||||
|
if (elapsed < targetTime) {
|
||||||
|
await new Promise(r => setTimeout(r, targetTime - elapsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameNum = String(i + 1).padStart(4, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 30 === 0) console.log(`Frame ${i + 1}/${totalFrames} (${Math.round((i/totalFrames)*100)}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create MP4
|
||||||
|
console.log('Creating MP4...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-v6.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${outputDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 18 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`\nMP4 created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
57
mcp-diagrams/mcp-animation-framework/capture-web-embed.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const outputDir = path.join(__dirname, 'output/web-embed-frames');
|
||||||
|
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
async function captureFrames() {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1280, height: 720 });
|
||||||
|
|
||||||
|
const filePath = path.resolve(__dirname, 'web-embed/stripe-simulation.html');
|
||||||
|
await page.goto(`file://${filePath}`);
|
||||||
|
|
||||||
|
// Wait for animation to start
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
// Capture 12 seconds at 10fps = 120 frames
|
||||||
|
const totalFrames = 120;
|
||||||
|
const frameInterval = 100;
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const frameNum = String(i + 1).padStart(3, '0');
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(outputDir, `frame-${frameNum}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
if (i % 10 === 0) console.log(`Captured frame ${frameNum}/${totalFrames}`);
|
||||||
|
await new Promise(r => setTimeout(r, frameInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Create MP4
|
||||||
|
console.log('Creating video...');
|
||||||
|
const mp4Path = path.join(__dirname, 'output/stripe-web-simulation.mp4');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`ffmpeg -y -framerate 10 -i "${outputDir}/frame-%03d.png" -c:v libx264 -pix_fmt yuv420p -crf 23 "${mp4Path}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`Video created: ${mp4Path}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('FFmpeg error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captureFrames().catch(console.error);
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "servicetitan-dispatch",
|
||||||
|
"title": "AI Assistant",
|
||||||
|
"dimensions": { "width": 1920, "height": 1080 },
|
||||||
|
"userPrompt": "Show me today's dispatch schedule and help me optimize routes",
|
||||||
|
"aiResponse": "Here's your dispatch schedule with optimized routing:",
|
||||||
|
"panel": {
|
||||||
|
"type": "list",
|
||||||
|
"icon": "ST",
|
||||||
|
"iconColor": "#f97316",
|
||||||
|
"title": "ServiceTitan",
|
||||||
|
"subtitle": "Today's Jobs • 5 scheduled",
|
||||||
|
"items": [
|
||||||
|
{ "avatar": "JT", "name": "John's HVAC - AC Repair", "status": "en-route", "statusColor": "#3b82f6", "meta": "8:30 AM • 2.1 mi" },
|
||||||
|
{ "avatar": "SM", "name": "Smith Residence - Plumbing", "status": "scheduled", "statusColor": "#8b5cf6", "meta": "10:00 AM • 4.8 mi" },
|
||||||
|
{ "avatar": "WH", "name": "Wilson Home - Furnace", "status": "scheduled", "statusColor": "#8b5cf6", "meta": "12:30 PM • 1.2 mi" },
|
||||||
|
{ "avatar": "AC", "name": "Acme Office - Maintenance", "status": "scheduled", "statusColor": "#8b5cf6", "meta": "2:00 PM • 3.5 mi" },
|
||||||
|
{ "avatar": "BR", "name": "Brown Family - Water Heater", "status": "urgent", "statusColor": "#dc2626", "meta": "ASAP • 0.8 mi" }
|
||||||
|
],
|
||||||
|
"footer": {
|
||||||
|
"left": "Total Drive: 2.4 hrs",
|
||||||
|
"right": "Est. Revenue: $2,450"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
452
mcp-diagrams/mcp-animation-framework/gen-three.js
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { mcpConfigs } from './mcp-configs.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// ONLY these 3 MCPs
|
||||||
|
const TARGETS = ['pipedrive', 'helpscout', 'basecamp'];
|
||||||
|
const configs = mcpConfigs.filter(c => TARGETS.includes(c.id));
|
||||||
|
|
||||||
|
console.log('Configs found:', configs.map(c => c.id));
|
||||||
|
|
||||||
|
function generateHTML(config) {
|
||||||
|
const { name, color, question, statLabel, statValue, statLabel2, statValue2, rows, insight } = config;
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1920, height=1080">
|
||||||
|
<title>${name} MCP Demo</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a12;
|
||||||
|
--card: #12121c;
|
||||||
|
--border: rgba(255,255,255,0.08);
|
||||||
|
--text: #ffffff;
|
||||||
|
--text-dim: #888;
|
||||||
|
--accent: ${color};
|
||||||
|
--gradient: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
--green: #22c55e;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.scene {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, ${color}15 0%, transparent 60%);
|
||||||
|
}
|
||||||
|
.chat {
|
||||||
|
width: 900px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 40px 80px rgba(0,0,0,0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.chat.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.chat-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.dots { display: flex; gap: 6px; }
|
||||||
|
.dot { width: 12px; height: 12px; border-radius: 50%; }
|
||||||
|
.dot.red { background: #ff5f56; }
|
||||||
|
.dot.yellow { background: #ffbd2e; }
|
||||||
|
.dot.green { background: #27ca40; }
|
||||||
|
.chat-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.msg.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.msg.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: var(--gradient);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.msg.ai {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
.typing {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.typing.visible { opacity: 1; }
|
||||||
|
.typing span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--text-dim);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1.2s infinite;
|
||||||
|
}
|
||||||
|
.typing span:nth-child(2) { animation-delay: 0.15s; }
|
||||||
|
.typing span:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 60%, 100% { transform: translateY(0); }
|
||||||
|
30% { transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
.mcp-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 12px;
|
||||||
|
box-shadow: 0 4px 20px ${color}25;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.mcp-card.visible { opacity: 1; transform: scale(1); }
|
||||||
|
.mcp-head {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mcp-logo {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.mcp-head-text { font-size: 13px; color: #1a1a2e; font-weight: 600; }
|
||||||
|
.mcp-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.stat-value { font-size: 20px; font-weight: 700; color: #1a1a2e; }
|
||||||
|
.stat-value.green { color: var(--green); }
|
||||||
|
.stat-label { font-size: 10px; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||||
|
.mcp-rows { padding: 8px 0; }
|
||||||
|
.mcp-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.mcp-row.visible { opacity: 1; transform: translateX(0); }
|
||||||
|
.mcp-row-label { font-weight: 500; }
|
||||||
|
.mcp-row-value { font-weight: 600; color: #64748b; }
|
||||||
|
.insight {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.insight.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.insight-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--green);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.insight-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.input-area {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.input-box.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px ${color}25;
|
||||||
|
}
|
||||||
|
.input-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.input-text .placeholder { color: var(--text-dim); }
|
||||||
|
.input-text .cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-left: 1px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
animation: blink 0.8s step-end infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
.send-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card);
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.send-btn.ready { background: var(--gradient); }
|
||||||
|
.send-btn svg { width: 16px; height: 16px; fill: #fff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="scene">
|
||||||
|
<div class="chat" id="chat">
|
||||||
|
<div class="chat-header">
|
||||||
|
<div class="dots">
|
||||||
|
<span class="dot red"></span>
|
||||||
|
<span class="dot yellow"></span>
|
||||||
|
<span class="dot green"></span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-title">AI Assistant · ${name} Connected</div>
|
||||||
|
<div style="width:50px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="messages">
|
||||||
|
<div class="msg user" id="userMsg">${question}</div>
|
||||||
|
<div class="typing" id="typing">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="msg ai" id="aiMsg">
|
||||||
|
Here's what I found:
|
||||||
|
<div class="mcp-card" id="mcpCard">
|
||||||
|
<div class="mcp-head">
|
||||||
|
<span class="mcp-logo">${name.substring(0, 2).toLowerCase()}</span>
|
||||||
|
<span class="mcp-head-text">${name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-stats">
|
||||||
|
<div>
|
||||||
|
<div class="stat-value green">${statValue}</div>
|
||||||
|
<div class="stat-label">${statLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="stat-value">${statValue2}</div>
|
||||||
|
<div class="stat-label">${statLabel2}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-rows">
|
||||||
|
<div class="mcp-row" id="row1">
|
||||||
|
<span class="mcp-row-label">${rows[0].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[0].value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-row" id="row2">
|
||||||
|
<span class="mcp-row-label">${rows[1].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[1].value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-row" id="row3">
|
||||||
|
<span class="mcp-row-label">${rows[2].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[2].value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="insight" id="insight">
|
||||||
|
<div class="insight-title">💡 Recommendation</div>
|
||||||
|
<div class="insight-text">${insight}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-box" id="inputBox">
|
||||||
|
<div class="input-text" id="inputText"><span class="placeholder">Ask anything...</span></div>
|
||||||
|
<button class="send-btn" id="sendBtn">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M2 21L23 12 2 3v7l15 2-15 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const question = ${JSON.stringify(question)};
|
||||||
|
function ease(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
||||||
|
function phase(scroll, start, end) {
|
||||||
|
if (scroll < start) return 0;
|
||||||
|
if (scroll > end) return 1;
|
||||||
|
return (scroll - start) / (end - start);
|
||||||
|
}
|
||||||
|
function updateScene(scroll) {
|
||||||
|
const chat = document.getElementById('chat');
|
||||||
|
const inputBox = document.getElementById('inputBox');
|
||||||
|
const inputText = document.getElementById('inputText');
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const userMsg = document.getElementById('userMsg');
|
||||||
|
const typing = document.getElementById('typing');
|
||||||
|
const aiMsg = document.getElementById('aiMsg');
|
||||||
|
const mcpCard = document.getElementById('mcpCard');
|
||||||
|
const row1 = document.getElementById('row1');
|
||||||
|
const row2 = document.getElementById('row2');
|
||||||
|
const row3 = document.getElementById('row3');
|
||||||
|
const insight = document.getElementById('insight');
|
||||||
|
|
||||||
|
chat.classList.toggle('visible', scroll >= 0.02);
|
||||||
|
|
||||||
|
const typeProgress = phase(scroll, 0.08, 0.28);
|
||||||
|
const chars = Math.floor(ease(typeProgress) * question.length);
|
||||||
|
|
||||||
|
if (scroll < 0.08) {
|
||||||
|
inputText.innerHTML = '<span class="placeholder">Ask anything...</span>';
|
||||||
|
inputBox.classList.remove('active');
|
||||||
|
sendBtn.classList.remove('ready');
|
||||||
|
} else if (scroll < 0.30) {
|
||||||
|
inputBox.classList.add('active');
|
||||||
|
inputText.innerHTML = (chars > 0 ? question.substring(0, chars) : '') + '<span class="cursor"></span>';
|
||||||
|
sendBtn.classList.toggle('ready', chars === question.length);
|
||||||
|
} else {
|
||||||
|
inputBox.classList.remove('active');
|
||||||
|
inputText.innerHTML = '<span class="placeholder">Ask anything...</span>';
|
||||||
|
sendBtn.classList.remove('ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
userMsg.classList.toggle('visible', scroll >= 0.30);
|
||||||
|
typing.classList.toggle('visible', scroll >= 0.38 && scroll < 0.48);
|
||||||
|
aiMsg.classList.toggle('visible', scroll >= 0.48);
|
||||||
|
mcpCard.classList.toggle('visible', scroll >= 0.55);
|
||||||
|
row1.classList.toggle('visible', scroll >= 0.62);
|
||||||
|
row2.classList.toggle('visible', scroll >= 0.67);
|
||||||
|
row3.classList.toggle('visible', scroll >= 0.72);
|
||||||
|
insight.classList.toggle('visible', scroll >= 0.80);
|
||||||
|
}
|
||||||
|
window.updateScene = updateScene;
|
||||||
|
updateScene(0);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderVideo(config, browser) {
|
||||||
|
const htmlPath = path.join(__dirname, 'output', config.id + '.html');
|
||||||
|
const framesDir = path.join(__dirname, 'output', config.id + '-frames');
|
||||||
|
const mp4Path = path.join(__dirname, 'output', config.id + '.mp4');
|
||||||
|
|
||||||
|
fs.writeFileSync(htmlPath, generateHTML(config));
|
||||||
|
fs.rmSync(framesDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(framesDir, { recursive: true });
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
|
||||||
|
const fps = 30;
|
||||||
|
const totalFrames = 300;
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const scroll = i / (totalFrames - 1);
|
||||||
|
await page.evaluate(s => window.updateScene(s), scroll);
|
||||||
|
await new Promise(r => setTimeout(r, 15));
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(framesDir, 'frame-' + String(i+1).padStart(4,'0') + '.png'),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.close();
|
||||||
|
|
||||||
|
execSync('ffmpeg -y -framerate ' + fps + ' -i "' + framesDir + '/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 20 "' + mp4Path + '"', { stdio: 'pipe' });
|
||||||
|
fs.rmSync(framesDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
return mp4Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('\n🎬 Generating ' + configs.length + ' MCP videos: ' + configs.map(c => c.name).join(', ') + '\n');
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < configs.length; i++) {
|
||||||
|
const config = configs[i];
|
||||||
|
const start = Date.now();
|
||||||
|
process.stdout.write('[' + (i+1) + '/' + configs.length + '] ' + config.name + '... ');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renderVideo(config, browser);
|
||||||
|
console.log('✓ (' + Math.round((Date.now() - start) / 1000) + 's)');
|
||||||
|
results.push({ name: config.name, id: config.id, success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.log('✗ ' + err.message);
|
||||||
|
results.push({ name: config.name, id: config.id, success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log('\n✓ Done! Videos saved to output/\n');
|
||||||
|
console.log('Results:');
|
||||||
|
results.forEach(r => console.log(' ' + (r.success ? '✓' : '✗') + ' ' + r.name + (r.error ? ': ' + r.error : '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
448
mcp-diagrams/mcp-animation-framework/generate-all.js
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { mcpConfigs } from './mcp-configs.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
function generateHTML(config) {
|
||||||
|
const { name, color, question, statLabel, statValue, statLabel2, statValue2, rows, insight } = config;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1920, height=1080">
|
||||||
|
<title>${name} MCP Demo</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a12;
|
||||||
|
--card: #12121c;
|
||||||
|
--border: rgba(255,255,255,0.08);
|
||||||
|
--text: #ffffff;
|
||||||
|
--text-dim: #888;
|
||||||
|
--accent: ${color};
|
||||||
|
--gradient: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
--green: #22c55e;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.scene {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, ${color}15 0%, transparent 60%);
|
||||||
|
}
|
||||||
|
.chat {
|
||||||
|
width: 900px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 40px 80px rgba(0,0,0,0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.chat.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.chat-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.dots { display: flex; gap: 6px; }
|
||||||
|
.dot { width: 12px; height: 12px; border-radius: 50%; }
|
||||||
|
.dot.red { background: #ff5f56; }
|
||||||
|
.dot.yellow { background: #ffbd2e; }
|
||||||
|
.dot.green { background: #27ca40; }
|
||||||
|
.chat-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.msg.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.msg.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: var(--gradient);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.msg.ai {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
.typing {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.typing.visible { opacity: 1; }
|
||||||
|
.typing span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--text-dim);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1.2s infinite;
|
||||||
|
}
|
||||||
|
.typing span:nth-child(2) { animation-delay: 0.15s; }
|
||||||
|
.typing span:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 60%, 100% { transform: translateY(0); }
|
||||||
|
30% { transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
.mcp-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 12px;
|
||||||
|
box-shadow: 0 4px 20px ${color}25;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.mcp-card.visible { opacity: 1; transform: scale(1); }
|
||||||
|
.mcp-head {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mcp-logo {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.mcp-head-text { font-size: 13px; color: #1a1a2e; font-weight: 600; }
|
||||||
|
.mcp-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.stat-value { font-size: 20px; font-weight: 700; color: #1a1a2e; }
|
||||||
|
.stat-value.green { color: var(--green); }
|
||||||
|
.stat-label { font-size: 10px; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||||
|
.mcp-rows { padding: 8px 0; }
|
||||||
|
.mcp-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.mcp-row.visible { opacity: 1; transform: translateX(0); }
|
||||||
|
.mcp-row-label { font-weight: 500; }
|
||||||
|
.mcp-row-value { font-weight: 600; color: #64748b; }
|
||||||
|
.insight {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.insight.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.insight-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--green);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.insight-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.input-area {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.input-box.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px ${color}25;
|
||||||
|
}
|
||||||
|
.input-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.input-text .placeholder { color: var(--text-dim); }
|
||||||
|
.input-text .cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-left: 1px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
animation: blink 0.8s step-end infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
.send-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card);
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.send-btn.ready { background: var(--gradient); }
|
||||||
|
.send-btn svg { width: 16px; height: 16px; fill: #fff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="scene">
|
||||||
|
<div class="chat" id="chat">
|
||||||
|
<div class="chat-header">
|
||||||
|
<div class="dots">
|
||||||
|
<span class="dot red"></span>
|
||||||
|
<span class="dot yellow"></span>
|
||||||
|
<span class="dot green"></span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-title">AI Assistant · ${name} Connected</div>
|
||||||
|
<div style="width:50px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="messages">
|
||||||
|
<div class="msg user" id="userMsg">${question}</div>
|
||||||
|
<div class="typing" id="typing">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="msg ai" id="aiMsg">
|
||||||
|
Here's what I found:
|
||||||
|
<div class="mcp-card" id="mcpCard">
|
||||||
|
<div class="mcp-head">
|
||||||
|
<span class="mcp-logo">${name.substring(0, 2).toLowerCase()}</span>
|
||||||
|
<span class="mcp-head-text">${name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-stats">
|
||||||
|
<div>
|
||||||
|
<div class="stat-value green">${statValue}</div>
|
||||||
|
<div class="stat-label">${statLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="stat-value">${statValue2}</div>
|
||||||
|
<div class="stat-label">${statLabel2}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-rows">
|
||||||
|
<div class="mcp-row" id="row1">
|
||||||
|
<span class="mcp-row-label">${rows[0].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[0].value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-row" id="row2">
|
||||||
|
<span class="mcp-row-label">${rows[1].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[1].value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-row" id="row3">
|
||||||
|
<span class="mcp-row-label">${rows[2].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[2].value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="insight" id="insight">
|
||||||
|
<div class="insight-title">💡 Recommendation</div>
|
||||||
|
<div class="insight-text">${insight}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-box" id="inputBox">
|
||||||
|
<div class="input-text" id="inputText"><span class="placeholder">Ask anything...</span></div>
|
||||||
|
<button class="send-btn" id="sendBtn">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M2 21L23 12 2 3v7l15 2-15 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const question = ${JSON.stringify(question)};
|
||||||
|
function ease(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
||||||
|
function phase(scroll, start, end) {
|
||||||
|
if (scroll < start) return 0;
|
||||||
|
if (scroll > end) return 1;
|
||||||
|
return (scroll - start) / (end - start);
|
||||||
|
}
|
||||||
|
function updateScene(scroll) {
|
||||||
|
const chat = document.getElementById('chat');
|
||||||
|
const inputBox = document.getElementById('inputBox');
|
||||||
|
const inputText = document.getElementById('inputText');
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const userMsg = document.getElementById('userMsg');
|
||||||
|
const typing = document.getElementById('typing');
|
||||||
|
const aiMsg = document.getElementById('aiMsg');
|
||||||
|
const mcpCard = document.getElementById('mcpCard');
|
||||||
|
const row1 = document.getElementById('row1');
|
||||||
|
const row2 = document.getElementById('row2');
|
||||||
|
const row3 = document.getElementById('row3');
|
||||||
|
const insight = document.getElementById('insight');
|
||||||
|
|
||||||
|
chat.classList.toggle('visible', scroll >= 0.02);
|
||||||
|
|
||||||
|
const typeProgress = phase(scroll, 0.08, 0.28);
|
||||||
|
const chars = Math.floor(ease(typeProgress) * question.length);
|
||||||
|
|
||||||
|
if (scroll < 0.08) {
|
||||||
|
inputText.innerHTML = '<span class="placeholder">Ask anything...</span>';
|
||||||
|
inputBox.classList.remove('active');
|
||||||
|
sendBtn.classList.remove('ready');
|
||||||
|
} else if (scroll < 0.30) {
|
||||||
|
inputBox.classList.add('active');
|
||||||
|
inputText.innerHTML = (chars > 0 ? question.substring(0, chars) : '') + '<span class="cursor"></span>';
|
||||||
|
sendBtn.classList.toggle('ready', chars === question.length);
|
||||||
|
} else {
|
||||||
|
inputBox.classList.remove('active');
|
||||||
|
inputText.innerHTML = '<span class="placeholder">Ask anything...</span>';
|
||||||
|
sendBtn.classList.remove('ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
userMsg.classList.toggle('visible', scroll >= 0.30);
|
||||||
|
typing.classList.toggle('visible', scroll >= 0.38 && scroll < 0.48);
|
||||||
|
aiMsg.classList.toggle('visible', scroll >= 0.48);
|
||||||
|
mcpCard.classList.toggle('visible', scroll >= 0.55);
|
||||||
|
row1.classList.toggle('visible', scroll >= 0.62);
|
||||||
|
row2.classList.toggle('visible', scroll >= 0.67);
|
||||||
|
row3.classList.toggle('visible', scroll >= 0.72);
|
||||||
|
insight.classList.toggle('visible', scroll >= 0.80);
|
||||||
|
}
|
||||||
|
window.updateScene = updateScene;
|
||||||
|
updateScene(0);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderVideo(config, browser) {
|
||||||
|
const htmlPath = path.join(__dirname, 'output', `${config.id}.html`);
|
||||||
|
const framesDir = path.join(__dirname, 'output', `${config.id}-frames`);
|
||||||
|
const mp4Path = path.join(__dirname, 'output', `${config.id}.mp4`);
|
||||||
|
|
||||||
|
// Generate HTML
|
||||||
|
fs.writeFileSync(htmlPath, generateHTML(config));
|
||||||
|
|
||||||
|
// Create frames directory
|
||||||
|
fs.rmSync(framesDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(framesDir, { recursive: true });
|
||||||
|
|
||||||
|
// Capture frames
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
|
||||||
|
const fps = 30;
|
||||||
|
const totalFrames = 300; // 10 seconds
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const scroll = i / (totalFrames - 1);
|
||||||
|
await page.evaluate(s => window.updateScene(s), scroll);
|
||||||
|
await new Promise(r => setTimeout(r, 15));
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(framesDir, `frame-${String(i+1).padStart(4,'0')}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.close();
|
||||||
|
|
||||||
|
// Encode video
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${framesDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 20 "${mp4Path}"`, { stdio: 'pipe' });
|
||||||
|
|
||||||
|
// Cleanup frames
|
||||||
|
fs.rmSync(framesDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
return mp4Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`\n🎬 Generating ${mcpConfigs.length} MCP videos...\n`);
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < mcpConfigs.length; i++) {
|
||||||
|
const config = mcpConfigs[i];
|
||||||
|
const start = Date.now();
|
||||||
|
process.stdout.write(`[${i+1}/${mcpConfigs.length}] ${config.name}... `);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renderVideo(config, browser);
|
||||||
|
console.log(`✓ (${Math.round((Date.now() - start) / 1000)}s)`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log(`\n✓ Done! Videos saved to output/\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
458
mcp-diagrams/mcp-animation-framework/generate-batch.js
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { mcpConfigs } from './mcp-configs.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Only render these 3 MCPs
|
||||||
|
const targetIds = ['lightspeed', 'bigcommerce', 'toast'];
|
||||||
|
const configs = mcpConfigs.filter(c => targetIds.includes(c.id));
|
||||||
|
|
||||||
|
function generateHTML(config) {
|
||||||
|
const { name, color, question, statLabel, statValue, statLabel2, statValue2, rows, insight } = config;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1920, height=1080">
|
||||||
|
<title>${name} MCP Demo</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a12;
|
||||||
|
--card: #12121c;
|
||||||
|
--border: rgba(255,255,255,0.08);
|
||||||
|
--text: #ffffff;
|
||||||
|
--text-dim: #888;
|
||||||
|
--accent: ${color};
|
||||||
|
--gradient: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
--green: #22c55e;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.scene {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, ${color}15 0%, transparent 60%);
|
||||||
|
}
|
||||||
|
.chat {
|
||||||
|
width: 900px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 40px 80px rgba(0,0,0,0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.chat.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.chat-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.dots { display: flex; gap: 6px; }
|
||||||
|
.dot { width: 12px; height: 12px; border-radius: 50%; }
|
||||||
|
.dot.red { background: #ff5f56; }
|
||||||
|
.dot.yellow { background: #ffbd2e; }
|
||||||
|
.dot.green { background: #27ca40; }
|
||||||
|
.chat-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.msg.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.msg.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
background: var(--gradient);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.msg.ai {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
.typing {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.typing.visible { opacity: 1; }
|
||||||
|
.typing span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--text-dim);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1.2s infinite;
|
||||||
|
}
|
||||||
|
.typing span:nth-child(2) { animation-delay: 0.15s; }
|
||||||
|
.typing span:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 60%, 100% { transform: translateY(0); }
|
||||||
|
30% { transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
.mcp-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 12px;
|
||||||
|
box-shadow: 0 4px 20px ${color}25;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.mcp-card.visible { opacity: 1; transform: scale(1); }
|
||||||
|
.mcp-head {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mcp-logo {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.mcp-head-text { font-size: 13px; color: #1a1a2e; font-weight: 600; }
|
||||||
|
.mcp-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.stat-value { font-size: 20px; font-weight: 700; color: #1a1a2e; }
|
||||||
|
.stat-value.green { color: var(--green); }
|
||||||
|
.stat-label { font-size: 10px; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||||
|
.mcp-rows { padding: 8px 0; }
|
||||||
|
.mcp-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.mcp-row.visible { opacity: 1; transform: translateX(0); }
|
||||||
|
.mcp-row-label { font-weight: 500; }
|
||||||
|
.mcp-row-value { font-weight: 600; color: #64748b; }
|
||||||
|
.insight {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.insight.visible { opacity: 1; transform: translateY(0); }
|
||||||
|
.insight-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--green);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.insight-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.input-area {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.input-box.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px ${color}25;
|
||||||
|
}
|
||||||
|
.input-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.input-text .placeholder { color: var(--text-dim); }
|
||||||
|
.input-text .cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin-left: 1px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
animation: blink 0.8s step-end infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
.send-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--card);
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.send-btn.ready { background: var(--gradient); }
|
||||||
|
.send-btn svg { width: 16px; height: 16px; fill: #fff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="scene">
|
||||||
|
<div class="chat" id="chat">
|
||||||
|
<div class="chat-header">
|
||||||
|
<div class="dots">
|
||||||
|
<span class="dot red"></span>
|
||||||
|
<span class="dot yellow"></span>
|
||||||
|
<span class="dot green"></span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-title">AI Assistant · ${name} Connected</div>
|
||||||
|
<div style="width:50px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="messages">
|
||||||
|
<div class="msg user" id="userMsg">${question}</div>
|
||||||
|
<div class="typing" id="typing">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="msg ai" id="aiMsg">
|
||||||
|
Here's what I found:
|
||||||
|
<div class="mcp-card" id="mcpCard">
|
||||||
|
<div class="mcp-head">
|
||||||
|
<span class="mcp-logo">${name.substring(0, 2).toLowerCase()}</span>
|
||||||
|
<span class="mcp-head-text">${name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-stats">
|
||||||
|
<div>
|
||||||
|
<div class="stat-value green">${statValue}</div>
|
||||||
|
<div class="stat-label">${statLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="stat-value">${statValue2}</div>
|
||||||
|
<div class="stat-label">${statLabel2}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-rows">
|
||||||
|
<div class="mcp-row" id="row1">
|
||||||
|
<span class="mcp-row-label">${rows[0].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[0].value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-row" id="row2">
|
||||||
|
<span class="mcp-row-label">${rows[1].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[1].value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-row" id="row3">
|
||||||
|
<span class="mcp-row-label">${rows[2].label}</span>
|
||||||
|
<span class="mcp-row-value">${rows[2].value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="insight" id="insight">
|
||||||
|
<div class="insight-title">💡 Recommendation</div>
|
||||||
|
<div class="insight-text">${insight}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-box" id="inputBox">
|
||||||
|
<div class="input-text" id="inputText"><span class="placeholder">Ask anything...</span></div>
|
||||||
|
<button class="send-btn" id="sendBtn">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M2 21L23 12 2 3v7l15 2-15 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const question = ${JSON.stringify(question)};
|
||||||
|
function ease(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
||||||
|
function phase(scroll, start, end) {
|
||||||
|
if (scroll < start) return 0;
|
||||||
|
if (scroll > end) return 1;
|
||||||
|
return (scroll - start) / (end - start);
|
||||||
|
}
|
||||||
|
function updateScene(scroll) {
|
||||||
|
const chat = document.getElementById('chat');
|
||||||
|
const inputBox = document.getElementById('inputBox');
|
||||||
|
const inputText = document.getElementById('inputText');
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const userMsg = document.getElementById('userMsg');
|
||||||
|
const typing = document.getElementById('typing');
|
||||||
|
const aiMsg = document.getElementById('aiMsg');
|
||||||
|
const mcpCard = document.getElementById('mcpCard');
|
||||||
|
const row1 = document.getElementById('row1');
|
||||||
|
const row2 = document.getElementById('row2');
|
||||||
|
const row3 = document.getElementById('row3');
|
||||||
|
const insight = document.getElementById('insight');
|
||||||
|
|
||||||
|
chat.classList.toggle('visible', scroll >= 0.02);
|
||||||
|
|
||||||
|
const typeProgress = phase(scroll, 0.08, 0.28);
|
||||||
|
const chars = Math.floor(ease(typeProgress) * question.length);
|
||||||
|
|
||||||
|
if (scroll < 0.08) {
|
||||||
|
inputText.innerHTML = '<span class="placeholder">Ask anything...</span>';
|
||||||
|
inputBox.classList.remove('active');
|
||||||
|
sendBtn.classList.remove('ready');
|
||||||
|
} else if (scroll < 0.30) {
|
||||||
|
inputBox.classList.add('active');
|
||||||
|
inputText.innerHTML = (chars > 0 ? question.substring(0, chars) : '') + '<span class="cursor"></span>';
|
||||||
|
sendBtn.classList.toggle('ready', chars === question.length);
|
||||||
|
} else {
|
||||||
|
inputBox.classList.remove('active');
|
||||||
|
inputText.innerHTML = '<span class="placeholder">Ask anything...</span>';
|
||||||
|
sendBtn.classList.remove('ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
userMsg.classList.toggle('visible', scroll >= 0.30);
|
||||||
|
typing.classList.toggle('visible', scroll >= 0.38 && scroll < 0.48);
|
||||||
|
aiMsg.classList.toggle('visible', scroll >= 0.48);
|
||||||
|
mcpCard.classList.toggle('visible', scroll >= 0.55);
|
||||||
|
row1.classList.toggle('visible', scroll >= 0.62);
|
||||||
|
row2.classList.toggle('visible', scroll >= 0.67);
|
||||||
|
row3.classList.toggle('visible', scroll >= 0.72);
|
||||||
|
insight.classList.toggle('visible', scroll >= 0.80);
|
||||||
|
}
|
||||||
|
window.updateScene = updateScene;
|
||||||
|
updateScene(0);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderVideo(config, browser) {
|
||||||
|
const htmlPath = path.join(__dirname, 'output', `${config.id}.html`);
|
||||||
|
const framesDir = path.join(__dirname, 'output', `${config.id}-frames`);
|
||||||
|
const mp4Path = path.join(__dirname, 'output', `${config.id}.mp4`);
|
||||||
|
|
||||||
|
// Generate HTML
|
||||||
|
fs.writeFileSync(htmlPath, generateHTML(config));
|
||||||
|
|
||||||
|
// Create frames directory
|
||||||
|
fs.rmSync(framesDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(framesDir, { recursive: true });
|
||||||
|
|
||||||
|
// Capture frames
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1920, height: 1080 });
|
||||||
|
await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle0' });
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
|
||||||
|
const fps = 30;
|
||||||
|
const totalFrames = 300; // 10 seconds
|
||||||
|
|
||||||
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
|
const scroll = i / (totalFrames - 1);
|
||||||
|
await page.evaluate(s => window.updateScene(s), scroll);
|
||||||
|
await new Promise(r => setTimeout(r, 15));
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(framesDir, `frame-${String(i+1).padStart(4,'0')}.png`),
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.close();
|
||||||
|
|
||||||
|
// Encode video
|
||||||
|
execSync(`ffmpeg -y -framerate ${fps} -i "${framesDir}/frame-%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 20 "${mp4Path}"`, { stdio: 'pipe' });
|
||||||
|
|
||||||
|
// Cleanup frames
|
||||||
|
fs.rmSync(framesDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
return mp4Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`\n🎬 Generating ${configs.length} MCP videos...\n`);
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < configs.length; i++) {
|
||||||
|
const config = configs[i];
|
||||||
|
const start = Date.now();
|
||||||
|
process.stdout.write(`[${i+1}/${configs.length}] ${config.name}... `);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renderVideo(config, browser);
|
||||||
|
console.log(`✓ (${Math.round((Date.now() - start) / 1000)}s)`);
|
||||||
|
results.push({ name: config.name, id: config.id, success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ ${err.message}`);
|
||||||
|
results.push({ name: config.name, id: config.id, success: false, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log(`\n✓ Done! Videos saved to output/\n`);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
393
mcp-diagrams/mcp-animation-framework/generate-single.js
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Animation Frame Generator - Single Panel Mode
|
||||||
|
* Usage: node generate-single.js configs/your-config.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const configPath = process.argv[2];
|
||||||
|
if (!configPath) {
|
||||||
|
console.error('Usage: node generate-single.js configs/your-config.json');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
const outputDir = path.join('output', config.name);
|
||||||
|
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
function generateListItems(items) {
|
||||||
|
return items.map(item => `
|
||||||
|
<div class="list-item">
|
||||||
|
<div class="item-avatar">${item.avatar}</div>
|
||||||
|
<div class="item-info">
|
||||||
|
<div class="item-name">${item.name}</div>
|
||||||
|
<div class="item-meta">${item.meta}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-status" style="background: ${item.statusColor};">${item.status}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePanel(panel) {
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
if (panel.type === 'list') {
|
||||||
|
content = `
|
||||||
|
<div class="panel-list">
|
||||||
|
${generateListItems(panel.items)}
|
||||||
|
</div>
|
||||||
|
${panel.footer ? `
|
||||||
|
<div class="panel-footer">
|
||||||
|
<span>${panel.footer.left}</span>
|
||||||
|
<span>${panel.footer.right}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="embed-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-icon" style="background: ${panel.iconColor};">${panel.icon}</div>
|
||||||
|
<div class="panel-titles">
|
||||||
|
<div class="panel-title">${panel.title}</div>
|
||||||
|
${panel.subtitle ? `<div class="panel-subtitle">${panel.subtitle}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHTML(config, frameType) {
|
||||||
|
const showUserMessage = ['exchange', 'typing-ai', 'loading', 'final'].includes(frameType);
|
||||||
|
const showTypingUser = frameType === 'typing-user';
|
||||||
|
const showTypingAi = frameType === 'typing-ai';
|
||||||
|
const showAiText = ['loading', 'final'].includes(frameType);
|
||||||
|
const showPanel = frameType === 'final';
|
||||||
|
const showLoading = frameType === 'loading';
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=${config.dimensions.width}, height=${config.dimensions.height}">
|
||||||
|
<title>${config.title}</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: #0f0f1a;
|
||||||
|
width: ${config.dimensions.width}px;
|
||||||
|
height: ${config.dimensions.height}px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.window {
|
||||||
|
width: 1400px;
|
||||||
|
height: 850px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 25px 80px rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #2a2a4a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.titlebar {
|
||||||
|
height: 52px;
|
||||||
|
background: #252540;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #2a2a4a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.traffic-lights { display: flex; gap: 8px; }
|
||||||
|
.light { width: 12px; height: 12px; border-radius: 50%; }
|
||||||
|
.light.red { background: #ff5f57; }
|
||||||
|
.light.yellow { background: #ffbd2e; }
|
||||||
|
.light.green { background: #28ca41; }
|
||||||
|
.title { flex: 1; text-align: center; color: #8888aa; font-size: 14px; font-weight: 500; }
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message { display: flex; flex-direction: column; }
|
||||||
|
.message.user { align-self: flex-end; max-width: 70%; }
|
||||||
|
.message.ai { align-self: flex-start; max-width: 85%; }
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.message.user .bubble {
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
}
|
||||||
|
.message.ai .bubble.text {
|
||||||
|
background: #2a2a4a;
|
||||||
|
color: #e2e2f0;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #2a2a4a;
|
||||||
|
border-radius: 20px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.typing-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #6366f1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: translateY(0); }
|
||||||
|
40% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-panel {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #12121f;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #2a2a4a;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-bottom: 1px solid #2a2a4a;
|
||||||
|
}
|
||||||
|
.panel-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.panel-titles { flex: 1; }
|
||||||
|
.panel-title { font-size: 15px; font-weight: 600; color: #e2e2f0; }
|
||||||
|
.panel-subtitle { font-size: 12px; color: #8888aa; margin-top: 2px; }
|
||||||
|
|
||||||
|
.panel-content { padding: 12px; }
|
||||||
|
.panel-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.item-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3a3a5a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8888aa;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.item-info { flex: 1; min-width: 0; }
|
||||||
|
.item-name {
|
||||||
|
color: #e2e2f0;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.item-meta { color: #6a6a8a; font-size: 12px; margin-top: 2px; }
|
||||||
|
.item-status {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-top: 1px solid #2a2a4a;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8888aa;
|
||||||
|
}
|
||||||
|
.panel-footer span:last-child { color: #22c55e; font-weight: 600; }
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(90deg, #2a2a4a 25%, #3a3a5a 50%, #2a2a4a 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 60px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
border-top: 1px solid #2a2a4a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
background: #12121f;
|
||||||
|
border: 1px solid #2a2a4a;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.input-field { flex: 1; color: #5a5a7a; font-size: 14px; }
|
||||||
|
.send-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.send-btn svg { width: 18px; height: 18px; color: white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="window">
|
||||||
|
<div class="titlebar">
|
||||||
|
<div class="traffic-lights">
|
||||||
|
<div class="light red"></div>
|
||||||
|
<div class="light yellow"></div>
|
||||||
|
<div class="light green"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">${config.title}</div>
|
||||||
|
<div style="width: 52px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-area">
|
||||||
|
${showTypingUser ? `
|
||||||
|
<div class="message user">
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${showUserMessage ? `
|
||||||
|
<div class="message user">
|
||||||
|
<div class="bubble">${config.userPrompt}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${showTypingAi ? `
|
||||||
|
<div class="message ai">
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${showAiText ? `
|
||||||
|
<div class="message ai">
|
||||||
|
<div class="bubble text">${config.aiResponse}</div>
|
||||||
|
${showLoading ? `
|
||||||
|
<div class="embed-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-icon" style="background: ${config.panel.iconColor};">${config.panel.icon}</div>
|
||||||
|
<div class="panel-titles">
|
||||||
|
<div class="panel-title">${config.panel.title}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
<div class="skeleton"></div>
|
||||||
|
<div class="skeleton"></div>
|
||||||
|
<div class="skeleton"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${showPanel ? generatePanel(config.panel) : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="input-field">Type a message...</span>
|
||||||
|
<div class="send-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate frames
|
||||||
|
const frames = [
|
||||||
|
{ name: 'frame-01-empty', type: 'empty' },
|
||||||
|
{ name: 'frame-02-typing-user', type: 'typing-user' },
|
||||||
|
{ name: 'frame-03-exchange', type: 'exchange' },
|
||||||
|
{ name: 'frame-04-typing-ai', type: 'typing-ai' },
|
||||||
|
{ name: 'frame-05-loading', type: 'loading' },
|
||||||
|
{ name: 'frame-06-final', type: 'final' }
|
||||||
|
];
|
||||||
|
|
||||||
|
frames.forEach(frame => {
|
||||||
|
const html = generateHTML(config, frame.type);
|
||||||
|
const filePath = path.join(outputDir, `${frame.name}.html`);
|
||||||
|
fs.writeFileSync(filePath, html);
|
||||||
|
console.log(`Generated: ${filePath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n✓ Generated ${frames.length} frames in ${outputDir}/`);
|
||||||
|
console.log(`\nTo preview: open ${outputDir}/frame-06-final.html`);
|
||||||
395
mcp-diagrams/mcp-animation-framework/generate.js
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Animation Frame Generator
|
||||||
|
* Usage: node generate.js configs/your-config.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const configPath = process.argv[2];
|
||||||
|
if (!configPath) {
|
||||||
|
console.error('Usage: node generate.js configs/your-config.json');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
const outputDir = path.join('output', config.name);
|
||||||
|
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
// Generate panel HTML based on type
|
||||||
|
function generatePanel(panel, index) {
|
||||||
|
const gridSpan = panel.gridSpan === 2 ? 'grid-row: span 2;' : '';
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
switch (panel.type) {
|
||||||
|
case 'list':
|
||||||
|
content = generateListPanel(panel);
|
||||||
|
break;
|
||||||
|
case 'stats':
|
||||||
|
content = generateStatsPanel(panel);
|
||||||
|
break;
|
||||||
|
case 'chart':
|
||||||
|
content = generateChartPanel(panel);
|
||||||
|
break;
|
||||||
|
case 'chat':
|
||||||
|
content = generateChatPanel(panel);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
content = '<div class="panel-content">Unknown panel type</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="embed-panel" style="${gridSpan}">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-icon" style="background: ${panel.iconColor};">${panel.icon}</div>
|
||||||
|
<div class="panel-title">${panel.title}</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateListPanel(panel) {
|
||||||
|
const items = panel.items.map(item => `
|
||||||
|
<div class="list-item">
|
||||||
|
<div class="item-avatar">${item.avatar}</div>
|
||||||
|
<div class="item-name">${item.name}</div>
|
||||||
|
<div class="item-status" style="background: ${item.statusColor};">${item.status}</div>
|
||||||
|
<div class="item-meta">${item.meta}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `<div class="item-list">${items}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateStatsPanel(panel) {
|
||||||
|
const stats = panel.stats.map(stat => `
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">${stat.label}</span>
|
||||||
|
<span class="stat-value">${stat.value}</span>
|
||||||
|
${stat.change ? `<span class="stat-change positive">${stat.change}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const highlight = panel.highlight ? `
|
||||||
|
<div class="stat-highlight" style="border-color: ${panel.highlight.color};">
|
||||||
|
<div class="highlight-label">${panel.highlight.label}</div>
|
||||||
|
<div class="highlight-value" style="color: ${panel.highlight.color};">${panel.highlight.value}</div>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
return `<div class="stats-grid">${stats}</div>${highlight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChartPanel(panel) {
|
||||||
|
return `
|
||||||
|
<div class="chart-content">
|
||||||
|
<div class="donut" style="background: conic-gradient(#6366f1 0% ${panel.value}%, #2a2a4a ${panel.value}% 100%);">
|
||||||
|
<div class="donut-inner">${panel.centerLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-label">${panel.bottomValue}</div>
|
||||||
|
<div class="chart-sublabel">${panel.bottomLabel}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChatPanel(panel) {
|
||||||
|
const messages = panel.messages.map(msg => `
|
||||||
|
<div class="mini-msg ${msg.direction}">${msg.text}</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `<div class="mini-chat">${messages}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base HTML template
|
||||||
|
function generateHTML(config, frameType) {
|
||||||
|
const showUserMessage = ['first-exchange', 'typing-second', 'loading', 'final'].includes(frameType);
|
||||||
|
const showAiResponse = ['first-exchange', 'typing-second', 'loading', 'final'].includes(frameType);
|
||||||
|
const showPanels = ['loading', 'final'].includes(frameType);
|
||||||
|
const showLoading = frameType === 'loading';
|
||||||
|
const showTyping = frameType === 'typing' || frameType === 'typing-second';
|
||||||
|
|
||||||
|
const panels = showPanels ? config.panels.map((p, i) => generatePanel(p, i)).join('') : '';
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=${config.dimensions.width}, height=${config.dimensions.height}">
|
||||||
|
<title>${config.title}</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: #0f0f1a;
|
||||||
|
width: ${config.dimensions.width}px;
|
||||||
|
height: ${config.dimensions.height}px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.window {
|
||||||
|
width: 1600px;
|
||||||
|
height: 900px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 25px 80px rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #2a2a4a;
|
||||||
|
}
|
||||||
|
.titlebar {
|
||||||
|
height: 48px;
|
||||||
|
background: #252540;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid #2a2a4a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.traffic-lights { display: flex; gap: 8px; }
|
||||||
|
.light { width: 12px; height: 12px; border-radius: 50%; }
|
||||||
|
.light.red { background: #ff5f57; }
|
||||||
|
.light.yellow { background: #ffbd2e; }
|
||||||
|
.light.green { background: #28ca41; }
|
||||||
|
.title { flex: 1; text-align: center; color: #8888aa; font-size: 14px; font-weight: 500; }
|
||||||
|
.chat-area { flex: 1; padding: 20px; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
|
||||||
|
.message { display: flex; flex-direction: column; max-width: 85%; }
|
||||||
|
.message.user { align-self: flex-end; }
|
||||||
|
.message.ai { align-self: flex-start; width: 100%; max-width: 100%; }
|
||||||
|
.bubble { padding: 16px 20px; border-radius: 18px; font-size: 16px; line-height: 1.5; }
|
||||||
|
.message.user .bubble {
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
}
|
||||||
|
.message.ai .bubble.text {
|
||||||
|
background: #2a2a4a;
|
||||||
|
color: #e2e2f0;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #2a2a4a;
|
||||||
|
border-radius: 18px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.typing-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #6366f1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: translateY(0); }
|
||||||
|
40% { transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
.embed-grid {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
height: 510px;
|
||||||
|
gap: 4px;
|
||||||
|
background: #2a2a4a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.embed-panel {
|
||||||
|
background: #12121f;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #2a2a4a;
|
||||||
|
}
|
||||||
|
.panel-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.panel-title { font-size: 16px; font-weight: 600; color: #e2e2f0; }
|
||||||
|
.panel-content { flex: 1; overflow: hidden; }
|
||||||
|
|
||||||
|
/* List Items */
|
||||||
|
.item-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.item-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3a3a5a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8888aa;
|
||||||
|
}
|
||||||
|
.item-name { color: #e2e2f0; flex: 1; font-weight: 500; }
|
||||||
|
.item-status { padding: 4px 8px; border-radius: 5px; font-size: 10px; font-weight: 600; color: white; text-transform: uppercase; }
|
||||||
|
.item-meta { color: #8888aa; font-size: 12px; font-weight: 500; }
|
||||||
|
|
||||||
|
/* Stats */
|
||||||
|
.stats-grid { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.stat-row { display: flex; align-items: center; padding: 12px; background: #1a1a2e; border-radius: 8px; }
|
||||||
|
.stat-label { flex: 1; color: #8888aa; font-size: 13px; }
|
||||||
|
.stat-value { color: #e2e2f0; font-weight: 600; font-size: 14px; margin-right: 8px; }
|
||||||
|
.stat-change { color: #22c55e; font-size: 12px; font-weight: 600; }
|
||||||
|
.stat-highlight { margin-top: 16px; padding: 16px; background: #1a1a2e; border-radius: 10px; text-align: center; border: 1px solid; }
|
||||||
|
.highlight-label { color: #8888aa; font-size: 12px; }
|
||||||
|
.highlight-value { font-size: 28px; font-weight: 700; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
.chart-content { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; }
|
||||||
|
.donut {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.donut-inner {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: #12121f;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
.chart-label { margin-top: 16px; font-size: 18px; font-weight: 600; color: #e2e2f0; }
|
||||||
|
.chart-sublabel { font-size: 13px; color: #8888aa; margin-top: 4px; }
|
||||||
|
|
||||||
|
/* Loading Skeleton */
|
||||||
|
.skeleton { background: linear-gradient(90deg, #2a2a4a 25%, #3a3a5a 50%, #2a2a4a 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 6px; }
|
||||||
|
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||||
|
|
||||||
|
.input-area { padding: 12px 20px 20px; border-top: 1px solid #2a2a4a; flex-shrink: 0; }
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
background: #12121f;
|
||||||
|
border: 1px solid #2a2a4a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
.input-field { flex: 1; color: #5a5a7a; font-size: 13px; }
|
||||||
|
.send-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.send-btn svg { width: 16px; height: 16px; color: white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="window">
|
||||||
|
<div class="titlebar">
|
||||||
|
<div class="traffic-lights">
|
||||||
|
<div class="light red"></div>
|
||||||
|
<div class="light yellow"></div>
|
||||||
|
<div class="light green"></div>
|
||||||
|
</div>
|
||||||
|
<div class="title">${config.title}</div>
|
||||||
|
<div style="width: 52px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-area">
|
||||||
|
${showUserMessage ? `
|
||||||
|
<div class="message user">
|
||||||
|
<div class="bubble">${config.userPrompt}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${showTyping && !showAiResponse ? `
|
||||||
|
<div class="message ai">
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${showAiResponse ? `
|
||||||
|
<div class="message ai">
|
||||||
|
<div class="bubble text">${config.aiResponse}</div>
|
||||||
|
${showPanels ? `<div class="embed-grid">${panels}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="input-area">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="input-field">Type a message...</span>
|
||||||
|
<div class="send-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate all frames
|
||||||
|
const frames = [
|
||||||
|
{ name: 'frame-01-empty', type: 'empty' },
|
||||||
|
{ name: 'frame-02-typing', type: 'typing' },
|
||||||
|
{ name: 'frame-03-first-exchange', type: 'first-exchange' },
|
||||||
|
{ name: 'frame-04-typing-second', type: 'typing-second' },
|
||||||
|
{ name: 'frame-05-loading', type: 'loading' },
|
||||||
|
{ name: 'frame-06-final', type: 'final' }
|
||||||
|
];
|
||||||
|
|
||||||
|
frames.forEach(frame => {
|
||||||
|
const html = generateHTML(config, frame.type);
|
||||||
|
const filePath = path.join(outputDir, `${frame.name}.html`);
|
||||||
|
fs.writeFileSync(filePath, html);
|
||||||
|
console.log(`Generated: ${filePath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n✓ Generated ${frames.length} frames in ${outputDir}/`);
|
||||||
|
console.log(`\nTo preview: open ${outputDir}/frame-06-final.html`);
|
||||||
|
console.log(`To export: use browser screenshot or puppeteer`);
|
||||||