Add LICENSE, README, and Docs tab to Mission Control

This commit is contained in:
root
2026-02-22 07:33:18 +00:00
parent 3e7b457d5f
commit 0817444dc5
68 changed files with 6677 additions and 1673 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 SiteMente - HolaCompi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+61
View File
@@ -0,0 +1,61 @@
# SiteMente 🧠
AI implementation agency for local businesses in Spain. We install intelligent agents (chatbots, voice assistants, automations) on your website so they respond, book, and sell 24/7 — no human intervention needed.
## 🏆 Why SiteMente?
- **Setup in 48 hours** — Fast deployment, no months of work
- **ROI from day one** — Measurable results, not vague promises
- **Bilingual (ES/EN)** — Serve both Spanish and English customers
- **Specialists** — Restaurants, real estate, car rental on Costa del Sol
## 📦 Products
| Product | Description | Price |
|---------|-------------|-------|
| **Smart Starter** | AI Chat + Automated Bookings | €900 setup + €299/mo |
| **Smart Site** | Full Next.js website with integrated AI | €2,500 setup + €749/mo |
| **AI Growth Partner** | Multichannel (web, WhatsApp, calls) + CRM | €3,500 setup + €1,950/mo |
## 🚀 Quick Start
```bash
# Install dependencies
npm install
# Run development server
npm run dev
```
## 🛠 Tech Stack
- **Frontend:** Next.js 14, React, TypeScript, Tailwind CSS
- **AI:** MiniMax, Vapi (Voice)
- **Payments:** Stripe
- **Hosting:** Vercel
## 📂 Project Structure
```
├── app/ # Next.js app router
│ ├── page.tsx # Main landing page
│ ├── api/ # API routes
│ │ └── site-mente/ # AI endpoints
│ └── demos/ # Vertical demo pages
├── components/ # React components
│ └── site-mente/ # SiteMente-specific
├── public/ # Static assets
└── lib/ # Utilities
```
## 🌐 Live Site
👉 https://sitemente.com
## 📄 License
MIT — See LICENSE file
---
Built with ❤️ by [HolaCompi](https://holacompi.com)
+42
View File
@@ -0,0 +1,42 @@
## Demo Pages Audit - 2026-02-19
### Issues Found
- /demos/restaurant: Redirects to /demos?vertical=restaurant but shows **real-estate content** (wrong vertical)
- /demos/real-estate: Loads correctly, shows real-estate content ✓
- /demos/clinic: Redirects to /demos?vertical=clinic but shows **real-estate content** (wrong vertical)
- /demos/car-rental: Redirects to /demos?vertical=car-rental but shows **real-estate content** (wrong vertical)
- All demo pages: No visible form for contact/demo request in fetched content
### Tech Debt
- Demo pages not rendering vertical-specific content - all verticals load same real-estate page
- Need browser console check for JS errors (web_fetch doesn't capture console)
- Form fields not visible in page content - may require interactive browser check
### Test Results
```
✓ /demos/restaurant redirects to /demos?vertical=restaurant
✓ /demos/real-estate redirects to /demos?vertical=real-estate
✓ /demos/clinic redirects to /demos?vertical=clinic
✓ /demos/car-rental redirects to /demos?vertical=car-rental
Results: 4 passed, 0 failed
```
(Tests only verify redirects, not content rendering)
---
## Server Migration Checklist (To Prevent Forgetting)
### Before Migration
- [ ] Document all domains on source server
- [ ] Document all services running
- [ ] List all environment variables
- [ ] Note any external integrations (APIs, cron jobs)
- [ ] Update MEMORY.md with old VPS IP and what was on it
### After Migration
- [ ] Verify all domains resolve to new IP
- [ ] Test all services
- [ ] Check DNS propagation
- [ ] Keep old server accessible for 48h as backup
- [ ] Update all documentation with new IP
+59
View File
@@ -0,0 +1,59 @@
# SiteMente Agent Team - Horus Coordination
## Team Structure
| Agent | Egyptian Name | Role |
|-------|--------------|------|
| **Horus** | 👁️ | Lead / Coordinator |
| **Thoth** | 🦅 | Manager - Planning & Delegation |
| **Ptah** | 🛠️ | Dev & Ops - Code & Infrastructure |
| **Seshat** | 📝 | Content & SEO - Copy & Keywords |
| **Anubis** | 🐺 | Outreach & Growth - Campaigns & Leads |
## Invocation
When a SiteMente-related task comes in, Horus:
1. Analyzes the request
2. Spawns relevant sub-agent(s) via `sessions_spawn`
3. Merges outputs into clean response
## Sub-Agent Configs
### Thoth (Manager)
- **ID**: sitamente-manager
- **Model**: minimax/MiniMax-M2.5
- **Instructions**: Owns SiteMente project planning, prioritization, task assignment, status reports
### Ptah (Dev & Ops)
- **ID**: sitamente-dev
- **Model**: minimax/MiniMax-M2.5
- **Instructions**: Technical implementation, Next.js code, Vapi/MiniMax integrations, VPS/PM2 deployment
### Seshat (Content & SEO)
- **ID**: sitamente-content
- **Model**: minimax/MiniMax-M2.5
- **Instructions**: Copywriting, SEO, keyword mapping for verticals (restaurant, real estate, clinic, car rental)
### Anubis (Outreach & Growth)
- **ID**: sitamente-outreach
- **Model**: minimax/MiniMax-M2.5
- **Instructions**: Lead segmentation, outreach sequences, campaigns, client acquisition experiments
## Status
**Working!** Sub-agents spawn via task prompts.
**How it works:**
- I spawn a sub-agent with a task that includes the agent's role
- Each agent has a specific prompt defining their responsibilities
- They run in isolated sub-sessions
**Agent Prompts:**
- **Thoth** 🦅 - Planning, prioritization, delegation
- **Ptah** 🏺 - Code, integrations, deployment
- **Seshat** 📝 - Copywriting, SEO, keywords
- **Anubis** 🐺 - Outreach, campaigns, lead generation
---
*Last updated: 2026-02-18*
+84
View File
@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const commandFile = path.join(process.cwd(), 'task-history.json')
export async function GET(request: NextRequest) {
try {
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const { searchParams } = new URL(request.url)
const project = searchParams.get('project')
if (project && project !== 'all') {
history = history.filter((h: any) => h.project === project)
}
return NextResponse.json({ history })
} catch (error) {
console.error('Error reading history:', error)
return NextResponse.json({ history: [] })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { task, command, project, action } = body
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const entry = {
id: Date.now().toString(),
task,
command,
project: project || 'sitemente',
reply: '',
action: action || 'task',
createdAt: new Date().toISOString(),
status: 'pending',
notified: false
}
history.push(entry)
// Keep only last 50 entries
if (history.length > 50) {
history = history.slice(-50)
}
fs.writeFileSync(commandFile, JSON.stringify(history, null, 2))
// Send Telegram notification to Horus
try {
const tgMessage = `📬 *New Task from MC*\n\n*Project:* ${project || 'sitemente'}\n*Task:* ${task}\n*Command:* ${command}\n\n_Reply via /api/command-reply/{id}_`
// Use message tool to notify (will work if Telegram is configured)
const { spawn } = require('child_process')
spawn('curl', ['-s', '-X', 'POST',
'http://localhost:3000/api/messages/send',
'-H', 'Content-Type: application/json',
'-d', JSON.stringify({
channel: 'telegram',
target: '382315644',
message: tgMessage
})
], { detached: true, stdio: 'ignore' })
} catch (e) {
console.log('Telegram notification skipped')
}
return NextResponse.json({ success: true, entry })
} catch (error) {
console.error('Error saving history:', error)
return NextResponse.json({ error: 'Failed to save' }, { status: 500 })
}
}
+61
View File
@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const commandFile = path.join(process.cwd(), 'task-history.json')
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json({ error: 'Missing id' }, { status: 400 })
}
try {
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const entry = history.find((h: any) => h.id === id)
if (!entry) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json({ entry })
} catch (error) {
return NextResponse.json({ error: 'Failed to get' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { id, reply } = body
if (!id || !reply) {
return NextResponse.json({ error: 'Missing id or reply' }, { status: 400 })
}
let history = []
if (fs.existsSync(commandFile)) {
history = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
const index = history.findIndex((h: any) => h.id === id)
if (index === -1) {
return NextResponse.json({ error: 'Entry not found' }, { status: 404 })
}
history[index].reply = reply
history[index].status = 'replied'
history[index].repliedAt = new Date().toISOString()
fs.writeFileSync(commandFile, JSON.stringify(history, null, 2))
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to save reply' }, { status: 500 })
}
}
+58
View File
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { command, task, action } = body
if (!command || !task) {
return NextResponse.json(
{ error: 'Missing command or task' },
{ status: 400 }
)
}
// Log the command - in production this would trigger Horus
console.log(`[Task Command] Task: ${task}, Command: ${command}, Action: ${action}`)
// Store command in a file for Horus to pick up
const fs = require('fs')
const path = require('path')
const commandFile = path.join(process.cwd(), 'pending-commands.json')
let commands = []
if (fs.existsSync(commandFile)) {
commands = JSON.parse(fs.readFileSync(commandFile, 'utf-8'))
}
commands.push({
id: Date.now().toString(),
task,
command,
action,
createdAt: new Date().toISOString(),
status: 'pending'
})
fs.writeFileSync(commandFile, JSON.stringify(commands, null, 2))
return NextResponse.json({
success: true,
message: `Command sent for task: ${task}`,
commandId: Date.now().toString()
})
} catch (error) {
console.error('Error processing command:', error)
return NextResponse.json(
{ error: 'Failed to process command' },
{ status: 500 }
)
}
}
export async function GET() {
return NextResponse.json({
status: 'ok',
message: 'Command API ready'
})
}
+39 -10
View File
@@ -3,24 +3,53 @@ import { runSiteMenteVoiceTurn } from "../../../../lib/ai/siteMenteAgent";
export const runtime = "nodejs"; export const runtime = "nodejs";
// Vapi webhook payload types
interface VapiMessage {
type: string;
role?: string;
transcript?: string;
}
interface VapiCall {
id: string;
}
interface VapiWebhookPayload {
message: VapiMessage;
call: VapiCall;
}
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = (await request.json()) as { transcript?: string }; const body = (await request.json()) as VapiWebhookPayload;
if (!body.transcript || typeof body.transcript !== "string") { // Extract transcript from Vapi's format
return NextResponse.json( const message = body.message;
{ error: "transcript is required." },
{ status: 400 } // Only process final transcripts
); if (!message || message.type !== "transcript" || !message.transcript) {
// Return empty response for non-transcript messages
return NextResponse.json({ results: [] });
} }
const transcript = message.transcript;
// Call MiniMax brain
const response = await runSiteMenteVoiceTurn({ const response = await runSiteMenteVoiceTurn({
transcript: body.transcript, transcript: transcript,
});
// Return in Vapi's expected format
return NextResponse.json({
results: [
{
result: response.reply,
},
],
}); });
return NextResponse.json(response);
} catch (error) { } catch (error) {
console.error("[SiteMente][API] Voice route failed", error); console.error("[SiteMente][Vapi] Voice route failed", error);
return NextResponse.json( return NextResponse.json(
{ error: "Failed to generate voice response." }, { error: "Failed to generate voice response." },
{ status: 500 } { status: 500 }
+84
View File
@@ -0,0 +1,84 @@
import { NextResponse } from "next/server";
const STRIPE_KEY = process.env.STRIPE_SECRET_KEY || "";
const PLANS: Record<string, { name: string; price: number; currency: string }> = {
"starter-setup": { name: "AI Chat Setup", price: 90000, currency: "eur" },
"starter-monthly": { name: "AI Chat Monthly", price: 29900, currency: "eur" },
"site-setup": { name: "Smart Site Setup", price: 350000, currency: "eur" },
"site-monthly": { name: "Smart Site Monthly", price: 74900, currency: "eur" },
"growth-setup": { name: "AI Growth Setup", price: 500000, currency: "eur" },
"growth-monthly": { name: "AI Growth Monthly", price: 195000, currency: "eur" },
"demo-real-estate-essential": { name: "Real Estate Essential", price: 39000, currency: "eur" },
"demo-real-estate-profesional": { name: "Real Estate Professional", price: 79000, currency: "eur" },
"demo-real-estate-premium": { name: "Real Estate Premium", price: 139000, currency: "eur" },
"demo-restaurant-essential": { name: "Restaurant Essential", price: 39000, currency: "eur" },
"demo-restaurant-profesional": { name: "Restaurant Professional", price: 79000, currency: "eur" },
"demo-restaurant-premium": { name: "Restaurant Premium", price: 139000, currency: "eur" },
"demo-clinic-essential": { name: "Clinic Essential", price: 39000, currency: "eur" },
"demo-clinic-profesional": { name: "Clinic Professional", price: 79000, currency: "eur" },
"demo-clinic-premium": { name: "Clinic Premium", price: 139000, currency: "eur" },
"demo-home-services-essential": { name: "Home Services Essential", price: 39000, currency: "eur" },
"demo-home-services-profesional": { name: "Home Services Professional", price: 79000, currency: "eur" },
"demo-home-services-premium": { name: "Home Services Premium", price: 139000, currency: "eur" },
};
export async function POST(request: Request) {
try {
const body = await request.json();
const { planId, email, name, planType = "monthly" } = body;
const origin = request.headers.get("origin") || "http://45.95.42.114:1284";
let planKey = planId;
if (!planId.startsWith("demo-") && !PLANS[planId]) {
planKey = `${planId}-${planType}`;
}
const plan = PLANS[planKey];
if (!plan) {
return NextResponse.json({ error: "Invalid plan selected" }, { status: 400 });
}
const params = new URLSearchParams();
params.append("payment_method_types[]", "card");
params.append("line_items[0][price_data][currency]", plan.currency);
params.append("line_items[0][price_data][product_data][name]", plan.name);
params.append("line_items[0][price_data][product_data][description]", `SiteMente - ${plan.name}`);
params.append("line_items[0][price_data][unit_amount]", String(plan.price));
params.append("line_items[0][quantity]", "1");
params.append("mode", planType === "monthly" ? "subscription" : "payment");
params.append("customer_email", email || "");
params.append("metadata[customerName]", name || "");
params.append("metadata[planId]", planId);
params.append("metadata[planType]", planType);
params.append("success_url", `${origin}/success?session_id={CHECKOUT_SESSION_ID}`);
params.append("cancel_url", `${origin}/?cancelled=true`);
if (planType === "monthly") {
params.append("line_items[0][price_data][recurring][interval]", "month");
}
const response = await fetch("https://api.stripe.com/v1/checkout/sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${STRIPE_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
});
const session = await response.json();
if (session.error) {
return NextResponse.json({ error: session.error.message }, { status: 400 });
}
return NextResponse.json({ sessionId: session.id, url: session.url });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Failed to create checkout session";
console.error("Stripe error:", message);
return NextResponse.json({ error: message }, { status: 500 });
}
}
+85
View File
@@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const dataFile = path.join(process.cwd(), 'trading-traders.json')
const defaultTraders = [
{
id: 'dopetrades',
name: 'DopeTrades',
status: 'learning',
framesAnalyzed: 382,
patterns: ['Double Top/Bottom', 'Head & Shoulders', 'Triangles', 'Flags', 'Wedges'],
entryRules: [
'Identify clear structure (swing highs/lows)',
'Wait for retest of level',
'Confirm momentum in desired direction',
'Higher timeframe alignment',
'Entry on break of structure or retest',
'Confirmation candle required'
],
exitRules: [
'Stop below recent swing low (long)',
'Take profit minimum 2:1',
'Scale 50% at 1:1',
'Trailing stop after 1:1 achieved',
'Never move stop loss further'
],
indicators: [
'9 EMA (short term)',
'20 EMA (medium term)',
'50 SMA (trend filter)',
'RSI 14 (momentum)',
'Volume profile'
],
riskParams: [
'Max 2% risk per trade',
'Max 3 concurrent trades',
'6% daily max loss',
'10% weekly max loss',
'Stop after 3 losses'
],
timeframe: 'Multi: 4H/Daily trend, 1H structure, 15min entries',
notes: 'Frame analysis: 3% bullish, 7% bearish, 90% neutral. Dark charts confirmed.'
}
]
export async function GET() {
try {
let traders = defaultTraders
if (fs.existsSync(dataFile)) {
const saved = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
if (saved.length > 0) {
traders = saved
}
} else {
fs.writeFileSync(dataFile, JSON.stringify(defaultTraders, null, 2))
}
return NextResponse.json({ traders })
} catch (error) {
return NextResponse.json({ traders: defaultTraders })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
let traders = defaultTraders
if (fs.existsSync(dataFile)) {
traders = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
}
traders.push({
...body,
id: body.name.toLowerCase().replace(/\s+/g, '-'),
createdAt: new Date().toISOString()
})
fs.writeFileSync(dataFile, JSON.stringify(traders, null, 2))
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to save' }, { status: 500 })
}
}
+39
View File
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs'
import * as path from 'path'
const dataFile = path.join(process.cwd(), 'trading-trades.json')
export async function GET() {
try {
let trades = []
if (fs.existsSync(dataFile)) {
trades = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
}
return NextResponse.json({ trades })
} catch (error) {
return NextResponse.json({ trades: [] })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
let trades = []
if (fs.existsSync(dataFile)) {
trades = JSON.parse(fs.readFileSync(dataFile, 'utf-8'))
}
trades.push({
...body,
id: Date.now().toString(),
openedAt: new Date().toISOString()
})
fs.writeFileSync(dataFile, JSON.stringify(trades, null, 2))
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to save' }, { status: 500 })
}
}
+515
View File
@@ -0,0 +1,515 @@
"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
type GameState = "upload" | "ready" | "playing" | "gameover";
type BossState = "idle" | "attacking" | "stunned" | "dizzy";
const powerUps: Record<string, { name: string; icon: string; duration: number }> = {
double: { name: "2x Damage", icon: "⚔️", duration: 5000 },
freeze: { name: "Stun Boss", icon: "❄️", duration: 3000 },
bomb: { name: "Bomb", icon: "💣", duration: 0 },
none: { name: "", icon: "", duration: 0 },
};
const playSound = (type: "punch" | "combo" | "powerup" | "ko" | "gameover" | "bossAttack" | "hit") => {
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
switch (type) {
case "punch":
oscillator.frequency.setValueAtTime(150, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.1);
break;
case "hit":
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.15);
gainNode.gain.setValueAtTime(0.4, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.15);
break;
case "bossAttack":
oscillator.frequency.setValueAtTime(80, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(120, audioCtx.currentTime + 0.1);
oscillator.frequency.setValueAtTime(80, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "combo":
oscillator.frequency.setValueAtTime(300, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(400, audioCtx.currentTime + 0.1);
oscillator.frequency.setValueAtTime(500, audioCtx.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "powerup":
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.3);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.3);
break;
case "ko":
oscillator.frequency.setValueAtTime(100, audioCtx.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(30, audioCtx.currentTime + 0.5);
gainNode.gain.setValueAtTime(0.8, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.5);
break;
case "gameover":
oscillator.frequency.setValueAtTime(400, audioCtx.currentTime);
oscillator.frequency.setValueAtTime(300, audioCtx.currentTime + 0.2);
oscillator.frequency.setValueAtTime(200, audioCtx.currentTime + 0.4);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + 0.6);
break;
}
} catch (e) {}
};
export default function BossPunch() {
const [bossImage, setBossImage] = useState<string | null>(null);
const [gameState, setGameState] = useState<GameState>("upload");
const [score, setScore] = useState(0);
const [bossHealth, setBossHealth] = useState(100);
const [playerHealth, setPlayerHealth] = useState(100);
const [combo, setCombo] = useState(0);
const [maxCombo, setMaxCombo] = useState(0);
const [lastHit, setLastHit] = useState<"left" | "right" | null>(null);
const [playerX, setPlayerX] = useState(0);
const [bossX, setBossX] = useState(0);
const [bossRotation, setBossRotation] = useState(0);
const [bossState, setBossState] = useState<BossState>("idle");
const [isPaid, setIsPaid] = useState(false);
const [showPayModal, setShowPayModal] = useState(false);
const [activePowerUp, setActivePowerUp] = useState<string>("none");
const [powerUpCooldown, setPowerUpCooldown] = useState(0);
const [floatingTexts, setFloatingTexts] = useState<{ id: number; text: string; x: number; y: number; type: string }[]>([]);
const [screenShake, setScreenShake] = useState(false);
const [particles, setParticles] = useState<{ id: number; x: number; y: number; vx: number; vy: number; color: string; life: number }[]>([]);
const [winQuote, setWinQuote] = useState("I am sorry Haitham!");
const [showWinQuoteInput, setShowWinQuoteInput] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const floatingIdRef = useRef(0);
const particleIdRef = useRef(0);
const attackIntervalRef = useRef<NodeJS.Timeout | null>(null);
const addFloatingText = (text: string, x: number, y: number, type: string = "damage") => {
const id = floatingIdRef.current++;
setFloatingTexts(prev => [...prev, { id, text, x, y, type }]);
setTimeout(() => {
setFloatingTexts(prev => prev.filter(t => t.id !== id));
}, 1500);
};
const addParticle = (x: number, y: number, color: string) => {
const id = particleIdRef.current++;
setParticles(prev => [...prev, {
id, x, y,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10 - 5,
color, life: 1
}]);
};
useEffect(() => {
if (particles.length > 0) {
const timer = setInterval(() => {
setParticles(prev => prev.map(p => ({
...p,
x: p.x + p.vx,
y: p.y + p.vy,
vy: p.vy + 0.5,
life: p.life - 0.05
})).filter(p => p.life > 0));
}, 16);
return () => clearInterval(timer);
}
}, [particles.length]);
useEffect(() => {
if (gameState === "playing" && bossState !== "stunned" && bossState !== "dizzy") {
attackIntervalRef.current = setInterval(() => {
if (bossState === "idle" && Math.random() < 0.3) {
bossAttack();
}
}, 2000);
}
return () => {
if (attackIntervalRef.current) clearInterval(attackIntervalRef.current);
};
}, [gameState, bossState]);
const bossAttack = () => {
if (bossState !== "idle") return;
setBossState("attacking");
playSound("bossAttack");
setBossX(-30);
setTimeout(() => {
setBossX(30);
setTimeout(() => {
const damage = 5 + Math.floor(Math.random() * 10);
setPlayerHealth(prev => Math.max(0, prev - damage));
playSound("hit");
setScreenShake(true);
setTimeout(() => setScreenShake(false), 200);
addFloatingText(`-${damage}`, 30, 60, "boss");
setBossX(0);
setBossState("idle");
if (playerHealth - damage <= 0) {
setGameState("gameover");
playSound("gameover");
}
}, 150);
}, 150);
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
setBossImage(event.target?.result as string);
setGameState("ready");
};
reader.readAsDataURL(file);
}
};
const startGame = () => {
setGameState("playing");
setScore(0);
setBossHealth(100);
setPlayerHealth(100);
setCombo(0);
setMaxCombo(0);
setActivePowerUp("none");
setBossState("idle");
};
const activatePowerUp = (type: string) => {
if (powerUpCooldown > 0 || gameState !== "playing") return;
playSound("powerup");
setActivePowerUp(type);
if (type === "bomb") {
setBossHealth(prev => Math.max(0, prev - 30));
addFloatingText("-30! 💣", 70, 30, "powerup");
setScreenShake(true);
setTimeout(() => setScreenShake(false), 300);
} else if (type === "freeze") {
setBossState("stunned");
setTimeout(() => setBossState("idle"), 3000);
} else {
setTimeout(() => setActivePowerUp("none"), powerUps[type]?.duration || 5000);
}
setPowerUpCooldown(10);
};
useEffect(() => {
if (powerUpCooldown > 0) {
const timer = setInterval(() => {
setPowerUpCooldown(prev => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}
}, [powerUpCooldown]);
const punch = useCallback((hand: "left" | "right") => {
if (gameState !== "playing" || bossState === "stunned") return;
if (Math.random() < 0.15 && bossState === "idle") {
setBossState("dizzy");
addFloatingText("Miss!", 70, 40, "miss");
setTimeout(() => setBossState("idle"), 500);
return;
}
setLastHit(hand);
setCombo(c => {
const newCombo = c + 1;
if (newCombo > maxCombo) setMaxCombo(newCombo);
if (newCombo % 10 === 0) {
playSound("combo");
addFloatingText(`${newCombo} COMBO! 🔥`, 50, 50, "combo");
}
return newCombo;
});
let damage = 5 + Math.floor(combo / 5);
damage = Math.min(damage, 20);
if (activePowerUp === "double") damage *= 2;
playSound("punch");
const newHealth = Math.max(0, bossHealth - damage);
setBossHealth(newHealth);
setScore(s => s + damage * 10);
setBossRotation(hand === "left" ? -20 - (damage * 2) : 20 + (damage * 2));
setBossX(hand === "left" ? -20 : 20);
setTimeout(() => { setBossRotation(0); setBossX(0); }, 150);
setPlayerX(hand === "left" ? 40 : -40);
setTimeout(() => setPlayerX(0), 100);
for (let i = 0; i < 5; i++) {
addParticle(70, 40, damage > 15 ? "#ff0000" : "#ffffff");
}
addFloatingText(`-${damage}`, 60 + (Math.random() * 20 - 10), 30 + (Math.random() * 20 - 10), "damage");
if (damage > 10) {
setScreenShake(true);
setTimeout(() => setScreenShake(false), 100);
}
setTimeout(() => setLastHit(null), 150);
if (newHealth <= 0) {
playSound("ko");
setGameState("gameover");
}
}, [gameState, combo, bossHealth, activePowerUp, maxCombo, bossState]);
const resetGame = () => {
setGameState("ready");
setScore(0);
setBossHealth(100);
setPlayerHealth(100);
setCombo(0);
setMaxCombo(0);
setActivePowerUp("none");
setBossState("idle");
};
const shakeClass = screenShake ? "animate-pulse" : "";
return (
<div className={`min-h-screen bg-gradient-to-b from-red-900 via-red-800 to-orange-900 flex flex-col items-center justify-center p-2 overflow-hidden ${shakeClass}`}>
{particles.map(p => (
<motion.div
key={p.id}
initial={{ x: `${p.x}%`, y: `${p.y}%`, opacity: 1 }}
animate={{ x: `${p.x + p.vx}%`, y: `${p.y + p.vy}%`, opacity: p.life }}
className="absolute w-3 h-3 rounded-full pointer-events-none"
style={{ backgroundColor: p.color }}
/>
))}
{floatingTexts.map(ft => (
<motion.div
key={ft.id}
initial={{ opacity: 1, y: 0, scale: 0.5 }}
animate={{ opacity: 0, y: -50, scale: ft.type === "boss" ? 1.5 : 1.2 }}
transition={{ duration: 1 }}
className={`absolute pointer-events-none text-2xl font-black ${ft.type === "boss" ? "text-red-400" : ft.type === "miss" ? "text-gray-400" : ft.type === "combo" ? "text-yellow-400" : "text-white"}`}
style={{ left: `${ft.x}%`, top: `${ft.y}%` }}
>
{ft.text}
</motion.div>
))}
<div className="absolute top-0 left-0 right-0 p-2 flex justify-between items-center z-10">
<h1 className="text-xl font-black text-white tracking-wider">
BOSS<span className="text-red-500">PUNCH</span>
</h1>
{gameState === "playing" && (
<div className="bg-black/50 rounded-full px-3 py-1 flex gap-3 text-xs">
<span className="text-white font-bold">Score: {score}</span>
<span className="text-yellow-400 font-bold">Combo: {combo}x</span>
</div>
)}
</div>
{gameState === "playing" && (
<div className="absolute top-12 left-2 right-2 flex flex-col gap-1 z-10">
<div className="flex items-center gap-2">
<span className="text-xs text-white w-12">BOSS</span>
<div className="flex-1 h-3 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-red-500 to-green-500 transition-all duration-200" style={{ width: `${bossHealth}%` }} />
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-white w-12">YOU</span>
<div className="flex-1 h-3 bg-gray-800 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-green-500 to-blue-500 transition-all duration-200" style={{ width: `${playerHealth}%` }} />
</div>
</div>
</div>
)}
{gameState === "playing" && (
<div className="absolute top-24 left-0 right-0 flex justify-center gap-2 z-10">
{(["double", "freeze", "bomb"] as string[]).map(pu => (
<button
key={pu}
onClick={() => activatePowerUp(pu)}
disabled={powerUpCooldown > 0 || bossState === "stunned"}
className={`bg-white/20 backdrop-blur rounded-full p-2 text-xl transition-all hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed ${activePowerUp === pu ? "ring-2 ring-yellow-400" : ""}`}
title={powerUps[pu]?.name}
>
{powerUps[pu]?.icon}
</button>
))}
{powerUpCooldown > 0 && <span className="absolute -bottom-2 text-xs text-white/70">{powerUpCooldown}s</span>}
</div>
)}
<div className="relative w-full max-w-lg aspect-square max-h-[45vh] mt-8">
<AnimatePresence>
{bossImage && (
<motion.div
className="absolute right-0 top-1/3"
animate={{
x: bossX,
rotate: bossState === "dizzy" ? [0, -10, 10, -10, 10, 0] : bossRotation,
scale: bossState === "stunned" ? 0.9 : bossHealth > 0 ? 1 : 0.8,
opacity: bossState === "stunned" ? 0.7 : 1
}}
transition={{ type: "spring", stiffness: 300 }}
>
<div className="relative w-32 h-32 md:w-48 md:h-48">
<div className={`absolute -left-6 top-1/2 w-10 h-10 ${activePowerUp === "freeze" ? "bg-cyan-300" : bossState === "dizzy" ? "bg-gray-400" : "bg-red-600"} rounded-full border-4 border-red-400 flex items-center justify-center`}>
<span className="text-white font-bold text-xs">L</span>
</div>
<div className={`absolute -right-6 top-1/3 w-10 h-10 ${activePowerUp === "freeze" ? "bg-cyan-300" : bossState === "dizzy" ? "bg-gray-400" : "bg-red-600"} rounded-full border-4 border-red-400 flex items-center justify-center`}>
<span className="text-white font-bold text-xs">R</span>
</div>
<div className={`w-full h-full rounded-2xl overflow-hidden border-4 ${activePowerUp === "freeze" ? "border-cyan-400" : bossState === "stunned" ? "border-blue-400" : "border-white"} shadow-2xl`}>
<img src={bossImage} alt="Boss" className="w-full h-full object-cover" />
</div>
{bossState === "stunned" && <div className="absolute -top-4 left-1/2 -translate-x-1/2 text-2xl">💫</div>}
{bossState === "dizzy" && <div className="absolute top-0 right-0 text-xl">😵</div>}
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{gameState === "playing" && (
<motion.div className="absolute left-0 bottom-0" animate={{ x: playerX }} transition={{ duration: 0.1 }}>
<div className="w-20 h-28 md:w-28 md:h-36 relative">
<motion.div className="absolute top-1/3 -right-3 w-12 h-12 bg-blue-600 border-4 border-blue-400 rounded-full cursor-pointer flex items-center justify-center text-2xl" whileTap={{ scale: 0.8 }} onClick={() => punch("right")}>👊</motion.div>
<motion.div className="absolute top-1/3 -left-3 w-12 h-12 bg-blue-600 border-4 border-blue-400 rounded-full cursor-pointer flex items-center justify-center text-2xl" whileTap={{ scale: 0.8 }} onClick={() => punch("left")}>👊</motion.div>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-16 h-20 bg-blue-700 rounded-t-xl" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-amber-200 rounded-full" />
</div>
</motion.div>
)}
</AnimatePresence>
{lastHit && (
<motion.div initial={{ opacity: 1, scale: 0.5 }} animate={{ opacity: 0, scale: 1.5 }} className="absolute right-1/4 top-1/3 text-5xl font-black text-yellow-400">
Pow!
</motion.div>
)}
</div>
<div className="mt-2 w-full max-w-md">
{gameState === "upload" && (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="text-center">
<div className="bg-white/10 backdrop-blur rounded-2xl p-6 border-2 border-dashed border-white/30">
<div className="text-5xl mb-3">👔</div>
<h2 className="text-xl font-bold text-white mb-2">Upload Your Boss!</h2>
<p className="text-white/70 mb-4">Take out your frustrations!</p>
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleImageUpload} className="hidden" />
<button onClick={() => fileInputRef.current?.click()} className="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-8 rounded-full text-lg transition-all hover:scale-105 shadow-lg">
📸 Upload Photo
</button>
<p className="text-xs text-white/50 mt-4">Free with ads $9.99 for ad-free</p>
</div>
</motion.div>
)}
{gameState === "ready" && bossImage && (
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="text-center">
<div className="bg-white/10 backdrop-blur rounded-xl p-3 mb-3 border border-white/20">
<button onClick={() => setShowWinQuoteInput(!showWinQuoteInput)} className="text-white/70 text-sm hover:text-white flex items-center justify-center gap-2">
🏆 {showWinQuoteInput ? "Hide" : "If u win he says..."}
</button>
{showWinQuoteInput && (
<input
type="text"
value={winQuote}
onChange={(e) => setWinQuote(e.target.value)}
placeholder="What does boss say when u win?"
className="w-full mt-2 bg-white/20 border border-white/30 rounded-lg px-3 py-2 text-white placeholder-white/50 text-sm"
/>
)}
</div>
<h2 className="text-xl font-bold text-white mb-2">Ready to Fight!</h2>
<p className="text-white/70 mb-4">Boss fights back! Don't lose!</p>
<button onClick={startGame} className="bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold py-3 px-10 rounded-full text-lg transition-all hover:scale-105 shadow-xl">
🥊 START FIGHT
</button>
</motion.div>
)}
{gameState === "playing" && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<p className="text-white/70 text-sm mb-2">Tap to punch!</p>
<div className="flex gap-4 justify-center">
<button onClick={() => punch("left")} className="bg-blue-600 hover:scale-110 w-20 h-20 rounded-full border-4 border-blue-400 flex items-center justify-center text-3xl active:scale-90 transition-transform">👊</button>
<button onClick={() => punch("right")} className="bg-blue-600 hover:scale-110 w-20 h-20 rounded-full border-4 border-blue-400 flex items-center justify-center text-3xl active:scale-90 transition-transform">👊</button>
</div>
</motion.div>
)}
{gameState === "gameover" && (
<motion.div initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} className="text-center">
<div className={`rounded-2xl p-6 border-4 ${bossHealth <= 0 ? "bg-gradient-to-b from-yellow-500 to-orange-500 border-yellow-300" : "bg-gradient-to-b from-red-600 to-red-800 border-red-400"}`}>
<div className="text-6xl mb-2">{bossHealth <= 0 ? "🏆" : "💀"}</div>
<h2 className="text-3xl font-black text-white mb-2">{bossHealth <= 0 ? "KNOCKOUT!" : "YOU LOSE!"}</h2>
{bossHealth <= 0 && (
<div className="bg-white/20 rounded-lg p-3 mb-3">
<p className="text-lg font-bold text-white">Boss says:</p>
<p className="text-white text-xl">"{winQuote}"</p>
</div>
)}
<p className="text-xl font-bold text-white mb-2">Score: {score}</p>
<p className="text-white/80 mb-4">Max Combo: {maxCombo}x</p>
<div className="space-y-2">
<button onClick={resetGame} className="w-full bg-white text-red-600 font-bold py-2 px-6 rounded-full text-base transition-all hover:scale-105">
🔄 Fight Again
</button>
{!isPaid && (
<button onClick={() => setShowPayModal(true)} className="w-full bg-yellow-400 text-yellow-900 font-bold py-2 px-6 rounded-full text-base transition-all hover:scale-105 flex items-center justify-center gap-2">
$9.99 - No Ads
</button>
)}
</div>
</div>
</motion.div>
)}
</div>
{/* Stripe Payment Modal */}
{showPayModal && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full text-center">
<h3 className="text-2xl font-bold text-gray-800 mb-2">Remove Ads</h3>
<p className="text-gray-600 mb-4">Get BossPunch ad-free forever!</p>
<p className="text-4xl font-black text-purple-600 mb-4">$9.99</p>
<button className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 rounded-lg mb-2 transition">
Pay with Card
</button>
<button onClick={() => setShowPayModal(false)} className="text-gray-500 text-sm">Cancel</button>
</div>
</div>
)}
{!isPaid && gameState !== "upload" && (
<div className="fixed bottom-0 left-0 right-0 bg-black/80 py-2 text-center">
<p className="text-white/60 text-xs">Advertisement</p>
</div>
)}
</div>
);
}
+315
View File
@@ -0,0 +1,315 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
type Booking = {
id: string;
customer: string;
service: string;
date: string;
time: string;
status: "confirmed" | "pending" | "cancelled";
};
type Lead = {
id: string;
name: string;
phone: string;
message: string;
date: string;
status: "new" | "contacted" | "qualified";
};
// Mock data for demo
const mockBookings: Booking[] = [
{ id: "1", customer: "María García", service: "Cita consulta", date: "2026-02-17", time: "10:00", status: "confirmed" },
{ id: "2", customer: "John Smith", service: "Treatment", date: "2026-02-17", time: "14:30", status: "pending" },
{ id: "3", customer: "Carlos Ruiz", service: "Reserva mesa", date: "2026-02-18", time: "20:00", status: "confirmed" },
{ id: "4", customer: "Ana López", service: "Cita estética", date: "2026-02-19", time: "11:00", status: "confirmed" },
{ id: "5", customer: "Pierre Dubois", service: "Booking", date: "2026-02-19", time: "16:00", status: "confirmed" },
];
const mockLeads: Lead[] = [
{ id: "1", name: "María García", phone: "+34 612 345 678", message: "Hola, me gustaría información sobre...", date: "2026-02-16", status: "new" },
{ id: "2", name: "John Smith", phone: "+44 7700 900123", message: "Do you have availability for next week?", date: "2026-02-16", status: "qualified" },
{ id: "3", name: "Carlos Ruiz", phone: "+34 654 987 321", message: "Quiero reservar para 4 personas", date: "2026-02-15", status: "contacted" },
];
export default function DashboardPage() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loginError, setLoginError] = useState("");
const [activeTab, setActiveTab] = useState<"bookings" | "leads" | "analytics">("bookings");
const handleLogin = () => {
if (email === "User" && password === "#SiteMenteUserpass2026") {
setIsLoggedIn(true);
setLoginError("");
} else {
setLoginError("Invalid credentials");
}
};
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-[#1a1625] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-md"
>
<div className="text-center mb-8">
<div className="text-5xl mb-4">🏢</div>
<h1 className="text-3xl font-bold text-white mb-2">SiteMente</h1>
<p className="text-white/60">Client Dashboard</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
<div className="space-y-4">
<div>
<label className="text-sm text-white/60 mb-1 block">Username</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="User"
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder:text-white/30"
/>
</div>
<div>
<label className="text-sm text-white/60 mb-1 block">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder:text-white/30"
/>
</div>
{loginError && (
<p className="text-red-400 text-sm">{loginError}</p>
)}
<button
onClick={handleLogin}
className="w-full py-3 bg-brand-pink rounded-lg font-semibold text-white hover:bg-[#ff7bc0] transition"
>
Login
</button>
<p className="text-center text-white/40 text-sm">
Ask SiteMente to create your dashboard
</p>
</div>
</div>
</motion.div>
</div>
);
}
const stats = {
totalBookings: mockBookings.length,
confirmed: mockBookings.filter(b => b.status === "confirmed").length,
pending: mockBookings.filter(b => b.status === "pending").length,
totalLeads: mockLeads.length,
newLeads: mockLeads.filter(l => l.status === "new").length,
};
return (
<div className="min-h-screen bg-[#1a1625] text-white">
{/* Header */}
<header className="border-b border-white/10 bg-[#1a1625]/90 backdrop-blur sticky top-0 z-50">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<span className="text-2xl">🏢</span>
<span className="font-bold text-xl">SiteMente Dashboard</span>
</div>
<div className="flex items-center gap-4">
<span className="text-white/60">SiteMente Restaurant Demo</span>
<button
onClick={() => setIsLoggedIn(false)}
className="text-sm text-white/60 hover:text-white"
>
Logout
</button>
</div>
</div>
</header>
<main className="mx-auto max-w-6xl px-6 py-8">
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-2xl font-bold text-brand-pink">{stats.totalBookings}</p>
<p className="text-sm text-white/60">Total Bookings</p>
</div>
<div className="bg-green-500/10 rounded-xl p-4 border border-green-500/30">
<p className="text-2xl font-bold text-green-400">{stats.confirmed}</p>
<p className="text-sm text-white/60">Confirmed</p>
</div>
<div className="bg-yellow-500/10 rounded-xl p-4 border border-yellow-500/30">
<p className="text-2xl font-bold text-yellow-400">{stats.pending}</p>
<p className="text-sm text-white/60">Pending</p>
</div>
<div className="bg-blue-500/10 rounded-xl p-4 border border-blue-500/30">
<p className="text-2xl font-bold text-blue-400">{stats.totalLeads}</p>
<p className="text-sm text-white/60">Total Leads</p>
</div>
<div className="bg-purple-500/10 rounded-xl p-4 border border-purple-500/30">
<p className="text-2xl font-bold text-purple-400">{stats.newLeads}</p>
<p className="text-sm text-white/60">New Leads</p>
</div>
</div>
{/* Tabs */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setActiveTab("bookings")}
className={`px-4 py-2 rounded-lg font-medium transition ${activeTab === "bookings" ? "bg-brand-pink" : "bg-white/10 hover:bg-white/20"}`}
>
📅 Bookings
</button>
<button
onClick={() => setActiveTab("leads")}
className={`px-4 py-2 rounded-lg font-medium transition ${activeTab === "leads" ? "bg-brand-pink" : "bg-white/10 hover:bg-white/20"}`}
>
📩 Leads
</button>
<button
onClick={() => setActiveTab("analytics")}
className={`px-4 py-2 rounded-lg font-medium transition ${activeTab === "analytics" ? "bg-brand-pink" : "bg-white/10 hover:bg-white/20"}`}
>
📈 Analytics
</button>
</div>
{/* Bookings Tab */}
{activeTab === "bookings" && (
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<table className="w-full">
<thead className="bg-white/5 border-b border-white/10">
<tr>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Customer</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Service</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Date</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Time</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Status</th>
</tr>
</thead>
<tbody>
{mockBookings.map((booking) => (
<tr key={booking.id} className="border-b border-white/5">
<td className="px-4 py-3">{booking.customer}</td>
<td className="px-4 py-3">{booking.service}</td>
<td className="px-4 py-3">{booking.date}</td>
<td className="px-4 py-3">{booking.time}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs ${
booking.status === "confirmed" ? "bg-green-500/20 text-green-400" :
booking.status === "pending" ? "bg-yellow-500/20 text-yellow-400" :
"bg-red-500/20 text-red-400"
}`}>
{booking.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Leads Tab */}
{activeTab === "leads" && (
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<table className="w-full">
<thead className="bg-white/5 border-b border-white/10">
<tr>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Name</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Phone</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Message</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Date</th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Status</th>
</tr>
</thead>
<tbody>
{mockLeads.map((lead) => (
<tr key={lead.id} className="border-b border-white/5">
<td className="px-4 py-3">{lead.name}</td>
<td className="px-4 py-3">
<a href={`tel:${lead.phone}`} className="text-brand-pink hover:underline">{lead.phone}</a>
</td>
<td className="px-4 py-3 text-white/70 text-sm">{lead.message}</td>
<td className="px-4 py-3">{lead.date}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs ${
lead.status === "new" ? "bg-blue-500/20 text-blue-400" :
lead.status === "qualified" ? "bg-green-500/20 text-green-400" :
"bg-yellow-500/20 text-yellow-400"
}`}>
{lead.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Analytics Tab */}
{activeTab === "analytics" && (
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-white/5 rounded-xl border border-white/10 p-6">
<h3 className="text-lg font-semibold mb-4">Bookings This Week</h3>
<div className="flex items-end gap-2 h-40">
{[3, 5, 2, 4, 6, 3, 5].map((h, i) => (
<div key={i} className="flex-1 bg-brand-pink/50 rounded-t" style={{ height: `${h * 14}%` }}></div>
))}
</div>
<div className="flex justify-between mt-2 text-xs text-white/40">
<span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span><span>Sun</span>
</div>
</div>
<div className="bg-white/5 rounded-xl border border-white/10 p-6">
<h3 className="text-lg font-semibold mb-4">Lead Sources</h3>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>🌐 Website</span>
<span className="text-brand-pink">65%</span>
</div>
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-brand-pink w-[65%]"></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>💬 WhatsApp</span>
<span className="text-green-400">25%</span>
</div>
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-green-500 w-[25%]"></div>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span>📞 Phone</span>
<span className="text-blue-400">10%</span>
</div>
<div className="h-2 bg-white/10 rounded-full overflow-hidden">
<div className="h-full bg-blue-500 w-[10%]"></div>
</div>
</div>
</div>
</div>
</div>
)}
<p className="text-center text-white/40 text-sm mt-8">
Powered by SiteMente AI Your AI Employee working 24/7
</p>
</main>
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Demos | SiteMente - Webs IA para cada industria",
description:
"Ver demos de webs con IA para inmobiliarias, restaurantes, clínicas y servicios del hogar. Precios claros desde €390/mes.",
keywords: [
"demo web IA España",
"inteligencia artificial inmobiliarias",
"AI restaurantesdemo",
"chatbot clínica",
"IA servicios hogar",
"web con AI Costa del Sol",
],
openGraph: {
title: "Demos SiteMente | Webs IA por industria",
description:
"Ver demos de webs con IA para cada tipo de negocio. Desde €390/mes.",
url: "https://sitemente.com/demos",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
},
],
},
};
export default function DemosLayout({ children }: { children: React.ReactNode }) {
return children;
}
+2 -1054
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -100,3 +100,11 @@ body {
opacity: 0; opacity: 0;
} }
} }
@keyframes gradient {
0% { background-position: 0% center; }
100% { background-position: 200% center; }
}
.animate-gradient {
animation: gradient 3s linear infinite;
}
+53
View File
@@ -0,0 +1,53 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "HolaCompi - AI Voice Agents for Spanish Businesses",
description: "Building the future of automated customer calls",
};
export default function HolaCompiPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-[#1a1625] to-[#2d1f3d] text-white flex items-center justify-center overflow-hidden">
{/* Background effects */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-20 left-10 text-6xl animate-pulse"></div>
<div className="absolute top-40 right-20 text-5xl animate-bounce" style={{ animationDuration: '2s' }}>🚀</div>
<div className="absolute bottom-40 left-20 text-5xl animate-pulse" style={{ animationDelay: '0.5s' }}>💫</div>
<div className="absolute bottom-20 right-10 text-6xl animate-bounce" style={{ animationDuration: '3s' }}>🤖</div>
</div>
<div className="text-center px-6 relative z-10">
<div className="text-7xl mb-8 animate-bounce" style={{ animationDuration: '2s' }}>
🤖📞
</div>
<h1 className="text-6xl font-bold mb-6 bg-gradient-to-r from-pink-500 via-purple-500 to-pink-500 bg-[length:200%_auto] animate-gradient bg-clip-text text-transparent">
HolaCompi
</h1>
<h2 className="text-3xl text-white/90 mb-6 font-light">
AI Voice Agents for Spanish Businesses
</h2>
<div className="inline-block bg-gradient-to-r from-pink-500/30 to-purple-500/30 border border-pink-500/40 rounded-full px-8 py-3 mb-10 backdrop-blur">
<span className="text-2xl font-medium">🚀 Launching March 2026</span>
</div>
<p className="text-xl text-white/70 mb-10 max-w-md mx-auto">
Building the future of automated customer calls
</p>
<div className="text-white/50 text-lg">
<p>For inquiries: <a href="mailto:Holac@HolaCompi.com" className="text-pink-400 hover:text-pink-300 transition">Holac@HolaCompi.com</a></p>
</div>
<div className="mt-16 pt-8 border-t border-white/10">
<Link href="/" className="text-pink-400 hover:text-pink-300 transition text-lg">
Back to SiteMente
</Link>
</div>
</div>
</div>
);
}
+337
View File
@@ -0,0 +1,337 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { motion } from "framer-motion";
type LeadStatus = "new" | "contacted" | "qualified" | "proposal" | "won" | "lost";
interface Lead {
id: string;
name: string;
category: "restaurant" | "real-estate" | "clinic" | "car-rental" | "hp-client" | "other";
phone: string;
email: string;
website: string;
address: string;
rating: number;
score: number;
notes: string;
status: LeadStatus;
lastContact: string;
nextAction: string;
createdAt: string;
}
const allLeads: Lead[] = [
// New leads from research Feb 17
{ id: "r11", name: "Restaurante Milan", category: "restaurant", phone: "+34 952 44 58 55", email: "", website: "", address: "Av. Federico Garcia Lorca 7, 29630 Benalmádena", rating: 0, score: 8, notes: "Italian restaurant - call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r12", name: "Tex Mex Gringos", category: "restaurant", phone: "", email: "reservas@restaurantespuertomarina.com", website: "restaurantespuertomarina.com", address: "Puerto Marina, Benalmádena", rating: 0, score: 8, notes: "Email found - send AI proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "r13", name: "Lime & Lemon Tapas", category: "restaurant", phone: "", email: "info@limeandlemonbenalmadena.com", website: "limeandlemonbenalmadena.com", address: "Av. Las Palmeras 1, 29630 Benalmádena", rating: 0, score: 8, notes: "Email found - send AI proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "r14", name: "El Parador", category: "restaurant", phone: "+34 952 44 92 93", email: "", website: "", address: "Av. Juan Luis Peralta 47, 29639 Benalmádena", rating: 4.0, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r15", name: "Escorpio Restaurante", category: "restaurant", phone: "+34 952 569 047", email: "", website: "", address: "Santo Domingo de Guzmán 7, Benalmádena", rating: 0, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r16", name: "The Bull Bar", category: "restaurant", phone: "+34 646 569 374", email: "", website: "", address: "Av del Chorrillo 15, Benalmádena", rating: 0, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r17", name: "La Plaza Restaurant", category: "restaurant", phone: "+34 952 44 84 83", email: "", website: "", address: "Plaza de Espana 2, 29639 Benalmádena", rating: 4.3, score: 8, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "r18", name: "Caliu Restaurant", category: "restaurant", phone: "", email: "caliu.torremolinos@gmail.com", website: "", address: "Torremolinos", rating: 0, score: 7, notes: "Email found - send proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "r19", name: "The Carvery", category: "restaurant", phone: "", email: "info@thecarverycompany.com", website: "", address: "Benalmádena", rating: 0, score: 7, notes: "Email found - send proposal", status: "new", lastContact: "", nextAction: "Email intro", createdAt: "2026-02-17" },
{ id: "re6", name: "Engel & Völkers Costa del Sol", category: "real-estate", phone: "+34 952 650 234", email: "", website: "", address: "CC Diana Local 23, 29688 Estepona", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re7", name: "Your Viva Marbella", category: "real-estate", phone: "+34 951 27 27 43", email: "", website: "", address: "CC El Rosario, 29604 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re8", name: "Panorama Properties", category: "real-estate", phone: "+34 952 774 266", email: "", website: "", address: "Hotel Local 23, 29602 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re9", name: "Marbella For Sale", category: "real-estate", phone: "+34 952 907 386", email: "", website: "", address: "Edif. Marina Banús, 29660 Puerto Banús", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re10", name: "Domus Venari", category: "real-estate", phone: "+34 952 444 295", email: "", website: "", address: "Ctra. N340 KM189, 29604 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re11", name: "Diana Morales Properties", category: "real-estate", phone: "+34 952 765 138", email: "", website: "", address: "Av. Cánovas del Castillo 4, 29601 Marbella", rating: 0, score: 8, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re12", name: "Hacienda Estates", category: "real-estate", phone: "+34 952 850 154", email: "", website: "", address: "CC Pinogolf Local 2, 29604 Elviria", rating: 0, score: 7, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "re13", name: "Sun Med Estates", category: "real-estate", phone: "+34 952 493 372", email: "", website: "", address: "c/ Sedella 3, La Cala de Mijas", rating: 0, score: 7, notes: "Premium agency - offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "c4", name: "Smart Dental (new)", category: "clinic", phone: "+34 911 98 04 65", email: "", website: "", address: "Av. Blas Infante 17, 29631 Benalmádena", rating: 0, score: 9, notes: "Call for AI demo", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
{ id: "c5", name: "Grupo Dental Clinics", category: "clinic", phone: "", email: "", website: "", address: "Av. Cdad. de Melilla 26, 29639 Benalmádena", rating: 0, score: 7, notes: "Find contact info", status: "new", lastContact: "", nextAction: "Find phone", createdAt: "2026-02-17" },
{ id: "cr3", name: "Marbesol Car Rental", category: "car-rental", phone: "+34 952 93 44 12", email: "", website: "", address: "Málaga Airport", rating: 0, score: 7, notes: "Offer AI employee", status: "new", lastContact: "", nextAction: "Call demo", createdAt: "2026-02-17" },
// Original leads
{ id: "r1", name: "Tex Mex Gringos", category: "restaurant", phone: "+34 951 777 848", email: "info@restaurantespuertomarina.com", website: "restaurantespuertomarina.com", address: "Calle La Fragata, s/n, 29630 Benalmádena", rating: 4.5, score: 7, notes: "Has website - offer AI employee 24/7", status: "new", lastContact: "", nextAction: "Call - offer AI employee", createdAt: "2026-02-16" },
{ id: "r2", name: "La Mar Chica", category: "restaurant", phone: "+34 951 634 708", email: "info.lamarchica@gmail.com", website: "mar-chica.com", address: "Calle Marbella, 1, 29639 Benalmádena Pueblo", rating: 4.3, score: 7, notes: "Has website - offer AI employee 24/7", status: "new", lastContact: "", nextAction: "Call - offer AI employee", createdAt: "2026-02-16" },
{ id: "r3", name: "SALU Grill & Wine", category: "restaurant", phone: "+34 951 715 736", email: "salu.spain@gmail.com", website: "salu-restaurant.com", address: "Calle San José, 6, Benalmádena Pueblo", rating: 4.6, score: 8, notes: "Good site - offer AI employee upgrade", status: "new", lastContact: "", nextAction: "Schedule demo", createdAt: "2026-02-16" },
{ id: "r4", name: "Basil", category: "restaurant", phone: "631 971 592", email: "", website: "basilbenalmadena.com", address: "Plaza Nueva Bonanza, 29630 Benalmádena", rating: 4.4, score: 6, notes: "Has WhatsApp - offer AI employee", status: "new", lastContact: "", nextAction: "Call - offer AI employee", createdAt: "2026-02-16" },
{ id: "r5", name: "Lime & Lemon Tapas", category: "restaurant", phone: "", email: "", website: "limeandlemonbenalmadena.com", address: "Benalmádena", rating: 0, score: 5, notes: "Has website - offer AI employee 24/7", status: "new", lastContact: "", nextAction: "Find phone first", createdAt: "2026-02-16" },
{ id: "r6", name: "Restaurant No7", category: "restaurant", phone: "+34 655 036 827", email: "", website: "", address: "Benalmádena", rating: 0, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "r7", name: "Capitan Bar & Restaurant", category: "restaurant", phone: "+34 674 591 584", email: "", website: "", address: "Benalmádena", rating: 0, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "r8", name: "TORO Puerto Marina", category: "restaurant", phone: "+34 952 913 177", email: "", website: "restaurantespuertomarina.com/toro-puerto-marina", address: "Puerto Marina, Benalmádena", rating: 4.5, score: 7, notes: "Has website - offer AI employee", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "r9", name: "Trocadero Benalmádena", category: "restaurant", phone: "+34 681 142 944", email: "", website: "", address: "Avenida del Sol 121, Benalmádena", rating: 4.2, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "r10", name: "Restaurante La Nina", category: "restaurant", phone: "+34 952 449 193", email: "", website: "", address: "Plaza de Espana, Benalmádena Pueblo", rating: 4.4, score: 10, notes: "NO WEBSITE - BIG OPPORTUNITY", status: "new", lastContact: "", nextAction: "CALL NOW", createdAt: "2026-02-16" },
{ id: "re1", name: "Hernán Bustos Real Estate", category: "real-estate", phone: "", email: "", website: "hernanbustos.com", address: "Benalmádena, Torremolinos", rating: 0, score: 9, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Find contact info", createdAt: "2026-02-16" },
{ id: "re2", name: "ViVi Real Estate", category: "real-estate", phone: "", email: "", website: "vivi-realestate.com", address: "Costa del Sol", rating: 0, score: 9, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Find contact info", createdAt: "2026-02-16" },
{ id: "re3", name: "Marbella Mundo", category: "real-estate", phone: "", email: "", website: "marbellamundo.es", address: "Fuengirola, Costa del Sol", rating: 0, score: 9, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "re4", name: "Homenetspain", category: "real-estate", phone: "+34 633 300 956", email: "", website: "homenetspain.com", address: "Fuengirola", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Call and demo", createdAt: "2026-02-16" },
{ id: "re5", name: "Costa Listings", category: "real-estate", phone: "", email: "", website: "costalistings.es", address: "Benalmádena", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Find contact info", createdAt: "2026-02-16" },
{ id: "c1", name: "Vithas Xanit Hospital", category: "clinic", phone: "+34 952 367 190", email: "info.xanit@vithas.es", website: "vithas.es", address: "Avenida de los Argonautas, s/n, 29631 Benalmádena", rating: 4.5, score: 8, notes: "BIG opportunity - AI employee", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "c2", name: "Smart Dental", category: "clinic", phone: "", email: "", website: "", address: "Benalmádena", rating: 4.5, score: 10, notes: "NO WEBSITE - BIG opportunity", status: "new", lastContact: "", nextAction: "Find phone", createdAt: "2026-02-16" },
{ id: "c3", name: "Rosasco Dental", category: "clinic", phone: "", email: "", website: "", address: "Benalmádena", rating: 4.2, score: 10, notes: "NO WEBSITE - BIG opportunity", status: "new", lastContact: "", nextAction: "Find phone", createdAt: "2026-02-16" },
{ id: "cr1", name: "Malaga U Drive", category: "car-rental", phone: "", email: "", website: "malagaudrive.com", address: "Malaga/Benalmádena", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
{ id: "cr2", name: "ALL IN Car Hire", category: "car-rental", phone: "", email: "", website: "allincarhire.com", address: "Malaga/Benalmádena", rating: 0, score: 8, notes: "Needs AI employee 24/7", status: "new", lastContact: "", nextAction: "Email introduction", createdAt: "2026-02-16" },
];
const categoryIcons: Record<string, string> = {
restaurant: "🍽️",
"real-estate": "🏠",
clinic: "🏥",
"car-rental": "🚗",
other: "📌",
};
const statusColors: Record<LeadStatus, string> = {
new: "bg-blue-500/20 text-blue-400 border-blue-500/30",
contacted: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
qualified: "bg-purple-500/20 text-purple-400 border-purple-500/30",
proposal: "bg-orange-500/20 text-orange-400 border-orange-500/30",
won: "bg-green-500/20 text-green-400 border-green-500/30",
lost: "bg-red-500/20 text-red-400 border-red-500/30",
};
type SortField = "name" | "category" | "score" | "status" | "phone";
export default function LeadsPage() {
const [leads, setLeads] = useState<Lead[]>(allLeads);
const [filter, setFilter] = useState<string>("all");
const [tab, setTab] = useState<"leads" | "email">("leads");
const [sortField, setSortField] = useState<SortField>("score");
const [sortAsc, setSortAsc] = useState(false);
const [selectedLead, setSelectedLead] = useState<Lead | null>(null);
const [search, setSearch] = useState("");
const leadsWithEmail = useMemo(() => leads.filter(l => l.email), [leads]);
useEffect(() => {
const saved = localStorage.getItem("sitemente:leads");
if (saved) {
setLeads(JSON.parse(saved));
} else {
localStorage.setItem("sitemente:leads", JSON.stringify(allLeads));
}
}, []);
const saveLeads = (newLeads: Lead[]) => {
setLeads(newLeads);
localStorage.setItem("sitemente:leads", JSON.stringify(newLeads));
};
const updateStatus = (id: string, status: LeadStatus) => {
const updated = leads.map((l) =>
l.id === id ? { ...l, status, lastContact: new Date().toISOString().split("T")[0] } : l
);
saveLeads(updated);
};
const filteredLeads = useMemo(() => {
return leads
.filter((l) => {
if (filter !== "all" && l.category !== filter && l.status !== filter) return false;
if (search && !l.name.toLowerCase().includes(search.toLowerCase())) return false;
return true;
})
.sort((a, b) => {
let cmp = 0;
if (sortField === "name") cmp = a.name.localeCompare(b.name);
else if (sortField === "category") cmp = a.category.localeCompare(b.category);
else if (sortField === "score") cmp = b.score - a.score;
else if (sortField === "status") cmp = a.status.localeCompare(b.status);
else if (sortField === "phone") cmp = (a.phone ? 1 : 0) - (b.phone ? 1 : 0);
return sortAsc ? cmp : -cmp;
});
}, [leads, filter, sortField, sortAsc, search]);
const stats = {
total: leads.length,
withPhone: leads.filter(l => l.phone).length,
noWebsite: leads.filter(l => !l.website).length,
hot: leads.filter(l => l.score >= 9).length,
new: leads.filter((l) => l.status === "new").length,
won: leads.filter((l) => l.status === "won").length,
};
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortAsc(!sortAsc);
} else {
setSortField(field);
setSortAsc(false);
}
};
const SortIcon = ({ field }: { field: SortField }) => (
<span className="ml-1 opacity-50">{sortField === field ? (sortAsc ? "↑" : "↓") : "↕"}</span>
);
return (
<div className="min-h-screen bg-[#1a1625] text-white" suppressHydrationWarning>
<header className="border-b border-white/10 bg-[#1a1625]/90 backdrop-blur sticky top-0 z-50">
<div className="mx-auto flex w-full max-w-7xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-4">
<a href="/" className="flex items-center gap-3">
<img src="/sitemente-logo-light.png" alt="SiteMente" width={40} height={40} className="h-10 w-auto" />
<span className="font-bold text-xl">Leads CRM</span>
</a>
</div>
<div className="flex items-center gap-3">
<a href="/mission-control" className="px-4 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition"> Mission Control</a>
</div>
</div>
</header>
<main className="mx-auto max-w-7xl px-6 py-8" suppressHydrationWarning>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<p className="text-2xl font-bold text-brand-pink">{stats.total}</p>
<p className="text-sm text-white/60">Total Leads</p>
</div>
<div className="bg-green-500/10 rounded-xl p-4 border border-green-500/30">
<p className="text-2xl font-bold text-green-400">{stats.withPhone}</p>
<p className="text-sm text-white/60">With Phone</p>
</div>
<div className="bg-red-500/10 rounded-xl p-4 border border-red-500/30">
<p className="text-2xl font-bold text-red-400">{stats.noWebsite}</p>
<p className="text-sm text-white/60">No Website</p>
</div>
<div className="bg-orange-500/10 rounded-xl p-4 border border-orange-500/30">
<p className="text-2xl font-bold text-orange-400">{stats.hot}</p>
<p className="text-sm text-white/60">Hot (9-10)</p>
</div>
<div className="bg-blue-500/10 rounded-xl p-4 border border-blue-500/30">
<p className="text-2xl font-bold text-blue-400">{stats.won}</p>
<p className="text-sm text-white/60">Won</p>
</div>
</div>
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex gap-2 bg-white/10 rounded-lg p-1">
<button onClick={() => setTab("leads")} className={`px-4 py-2 rounded-lg transition ${tab === "leads" ? "bg-brand-pink text-white" : "hover:bg-white/10"}`}>📋 Leads ({leads.length})</button>
<button onClick={() => setTab("email")} className={`px-4 py-2 rounded-lg transition ${tab === "email" ? "bg-brand-pink text-white" : "hover:bg-white/10"}`}> Email Outreach ({leadsWithEmail.length})</button>
</div>
{tab === "leads" && (
<>
<input type="text" placeholder="Search leads..." value={search} onChange={(e) => setSearch(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-4 py-2 min-w-[200px]" />
<select value={filter} onChange={(e) => setFilter(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-4 py-2">
<option value="all">All ({leads.length})</option>
<option value="restaurant">Restaurants ({leads.filter(l => l.category === 'restaurant').length})</option>
<option value="real-estate">Real Estate ({leads.filter(l => l.category === 'real-estate').length})</option>
<option value="clinic">Clinics ({leads.filter(l => l.category === 'clinic').length})</option>
<option value="car-rental">Car Rental ({leads.filter(l => l.category === 'car-rental').length})</option>
</select>
</>
)}
</div>
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-white/5 border-b border-white/10">
<tr>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("name")}>Lead <SortIcon field="name" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("category")}>Category <SortIcon field="category" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("phone")}>Phone <SortIcon field="phone" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("score")}>Score <SortIcon field="score" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60 cursor-pointer hover:text-white" onClick={() => handleSort("status")}>Status <SortIcon field="status" /></th>
<th className="text-left px-4 py-3 text-sm font-medium text-white/60">Actions</th>
</tr>
</thead>
<tbody>
{filteredLeads.map((lead) => (
<motion.tr key={lead.id} initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="border-b border-white/5 hover:bg-white/5">
<td className="px-4 py-3">
<p className="font-medium">{lead.name}</p>
<p className="text-xs text-white/50 truncate max-w-[200px]">{lead.address}</p>
</td>
<td className="px-4 py-3"><span className="text-lg">{categoryIcons[lead.category]}</span></td>
<td className="px-4 py-3">
{lead.phone ? <a href={"tel:" + lead.phone} className="text-green-400 hover:underline">{lead.phone}</a> : <span className="text-red-400 text-xs">No phone</span>}
</td>
<td className="px-4 py-3">
<span className={"inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-bold " + (lead.score >= 9 ? "bg-green-500/20 text-green-400" : lead.score >= 7 ? "bg-yellow-500/20 text-yellow-400" : "bg-red-500/20 text-red-400")}>{lead.score}</span>
</td>
<td className="px-4 py-3">
<select value={lead.status} onChange={(e) => updateStatus(lead.id, e.target.value as LeadStatus)} className={"text-xs px-2 py-1 rounded-full border " + statusColors[lead.status as LeadStatus]}>
<option value="new">New</option>
<option value="contacted">Contacted</option>
<option value="qualified">Qualified</option>
<option value="proposal">Proposal</option>
<option value="won">Won</option>
<option value="lost">Lost</option>
</select>
</td>
<td className="px-4 py-3">
<div className="flex gap-1">
{lead.phone && <a href={"tel:" + lead.phone} className="p-2 bg-green-500/20 rounded-lg hover:bg-green-500/30 transition" title="Call">📞</a>}
{lead.email && <a href={"mailto:" + lead.email} className="p-2 bg-blue-500/20 rounded-lg hover:bg-blue-500/30 transition" title="Email"></a>}
{lead.website && <a href={lead.website.startsWith("http") ? lead.website : "https://" + lead.website} target="_blank" rel="noopener noreferrer" className="p-2 bg-purple-500/20 rounded-lg hover:bg-purple-500/30 transition" title="Website">🌐</a>}
<button onClick={() => setSelectedLead(lead)} className="p-2 bg-white/10 rounded-lg hover:bg-white/20 transition" title="Details">👁</button>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</div>
{/* Email Outreach Tab */}
{tab === "email" && (
<div className="space-y-6">
<div className="bg-blue-500/10 border border-blue-500/30 rounded-xl p-6">
<h3 className="text-lg font-bold mb-2">📧 Email Outreach</h3>
<p className="text-white/60 mb-4">Leads with email addresses. Click "Send" to open your email client with pre-filled template.</p>
<div className="grid gap-4">
{leads.filter(l => l.email).map(lead => (
<div key={lead.id} className="bg-white/5 rounded-lg p-4 flex items-center justify-between">
<div>
<p className="font-bold">{lead.name}</p>
<p className="text-sm text-white/60">{lead.email}</p>
<p className="text-xs text-white/40">{categoryIcons[lead.category]} {lead.category}</p>
</div>
<div className="flex gap-2">
<button onClick={() => updateStatus(lead.id, "contacted")} className="px-3 py-1 bg-yellow-500/20 text-yellow-400 rounded text-sm">Mark Emailed</button>
<a href={`mailto:${lead.email}?subject=Tu empleado IA 24/7 - SiteMente&body=Hola,%0D%0A%0D%0AVi tu negocio y me gustaría ofrecerte una solución que puede revolucionar tu atención al cliente.%0D%0A%0D%0ACon SiteMente tienes un empleado IA disponible 24/7 que:%0D%0A- Responde preguntas de clientes%0D%0A- Gestiona reservas automáticamente%0D%0A- Habla en español, inglés, francés, alemán...%0D%0A%0D%0A¿Te interesa ver una demo? Es gratis y sin compromiso.%0D%0A%0D%0ASaludos`} className="px-4 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg font-medium"> Send Email</a>
</div>
</div>
))}
{leads.filter(l => l.email).length === 0 && (
<p className="text-white/40 text-center py-8">No leads with email found. Research more leads!</p>
)}
</div>
</div>
<div className="bg-green-500/10 border border-green-500/30 rounded-xl p-6">
<h3 className="text-lg font-bold mb-2">🤖 AI Calling (Coming Soon)</h3>
<p className="text-white/60">Automated voice calls to leads - requires your confirmation before each call.</p>
<p className="text-sm text-white/40 mt-2">Integrating with Vapi - stay tuned!</p>
</div>
</div>
)}
</main>
{selectedLead && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-[#1a1625] border border-white/20 rounded-2xl p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-xl font-bold">{selectedLead.name}</h3>
<p className="text-white/60">{categoryIcons[selectedLead.category]} {selectedLead.category}</p>
</div>
<button onClick={() => setSelectedLead(null)} className="text-white/40 hover:text-white"></button>
</div>
<div className="space-y-4">
<div><label className="text-sm text-white/60">Address</label><p>{selectedLead.address}</p></div>
<div className="flex gap-4">
<div className="flex-1"><label className="text-sm text-white/60">Phone</label><p className={selectedLead.phone ? "text-green-400" : "text-red-400"}>{selectedLead.phone || "—"}</p></div>
<div className="flex-1"><label className="text-sm text-white/60">Website</label><p className={selectedLead.website ? "" : "text-red-400"}>{selectedLead.website || "—"}</p></div>
</div>
<div><label className="text-sm text-white/60">Score</label><p className="text-2xl font-bold text-brand-pink">{selectedLead.score}/10</p></div>
<div><label className="text-sm text-white/60">Notes</label><p>{selectedLead.notes}</p></div>
<div><label className="text-sm text-white/60">Next Action</label><p className="text-green-400">{selectedLead.nextAction}</p></div>
</div>
<div className="flex gap-2 mt-6">
{selectedLead.phone && <a href={"tel:" + selectedLead.phone} className="flex-1 py-2 bg-green-500 rounded-lg text-center font-medium">📞 Call</a>}
{selectedLead.email && <a href={"mailto:" + selectedLead.email} className="flex-1 py-2 bg-blue-500 rounded-lg text-center font-medium"> Email</a>}
<button onClick={() => { updateStatus(selectedLead.id, "won"); setSelectedLead(null); }} className="flex-1 py-2 bg-green-600 rounded-lg font-medium"> Won</button>
</div>
</div>
</div>
)}
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
# 📚 SiteMente Docs
Long-term documentation for SiteMente operations.
## 🚀 Getting Started
- [Quick Start Guide](/docs/quick-start)
- [Architecture Overview](/docs/architecture)
- [Environment Variables](/docs/env)
## 🛠 Products
- [Smart Starter](/docs/products/starter)
- [Smart Site](/docs/products/site)
- [AI Growth Partner](/docs/products/growth)
## 🔌 Integrations
- [Vapi Voice AI](/docs/integrations/vapi)
- [MiniMax AI](/docs/integrations/minimax)
- [Stripe Payments](/docs/integrations/stripe)
## 📋 Operational
- [Onboarding Checklist](/docs/ops/onboarding)
- [Troubleshooting Guide](/docs/ops/troubleshooting)
- [API Endpoints](/docs/ops/api)
---
*Auto-generated by Horus* 👁
+107 -1
View File
@@ -1,10 +1,116 @@
"use client";
import { useState, useEffect } from "react";
import MissionControlDashboard from "@/components/mission-control/MissionControlDashboard"; import MissionControlDashboard from "@/components/mission-control/MissionControlDashboard";
import { MissionControlProvider } from "@/lib/mission-control/store"; import { MissionControlProvider } from "@/lib/mission-control/store";
const CORRECT_USER = "Marshall";
const CORRECT_PASS = "#1284YallaHorus";
export default function MissionControlPage() { export default function MissionControlPage() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [mounted, setMounted] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("sitemente:mc-auth");
if (saved === "true") {
setIsAuthenticated(true);
}
setMounted(true);
}, []);
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (username === CORRECT_USER && password === CORRECT_PASS) {
localStorage.setItem("sitemente:mc-auth", "true");
setIsAuthenticated(true);
setError("");
} else {
setError("Invalid credentials");
}
};
const handleLogout = () => {
localStorage.removeItem("sitemente:mc-auth");
setIsAuthenticated(false);
setUsername("");
setPassword("");
};
// Show loading until we've checked localStorage on mount
if (!mounted) {
return (
<div className="min-h-screen bg-[#1a1625] flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="text-5xl mb-4">👁</div>
<h1 className="text-3xl font-bold text-white mb-2">Mission Control</h1>
<p className="text-white/60">Loading...</p>
</div>
</div>
</div>
);
}
if (!isAuthenticated) {
return (
<div className="min-h-screen bg-[#1a1625] flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="text-5xl mb-4">👁</div>
<h1 className="text-3xl font-bold text-white mb-2">Mission Control</h1>
<p className="text-white/60">SiteMente Operations</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-white/70 mb-1">Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
placeholder="Enter username"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
placeholder="Enter password"
/>
</div>
{error && (
<p className="text-red-400 text-sm">{error}</p>
)}
<button
type="submit"
className="w-full py-3 bg-brand-pink rounded-lg font-semibold text-white hover:bg-[#ff7bc0] transition"
>
Access Control
</button>
</form>
<p className="text-center text-white/30 text-xs mt-6">
Restricted access. Authorized personnel only.
</p>
</div>
</div>
);
}
return ( return (
<MissionControlProvider> <MissionControlProvider>
<MissionControlDashboard /> <MissionControlDashboard onLogout={handleLogout} />
</MissionControlProvider> </MissionControlProvider>
); );
} }
+94
View File
@@ -18,6 +18,13 @@ const fadeUp = {
type Language = "es" | "en"; type Language = "es" | "en";
type LeadFormData = {
name: string;
phone: string;
business: string;
type: string;
};
const contentByLang = { const contentByLang = {
es: { es: {
nav: { nav: {
@@ -718,6 +725,8 @@ const servicesByLang = {
export default function HomePage() { export default function HomePage() {
const [lang, setLang] = useState<Language>("es"); const [lang, setLang] = useState<Language>("es");
const [contactOpen, setContactOpen] = useState(false); const [contactOpen, setContactOpen] = useState(false);
const [leadForm, setLeadForm] = useState<LeadFormData>({ name: "", phone: "", business: "", type: "restaurant" });
const [leadSubmitted, setLeadSubmitted] = useState(false);
const content = useMemo(() => contentByLang[lang], [lang]); const content = useMemo(() => contentByLang[lang], [lang]);
const heroSlides = useMemo<HeroSlide[]>( const heroSlides = useMemo<HeroSlide[]>(
() => [ () => [
@@ -891,6 +900,91 @@ export default function HomePage() {
))} ))}
</motion.div> </motion.div>
{/* Lead Capture Form */}
{!leadSubmitted && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mt-12 max-w-md mx-auto"
>
<div className="bg-white/10 backdrop-blur rounded-xl p-6 border border-white/20">
<h3 className="text-lg font-bold mb-4 text-center">🚀 Pruébalo Ahora - Es Gratis</h3>
<p className="text-sm text-white/70 mb-4 text-center">Completa y te mostraremos tu AI en acción</p>
<form
onSubmit={(e) => {
e.preventDefault();
setLeadSubmitted(true);
// Here we'll connect to GHL later
}}
className="space-y-3"
>
<input
type="text"
placeholder="Tu nombre"
required
value={leadForm.name}
onChange={(e) => setLeadForm({ ...leadForm, name: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-white/50"
/>
<input
type="tel"
placeholder="Tu teléfono"
required
value={leadForm.phone}
onChange={(e) => setLeadForm({ ...leadForm, phone: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-white/50"
/>
<input
type="text"
placeholder="Nombre de tu negocio"
required
value={leadForm.business}
onChange={(e) => setLeadForm({ ...leadForm, business: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-white/50"
/>
<select
value={leadForm.type}
onChange={(e) => setLeadForm({ ...leadForm, type: e.target.value })}
className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white"
>
<option value="restaurant" className="text-black">🍽 Restaurante</option>
<option value="real-estate" className="text-black">🏠 Inmobiliaria</option>
<option value="clinic" className="text-black">🏥 Clínica</option>
<option value="car-rental" className="text-black">🚗 Alquiler de coches</option>
<option value="other" className="text-black">📌 Otro</option>
</select>
<button
type="submit"
className="w-full bg-brand-pink hover:bg-[#ff7bc0] text-white font-bold py-3 rounded-lg transition"
>
Ver Demo AI Ahora
</button>
</form>
</div>
</motion.div>
)}
{leadSubmitted && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="mt-12 max-w-md mx-auto text-center"
>
<div className="bg-green-500/20 backdrop-blur rounded-xl p-8 border border-green-500/30">
<div className="text-5xl mb-4">🎉</div>
<h3 className="text-2xl font-bold mb-2">Perfecto, {leadForm.name}!</h3>
<p className="text-white/70 mb-4">Preparando tu demo personalizada...</p>
<a
href={`/demos?type=${leadForm.type}&name=${encodeURIComponent(leadForm.business)}`}
className="inline-block bg-brand-pink hover:bg-[#ff7bc0] text-white font-bold px-8 py-3 rounded-lg transition"
>
🚀 Entrar a Demo AI
</a>
</div>
</motion.div>
)}
</section> </section>
</header> </header>
+30
View File
@@ -0,0 +1,30 @@
import Link from "next/link";
export default function SuccessPage() {
return (
<div className="min-h-screen bg-[#5e4a8a] flex items-center justify-center p-4">
<div className="max-w-md w-full text-center">
<div className="text-6xl mb-6"></div>
<h1 className="text-3xl font-bold text-white mb-4">¡Pago Exitoso!</h1>
<p className="text-white/80 mb-8">
Thank you for your payment. We&apos;ve received your order and will contact you shortly to start setting up your AI solution.
</p>
<div className="bg-white/10 rounded-xl p-6 mb-8">
<h2 className="text-lg font-semibold text-white mb-2">What happens next?</h2>
<ul className="text-left text-white/70 space-y-2 text-sm">
<li> You&apos;ll receive a confirmation email</li>
<li> Our team will contact you within 24 hours</li>
<li> We&apos;ll schedule your onboarding call</li>
<li> Your AI solution will be live within 48 hours</li>
</ul>
</div>
<Link
href="/"
className="inline-block px-6 py-3 bg-brand-pink rounded-lg font-semibold text-white hover:bg-[#ff7bc0] transition"
>
Back to Home
</Link>
</div>
</div>
);
}
+153 -468
View File
@@ -1,496 +1,181 @@
"use client"; "use client";
import { useEffect, useMemo, useRef, useState } from "react"; import { useState, useRef } from "react";
import Vapi from "@vapi-ai/web";
// eslint-disable-next-line @typescript-eslint/no-explicit-any const VAPI_PUBLIC_KEY = "d44a0025-24bb-426d-919a-cb0a96416ed4";
type SpeechRecognitionInstance = any; const ASSISTANT_ID = "92630ca5-e165-4360-bce0-dd8730882569";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
type ChatMessage = { interface SiteMenteVoiceWidgetProps {
role: "user" | "assistant"; businessName?: string;
content: string; businessType?: "restaurant" | "real-estate" | "clinic" | "car-rental" | "default";
timestamp: number; theme?: "dark" | "light";
}; }
type ApiResponse = {
response: string;
shouldCaptureEmail: boolean;
suggestedActions: string[];
};
type SiteMenteVoiceWidgetProps = {
initialLang?: "es" | "en";
};
const quickActions = {
es: [
{ label: "¿Cuánto cuesta?", icon: "💰" },
{ label: "Ver casos de éxito", icon: "🎯" },
{ label: "¿Cómo funciona?", icon: "⚙️" },
],
en: [
{ label: "Pricing?", icon: "💰" },
{ label: "Success stories", icon: "🎯" },
{ label: "How it works?", icon: "⚙️" },
],
} as const;
const initialGreeting = {
es: "Hola, soy el cerebro de SiteMente. ¿En qué te puedo ayudar hoy?",
en: "Hi, I'm the SiteMente brain. How can I help you today?",
} as const;
export default function SiteMenteVoiceWidget({ export default function SiteMenteVoiceWidget({
initialLang = "es", businessName = "SiteMente",
businessType = "default",
theme = "dark"
}: SiteMenteVoiceWidgetProps) { }: SiteMenteVoiceWidgetProps) {
const [isOpen, setIsOpen] = useState(false); const [isActive, setIsActive] = useState(false);
const [lang, setLang] = useState<"es" | "en">(initialLang); const [status, setStatus] = useState<"idle" | "connecting" | "active" | "error">("idle");
const [messages, setMessages] = useState<ChatMessage[]>([
{
role: "assistant",
content: initialGreeting[initialLang],
timestamp: Date.now(),
},
]);
const [input, setInput] = useState("");
const [voiceMode, setVoiceMode] = useState(true);
const [isRecording, setIsRecording] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [transcript, setTranscript] = useState(""); const [transcript, setTranscript] = useState("");
const [speechSupported, setSpeechSupported] = useState(true); const [errorMsg, setErrorMsg] = useState<string>("");
const [showTooltip, setShowTooltip] = useState(false); const vapiRef = useRef<any>(null);
const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
const isRecordingRef = useRef(false);
const transcriptRef = useRef("");
const messagesEndRef = useRef<HTMLDivElement | null>(null);
const localeLabel = useMemo(
() => (lang === "es" ? "ES" : "EN"),
[lang]
);
useEffect(() => {
setLang(initialLang);
}, [initialLang]);
useEffect(() => {
const seen = window.localStorage.getItem("sitemente:voice-tooltip");
if (!seen) {
setShowTooltip(true);
window.localStorage.setItem("sitemente:voice-tooltip", "1");
const timeout = window.setTimeout(() => setShowTooltip(false), 4000);
return () => window.clearTimeout(timeout);
}
return undefined;
}, []);
useEffect(() => {
isRecordingRef.current = isRecording;
}, [isRecording]);
useEffect(() => {
transcriptRef.current = transcript;
}, [transcript]);
useEffect(() => {
const SpeechRecognitionImpl =
typeof window !== "undefined"
? ((window as typeof window & {
webkitSpeechRecognition?: SpeechRecognitionConstructor;
}).SpeechRecognition ||
(window as typeof window & {
webkitSpeechRecognition?: SpeechRecognitionConstructor;
}).webkitSpeechRecognition)
: undefined;
if (!SpeechRecognitionImpl) {
setSpeechSupported(false);
return;
}
const recognition = new SpeechRecognitionImpl();
recognition.lang = lang === "es" ? "es-ES" : "en-US";
recognition.interimResults = true;
recognition.continuous = false;
recognition.onresult = (event) => {
const result = Array.from(event.results)
.map((res) => res[0]?.transcript ?? "")
.join(" ");
setTranscript(result.trim());
};
recognition.onerror = () => {
setIsRecording(false);
};
recognition.onend = () => {
if (isRecordingRef.current) {
setIsRecording(false);
const finalTranscript = transcriptRef.current.trim();
if (finalTranscript) {
handleSend(finalTranscript);
}
setTranscript("");
}
};
recognitionRef.current = recognition;
}, [lang]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isOpen]);
useEffect(() => {
if (!isSpeaking) return;
return () => {
window.speechSynthesis?.cancel();
};
}, [isSpeaking]);
useEffect(() => {
setMessages((prev) => {
if (prev.length === 0) return prev;
const updated = [...prev];
if (updated[0].role === "assistant") {
updated[0] = {
...updated[0],
content: initialGreeting[lang],
};
}
return updated;
});
}, [lang]);
const startRecording = () => {
if (!speechSupported || !recognitionRef.current) return;
setTranscript("");
setIsRecording(true);
recognitionRef.current.start();
};
const stopRecording = () => {
if (!recognitionRef.current) return;
recognitionRef.current.stop();
setIsRecording(false);
};
const speak = (text: string) => {
if (!("speechSynthesis" in window)) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang === "es" ? "es-ES" : "en-US";
utterance.onstart = () => setIsSpeaking(true);
utterance.onend = () => setIsSpeaking(false);
utterance.onerror = () => setIsSpeaking(false);
window.speechSynthesis.speak(utterance);
};
const handleSend = async (text: string) => {
if (!text.trim() || isLoading) return;
const userMessage: ChatMessage = {
role: "user",
content: text,
timestamp: Date.now(),
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
const startCall = async () => {
try { try {
const response = await fetch("/api/chat/agent", { console.log("Starting call - initializing Vapi inside click handler...");
method: "POST", setErrorMsg("");
headers: { "Content-Type": "application/json" }, setStatus("connecting");
body: JSON.stringify({
message: text, // Step 1: Verify mic exists
locale: lang, try {
history: messages.slice(-6), const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
}), console.log("✅ Mic stream created:", stream);
console.log("✅ Audio tracks:", stream.getAudioTracks().length);
console.log("✅ Track enabled:", stream.getAudioTracks()[0]?.enabled);
console.log("✅ Track settings:", stream.getAudioTracks()[0]?.getSettings());
} catch (micErr) {
console.log("❌ Mic error:", micErr);
}
// Initialize Vapi INSIDE the click handler (required for iOS)
const vapi = new Vapi(VAPI_PUBLIC_KEY);
vapiRef.current = vapi;
// Set up event listeners
vapi.on("error", (error: any) => {
console.log("Vapi error:", error);
const msg = String(error?.message || error?.error?.message || JSON.stringify(error) || "Error desconocido");
setErrorMsg(msg);
setStatus("error");
setIsActive(false);
}); });
if (!response.ok) { vapi.on("call-start", () => {
throw new Error("Failed to fetch response."); console.log("✅ Call started!");
} setStatus("active");
// Check peer connection for audio senders
setTimeout(() => {
try {
// @ts-ignore - internal property
const pc = vapiRef.current?._call?._pc;
if (pc) {
console.log("📡 PeerConnection found");
pc.getSenders().forEach((sender: any, i: number) => {
console.log(`Sender ${i}:`, sender.track?.kind, sender.track?.enabled);
});
} else {
console.log("⚠️ No PeerConnection found");
}
} catch (e) {
console.log("Error checking PC:", e);
}
}, 2000);
});
const data = (await response.json()) as ApiResponse; vapi.on("call-end", (e: any) => {
const assistantMessage: ChatMessage = { console.log("Call ended", e);
role: "assistant", setStatus("idle");
content: data.response, setIsActive(false);
timestamp: Date.now(), });
};
setMessages((prev) => [...prev, assistantMessage]);
if (voiceMode) { vapi.on("message", (m: any) => {
speak(data.response); console.log("Vapi message:", m);
} });
} catch (error) {
const fallbackMessage: ChatMessage = { vapi.on("speech-start", () => {
role: "assistant", console.log("User speech detected!");
content: });
lang === "es"
? "Hubo un problema al responder. ¿Quieres intentarlo de nuevo?" vapi.on("speech-end", () => {
: "There was a problem responding. Want to try again?", console.log("User speech ended");
timestamp: Date.now(), });
};
setMessages((prev) => [...prev, fallbackMessage]); vapi.on("transcript", (transcript: any) => {
} finally { console.log("Transcript:", transcript);
setIsLoading(false); if (typeof transcript === "string") {
setTranscript(transcript);
} else if (transcript?.text) {
setTranscript(transcript.text);
}
});
console.log("Calling assistant:", ASSISTANT_ID);
// Start the call
await vapi.start(ASSISTANT_ID);
console.log("Call started successfully");
setIsActive(true);
} catch (error: any) {
console.log("Start error:", error);
const msg = String(error?.message || error?.error?.message || JSON.stringify(error) || "Error al iniciar");
setErrorMsg(msg);
setStatus("error");
} }
}; };
const voiceIndicator = isRecording const endCall = async () => {
? "🎤" try {
: isSpeaking if (vapiRef.current) {
? "🔊" await vapiRef.current.stop();
: "🎤"; }
setIsActive(false);
setStatus("idle");
setTranscript("");
} catch (error) {
console.error("End call error:", error);
}
};
const buttonColor = theme === "dark" ? "bg-brand-pink" : "bg-blue-600";
return ( return (
<> <div className="fixed bottom-6 right-6 z-50">
{!isOpen && ( {status === "error" && errorMsg && (
<div className="fixed bottom-6 right-6 z-[9999] flex flex-col items-end gap-2"> <div className="absolute bottom-16 right-0 w-64 bg-red-600 text-white text-xs p-2 rounded-lg mb-2">
<div className="relative"> {errorMsg}
<button </div>
type="button" )}
onClick={() => setIsOpen(true)}
className="relative flex h-[68px] w-[68px] items-center justify-center rounded-full bg-gradient-to-br from-[#8B5CF6] to-[#EC4899] text-white shadow-lg transition hover:scale-110 hover:shadow-[0_12px_30px_rgba(236,72,153,0.45)]" {isActive && (
> <div className="absolute bottom-16 right-0 w-80 bg-[#1a1625] border border-white/20 rounded-xl p-4 shadow-2xl mb-2">
<div className="flex items-center gap-1"> <div className="flex items-center justify-between mb-2">
<span className="h-3 w-1 rounded-full bg-white/80 animate-pulse" /> <span className="text-sm font-medium text-white">🤖 AI</span>
<span className="h-5 w-1 rounded-full bg-white/90 animate-pulse" /> <span className={`w-2 h-2 rounded-full ${status === "active" ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`}></span>
<span className="h-4 w-1 rounded-full bg-white/80 animate-pulse" /> </div>
</div> <div className="h-32 overflow-y-auto text-sm text-white/70 bg-white/5 rounded-lg p-2">
</button> {transcript || "Escuchando..."}
{showTooltip && (
<div className="absolute right-[76px] top-1/2 -translate-y-1/2 rounded-full bg-white px-3 py-1 text-xs font-semibold text-brand-purple-dark shadow-md">
{lang === "es" ? "Prueba la voz" : "Try voice"}
</div>
)}
<div className="absolute -top-6 right-2 rounded-full bg-white/20 px-2 py-1 text-[10px] font-semibold text-white backdrop-blur">
Demo
</div>
</div> </div>
</div> </div>
)} )}
{isOpen && ( <button
<div className="fixed inset-0 z-[9999] flex items-end justify-end p-4 sm:p-6"> onClick={isActive ? endCall : startCall}
<div className="absolute inset-0 bg-black/50" onClick={() => setIsOpen(false)} /> className={`${buttonColor} w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 ${
<div className="relative z-10 flex h-full w-full max-w-[440px] flex-col overflow-hidden rounded-3xl border border-white/15 bg-[#4f3a78] shadow-[0_30px_80px_rgba(0,0,0,0.45)] sm:h-[700px]"> isActive ? "animate-pulse ring-4 ring-red-500/50" : ""
<div className="flex items-center justify-between bg-gradient-to-r from-[#6d4cc2] to-[#ff66b5] px-5 py-4 text-white"> }`}
<div> title={isActive ? "Colgar" : "Hablar con IA"}
<div className="flex items-center gap-2 text-lg font-semibold"> >
<span>SiteMente IA</span> {isActive ? (
<span className="flex items-center gap-1"> <svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 8l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M5 3a2 2 0 00-2 2v1c0 8.284 6.716 15 15 15h1a2 2 0 002-2v-3.28a1 1 0 00-.684-.948l-4.493-1.498a1 1 0 00-1.21.502l-1.13 2.257a11.042 11.042 0 01-5.516-5.517l2.257-1.128a1 1 0 00.502-1.21L9.228 3.683A1 1 0 008.279 3H5z" />
<span className="h-3 w-1 rounded-full bg-white/90 animate-pulse" /> </svg>
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" /> ) : (
</span> <svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</div> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
<p className="text-xs text-white/80"> </svg>
{lang === "es" ? "El cerebro de tu web" : "Your website brain"} )}
</p> </button>
</div>
<div className="flex items-center gap-3 text-lg">
<span>{voiceIndicator}</span>
<button
type="button"
onClick={() => setLang((prev) => (prev === "es" ? "en" : "es"))}
className="rounded-full border border-white/30 px-2 py-1 text-xs font-semibold"
>
{localeLabel}
</button>
<button
type="button"
onClick={() => setIsOpen(false)}
className="text-xl"
>
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto px-5 py-4"> {status === "connecting" && (
{messages.length === 1 && ( <div className="absolute -top-8 right-0 bg-white/10 backdrop-blur px-3 py-1 rounded-full text-xs text-white">
<div className="mb-4 flex flex-wrap gap-2"> Conectando...
{quickActions[lang].map((action) => (
<button
key={action.label}
type="button"
onClick={() => handleSend(action.label)}
className="rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs text-white/90 transition hover:bg-white/20"
>
{action.icon} {action.label}
</button>
))}
</div>
)}
<div className="space-y-4">
{messages.map((message) => (
<div
key={`${message.timestamp}-${message.role}`}
className={`group flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm shadow-md ${
message.role === "user"
? "bg-[#c4a1ff] text-[#3b1c66]"
: "bg-[#6a4bb0] text-white"
}`}
>
<div className="flex items-center gap-2">
{message.role === "assistant" && (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-white/20 text-xs font-semibold">
SM
</span>
)}
<p>{message.content}</p>
</div>
<span className="mt-2 block text-[10px] text-white/60 opacity-0 transition group-hover:opacity-100">
{new Date(message.timestamp).toLocaleTimeString(
lang === "es" ? "es-ES" : "en-US",
{ hour: "2-digit", minute: "2-digit" }
)}
</span>
</div>
</div>
))}
{isLoading && (
<div className="flex items-center gap-2 text-white/70">
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/70" />
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/60 delay-150" />
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/50 delay-300" />
</div>
)}
{isSpeaking && (
<div className="flex items-center gap-2 text-xs text-white/70">
<span className="flex items-center gap-1">
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
<span className="h-3 w-1 rounded-full bg-white/90 animate-pulse" />
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
</span>
{lang === "es" ? "Hablando..." : "Speaking..."}
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
<div className="border-t border-white/10 bg-[#4a3572] px-5 py-4">
{voiceMode ? (
<div className="flex flex-col items-center gap-3">
{transcript && (
<p className="w-full rounded-xl bg-white/10 px-4 py-2 text-sm text-white/90">
{transcript}
</p>
)}
<button
type="button"
onClick={() => {
if (isRecording) {
stopRecording();
return;
}
if (isSpeaking) {
window.speechSynthesis?.cancel();
setIsSpeaking(false);
return;
}
startRecording();
}}
className={`flex h-14 w-14 items-center justify-center rounded-full text-white transition ${
isRecording
? "bg-red-500 animate-pulse"
: isSpeaking
? "bg-blue-500 animate-pulse"
: "bg-white/20 hover:bg-white/30"
}`}
disabled={!speechSupported}
>
{isRecording ? "🔴" : isSpeaking ? "🔊" : "🎤"}
</button>
<p className="text-xs text-white/80">
{isRecording
? lang === "es"
? "Escuchando..."
: "Listening..."
: isSpeaking
? lang === "es"
? "Hablando..."
: "Speaking..."
: lang === "es"
? "Toca para hablar"
: "Tap to talk"}
</p>
{isSpeaking && (
<div className="h-1 w-full overflow-hidden rounded-full bg-white/10">
<div className="h-full w-1/2 animate-pulse rounded-full bg-brand-pink/80" />
</div>
)}
<div className="flex w-full items-center justify-between text-xs text-white/70">
<button
type="button"
onClick={() => setVoiceMode(false)}
className="flex items-center gap-1"
>
{lang === "es" ? "Texto" : "Text"}
</button>
<button
type="button"
onClick={() => {
window.speechSynthesis?.cancel();
setIsSpeaking(false);
}}
className="flex items-center gap-1"
>
{lang === "es" ? "Pausar" : "Pause"}
</button>
</div>
</div>
) : (
<div className="flex items-center gap-3">
<input
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder={
lang === "es"
? "Escribe tu mensaje..."
: "Type your message..."
}
className="flex-1 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm text-white placeholder:text-white/50 focus:border-white/50 focus:outline-none"
/>
<button
type="button"
onClick={() => handleSend(input)}
className="rounded-full bg-brand-pink px-4 py-2 text-sm font-semibold text-white"
>
</button>
<button
type="button"
onClick={() => setVoiceMode(true)}
className="rounded-full border border-white/20 px-3 py-2 text-white/80"
>
🎤
</button>
</div>
)}
{!speechSupported && (
<p className="mt-2 text-center text-xs text-white/60">
{lang === "es"
? "Tu navegador no soporta voz. Usa el modo texto."
: "Your browser doesn't support voice. Use text mode."}
</p>
)}
</div>
</div>
</div> </div>
)} )}
</> </div>
); );
} }
@@ -4,6 +4,10 @@ import { useState, useEffect } from "react";
import { useMissionControl } from "@/lib/mission-control/store"; import { useMissionControl } from "@/lib/mission-control/store";
import { TaskStatus } from "@/lib/mission-control/types"; import { TaskStatus } from "@/lib/mission-control/types";
import VoiceChat from "./VoiceChat"; import VoiceChat from "./VoiceChat";
import MondayBoard from "./MondayBoard";
import { TaskCardsPanel } from "./TaskCardsPanel";
import { TaskHistoryPanel } from "./TaskHistoryPanel";
import { TradingPanel } from "./TradingPanel";
import AIManagement from "@/components/ai-management/AIManagement"; import AIManagement from "@/components/ai-management/AIManagement";
import Council from "@/components/council/Council"; import Council from "@/components/council/Council";
@@ -23,13 +27,27 @@ interface SidebarCategory {
} }
const sidebarCategories: SidebarCategory[] = [ const sidebarCategories: SidebarCategory[] = [
{ id: "leads", name: "Leads", icon: "📈", items: [
{ id: "leads-crm", name: "CRM", icon: "📊", category: "leads" },
{ id: "dashboard", name: "Client Dashboard", icon: "🏢", category: "dashboard" },
]},
{ id: "projects", name: "Projects", icon: "🎯", items: [ { id: "projects", name: "Projects", icon: "🎯", items: [
{ id: "monday", name: "Monday Board", icon: "📊", category: "monday" },
{ id: "sitemente", name: "SiteMente", icon: "🌐", color: "#ff7bc0", category: "projects" }, { id: "sitemente", name: "SiteMente", icon: "🌐", color: "#ff7bc0", category: "projects" },
{ id: "demos", name: "Demo Pages", icon: "🎨", category: "demos" }, { id: "demos", name: "Demo Pages", icon: "🎨", category: "demos" },
{ id: "holacompi", name: "HolaCompi", icon: "🤝", color: "#6366f1", category: "projects" }, { id: "holacompi", name: "HolaCompi", icon: "🤝", color: "#6366f1", category: "projects" },
{ id: "arabredox", name: "Arabredox", icon: "💚", color: "#22c55e", category: "projects" },
{ id: "infrastructure", name: "Infra", icon: "⚙️", color: "#10b981", category: "projects" },
]},
{ id: "trading", name: "Trading", icon: "📈", items: [
{ id: "trading-research", name: "Deep Research", icon: "🔬", category: "trading" },
{ id: "trading-strategies", name: "Strategies", icon: "🎯", category: "trading" },
{ id: "trading-execution", name: "Execution", icon: "⚡", category: "trading" },
{ id: "trading-journal", name: "Journal", icon: "📔", category: "trading" },
]}, ]},
{ id: "tasks", name: "Tasks", icon: "✓", items: [ { id: "tasks", name: "Tasks", icon: "✓", items: [
{ id: "all", name: "All Tasks", icon: "📋", category: "tasks" }, { id: "task-cards", name: "Task Cards", icon: "☑️", category: "task-cards" },
{ id: "task-history", name: "History", icon: "📜", category: "task-history" },
]}, ]},
{ id: "chat", name: "Chat", icon: "💬", items: [ { id: "chat", name: "Chat", icon: "💬", items: [
{ id: "voice", name: "Voice Chat", icon: "🎤", category: "chat" }, { id: "voice", name: "Voice Chat", icon: "🎤", category: "chat" },
@@ -44,6 +62,9 @@ const sidebarCategories: SidebarCategory[] = [
{ id: "memory", name: "Memory", icon: "🧠", items: [ { id: "memory", name: "Memory", icon: "🧠", items: [
{ id: "logs", name: "Session Logs", icon: "📝", category: "memory" }, { id: "logs", name: "Session Logs", icon: "📝", category: "memory" },
]}, ]},
{ id: "docs", name: "Docs", icon: "📚", items: [
{ id: "docs-index", name: "Documentation", icon: "📚", category: "docs" },
]},
]; ];
const statusConfig: Record<TaskStatus, { label: string; color: string }> = { const statusConfig: Record<TaskStatus, { label: string; color: string }> = {
@@ -54,8 +75,13 @@ const statusConfig: Record<TaskStatus, { label: string; color: string }> = {
paused: { label: "Paused", color: "text-gray-400" }, paused: { label: "Paused", color: "text-gray-400" },
}; };
export default function MissionControlDashboard() { interface MissionControlDashboardProps {
onLogout?: () => void;
}
export default function MissionControlDashboard({ onLogout }: MissionControlDashboardProps) {
const { tasks, toggleTask, updateTaskStatus, addTask, getProjectProgress, getTasksByProject } = useMissionControl(); const { tasks, toggleTask, updateTaskStatus, addTask, getProjectProgress, getTasksByProject } = useMissionControl();
const [mounted, setMounted] = useState(false);
const [selectedItem, setSelectedItem] = useState<string>("sitemente"); const [selectedItem, setSelectedItem] = useState<string>("sitemente");
const [filter, setFilter] = useState<TaskStatus | "all">("all"); const [filter, setFilter] = useState<TaskStatus | "all">("all");
const [expandedCategories, setExpandedCategories] = useState<string[]>(["projects"]); const [expandedCategories, setExpandedCategories] = useState<string[]>(["projects"]);
@@ -64,6 +90,10 @@ export default function MissionControlDashboard() {
const [newTaskTitle, setNewTaskTitle] = useState(""); const [newTaskTitle, setNewTaskTitle] = useState("");
const [newTaskProject, setNewTaskProject] = useState("sitemente"); const [newTaskProject, setNewTaskProject] = useState("sitemente");
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "/" && !e.ctrlKey && !e.metaKey) { if (e.key === "/" && !e.ctrlKey && !e.metaKey) {
@@ -115,8 +145,36 @@ export default function MissionControlDashboard() {
setNewTaskTitle(""); setNewTaskTitle("");
}; };
if (!mounted) {
return (
<div className="min-h-screen bg-[#1a1625] text-white flex items-center justify-center">
<div className="text-white/60">Loading...</div>
</div>
);
}
// Golden Wisdom Banner
const goldenNotes = (
<div className="fixed bottom-4 right-4 z-50 max-w-sm">
<div className="bg-gradient-to-br from-[#1a1625] to-[#2d1f3d] border border-amber-500/30 rounded-xl p-4 shadow-2xl shadow-amber-500/10">
<div className="flex items-center gap-2 mb-2">
<span className="text-amber-400">🔥</span>
<h3 className="font-bold text-amber-400 text-sm">GOLDEN NOTES</h3>
</div>
<ul className="text-xs text-white/80 space-y-1">
<li> Fun first. Learning second. Outcome last.</li>
<li> Signal intent early don't wait too long</li>
<li>• Warmth + tension = desire, not friendzone</li>
<li>• Lead more — calm + leading = attractive</li>
<li>• Feel more, optimize less</li>
</ul>
</div>
</div>
)
return ( return (
<div className="min-h-screen bg-[#1a1625] text-white flex"> <div className="min-h-screen bg-[#1a1625] text-white flex">
{goldenNotes}
<aside className="w-64 border-r border-white/10 bg-[#1a1625] p-4"> <aside className="w-64 border-r border-white/10 bg-[#1a1625] p-4">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-white/10"> <div className="flex items-center gap-3 mb-6 pb-4 border-b border-white/10">
<div className="w-10 h-10 rounded-xl bg-brand-pink flex items-center justify-center text-xl">👁️</div> <div className="w-10 h-10 rounded-xl bg-brand-pink flex items-center justify-center text-xl">👁️</div>
@@ -145,6 +203,9 @@ export default function MissionControlDashboard() {
<p className="text-xs text-white/40 uppercase mb-2 px-3">Quick</p> <p className="text-xs text-white/40 uppercase mb-2 px-3">Quick</p>
<a href="/" className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/5 hover:text-white transition">🏠 SiteMente Site</a> <a href="/" className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/5 hover:text-white transition">🏠 SiteMente Site</a>
<button onClick={collapseAll} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/5 hover:text-white transition">⊖ Collapse All</button> <button onClick={collapseAll} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-white/60 hover:bg-white/5 hover:text-white transition">⊖ Collapse All</button>
{onLogout && (
<button onClick={onLogout} className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-red-400 hover:bg-red-500/10 transition">🚪 Logout</button>
)}
</div> </div>
</aside> </aside>
@@ -169,10 +230,14 @@ export default function MissionControlDashboard() {
{currentItem?.category === "chat" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><VoiceChat /></div>} {currentItem?.category === "chat" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><VoiceChat /></div>}
{currentItem?.category === "monday" && <div className="h-full"><MondayBoard /></div>}
{currentItem?.category === "council" && currentItem?.id === "teams" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><Council /></div>} {currentItem?.category === "council" && currentItem?.id === "teams" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><Council /></div>}
{currentItem?.category === "council-settings" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><AIManagement /></div>} {currentItem?.category === "council-settings" && <div className="rounded-xl border border-white/10 bg-white/5 p-6"><AIManagement /></div>}
{currentItem?.category === "trading" && <TradingPanel />}
{currentItem?.category === "calendar" && ( {currentItem?.category === "calendar" && (
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-xl border border-white/10 bg-white/5 p-6"> <div className="rounded-xl border border-white/10 bg-white/5 p-6">
@@ -191,6 +256,37 @@ export default function MissionControlDashboard() {
<div className="space-y-2 text-sm text-white/70"><p>Memory is stored in:</p><ul className="text-white/50 space-y-1"><li>• localStorage (browser)</li><li>• GitHub repo (daily commits)</li><li>• MEMORY.md (curated)</li></ul></div> <div className="space-y-2 text-sm text-white/70"><p>Memory is stored in:</p><ul className="text-white/50 space-y-1"><li>• localStorage (browser)</li><li>• GitHub repo (daily commits)</li><li>• MEMORY.md (curated)</li></ul></div>
</div> </div>
)} )}
{currentItem?.category === "docs" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">📚 Documentation</h3>
<p className="text-white/60 mb-4">Long-term docs and guides</p>
<div className="space-y-3">
<a href="/mission-control/docs" target="_blank" className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition">
<span className="text-2xl">📖</span>
<div><p className="font-medium">Docs Index</p><p className="text-xs text-white/50">All documentation</p></div>
</a>
<a href="https://github.com/HaithamEKhalifa/SiteMente" target="_blank" className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition">
<span className="text-2xl">🐙</span>
<div><p className="font-medium">GitHub Repo</p><p className="text-xs text-white/50">Source code & issues</p></div>
</a>
<a href="https://sitemente.com" target="_blank" className="flex items-center gap-3 p-3 rounded-lg bg-white/5 hover:bg-white/10 transition">
<span className="text-2xl">🌐</span>
<div><p className="font-medium">Live Site</p><p className="text-xs text-white/50">sitemente.com</p></div>
</a>
</div>
</div>
)}
{/* Task Cards View */}
{currentItem?.category === "task-cards" && (
<TaskCardsPanel tasks={tasks} toggleTask={toggleTask} />
)}
{/* Task History View */}
{currentItem?.category === "task-history" && (
<TaskHistoryPanel />
)}
{currentItem?.category === "demos" && ( {currentItem?.category === "demos" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6"> <div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8"> <div className="text-center py-8">
@@ -208,6 +304,32 @@ export default function MissionControlDashboard() {
</div> </div>
)} )}
{currentItem?.category === "leads" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">📈</div>
<h3 className="text-xl font-bold mb-2">Leads CRM</h3>
<p className="text-white/60 mb-6">Track and manage your leads</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="/leads" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">📈 Open Leads CRM</a>
</div>
</div>
</div>
)}
{currentItem?.category === "dashboard" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<div className="text-center py-8">
<div className="text-5xl mb-4">🏢</div>
<h3 className="text-xl font-bold mb-2">Client Dashboard</h3>
<p className="text-white/60 mb-6">Where your clients see bookings & leads</p>
<div className="flex flex-wrap justify-center gap-3">
<a href="/dashboard" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">🏢 Open Dashboard Demo</a>
</div>
</div>
</div>
)}
{currentItem?.category === "projects" && ( {currentItem?.category === "projects" && (
<> <>
@@ -237,6 +359,7 @@ export default function MissionControlDashboard() {
<select value={newTaskProject} onChange={(e) => setNewTaskProject(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-3 py-1.5 text-sm"> <select value={newTaskProject} onChange={(e) => setNewTaskProject(e.target.value)} className="bg-white/10 border border-white/20 rounded-lg px-3 py-1.5 text-sm">
<option value="sitemente">SiteMente</option> <option value="sitemente">SiteMente</option>
<option value="holacompi">HolaCompi</option> <option value="holacompi">HolaCompi</option>
<option value="arabredox">Arabredox</option>
<option value="infrastructure">Infrastructure</option> <option value="infrastructure">Infrastructure</option>
</select> </select>
<button onClick={handleAddTask} className="px-4 py-1.5 bg-brand-pink rounded-lg text-sm font-medium">Add Task</button> <button onClick={handleAddTask} className="px-4 py-1.5 bg-brand-pink rounded-lg text-sm font-medium">Add Task</button>
+261
View File
@@ -0,0 +1,261 @@
"use client";
import { useState, useEffect } from "react";
// Monday-style columns
const COLUMNS = [
{ id: "brainstorming", name: "💡 Brainstorming", color: "#ff6b6b" },
{ id: "planning", name: "📋 Planning", color: "#ffd93d" },
{ id: "ready", name: "✅ Ready", color: "#6bcb77" },
{ id: "in-progress", name: "🚀 In Progress", color: "#4d96ff" },
{ id: "review", name: "👀 Review", color: "#9b59b6" },
{ id: "done", name: "🎉 Done", color: "#00d2d3" },
];
interface Feature {
id: string;
title: string;
description: string;
column: string;
projectId: string;
approved: boolean;
implemented: boolean;
}
// Project features - these will be listed for approval
const PROJECT_FEATURES_INITIAL: Feature[] = [
// SiteMente v1
{ id: "s1", title: "Vertical Pack Cards", description: "Beautiful cards for Real Estate, Restaurant, Clinic verticals on landing", column: "brainstorming", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s2", title: "Contact/Onboarding Form", description: "Lead capture form with business type selection", column: "brainstorming", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s3", title: "AI Widget Live Demo", description: "Working voice/chat AI widget on landing page", column: "brainstorming", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s4", title: "Mobile Responsive Pass", description: "Full mobile responsiveness audit and fixes", column: "planning", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s5", title: "Demo Pages SEO", description: "Per-vertical landing pages with unique meta tags", column: "planning", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s6", title: "Lead Capture Integration", description: "Connect forms to email/CRM notification", column: "planning", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s7", title: "WhatsApp Business Integration", description: "Auto-send leads to WhatsApp", column: "planning", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s8", title: "Pricing Toggle", description: "Monthly/Annual toggle with discount display", column: "ready", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s9", title: "Case Studies Section", description: "Real testimonials from Costa del Sol clients", column: "ready", projectId: "sitemente-v1", approved: false, implemented: false },
{ id: "s10", title: "Blog Section", description: "SEO blog with AI-generated content tips", column: "in-progress", projectId: "sitemente-v1", approved: false, implemented: false },
// HolaCompi v1
{ id: "h1", title: "Voice AI Agent", description: "Real voice conversation with callers", column: "brainstorming", projectId: "holacompi-v1", approved: false, implemented: false },
{ id: "h2", title: "WhatsApp Integration", description: "AI responds on WhatsApp Business", column: "brainstorming", projectId: "holacompi-v1", approved: false, implemented: false },
{ id: "h3", title: "Consumer Rights Chatbot", description: "Legal info chatbot for immigrants", column: "brainstorming", projectId: "holacompi-v1", approved: false, implemented: false },
{ id: "h4", title: "Spanish Bureaucracy Guide", description: "AI guide through Spanish paperwork", column: "planning", projectId: "holacompi-v1", approved: false, implemented: false },
{ id: "h5", title: "Multi-language Support", description: "ES, EN, FR, AR, RO language options", column: "planning", projectId: "holacompi-v1", approved: false, implemented: false },
{ id: "h6", title: "Emergency Contacts", description: "Quick access to emergency services by location", column: "ready", projectId: "holacompi-v1", approved: false, implemented: false },
// Infrastructure
{ id: "i1", title: "Production Deployment", description: "Deploy to production domain (sitemente.com)", column: "planning", projectId: "infrastructure", approved: false, implemented: false },
{ id: "i2", title: "Cloud Backups", description: "Automated daily backups to cloud storage", column: "planning", projectId: "infrastructure", approved: false, implemented: false },
{ id: "i3", title: "Monitoring Setup", description: "Uptime monitoring and alerts", column: "planning", projectId: "infrastructure", approved: false, implemented: false },
{ id: "i4", title: "SSL Certificate", description: "Proper HTTPS for production", column: "ready", projectId: "infrastructure", approved: false, implemented: false },
{ id: "i5", title: "Email Notifications", description: "System sends emails for important events", column: "in-progress", projectId: "infrastructure", approved: false, implemented: false },
];
const PROJECTS = [
{ id: "sitemente-v1", name: "SiteMente v1", emoji: "🏢", description: "AI website platform for local businesses", color: "#8B5CF6" },
{ id: "holacompi-v1", name: "HolaCompi v1", emoji: "🤝", description: "AI ally for immigrants & consumers", color: "#EC4899" },
{ id: "infrastructure", name: "Infrastructure", emoji: "⚙️", description: "DevOps, deployment & maintenance", color: "#10B981" },
];
export default function MondayBoard() {
const [mounted, setMounted] = useState(false);
const [selectedProject, setSelectedProject] = useState<string>("sitemente-v1");
const [features, setFeatures] = useState<Feature[]>([]);
const [showApproval, setShowApproval] = useState<string | null>(null);
useEffect(() => {
setMounted(true);
setFeatures(PROJECT_FEATURES_INITIAL);
}, []);
if (!mounted) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-white/60">Loading...</div>
</div>
);
}
const projectFeatures = features.filter(f => f.projectId === selectedProject);
const getColumnFeatures = (columnId: string) => {
return projectFeatures.filter(f => f.column === columnId);
};
const approveFeature = (featureId: string) => {
setFeatures(prev => prev.map(f =>
f.id === featureId ? { ...f, approved: true, column: "ready" } : f
));
setShowApproval(null);
};
const markImplemented = (featureId: string) => {
setFeatures(prev => prev.map(f =>
f.id === featureId ? { ...f, implemented: true, column: "done" } : f
));
};
const getProgress = () => {
const total = projectFeatures.length;
const done = projectFeatures.filter(f => f.column === "done").length;
return total > 0 ? Math.round((done / total) * 100) : 0;
};
const currentFeature = showApproval ? features.find(f => f.id === showApproval) : null;
return (
<div className="h-full flex flex-col">
{/* Project Selector */}
<div className="flex items-center gap-3 p-4 border-b border-white/10">
<span className="text-white/60 text-sm">Project:</span>
<div className="flex gap-2">
{PROJECTS.map(proj => (
<button
key={proj.id}
onClick={() => setSelectedProject(proj.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition ${
selectedProject === proj.id
? "bg-white/20 text-white"
: "text-white/60 hover:text-white hover:bg-white/10"
}`}
>
<span>{proj.emoji}</span>
<span>{proj.name}</span>
</button>
))}
</div>
<div className="ml-auto flex items-center gap-2">
<div className="w-32 h-2 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-brand-pink transition-all"
style={{ width: `${getProgress()}%` }}
/>
</div>
<span className="text-xs text-white/60">{getProgress()}%</span>
</div>
</div>
{/* Board */}
<div className="flex-1 overflow-x-auto p-4">
<div className="flex gap-3 min-w-max h-full">
{COLUMNS.map(column => {
const columnFeatures = getColumnFeatures(column.id);
return (
<div
key={column.id}
className="w-72 flex-shrink-0 flex flex-col rounded-xl bg-white/5 border border-white/10"
>
{/* Column Header */}
<div
className="p-3 rounded-t-xl border-b border-white/10"
style={{ backgroundColor: `${column.color}20` }}
>
<div className="flex items-center justify-between">
<span className="font-semibold text-sm" style={{ color: column.color }}>
{column.name}
</span>
<span className="text-xs bg-white/10 px-2 py-0.5 rounded-full text-white/70">
{columnFeatures.length}
</span>
</div>
</div>
{/* Features List */}
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{columnFeatures.map(feature => (
<div
key={feature.id}
className={`p-3 rounded-lg border transition-all cursor-pointer ${
feature.approved
? "bg-brand-pink/20 border-brand-pink/50"
: feature.implemented
? "bg-green-500/20 border-green-500/50"
: "bg-white/5 border-white/10 hover:border-white/30"
}`}
onClick={() => {
if (!feature.approved && !feature.implemented) {
setShowApproval(feature.id);
}
}}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<h4 className="text-sm font-medium text-white">{feature.title}</h4>
<p className="text-xs text-white/50 mt-1">{feature.description}</p>
</div>
<div className="flex flex-col gap-1">
{feature.approved && !feature.implemented && (
<button
onClick={(e) => {
e.stopPropagation();
markImplemented(feature.id);
}}
className="text-xs bg-green-500/30 hover:bg-green-500/50 text-green-300 px-2 py-1 rounded transition"
>
Done
</button>
)}
{feature.approved && (
<span className="text-xs text-brand-pink"> Approved</span>
)}
{feature.implemented && (
<span className="text-xs text-green-400">🎉 Live</span>
)}
</div>
</div>
</div>
))}
{columnFeatures.length === 0 && (
<div className="text-center text-white/30 text-xs py-4">
No items
</div>
)}
</div>
</div>
);
})}
</div>
</div>
{/* Approval Modal */}
{showApproval && currentFeature && (
<div
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
onClick={() => setShowApproval(null)}
>
<div
className="bg-[#1a1625] border border-white/20 rounded-2xl p-6 max-w-md w-full mx-4"
onClick={e => e.stopPropagation()}
>
<h3 className="text-xl font-bold text-white mb-2">🚀 Approve Feature?</h3>
<p className="text-white/70 mb-4">{currentFeature.description}</p>
<div className="bg-white/5 rounded-xl p-4 mb-6">
<h4 className="text-sm font-semibold text-brand-pink mb-2">{currentFeature.title}</h4>
<p className="text-xs text-white/50">{currentFeature.description}</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowApproval(null)}
className="flex-1 px-4 py-2 rounded-lg border border-white/20 text-white/70 hover:bg-white/10 transition"
>
Cancel
</button>
<button
onClick={() => approveFeature(currentFeature.id)}
className="flex-1 px-4 py-2 rounded-lg bg-brand-pink text-white font-semibold hover:bg-[#ff7bc0] transition"
>
Approve
</button>
</div>
<p className="text-xs text-white/40 mt-4 text-center">
Clicking approve will move this to Ready column for implementation
</p>
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,279 @@
'use client'
import { useState, useEffect } from 'react'
import { Task, TaskStatus } from '@/lib/mission-control/types'
interface TaskCardsPanelProps {
tasks: Task[]
toggleTask: (id: string) => void
}
const projects = ['sitemente', 'holacompi', 'arabredox', 'infrastructure', 'trading']
export function TaskCardsPanel({ tasks, toggleTask }: TaskCardsPanelProps) {
const [selectedProject, setSelectedProject] = useState<string>('sitemente')
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null)
const [command, setCommand] = useState('')
const [responses, setResponses] = useState<{id: string, task: string, command: string, reply: string, createdAt: string}[]>([])
// Poll for replies every 10 seconds
useEffect(() => {
const pollReplies = async () => {
try {
const res = await fetch('/api/command-history')
if (res.ok) {
const data = await res.json()
// Get entries with replies that haven't been shown
const withReplies = (data.history || []).filter((h: any) => h.reply && h.reply.length > 0)
setResponses(withReplies.slice(-5).reverse())
}
} catch (e) {}
}
pollReplies()
const interval = setInterval(pollReplies, 10000)
return () => clearInterval(interval)
}, [])
const projectTasks = tasks.filter(t =>
t.project === selectedProject &&
(t.status === 'todo' || t.status === 'in_progress')
)
const handleTaskClick = (taskId: string) => {
setSelectedTaskId(selectedTaskId === taskId ? null : taskId)
setCommand('')
}
const handleConfirm = () => {
if (!selectedTaskId || !command.trim()) return
const task = tasks.find(t => t.id === selectedTaskId)
if (!task) return
fetch('/api/command-history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task: task.title,
command: command,
project: selectedProject,
action: 'continue-task'
})
}).catch(() => {})
fetch('/api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: command,
task: task.title,
action: 'continue-task'
})
}).catch(() => {})
alert(`✅ Sent to Horus: "${command}"`)
setCommand('')
setSelectedTaskId(null)
}
const handleQuickChat = () => {
if (!command.trim()) return
// Save quick chat to history - this will notify Horus via the API
fetch('/api/command-history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task: `Quick message - ${selectedProject}`,
command: command,
project: selectedProject,
action: 'quick-message'
})
}).catch(() => {})
alert(`✅ Sent to Horus! I'll reply shortly.`)
setCommand('')
}
const selectedTask = tasks.find(t => t.id === selectedTaskId)
return (
<div className="space-y-4">
{/* Project Tabs */}
<div className="flex gap-2 flex-wrap">
{projects.map(project => {
const count = tasks.filter(t =>
t.project === project &&
(t.status === 'todo' || t.status === 'in_progress')
).length
return (
<button
key={project}
onClick={() => {
setSelectedProject(project)
setSelectedTaskId(null)
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
selectedProject === project
? 'bg-brand-pink text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
}`}
>
{project.charAt(0).toUpperCase() + project.slice(1)}
<span className="ml-2 text-xs opacity-70">({count})</span>
</button>
)
})}
</div>
{/* Task Cards */}
<div className="border border-white/20 rounded-lg p-4 bg-white/5">
<h3 className="text-lg font-bold mb-4">
📋 {selectedProject.toUpperCase()} TASKS ({projectTasks.length})
</h3>
<div className="space-y-2">
{projectTasks.map((task) => (
<div
key={task.id}
className={`p-3 rounded-lg border transition-all ${
selectedTaskId === task.id
? 'border-brand-pink bg-brand-pink/10'
: 'border-white/10 bg-white/5 hover:border-white/30'
}`}
onClick={() => handleTaskClick(task.id)}
>
<div className="flex items-center gap-3">
<button
onClick={(e) => {
e.stopPropagation()
toggleTask(task.id)
}}
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center transition ${
task.status === 'done'
? 'border-green-500 bg-green-500'
: task.status === 'in_progress'
? 'border-yellow-500 bg-yellow-500'
: 'border-white/30 hover:border-white/50'
}`}
>
{task.status === 'done' && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<div className="flex-1">
<p className={`font-medium ${task.status === 'done' ? 'line-through opacity-50' : ''}`}>
{task.title}
</p>
{task.description && (
<p className="text-xs text-white/50 truncate">{task.description}</p>
)}
</div>
<span className="text-lg">
{selectedTaskId === task.id ? '▼' : '▶'}
</span>
</div>
{/* Expanded Input */}
{selectedTaskId === task.id && (
<div className="mt-3 ml-8">
<textarea
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder={`What to do with "${task.title}"?`}
className="w-full px-3 py-2 bg-black/50 border border-white/20 rounded text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink resize-none"
rows={3}
onClick={(e) => e.stopPropagation()}
autoFocus
/>
<div className="flex gap-2 mt-2">
<button
onClick={(e) => {
e.stopPropagation()
handleConfirm()
}}
disabled={!command.trim()}
className={`px-4 py-1.5 rounded text-xs font-bold ${
command.trim()
? 'bg-brand-pink text-white hover:bg-[#ff7bc0]'
: 'bg-white/10 text-white/30 cursor-not-allowed'
}`}
>
SEND TO HORUS
</button>
<button
onClick={(e) => {
e.stopPropagation()
setSelectedTaskId(null)
setCommand('')
}}
className="px-3 py-1.5 text-xs text-white/60 hover:text-white"
>
Cancel
</button>
</div>
</div>
)}
</div>
))}
</div>
{projectTasks.length === 0 && (
<p className="text-white/50 text-center py-4">No pending tasks </p>
)}
{/* Quick Chat - always available */}
<div className="mt-4 pt-4 border-t border-white/10">
<p className="text-xs text-white/50 mb-2">💬 Quick message for {selectedProject}</p>
<div className="flex gap-2">
<textarea
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder={`New task or message for ${selectedProject}...`}
className="flex-1 px-3 py-2 bg-black/50 border border-white/20 rounded text-sm text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink resize-none"
rows={2}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleQuickChat()
}
}}
/>
<button
onClick={handleQuickChat}
disabled={!command.trim()}
className={`px-4 rounded text-sm font-bold ${
command.trim()
? 'bg-brand-pink text-white hover:bg-[#ff7bc0]'
: 'bg-white/10 text-white/30 cursor-not-allowed'
}`}
>
</button>
</div>
</div>
{/* Responses from Horus */}
{responses.length > 0 && (
<div className="mt-4 pt-4 border-t border-white/10">
<p className="text-xs text-white/50 mb-2">💬 Recent replies from Horus</p>
<div className="space-y-2 max-h-40 overflow-y-auto">
{responses.map((r) => (
<div key={r.id} className="p-2 rounded bg-brand-pink/10 border border-brand-pink/30 text-sm">
<p className="text-white/60 text-xs">You: {r.command}</p>
<p className="text-brand-pink mt-1">👁 {r.reply}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}
@@ -0,0 +1,122 @@
'use client'
import { useState, useEffect } from 'react'
interface HistoryEntry {
id: string
task: string
command: string
project: string
action: string
createdAt: string
status: string
reply?: string
}
const projects = ['all', 'sitemente', 'holacompi', 'arabredox', 'infrastructure', 'trading']
export function TaskHistoryPanel() {
const [history, setHistory] = useState<HistoryEntry[]>([])
const [loading, setLoading] = useState(true)
const [selectedProject, setSelectedProject] = useState<string>('all')
useEffect(() => {
loadHistory()
}, [selectedProject])
const loadHistory = async () => {
setLoading(true)
try {
const url = selectedProject === 'all'
? '/api/command-history'
: `/api/command-history?project=${selectedProject}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
setHistory(data.history || [])
}
} catch (error) {
console.error('Failed to load history:', error)
}
setLoading(false)
}
return (
<div className="space-y-4">
{/* Project Filter Tabs */}
<div className="flex gap-2 flex-wrap">
{projects.map(project => (
<button
key={project}
onClick={() => setSelectedProject(project)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
selectedProject === project
? 'bg-brand-pink text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
}`}
>
{project === 'all' ? 'All Projects' : project.charAt(0).toUpperCase() + project.slice(1)}
</button>
))}
</div>
{/* History List */}
<div className="border border-white/20 rounded-lg p-4 bg-white/5">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
📜 TASK HISTORY
<span className="text-sm font-normal text-white/50">({history.length} entries)</span>
</h3>
{loading ? (
<p className="text-white/50 text-center py-8">Loading...</p>
) : history.length === 0 ? (
<p className="text-white/50 text-center py-8">
No history for this project.
</p>
) : (
<div className="space-y-4 max-h-[500px] overflow-y-auto">
{history.slice().reverse().map((entry) => (
<div
key={entry.id}
className="p-4 rounded-lg bg-white/5 border border-white/10"
>
<div className="flex items-start justify-between mb-2">
<div>
<p className="font-medium text-brand-pink">{entry.task}</p>
<p className="text-xs text-white/40">
{new Date(entry.createdAt).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-1 rounded text-xs bg-white/10 text-white/70">
{entry.project}
</span>
<span className={`px-2 py-1 rounded text-xs ${
entry.status === 'completed'
? 'bg-green-500/20 text-green-400'
: 'bg-white/10 text-white/50'
}`}>
{entry.status}
</span>
</div>
</div>
<div className="mt-3 p-3 rounded bg-black/30 border border-white/10">
<p className="text-xs text-white/50 mb-1">You sent:</p>
<p className="text-sm">{entry.command}</p>
</div>
{entry.reply && (
<div className="mt-2 p-3 rounded bg-brand-pink/10 border border-brand-pink/30">
<p className="text-xs text-brand-pink mb-1">Horus replied:</p>
<p className="text-sm text-white/90">{entry.reply}</p>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
)
}
+356
View File
@@ -0,0 +1,356 @@
'use client'
import { useState, useEffect, useRef } from 'react'
interface Trade {
id: string
date: string
pair: string
direction: 'long' | 'short'
entry: number
stopLoss: number
takeProfit: number
result?: 'win' | 'loss' | 'open'
pnl?: number
rr?: number
}
interface ChartData {
time: number
open: number
high: number
low: number
close: number
volume?: number
}
interface ThothView {
thought: string
trend: string
phase: string
key_level: number
bias: string
confidence: number
reason: string
next_action: string
updated_at: string
bias_history?: { time: number; bias: string; price: number }[]
support_zones?: { level: number; strength: number }[]
resistance_zones?: { level: number; strength: number }[]
}
interface PriceData {
price: number
change24h: number
}
interface IndicatorState {
ema20: boolean
ema50: boolean
ema200: boolean
bb: boolean
rsi: boolean
macd: boolean
thoth: boolean
volume: boolean
srZones: boolean
news: boolean
patterns: boolean
fib: boolean
countdown: boolean
calendar: boolean
correlation: boolean
funding: boolean
}
interface PatternMatch {
index: number
type: string
}
// Indicator calculations
const calculateEMA = (data: number[], period: number): (number | null)[] => {
const ema: (number | null)[] = []
const multiplier = 2 / (period + 1)
for (let i = 0; i < data.length; i++) {
if (i < period - 1) ema.push(null)
else if (i === period - 1) {
let sum = 0; for (let j = 0; j < period; j++) sum += data[i - j]
ema.push(sum / period)
} else {
ema.push(data[i] * multiplier + ema[i - 1]! * (1 - multiplier))
}
}
return ema
}
const calculateSMA = (data: number[], period: number): (number | null)[] => {
const sma: (number | null)[] = []
for (let i = 0; i < data.length; i++) {
if (i < period - 1) sma.push(null)
else { let sum = 0; for (let j = 0; j < period; j++) sum += data[i - j]; sma.push(sum / period) }
}
return sma
}
const calculateStdDev = (data: number[], period: number): (number | null)[] => {
const stdDev: (number | null)[] = []
for (let i = 0; i < data.length; i++) {
if (i < period - 1) stdDev.push(null)
else {
const slice = data.slice(i - period + 1, i + 1)
const mean = slice.reduce((a, b) => a + b, 0) / period
stdDev.push(Math.sqrt(slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period))
}
}
return stdDev
}
const calculateBollingerBands = (data: number[], period: number = 20, stdDevMult: number = 2) => {
const sma = calculateSMA(data, period)
const stdDev = calculateStdDev(data, period)
const upper: (number | null)[] = [], lower: (number | null)[] = []
for (let i = 0; i < data.length; i++) {
if (sma[i] === null || stdDev[i] === null) { upper.push(null); lower.push(null) }
else { upper.push(sma[i]! + stdDev[i]! * stdDevMult); lower.push(sma[i]! - stdDev[i]! * stdDevMult) }
}
return { middle: sma, upper, lower }
}
const calculateRSI = (data: number[], period: number = 14): (number | null)[] => {
const rsi: (number | null)[] = []
let gains: number[] = [], losses: number[] = []
for (let i = 0; i < data.length; i++) {
if (i === 0) { rsi.push(null); continue }
const change = data[i] - data[i - 1]
gains.push(change > 0 ? change : 0)
losses.push(change < 0 ? Math.abs(change) : 0)
if (i < period) rsi.push(null)
else {
const avgGain = gains.slice(-period).reduce((a, b) => a + b, 0) / period
const avgLoss = losses.slice(-period).reduce((a, b) => a + b, 0) / period
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss
rsi.push(100 - (100 / (1 + rs)))
}
}
return rsi
}
const calculateMACD = (data: number[], fast: number = 12, slow: number = 26, signal: number = 9) => {
const emaFast = calculateEMA(data, fast)
const emaSlow = calculateEMA(data, slow)
const macdLine: (number | null)[] = []
for (let i = 0; i < data.length; i++) {
if (emaFast[i] === null || emaSlow[i] === null) macdLine.push(null)
else macdLine.push(emaFast[i]! - emaSlow[i]!)
}
const validMacd = macdLine.filter((v): v is number => v !== null)
const signalLine = calculateEMA(validMacd, signal)
const signalLineAligned: (number | null)[] = []
let signalIdx = 0
for (let i = 0; i < macdLine.length; i++) {
if (macdLine[i] === null) signalLineAligned.push(null)
else { signalLineAligned.push(signalLine[signalIdx] ?? null); signalIdx++ }
}
const histogram: (number | null)[] = []
for (let i = 0; i < macdLine.length; i++) {
if (macdLine[i] === null || signalLineAligned[i] === null) histogram.push(null)
else histogram.push(macdLine[i]! - signalLineAligned[i]!)
}
return { macd: macdLine, signal: signalLineAligned, histogram }
}
const detectPatterns = (data: ChartData[]): PatternMatch[] => {
const patterns: PatternMatch[] = []
const closes = data.map(d => d.close)
const highs = data.map(d => d.high)
const lows = data.map(d => d.low)
for (let i = 20; i < data.length - 5; i++) {
if (highs[i] > highs[i-1] && highs[i] > highs[i+1] && highs[i-5] > highs[i-4] && Math.abs(highs[i] - highs[i-5]) < highs[i] * 0.02)
patterns.push({ index: i, type: 'double_top' })
if (lows[i] < lows[i-1] && lows[i] < lows[i+1] && lows[i-5] < lows[i-4] && Math.abs(lows[i] - lows[i-5]) < lows[i] * 0.02)
patterns.push({ index: i, type: 'double_bottom' })
if (closes[i] > highs[i-5] && closes[i-1] < highs[i-5]) patterns.push({ index: i, type: 'breakout' })
if (closes[i] < lows[i-5] && closes[i-1] > lows[i-5]) patterns.push({ index: i, type: 'breakdown' })
}
return patterns
}
declare global {
interface Window {
Chart: any
}
}
export function TradingChart() {
const mainChartRef = useRef<HTMLCanvasElement>(null)
const rsiChartRef = useRef<HTMLCanvasElement>(null)
const macdChartRef = useRef<HTMLCanvasElement>(null)
const [selectedAsset, setSelectedAsset] = useState<'BTC' | 'SOL' | 'ETH'>('BTC')
const [selectedTimeframe, setSelectedTimeframe] = useState<'15m' | '1h' | '4h' | '1D'>('1h')
const [secondTimeframe, setSecondTimeframe] = useState<'15m' | '1h' | '4h' | '1D' | null>(null)
const [chartData, setChartData] = useState<ChartData[]>([])
const [secondChartData, setSecondChartData] = useState<ChartData[]>([])
const [trades, setTrades] = useState<Trade[]>([])
const [thothView, setThothView] = useState<Record<string, ThothView>>({})
const [priceData, setPriceData] = useState<PriceData>({ price: 0, change24h: 0 })
const [loading, setLoading] = useState(true)
const [patterns, setPatterns] = useState<PatternMatch[]>([])
const [indicators, setIndicators] = useState<IndicatorState>({
ema20: false, ema50: false, ema200: false, bb: false, rsi: false, macd: false, thoth: true, volume: true, srZones: true, news: false, patterns: true, fib: false, countdown: true, calendar: false, correlation: false, funding: false
})
const mainChartRefInstance = useRef<any>(null)
const rsiChartRefInstance = useRef<any>(null)
const macdChartRefInstance = useRef<any>(null)
const getCandleLimit = (tf: string) => ({ '15m': 80, '1h': 100, '4h': 60, '1D': 90 }[tf] || 100)
useEffect(() => { fetchChartData(); fetchSecondChartData(); fetchPriceData(); const i = setInterval(() => { fetchChartData(); fetchSecondChartData(); fetchPriceData(); }, 60000); return () => clearInterval(i) }, [selectedAsset, selectedTimeframe, secondTimeframe])
useEffect(() => { fetchTrades(); fetchThothView() }, [])
useEffect(() => {
if (chartData.length > 0) { setPatterns(detectPatterns(chartData)); renderCharts() }
return () => { if (mainChartRefInstance.current) mainChartRefInstance.current.destroy(); if (rsiChartRefInstance.current) rsiChartRefInstance.current.destroy(); if (macdChartRefInstance.current) macdChartRefInstance.current.destroy() }
}, [chartData, indicators])
const fetchPriceData = async () => {
try {
const idMap: Record<string, string> = { 'BTC': 'bitcoin', 'SOL': 'solana', 'ETH': 'ethereum' }
const res = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${idMap[selectedAsset]}&vs_currencies=usd&include_24hr_change=true`)
const data = await res.json()
setPriceData({ price: data[idMap[selectedAsset]].usd, change24h: data[idMap[selectedAsset]].usd_24h_change })
} catch (e) { console.error(e) }
}
const fetchChartData = async () => {
setLoading(true)
try {
const symbol = selectedAsset === 'BTC' ? 'BTCUSDT' : selectedAsset === 'SOL' ? 'SOLUSDT' : 'ETHUSDT'
const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval={${{ '15m': '15', '1h': '1h', '4h': '4h', '1D': '1d' }[selectedTimeframe]}&limit=${getCandleLimit(selectedTimeframe)}`)
const data = await res.json()
setChartData(data.map((k: any[]) => ({ time: k[0], open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]), volume: parseFloat(k[5]) })))
} catch (e) { console.error(e) }
finally { setLoading(false) }
}
const fetchSecondChartData = async () => {
if (!secondTimeframe) return
try {
const symbol = selectedAsset === 'BTC' ? 'BTCUSDT' : selectedAsset === 'SOL' ? 'SOLUSDT' : 'ETHUSDT'
const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval={${{ '15m': '15', '1h': '1h', '4h': '4h', '1D': '1d' }[secondTimeframe]}&limit=${getCandleLimit(secondTimeframe)}`)
const data = await res.json()
setSecondChartData(data.map((k: any[]) => ({ time: k[0], open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]) })))
} catch (e) { console.error(e) }
}
const fetchTrades = async () => { try { const res = await fetch('/api/trading/trades'); if (res.ok) setTrades((await res.json()).trades || []) } catch (e) { console.warn(e) } }
const fetchThothView = async () => { try { const res = await fetch('/thoth_view.json'); if (res.ok) setThothView(await res.json()) } catch (e) { console.warn(e) } }
const toggleIndicator = (key: keyof IndicatorState) => setIndicators(p => ({ ...p, [key]: !p[key] }))
const renderCharts = () => {
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/chart.js'
script.onload = () => { renderMainChart(); if (indicators.rsi) renderRSIChart(); if (indicators.macd) renderMACDChart() }
document.head.appendChild(script)
}
const renderMainChart = () => {
if (!mainChartRef.current) return
if (mainChartRefInstance.current) mainChartRefInstance.current.destroy()
const ctx = mainChartRef.current.getContext('2d')
if (!ctx) return
const closes = chartData.map(d => d.close)
const labels = chartData.map(d => new Date(d.time))
const ema20 = calculateEMA(closes, 20), ema50 = calculateEMA(closes, 50), ema200 = calculateEMA(closes, 200), bb = calculateBollingerBands(closes, 20, 2)
const minP = Math.min(...chartData.flatMap(d => [d.high, d.low])), maxP = Math.max(...chartData.flatMap(d => [d.high, d.low])), pad = (maxP - minP) * 0.15
const datasets: any[] = [{ type: 'bar', label: 'Price', data: chartData.map(d => [d.low, d.high]), backgroundColor: chartData.map(d => d.close >= d.open ? '#22c55e' : '#ef4444'), borderColor: chartData.map(d => d.close >= d.open ? '#22c55e' : '#ef4444'), borderWidth: 1, borderSkipped: false }]
if (indicators.volume && chartData[0]?.volume) datasets.push({ type: 'bar', label: 'Volume', data: chartData.map(d => d.volume || 0), backgroundColor: chartData.map(d => d.close >= d.open ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'), borderWidth: 0, yAxisID: 'y_vol' })
if (indicators.ema20) datasets.push({ type: 'line', data: ema20, borderColor: '#eab308', borderWidth: 2, pointRadius: 0, tension: 0.4, yAxisID: 'y' })
if (indicators.ema50) datasets.push({ type: 'line', data: ema50, borderColor: '#3b82f6', borderWidth: 2, pointRadius: 0, tension: 0.4, yAxisID: 'y' })
if (indicators.ema200) datasets.push({ type: 'line', data: ema200, borderColor: '#ffffff', borderWidth: 2, pointRadius: 0, tension: 0.4, yAxisID: 'y' })
if (indicators.bb) { datasets.push({ type: 'line', data: bb.upper, borderColor: '#a855f7', borderWidth: 1, pointRadius: 0, yAxisID: 'y' }); datasets.push({ type: 'line', data: bb.lower, borderColor: '#a855f7', borderWidth: 1, pointRadius: 0, backgroundColor: 'rgba(168,85,247,0.1)', fill: '-1', yAxisID: 'y' }) }
const currentView = thothView[selectedAsset]
if (indicators.srZones && currentView?.support_zones) currentView.support_zones.forEach(z => datasets.push({ type: 'line', data: chartData.map(() => z.level), borderColor: 'rgba(34,197,94,0.5)', borderWidth: 2, borderDash: [5,5], pointRadius: 0, yAxisID: 'y' }))
if (indicators.srZones && currentView?.resistance_zones) currentView.resistance_zones.forEach(z => datasets.push({ type: 'line', data: chartData.map(() => z.level), borderColor: 'rgba(239,68,68,0.5)', borderWidth: 2, borderDash: [5,5], pointRadius: 0, yAxisID: 'y' }))
if (indicators.patterns && patterns.length) datasets.push({ type: 'scatter', data: patterns.map(p => ({ x: labels[p.index], y: chartData[p.index]?.high || 0 })), backgroundColor: patterns.map(p => p.type === 'breakout' ? '#22c55e' : p.type === 'breakdown' ? '#ef4444' : '#fbbf24'), pointStyle: 'star', pointRadius: 10, yAxisID: 'y' })
if (indicators.thoth && currentView?.bias_history) {
const bc = currentView.bias_history.map(b => { let ci = 0, md = Infinity; chartData.forEach((d, i) => { const df = Math.abs(d.time - b.time); if (df < md) { md = df; ci = i } }); return { idx: ci, bias: b.bias } })
datasets.push({ type: 'scatter', data: bc.map(b => ({ x: labels[b.idx], y: chartData[b.idx]?.high || 0 })), backgroundColor: bc.map(b => b.bias === 'bullish' ? '#fbbf24' : b.bias === 'bearish' ? '#ef4444' : '#a0a0a0'), pointStyle: 'rectRot', pointRadius: 12, yAxisID: 'y' })
}
const scales: any = { x: { display: true, grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0', maxTicksLimit: 12 } }, y: { position: 'right', min: minP - pad, max: maxP + pad, grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0', callback: (v: any) => '$' + v.toFixed(0) } } }
if (indicators.volume && chartData[0]?.volume) scales.y_vol = { display: false, max: Math.max(...chartData.map(d => d.volume || 0)) * 3 }
mainChartRefInstance.current = new window.Chart(ctx, { type: 'bar', data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1a1a2e', titleColor: '#fff', bodyColor: '#a0a0a0', borderColor: '#2a2a4e', borderWidth: 1 } }, scales } })
}
const renderRSIChart = () => {
if (!rsiChartRef.current) return
if (rsiChartRefInstance.current) rsiChartRefInstance.current.destroy()
const ctx = rsiChartRef.current.getContext('2d')
if (!ctx) return
const rsi = calculateRSI(chartData.map(d => d.close), 14)
rsiChartRefInstance.current = new window.Chart(ctx, { type: 'line', data: { labels: chartData.map(d => d.time), datasets: [{ data: rsi, borderColor: '#a0a0a0', borderWidth: 2, pointRadius: 0, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { min: 0, max: 100, position: 'right', grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0' } } } } })
}
const renderMACDChart = () => {
if (!macdChartRef.current) return
if (macdChartRefInstance.current) macdChartRefInstance.current.destroy()
const ctx = macdChartRef.current.getContext('2d')
if (!ctx) return
const { macd, signal, histogram } = calculateMACD(chartData.map(d => d.close))
macdChartRefInstance.current = new window.Chart(ctx, { type: 'bar', data: { labels: chartData.map(d => d.time), datasets: [{ data: histogram, backgroundColor: histogram.map(v => v === null ? 'transparent' : v >= 0 ? '#22c55e' : '#ef4444'), borderWidth: 0 }, { type: 'line', data: macd, borderColor: '#3b82f6', borderWidth: 2, pointRadius: 0 }, { type: 'line', data: signal, borderColor: '#f59e0b', borderWidth: 2, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { position: 'right', grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0' } } } } })
}
const closedTrades = trades.filter(t => t.result === 'win' || t.result === 'loss')
const wins = closedTrades.filter(t => t.result === 'win').length, winRate = closedTrades.length ? Math.round(wins / closedTrades.length * 100) : 0, totalPnl = closedTrades.reduce((s, t) => s + (t.pnl || 0), 0), avgRr = closedTrades.length ? closedTrades.reduce((s, t) => s + (t.rr || 0), 0) / closedTrades.length : 0
const cv = thothView[selectedAsset]
const getTE = (t: string) => t === 'uptrend' ? '🟢' : t === 'downtrend' ? '🔴' : '⚪️'
const getBC = (b: string) => b === 'bullish' ? 'text-green-400' : b === 'bearish' ? 'text-red-400' : 'text-yellow-400'
return (
<div className="space-y-4">
<div className="flex gap-4 justify-between flex-wrap">
<div className="flex gap-2">{(['BTC', 'SOL', 'ETH'] as const).map(a => <button key={a} onClick={() => setSelectedAsset(a)} className={`px-4 py-2 rounded-lg font-medium ${selectedAsset === a ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/70'}`}>{a}</button>)}</div>
<div className="flex gap-2">{(['15m', '1h', '4h', '1D'] as const).map(tf => <button key={tf} onClick={() => setSelectedTimeframe(tf)} className={`px-3 py-1 rounded text-sm ${selectedTimeframe === tf ? 'bg-green-500/20 text-green-400 border border-green-500/30' : 'bg-white/10 text-white/50'}`}>{tf}</button>)}</div>
</div>
<div className="flex gap-2 items-center"><span className="text-white/50 text-sm">Compare:</span><button onClick={() => setSecondTimeframe(null)} className={`px-2 py-1 rounded text-xs ${!secondTimeframe ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>None</button>{(['15m', '1h', '4h', '1D'] as const).map(tf => <button key={tf} onClick={() => setSecondTimeframe(tf)} className={`px-2 py-1 rounded text-xs ${secondTimeframe === tf ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>{tf}</button>)}</div>
<div className="flex gap-2 flex-wrap">{(Object.keys(indicators) as (keyof IndicatorState)[]).map(k => <button key={k} onClick={() => toggleIndicator(k)} className={`px-3 py-1 rounded text-sm ${indicators[k] ? 'bg-brand-pink text-white' : 'bg-white/10 text-white/50'}`}>{k === 'thoth' ? '👁 THOTH' : k === 'srZones' ? 'S/R' : k === 'volume' ? 'VOL' : k === 'news' ? 'NEWS' : k === 'patterns' ? 'PATTERNS' : k === 'fib' ? 'FIB' : k === 'countdown' ? '⏱️' : k === 'calendar' ? '📅' : k === 'correlation' ? '📊 CORR' : k === 'funding' ? '💰 FUND' : k.toUpperCase()}</button>)}</div>
<div className="flex justify-between p-4 rounded-lg bg-black/50 border border-white/10"><div><span className="text-2xl font-bold">{selectedAsset}/USD</span></div><div className="text-right"><div className="text-2xl font-bold">${priceData.price.toLocaleString()}</div><div className={`text-sm ${priceData.change24h >= 0 ? 'text-green-400' : 'text-red-400'}`}>{priceData.change24h >= 0 ? '↑' : '↓'} {Math.abs(priceData.change24h).toFixed(2)}%</div></div></div>
{cv && <div className="rounded-lg border border-brand-pink/30 bg-brand-pink/5 p-4"><div className="flex items-center gap-2 mb-3"><span className="text-xl">👁</span><h3 className="font-bold text-brand-pink">THOTH'S VIEW</h3><span className="text-xs text-white/40 ml-auto">{new Date(cv.updated_at).toLocaleString()}</span></div><p className="text-white/90 mb-4 italic">"{cv.thought}"</p><div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm"><div><p className="text-white/50 text-xs">Trend</p><p className="font-medium">{getTE(cv.trend)} {cv.trend}</p></div><div><p className="text-white/50 text-xs">Phase</p><p className="font-medium">{cv.phase}</p></div><div><p className="text-white/50 text-xs">Key Level</p><p className="font-medium">${cv.key_level.toLocaleString()}</p></div><div><p className="text-white/50 text-xs">Bias</p><p className={`font-medium ${getBC(cv.bias)}`}>{cv.bias.toUpperCase()} ({cv.confidence}/10)</p></div></div></div>}
<div className="relative rounded-lg bg-black/50 border border-white/10" style={{ height: '400px' }}>{loading && <div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10"><span className="text-white/50">Loading...</span></div>}<canvas ref={mainChartRef} /></div>
{secondTimeframe && secondChartData.length > 0 && <div className="rounded-lg bg-black/50 border border-white/10" style={{ height: '200px' }}><div className="px-4 py-2 border-b border-white/10 text-sm font-bold">{secondTimeframe} Chart</div><SecondChart data={secondChartData} /></div>}
{indicators.rsi && <div className="rounded-lg bg-black/50 border border-white/10" style={{ height: '150px' }}><canvas ref={rsiChartRef} /></div>}
{indicators.macd && <div className="rounded-lg bg-black/50 border border-white/10" style={{ height: '150px' }}><canvas ref={macdChartRef} /></div>}
<div className="grid grid-cols-5 gap-3">{[{ v: trades.filter(t => t.result === 'open').length, l: 'Open' }, { v: totalPnl, l: 'P&L', c: 'text-green-400' }, { v: winRate + '%', l: 'Win Rate' }, { v: closedTrades.length, l: 'Trades' }, { v: avgRr.toFixed(1) + ':1', l: 'Avg R:R' }].map((s, i) => <div key={i} className="p-3 rounded-lg bg-white/5 border border-white/10 text-center"><p className={`text-2xl font-bold ${s.c || ''}`}>{s.v}</p><p className="text-xs text-white/50">{s.l}</p></div>)}</div>
<div className="rounded-lg border border-white/10 bg-white/5 overflow-hidden"><div className="px-4 py-3 border-b border-white/10"><h3 className="font-bold">📊 Trade History</h3></div>{trades.length === 0 ? <div className="p-8 text-center text-white/50">No trades yet</div> : <div className="max-h-48 overflow-y-auto"><table className="w-full text-sm"><thead className="bg-white/5 text-white/70 sticky top-0"><tr><th className="px-4 py-2 text-left">Date</th><th className="px-4 py-2">Asset</th><th className="px-4 py-2">Dir</th><th className="px-4 py-2 text-right">Entry</th><th className="px-4 py-2 text-right">SL</th><th className="px-4 py-2 text-right">TP</th><th className="px-4 py-2 text-right">R:R</th><th className="px-4 py-2 text-right">Result</th></tr></thead><tbody>{trades.map((t, i) => <tr key={i} className={`border-t border-white/5 ${t.result === 'win' ? 'bg-green-500/10' : t.result === 'loss' ? 'bg-red-500/10' : 'bg-yellow-500/10'}`}><td className="px-4 py-2">{t.date}</td><td className="px-4 py-2 text-center">{t.pair}</td><td className="px-4 py-2 text-center"><span className={t.direction === 'long' ? 'text-green-400' : 'text-red-400'}>{t.direction.toUpperCase()}</span></td><td className="px-4 py-2 text-right">${t.entry.toLocaleString()}</td><td className="px-4 py-2 text-right text-red-400">${t.stopLoss.toLocaleString()}</td><td className="px-4 py-2 text-right text-green-400">${t.takeProfit.toLocaleString()}</td><td className="px-4 py-2 text-right">{t.rr?.toFixed(1)}:1</td><td className="px-4 py-2 text-right">{t.result === 'win' ? '' : t.result === 'loss' ? '' : ''}</td></tr>)}</tbody></table></div>}</div>
</div>
)
}
function SecondChart({ data }: { data: ChartData[] }) {
const ref = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (!ref.current || !data.length) return
const ctx = ref.current.getContext('2d')
if (!ctx) return
if (ref.current.chart) ref.current.chart.destroy()
const chart = new window.Chart(ctx, { type: 'line', data: { labels: data.map(d => new Date(d.time).toLocaleTimeString()), datasets: [{ data: data.map(d => d.close), borderColor: '#a0a0a0', borderWidth: 2, pointRadius: 0, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { position: 'right', grid: { color: '#1a1a2e' }, ticks: { color: '#a0a0a0' } } } } })
ref.current.chart = chart
}, [data])
return <canvas ref={ref} />
}
+341
View File
@@ -0,0 +1,341 @@
'use client'
import { useState, useEffect } from 'react'
import { TradingChart } from './TradingChart'
type TradingTab = 'research' | 'strategies' | 'execution' | 'journal'
interface Trader {
id: string
name: string
status: 'learning' | 'active' | 'paused'
framesAnalyzed: number
patterns: string[]
entryRules: string[]
exitRules: string[]
indicators: string[]
riskParams: string[]
}
interface Trade {
id: string
trader: string
pair: string
direction: 'long' | 'short'
entryPrice: number
exitPrice?: number
status: 'open' | 'closed' | 'cancelled'
pnl?: number
pnlPercent?: number
reason: string
setup: string
timeframe: string
openedAt: string
closedAt?: string
notes: string
isDemo: boolean
}
const defaultTraders: Trader[] = [
{
id: 'dopetrades',
name: 'DopeTrades',
status: 'learning',
framesAnalyzed: 0,
patterns: [],
entryRules: [],
exitRules: [],
indicators: [],
riskParams: []
}
]
export function TradingPanel() {
const [activeTab, setActiveTab] = useState<TradingTab>('research')
const [traders, setTraders] = useState<Trader[]>(defaultTraders)
const [selectedTrader, setSelectedTrader] = useState<string>('dopetrades')
const [trades, setTrades] = useState<Trade[]>([])
const [journalFilter, setJournalFilter] = useState<'all' | 'demo' | 'real'>('all')
// Load data
useEffect(() => {
loadTraders()
loadTrades()
}, [])
const loadTraders = async () => {
try {
const res = await fetch('/api/trading/traders')
if (res.ok) {
const data = await res.json()
if (data.traders?.length > 0) setTraders(data.traders)
}
} catch (e) {}
}
const loadTrades = async () => {
try {
const res = await fetch('/api/trading/trades')
if (res.ok) {
const data = await res.json()
if (data.trades) setTrades(data.trades)
}
} catch (e) {}
}
const tabs = [
{ id: 'research', label: '🔬 Deep Research', count: traders.filter(t => t.status === 'learning').length },
{ id: 'strategies', label: '🎯 Strategies', count: traders.filter(t => t.status === 'active').length },
{ id: 'execution', label: '⚡ Execution', count: trades.filter(t => t.status === 'open').length },
{ id: 'journal', label: '📔 Journal', count: trades.length },
]
const filteredTrades = trades.filter(t => {
if (journalFilter === 'all') return true
if (journalFilter === 'demo') return t.isDemo
return !t.isDemo
})
const openTrades = trades.filter(t => t.status === 'open')
const closedDemoTrades = trades.filter(t => t.status === 'closed' && t.isDemo)
const closedRealTrades = trades.filter(t => t.status === 'closed' && !t.isDemo)
const totalPnl = closedRealTrades.reduce((sum, t) => sum + (t.pnl || 0), 0)
const winRate = closedRealTrades.length > 0
? Math.round((closedRealTrades.filter(t => (t.pnl || 0) > 0).length / closedRealTrades.length) * 100)
: 0
return (
<div className="space-y-4">
{/* Tab Navigation */}
<div className="flex gap-2 flex-wrap">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as TradingTab)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
activeTab === tab.id
? 'bg-brand-pink text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
}`}
>
{tab.label}
<span className="ml-2 text-xs opacity-70">({tab.count})</span>
</button>
))}
</div>
{/* Research Tab */}
{activeTab === 'research' && (
<div className="border border-white/20 rounded-lg p-4 bg-white/5">
<h3 className="text-lg font-bold mb-4">🔬 Deep Research</h3>
<p className="text-white/60 mb-4">Learn trading strategies from experts by analyzing their content.</p>
<div className="space-y-3">
{traders.map(trader => (
<div
key={trader.id}
className="p-4 rounded-lg bg-white/5 border border-white/10"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<span className="text-2xl">👨🏫</span>
<div>
<p className="font-medium">{trader.name}</p>
<p className="text-xs text-white/50">{trader.framesAnalyzed} frames analyzed</p>
</div>
</div>
<span className={`px-2 py-1 rounded text-xs ${
trader.status === 'learning' ? 'bg-yellow-500/20 text-yellow-400' :
trader.status === 'active' ? 'bg-green-500/20 text-green-400' :
'bg-white/10 text-white/50'
}`}>
{trader.status}
</span>
</div>
{trader.patterns.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{trader.patterns.map(p => (
<span key={p} className="px-2 py-0.5 bg-white/10 rounded text-xs">{p}</span>
))}
</div>
)}
<button className="mt-3 text-sm text-brand-pink hover:underline">
View full analysis
</button>
</div>
))}
</div>
<button className="mt-4 w-full py-2 border border-dashed border-white/20 rounded-lg text-white/50 hover:border-white/40 hover:text-white transition">
+ Add new trader to research
</button>
</div>
)}
{/* Strategies Tab */}
{activeTab === 'strategies' && (
<div className="border border-white/20 rounded-lg p-4 bg-white/5">
<h3 className="text-lg font-bold mb-4">🎯 Trading Strategies</h3>
<p className="text-white/60 mb-4">Select a trader style to follow for your next trade.</p>
<div className="flex gap-2 flex-wrap mb-4">
{traders.filter(t => t.status === 'active').map(trader => (
<button
key={trader.id}
onClick={() => setSelectedTrader(trader.id)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
selectedTrader === trader.id
? 'bg-brand-pink text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
}`}
>
{trader.name}
</button>
))}
</div>
{traders.find(t => t.id === selectedTrader) && (
<div className="p-4 rounded-lg bg-black/30 border border-white/10">
<h4 className="font-medium mb-3">{traders.find(t => t.id === selectedTrader)?.name} Strategy</h4>
<div className="space-y-3 text-sm">
<div>
<p className="text-white/50 text-xs mb-1">Entry Rules</p>
<ul className="list-disc list-inside space-y-1">
{traders.find(t => t.id === selectedTrader)?.entryRules.map(r => (
<li key={r}>{r}</li>
))}
</ul>
{traders.find(t => t.id === selectedTrader)?.entryRules.length === 0 && (
<p className="text-white/30 italic">No entry rules defined yet</p>
)}
</div>
<div>
<p className="text-white/50 text-xs mb-1">Exit Rules</p>
<ul className="list-disc list-inside space-y-1">
{traders.find(t => t.id === selectedTrader)?.exitRules.map(r => (
<li key={r}>{r}</li>
))}
</ul>
</div>
<div>
<p className="text-white/50 text-xs mb-1">Indicators</p>
<div className="flex flex-wrap gap-1">
{traders.find(t => t.id === selectedTrader)?.indicators.map(i => (
<span key={i} className="px-2 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs">{i}</span>
))}
</div>
</div>
<div>
<p className="text-white/50 text-xs mb-1">Risk Parameters</p>
<ul className="list-disc list-inside space-y-1">
{traders.find(t => t.id === selectedTrader)?.riskParams.map(r => (
<li key={r}>{r}</li>
))}
</ul>
</div>
</div>
<button className="mt-4 w-full py-2 bg-brand-pink rounded-lg font-medium hover:bg-[#ff7bc0] transition">
Execute Trade in {traders.find(t => t.id === selectedTrader)?.name} Style
</button>
</div>
)}
</div>
)}
{/* Execution Tab */}
{activeTab === 'execution' && (
<TradingChart />
)}
{/* Journal Tab */}
{activeTab === 'journal' && (
<div className="border border-white/20 rounded-lg p-4 bg-white/5">
<h3 className="text-lg font-bold mb-4">📔 Trading Journal</h3>
{/* Stats */}
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="p-3 rounded bg-white/5 text-center">
<p className="text-2xl font-bold text-green-400">${totalPnl.toFixed(2)}</p>
<p className="text-xs text-white/50">Real P&L</p>
</div>
<div className="p-3 rounded bg-white/5 text-center">
<p className="text-2xl font-bold">{winRate}%</p>
<p className="text-xs text-white/50">Win Rate</p>
</div>
<div className="p-3 rounded bg-white/5 text-center">
<p className="text-2xl font-bold">{closedRealTrades.length}</p>
<p className="text-xs text-white/50">Real Trades</p>
</div>
</div>
{/* Filter */}
<div className="flex gap-2 mb-4">
{(['all', 'demo', 'real'] as const).map(filter => (
<button
key={filter}
onClick={() => setJournalFilter(filter)}
className={`px-3 py-1 rounded text-xs ${
journalFilter === filter
? 'bg-brand-pink text-white'
: 'bg-white/10 text-white/60'
}`}
>
{filter === 'all' ? 'All' : filter === 'demo' ? 'Demo' : 'Real'}
</button>
))}
</div>
{/* Trades List */}
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredTrades.length === 0 ? (
<p className="text-white/40 text-center py-4">No trades yet</p>
) : (
filteredTrades.map(trade => (
<div
key={trade.id}
className={`p-3 rounded border ${
trade.isDemo
? 'bg-white/5 border-white/10'
: trade.status === 'open'
? 'bg-yellow-500/10 border-yellow-500/30'
: (trade.pnl || 0) > 0
? 'bg-green-500/10 border-green-500/30'
: 'bg-red-500/10 border-red-500/30'
}`}
>
<div className="flex justify-between items-start">
<div>
<span className="font-medium">{trade.pair}</span>
<span className={`ml-2 text-xs px-1.5 py-0.5 rounded ${
trade.direction === 'long' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{trade.direction.toUpperCase()}
</span>
{trade.isDemo && <span className="ml-1 text-xs text-white/40">(Demo)</span>}
</div>
{trade.status === 'closed' && (
<span className={`font-bold ${(trade.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{trade.pnl >= 0 ? '+' : ''}{trade.pnl?.toFixed(2)} ({trade.pnlPercent?.toFixed(1)}%)
</span>
)}
</div>
<p className="text-xs text-white/50 mt-1">{trade.setup}</p>
<div className="flex gap-4 mt-2 text-xs text-white/40">
<span>{trade.timeframe}</span>
<span>{new Date(trade.openedAt).toLocaleDateString()}</span>
</div>
</div>
))
)}
</div>
</div>
)}
</div>
)
}
@@ -168,6 +168,31 @@ export default function MorningBriefCalendar() {
))} ))}
</ul> </ul>
</div> </div>
{/* OpenClaw Use Cases */}
{todayBrief.openclowUseCases && todayBrief.openclowUseCases.skillIdeas.length > 0 && (
<div className="space-y-2 md:col-span-2">
<p className="text-xs text-white/50 uppercase">🦞 OpenClaw Skill Ideas</p>
<ul className="space-y-1">
{todayBrief.openclowUseCases.skillIdeas.map((idea, i) => (
<li key={i} className="text-sm text-purple-300 flex items-center gap-2">
<input
type="checkbox"
checked={idea.selected}
onChange={() => {
const updated = { ...todayBrief.openclowUseCases };
updated.skillIdeas[i].selected = !idea.selected;
// Save to localStorage
localStorage.setItem("sitemente:openclaw-usecases", JSON.stringify(updated));
}}
className="w-4 h-4 accent-purple-500"
/>
{idea.name}
</li>
))}
</ul>
</div>
)}
</div> </div>
</div> </div>
)} )}
@@ -248,6 +273,26 @@ export default function MorningBriefCalendar() {
}`}>{viewingBrief.market.sentiment.toUpperCase()}</span> }`}>{viewingBrief.market.sentiment.toUpperCase()}</span>
</p> </p>
</section> </section>
{/* OpenClaw Use Cases */}
{viewingBrief.openclowUseCases && viewingBrief.openclowUseCases.skillIdeas.length > 0 && (
<section>
<h4 className="text-sm font-semibold text-white/70 mb-2">🦞 OpenClaw Skill Ideas</h4>
<ul className="space-y-1">
{viewingBrief.openclowUseCases.skillIdeas.map((idea, i) => (
<li key={i} className="text-sm text-purple-300">• {idea}</li>
))}
</ul>
{viewingBrief.openclowUseCases.topUseCases.length > 0 && (
<div className="mt-2">
<p className="text-xs text-white/50">Top Use Cases:</p>
{viewingBrief.openclowUseCases.topUseCases.map((uc, i) => (
<p key={i} className="text-sm text-white/60">• {uc}</p>
))}
</div>
)}
</section>
)}
</div> </div>
</motion.div> </motion.div>
</div> </div>
+2 -1
View File
@@ -128,7 +128,8 @@ export default function PricingTable({ lang, onContact }: PricingTableProps) {
monthly: 299, monthly: 299,
term: "6-12 months", term: "6-12 months",
features: [ features: [
"AI website chat (1 language)", "AI website chat (24/7)",
"2 languages (ES+EN)",
"Booking & lead capture", "Booking & lead capture",
"Light lead inbox", "Light lead inbox",
"Basic analytics", "Basic analytics",
+116 -94
View File
@@ -2,6 +2,7 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import PaymentButton from "@/components/stripe/PaymentButton";
type ServicesAndPricingProps = { type ServicesAndPricingProps = {
lang: "es" | "en"; lang: "es" | "en";
@@ -103,19 +104,20 @@ const serviceCards: Record<"es" | "en", ServiceCard[]> = {
es: [ es: [
{ {
id: "starter", id: "starter",
icon: "💬", icon: "🤖",
title: "AI Chat + Reservas Automatizadas", title: "Empleado IA - Chat",
for: "Restaurantes, clínicas, negocios locales con web existente", for: "Restaurantes, clínicas, negocios que quieren su primer empleado IA",
description: [ description: [
"¿Tu web pierde clientes fuera de horario? Tu sitio responde preguntas, toma reservas y captura leads mientras duermes.", "Un empleado virtual que trabaja 24/7, 365 días al año. Responde preguntas, toma reservas y captura leads mientras duermes.",
], ],
sections: [ sections: [
{ {
title: "Resultados típicos:", title: "Un empleado IA que:",
items: [ items: [
"24/7 reservas sin intervención humana", "Responde 24/7 (también a las 3 AM)",
"Reduce llamadas repetitivas en un 70%", "Habla español, inglés, francés, alemán...",
"ROI positivo en el primer mes", "Toma reservas y citas automáticas",
"Nunca se enferma, nunca se va de vacaciones",
], ],
}, },
], ],
@@ -123,75 +125,80 @@ const serviceCards: Record<"es" | "en", ServiceCard[]> = {
monthly: 299, monthly: 299,
term: "6-12 meses", term: "6-12 meses",
features: [ features: [
"Chat IA en la web (1 idioma)", "🤖 Chat IA en tu web",
"Reservas y captación de leads", "🌍 Hasta 3 idiomas",
"Analíticas básicas", "📅 Reservas automáticas",
"📊 Dashboard de clientes",
"📱 Notificaciones WhatsApp/SMS",
"📱 Notificaciones en tiempo real",
], ],
optional: ["+WhatsApp (+100€/mes)", "+Idioma extra (+80€/mes)"], optional: [],
cta: "Empezar", cta: "Empezar",
}, },
{ {
id: "site", id: "site",
icon: "🌐", icon: "📞",
title: "Web Inteligente Completa", title: "Empleado IA - Chat + Voz",
for: "Negocios listos para una web moderna + cerebro IA", for: "Negocios que no quieren perder llamadas",
description: [ description: [
"Construimos tu nuevo sitio web Next.js (rápido, seguro, SEO) e integramos IA que vende, reserva y da soporte en español e inglés.", "Tu empleado IA ahora también contesta el teléfono. Disponible 24/7 para responder, cualificar yivar llamadas a tu móvil o agendar directamente.",
], ],
sections: [ sections: [
{ {
title: "Incluye:", title: "Todo lo del plan Chat, más:",
items: [ items: [
"Sitio que convierte visitas en ventas", "📞 Contesta llamadas entrantes",
"Chat + WhatsApp incluidos", "🎯 Cualifica leads por ti",
"2 idiomas desde el día uno", "📋 Envía resúmenes post-llamada",
"Analíticas avanzadas", "🔗 Integración con tu calendario",
], ],
}, },
], ],
setup: "3.500 €", setup: "2.500 €",
monthly: 749, monthly: 599,
term: "6-12 meses", term: "6-12 meses",
features: [ features: [
"Todo lo de Starter", "Todo lo de Chat IA",
"Nuevo sitio web Next.js", "📞 Contesta llamadas",
"WhatsApp incluido", "🌍 Hasta 5 idiomas",
"2 idiomas (ES+EN)", "📅 Sincroniza con tu calendario",
"📋 Resúmenes de conversaciones",
], ],
optional: ["+Llamadas de voz (+150€/mes)", "+Ubicación extra (+120€/mes)"], optional: [],
cta: "Empezar", cta: "Empezar",
popular: true, popular: true,
}, },
{ {
id: "growth", id: "growth",
icon: "🚀", icon: "🚀",
title: "Partner Estratégico de IA", title: "Empresa IA",
for: "Grupos de restaurantes, inmobiliarias, cadenas de alquiler", for: "Cadenas, grupos o negocios con alto volumen",
description: [ description: [
"Socio estratégico: IA multicanal (web, WhatsApp, llamadas), integraciones CRM y consultoría mensual.", "IA para toda tu operación: múltiples ubicaciones, CRM, analytics avanzado y consultoría mensual para maximizar resultados.",
], ],
sections: [ sections: [
{ {
title: "Incluye:", title: "Incluye:",
items: [ items: [
"Todo lo de Smart Site", "Todo de Chat + Voz",
"Llamadas de voz incluidas", "Múltiples ubicaciones",
"Soporte multiubicación", "CRM completo",
"Integraciones CRM", "Analytics avanzado",
"Llamada estratégica mensual", "Consultoría mensual",
], ],
}, },
], ],
setup: "5.000 €", setup: "5.000 €",
monthly: 1950, monthly: 1499,
term: "12 meses", term: "12 meses",
features: [ features: [
"Todo lo de Smart Site", "Todo de Chat + Voz",
"Llamadas de voz incluidas", "🏢 Múltiples ubicaciones",
"Integraciones CRM", "🔗 Integraciones CRM",
"Soporte prioritario", "📈 Analytics avanzado",
"👤 Account manager dedicado",
], ],
optional: ["+Pack contenido IA (+200€/mes)"], optional: [],
cta: "Hablemos", cta: "Hablemos",
}, },
{ {
@@ -219,95 +226,100 @@ const serviceCards: Record<"es" | "en", ServiceCard[]> = {
en: [ en: [
{ {
id: "starter", id: "starter",
icon: "💬", icon: "🤖",
title: "AI Chat + Automated Bookings", title: "AI Employee - Chat",
for: "Restaurants, clinics, local businesses with existing websites", for: "Restaurants, clinics, businesses wanting their first AI employee",
description: [ description: [
"Is your website losing customers after hours? Your site answers questions, takes bookings, and captures leads while you sleep.", "A virtual employee that works 24/7, 365 days a year. Answers questions, takes bookings and captures leads while you sleep.",
], ],
sections: [ sections: [
{ {
title: "Typical results:", title: "An AI employee that:",
items: [ items: [
"24/7 bookings without human intervention", "Responds 24/7 (even at 3 AM)",
"Reduce repetitive calls by 70%", "Speaks Spanish, English, French, German...",
"Positive ROI in the first month", "Takes bookings and appointments automatically",
"Never gets sick, never goes on vacation",
], ],
}, },
], ],
setup: "900", setup: "900",
monthly: 299, monthly: 299,
term: "6-12 months", term: "6-12 months",
features: [ features: [
"AI website chat (1 language)", "🤖 AI Chat on your website",
"Booking & lead capture", "🌍 Up to 3 languages",
"Basic analytics", "📅 Automated bookings",
"📊 Customer dashboard",
"📱 Real-time notifications",
], ],
optional: ["+WhatsApp (+100€/mo)", "+Extra language (+80€/mo)"], optional: [],
cta: "Get started", cta: "Get Started",
}, },
{ {
id: "site", id: "site",
icon: "🌐", icon: "📞",
title: "Full Smart Website", title: "AI Employee - Chat + Voice",
for: "Businesses ready for a modern site + AI brain", for: "Businesses that can't miss calls",
description: [ description: [
"We build your new Next.js website (fast, secure, SEO) and integrate AI that sells, books, and supports in Spanish and English.", "Your AI employee now also answers the phone. Available 24/7 to respond, qualify, and forward calls to your mobile or book directly in your calendar.",
], ],
sections: [ sections: [
{ {
title: "Includes:", title: "Everything in Chat plan, plus:",
items: [ items: [
"Site that converts visits into sales", "📞 Answers incoming calls",
"Chat + WhatsApp included", "🎯 Qualifies leads for you",
"2 languages from day one", "📋 Post-call summaries",
"Advanced analytics", "🔗 Calendar integration",
], ],
}, },
], ],
setup: "3,500", setup: "€2,500",
monthly: 749, monthly: 599,
term: "6-12 months", term: "6-12 months",
features: [ features: [
"Everything in Starter", "Everything in Chat AI",
"New Next.js website", "📞 Answers calls",
"WhatsApp included", "🌍 Up to 5 languages",
"2 languages (ES+EN)", "📅 Syncs with your calendar",
"📋 Conversation summaries",
], ],
optional: ["+Voice calls (+150€/mo)", "+Extra location (+120€/mo)"], optional: [],
cta: "Get started", cta: "Get Started",
popular: true, popular: true,
}, },
{ {
id: "growth", id: "growth",
icon: "🚀", icon: "🚀",
title: "AI Strategy Partner", title: "AI Company",
for: "Restaurant groups, real estate teams, rental chains", for: "Chains, groups or high-volume businesses",
description: [ description: [
"Strategic partner: multichannel AI (web, WhatsApp, calls), CRM integrations, monthly consulting.", "AI for your entire operation: multiple locations, CRM, advanced analytics and monthly consulting to maximize results.",
], ],
sections: [ sections: [
{ {
title: "Includes:", title: "Includes:",
items: [ items: [
"Everything in Smart Site", "Everything in Chat + Voice",
"Voice calls included", "Multiple locations",
"Multi-location support", "Full CRM",
"CRM integrations", "Advanced analytics",
"Monthly strategy call", "Monthly consulting",
], ],
}, },
], ],
setup: "5,000", setup: "5,000",
monthly: 1950, monthly: 1499,
term: "12 months", term: "12 months",
features: [ features: [
"Everything in Smart Site", "Everything in Chat + Voice",
"Voice calls included", "🏢 Multiple locations",
"CRM integrations", "🔗 CRM integrations",
"Priority support", "📈 Advanced analytics",
"👤 Dedicated account manager",
], ],
optional: ["+AI content pack (+200€/mo)"], optional: [],
cta: "Let's talk", cta: "Let's talk",
}, },
{ {
@@ -485,12 +497,22 @@ export default function ServicesAndPricing({
</p> </p>
)} )}
<button {/* Enterprise/Custom plan: contact form, others: Stripe payment */}
onClick={onContact} {card.id === "enterprise" ? (
className="mt-auto mt-6 w-full rounded-lg bg-brand-pink py-2.5 text-sm font-semibold text-white transition hover:bg-[#ff7bc0]" <button
> onClick={onContact}
{card.cta} className="mt-auto mt-6 w-full rounded-lg bg-brand-pink py-2.5 text-sm font-semibold text-white transition hover:bg-[#ff7bc0]"
</button> >
{card.cta}
</button>
) : (
<PaymentButton
planId={card.id}
planType="monthly"
label={card.cta}
variant="primary"
/>
)}
</motion.div> </motion.div>
))} ))}
</div> </div>
+99
View File
@@ -0,0 +1,99 @@
"use client";
import { useState } from "react";
interface PaymentButtonProps {
planId: string;
planType: "setup" | "monthly";
label: string;
variant?: "primary" | "secondary";
}
export default function PaymentButton({ planId, planType, label, variant = "primary" }: PaymentButtonProps) {
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState("");
const [showModal, setShowModal] = useState(false);
const handleCheckout = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch("/api/stripe/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
planId,
planType,
email,
}),
});
const data = await response.json();
if (data.url) {
window.location.href = data.url;
} else {
alert("Error initiating checkout. Please try again.");
}
} catch (error) {
console.error("Checkout error:", error);
alert("Error initiating checkout. Please try again.");
} finally {
setLoading(false);
}
};
const buttonClass = variant === "primary"
? "bg-brand-pink hover:bg-[#ff7bc0]"
: "border border-white/20 hover:bg-white/10";
return (
<>
<button
onClick={() => setShowModal(true)}
className={`w-full mt-6 py-2.5 rounded-lg font-semibold text-white transition ${buttonClass}`}
disabled={loading}
>
{loading ? "Processing..." : label}
</button>
{showModal && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-[#1a1625] border border-white/20 rounded-2xl p-6 max-w-md w-full">
<h3 className="text-xl font-bold text-white mb-4">Enter your email to continue</h3>
<p className="text-white/60 text-sm mb-4">
We&apos;ll send the payment link to your email.
</p>
<form onSubmit={handleCheckout}>
<input
type="email"
required
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 text-white placeholder:text-white/40 focus:outline-none focus:border-brand-pink mb-4"
/>
<div className="flex gap-3">
<button
type="button"
onClick={() => setShowModal(false)}
className="flex-1 px-4 py-2 rounded-lg border border-white/20 text-white/70 hover:bg-white/10 transition"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2 rounded-lg bg-brand-pink text-white font-semibold hover:bg-[#ff7bc0] transition disabled:opacity-50"
>
{loading ? "Processing..." : "Continue to Payment"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}
+153
View File
@@ -0,0 +1,153 @@
# Car Rental AI Agent - Demostración
## Hero Section
**Headline:** Reservas a las 3 AM — tu agencia de alquiler nunca duerme
**Subheadline:** El agente de IA que cotiza, reserva y responde sobre seguros y pickups — 24/7, en español e inglés.
**Key Phrases (Spanish):**
- "¿Cuánto cuesta rentar un coche para este fin de semana?"
- "¿Qué seguros incluye la renta?"
- "¿Tienen pickups en el aeropuerto?"
- "¿A qué hora puedo devolver el auto?"
- "¿Necesito tarjeta de crédito?"
---
## Problema
Los clientes buscan alquiler de autos a cualquier hora — especialmente viajeros en terminales aéreas. Perder una reserva a las 3 AM significa perder un cliente que va a la competencia.
## Solución: Agente IA para Alquiler de Autos
Un asistente virtual que cotiza, reserva y responde dudas sobre seguros — convirtiendo cada inquiry en una renta confirmada.
---
## Características
### 💰 Cotizaciones Instantáneas
- Cotiza en tiempo real según disponibilidad
- Considera fechas, duración y tipo de auto
- Aplica promociones y descuentos
- Compara categorías de vehículos
### 📅 Reservas 24/7
- Procesa reservas a cualquier hora
- Confirma disponibilidad automáticamente
- Solicita documentos necesarios
- Envía confirmación inmediata
### 🛡️ Información de Seguros
- Explica coberturas incluidas
- Compara opciones de seguros adicionales
- Informa sobre deducibles
- Resuelve dudas sobre protegección
### 🚗 Pickup y Devolución
- Coordina puntos de entrega
- Informa horarios de atención
- Maneja cambios de horario
- Responde sobre políticas de combustible
---
## Flujo de Conversación (Ejemplo)
```
Cliente: Hola, quiero rentar un auto
IA: ¡Con gusto! ¿Qué tipo de auto buscas?
Cliente: Algo económico para 2 personas
IA: Perfecto. ¿Qué fechas necesitas el vehículo?
Cliente: Desde el viernes hasta el domingo
IA: ¿En qué ubicación necesitas el pickup?
Cliente: En el aeropuerto
IA: Te tengo disponible un Nissan Versa o similar
para ese fin de semana, desde $45/día.
¿Te incluye el seguro básico.
¿Querés agregar cobertura completa por $15/día más?
Cliente: Sí, quiero la cobertura completa
IA: ¡Entendido! El total con seguro completo
sería $180 para los 3 días. ¿Confirmo la reserva?
```
---
## CTA Section
### No pierdas más reservas nocturnas
**TODO:** [Agregar link al formulario de contacto o calendario de demo]
**O llama directamente:** TODO: [Número de teléfono]
---
## Pricing
**TODO:** [Agregar planes y precios]
- Plan Básico: $119/mes — 150 reservas/mes
- Plan Profesional: $229/mes — Reservas ilimitadas
- Plan Enterprise: Custom — Flota grande, múltiples ubicaciones
---
## Flota Compatible
**TODO:** [Agregar categorías de vehículos]
- Económico
- Sedán
- SUV
- Camioneta
- Lujo
- Van
---
## Integraciones
**TODO:** [Agregar logos de integraciones]
- Rental Manager
- Booking.com
- Expedia
- Google Flights
---
## Testimonios
**TODO:** [Agregar testimonios de clientes]
> "Aumentamos 35% en reservas nocturnas. El agente maneja el 80% de las preguntas sobre seguros." — *TODO: Nombre de agencia*
---
## FAQ
**¿Puede procesar pagos?**
El agente recopila la información y la envía a tu sistema de pago para procesamiento seguro.
**¿Qué pasa si el cliente tiene preguntas complejas?**
Transfiere a un agente humano con el contexto completo de la conversación.
**¿Soporta múltiples ubicaciones?**
Sí, puede manejar diferentes puntos de pickup y entrega.
---
## Footer
**TODO:** [Links a otras páginas del sitio]
- Inicio
- Precios
- Contacto
- Blog
---
*Última actualización: 2026-02-19*
+142
View File
@@ -0,0 +1,142 @@
# Clinic AI Agent - Demostración
## Hero Section
**Headline:** Tu consultorio nunca pierde un paciente — ni a medianoche
**Subheadline:** El agente de IA que agenda citas, responde sobre seguros y envía recordatorios automáticos — 24/7, en español e inglés.
**Key Phrases (Spanish):**
- "¿Tienen disponibilidad mañana?"
- "¿Aceptan mi seguro médico?"
- "¿Cuánto cuesta una consulta?"
- "Necesito agendar una cita para mi hijo"
- "¿El doctor atiende los sábados?"
---
## Problema
Los pacientes llaman a medianoche con emergencias menores, preguntan sobre coberturas de seguro, y frecuentemente olvidan sus citas. Tu recepcionista no puede estar disponible 24/7.
## Solución: Agente IA para Clínicas
Un asistente virtual que maneja la agenda, resuelve dudas de pacientes y reduce ausencias — mejorando la experiencia y tus ingresos.
---
## Características
### 📅 Agenda de Citas
- Agenda citas 24/7, incluso fuera de horario
- Confirma disponibilidad en tiempo real
- Maneja cancelaciones y reprogramaciones
- Evita doble-booking
### 💳 Información de Seguros
- Verifica cobertura de seguros aceptados
- Explica deducibles y copagos
- Responde sobre procedimientos cubiertos
- Coordina con tu sistema de facturación
### 🔔 Recordatorios Automáticos
- Envía recordatorios 24h y 2h antes
- Por WhatsApp, SMS o email
- Incluye instrucciones de preparación
- Confirmación de asistencia
### 🏥 Información de Servicios
- Explica tratamientos y procedimientos
- Informa sobre preparación para citas
- Proporciona horarios de atención
- Responde preguntas frecuentes
---
## Flujo de Conversación (Ejemplo)
```
Paciente: Hola, necesito agendar una cita
IA: ¡Con gusto! ¿Qué tipo de consulta necesitas?
Paciente: Es una revisión general
IA: ¿Tienes algún doctor preferido?
Paciente: No, el que esté disponible
IA: Perfecto. ¿Qué día te funciona?
Paciente: Mañana en la mañana
IA: Te tengo disponible mañana a las 10:00 AM
con el Dr. Martínez. ¿Te funciona?
Paciente: Sí, está bien
IA: Excelente. ¿Podrías confirmarme tu nombre
y número de teléfono?
Paciente: María López, 555-123-4567
IA: ¡Listo! Te envío la confirmación por WhatsApp.
¿Tienes alguna duda sobre la preparación?
```
---
## CTA Section
### Reduce no-shows y mejora la experiencia de tus pacientes
**TODO:** [Agregar link al formulario de contacto o calendario de demo]
**O llama directamente:** TODO: [Número de teléfono]
---
## Pricing
**TODO:** [Agregar planes y precios]
- Plan Básico: $129/mes — 100 pacientes/mes
- Plan Profesional: $249/mes — Pacientes ilimitados
- Plan Enterprise: Custom — Múltiples ubicaciones, especialidades
---
## Especialidades
**TODO:** [Agregar opciones de especialidades]
- Medicina General
- Odontología
- Veterinaria
- Psicología
- Fisioterapia
---
## Testimonios
**TODO:** [Agregar testimonios de clientes]
> "Reducimos nuestros no-shows en 60%. Los pacientes aman la conveniencia de agendar a cualquier hora." — *TODO: Nombre de clínica*
---
## FAQ
**¿Se integra con mi sistema de expediente electrónico?**
Sí, tenemos integraciones con los principales sistemas.
**¿Puede manejar varias especialidades?**
Sí, puedes configurar diferentes flujos por tipo de consulta.
**¿Los pacientes se sienten cómodos hablando con IA?**
Sí, el tono profesional y empático genera confianza inmediata.
---
## Footer
**TODO:** [Links a otras páginas del sitio]
- Inicio
- Precios
- Contacto
- Blog
---
*Última actualización: 2026-02-19*
+138
View File
@@ -0,0 +1,138 @@
# Real Estate AI Agent - Demostración
## Hero Section
**Headline:** Cada lead importa — tu agente IA qualifyca compradores 24/7
**Subheadline:** El asistente virtual que qualifyca leads, programa visitas y envía propiedades personalizadas — sin que pierdas tiempo en llamadas que no cierran.
**Key Phrases (Spanish):**
- "¿Tienen departamentos en venta en la zona sur?"
- "¿Cuál es el presupuesto mínimo para comprar?"
- "¿El precio incluye muebles?"
- "Quiero agendar una visita para este fin de semana"
- "¿Aceptan financiamiento?"
---
## Problema
Tu sitio web recibe cientos de visitas al mes, pero tu equipo no puede atender a todos los leads. Mientras duermes, prospectos potenciales están buscando en otros lados.
## Solución: Agente IA para Bienes Raíces
Un asistente virtual que conversacon tus visitantes, entiende lo que buscan y convierte interesados en citas programadas.
---
## Características
### 🎯 Calificación de Leads
- Pregunta sobre presupuesto, zona preferida y tipo de propiedad
- Identifica compradores serios vs. navegadores
- Prioriza leads según criterios que definas
- Score de urgencia y probabilidad de cierre
### 📅 Programación de Visitas
- Coordina horarios con tu calendario
- Confirma citas automáticamente
- Envía recordatorios por WhatsApp/Email
- Maneja cancelaciones y reprogramaciones
### 🏠 Envío de Propiedades
- Matching automático con tu inventario
- Envía propiedades relevantes al instante
- Incluye fotos, precios y detalles
- Follow-up automático con propiedades nuevas
### 💬 Seguimiento Inteligente
- Nurturing automático de leads tibios
- Recordatorios para tu equipo de ventas
- Historial completo de conversaciones
- Notificaciones de leads calientes
---
## Flujo de Conversación (Ejemplo)
```
Cliente: Hola, estoy buscando un departamento
IA: ¡Hola! Excelente, ¿qué zona te interesa?
Cliente: Prefiero la zona norte, cerca del metro
IA: Perfecto. ¿Tienes un rango de presupuesto en mente?
Cliente: Entre 30 y 40 millones
IA: ¿Necesitas financiamiento o tienes recursos propios?
Cliente: Probably necesito crédito
IA: ¡Entendido! Te tengo varias opciones en la zona norte
entre 30-40M. ¿Te gustaría que te envíe algunas opciones
y agendamos una visita?
Cliente: Sí, por favor
IA: ¿Qué días de esta semana te funcionan para visitar?
```
---
## CTA Section
### No dejes que más leads se vayan a la competencia
**TODO:** [Agregar link al formulario de contacto o calendario de demo]
**O llama directamente:** TODO: [Número de teléfono]
---
## Pricing
**TODO:** [Agregar planes y precios]
- Plan Básico: $149/mes — 150 leads/mes
- Plan Profesional: $299/mes — Leads ilimitados + CRM
- Plan Enterprise: Custom — Múltiples agentes, integraciones
---
## Integraciones
**TODO:** [Agregar logos de integraciones]
- MLS
- CRM (Salesforce, HubSpot)
- Calendarios (Google, Outlook)
- WhatsApp Business
---
## Testimonios
**TODO:** [Agregar testimonios de clientes]
> "El agente IAqualifyca el 80% de los leads antes de que mi equipo los contacte. Ahorramos 20 horas semanales." — *TODO: Nombre de agencia*
---
## FAQ
**¿Puede el agente IA manejar varias propiedades a la vez?**
Sí, puede consultar tu inventario en tiempo real y enviar múltiples opciones.
**¿Qué pasa si el lead quiere hablar con un agente humano?**
Transfiere inmediatamente y proporciona el historial completo de la conversación.
**¿Se integra con mi sitio web actual?**
Sí, funciona con cualquier sitio mediante widget o API.
---
## Footer
**TODO:** [Links a otras páginas del sitio]
- Inicio
- Precios
- Contacto
- Blog
---
*Última actualización: 2026-02-19*
+128
View File
@@ -0,0 +1,128 @@
# Restaurant AI Agent - Demostración
## Hero Section
**Headline:** Tu restaurant nunca pierde una llamada — ni a las 2 AM
**Subheadline:** El agente de IA que toma reservaciones, responde sobre el menú y maneja pedidos para llevar — 24/7, en español e inglés.
**Key Phrases (Spanish):**
- "¿Tienen mesa disponible para 4 personas este viernes a las 8?"
- "¿El menú tiene opciones sin gluten?"
- "¿A qué hora cierran hoy?"
- "Quiero hacer una reservación para mañana"
- "¿Tienen espacio para un grupo de 10?"
---
## Problema
Tu restaurant pierde clientes cada noche cuando el teléfono suena después de cerrar, o cuando tu personal no puede responder todas las preguntas sobre аллергены en el menú.
## Solución: Agente IA para Restaurantes
Un asistente virtual que responde instantáneamente, toma reservaciones y mantiene a tus clientes felices — sin que levantes un dedo.
---
## Características
### 📞 Reservaciones Inteligentes
- Toma reservaciones 24/7, incluso fuera de horario
- Confirma disponibilidad en tiempo real
- Envía recordatorios automáticos por WhatsApp/SMS
- Maneja cancelaciones y modificaciones
### 🍽️ Información del Menú
- Responde sobre ingredientes y аллергены
- Informa sobre opciones vegetarianas/veganas/sin gluten
- Indica precios y disponibilidad de platos
- Recomienda platos según preferencias
### 🛵 Pedidos para Llevar
- Acepta pedidos para llevar
- Calcula tiempos de preparación
- Confirma métodos de pago
- Coordina recogida en restaurante
### 🌐 Multilingüe
- Español e inglés fluidos
- Personalizable según tu marca
- Tono profesional y amigable
---
## Flujo de Conversación (Ejemplo)
```
Cliente: Hola, quiero hacer una reservación
IA: ¡Con gusto! ¿Para cuántas personas?
Cliente: Para 2 personas
IA: ¿Qué día y a qué hora les gustaría?
Cliente: Este sábado a las 9
IA: Perfecto. Les tengo disponible para 2 personas
el sábado a las 21:00. ¿A nombre de quién?
Cliente: A nombre de Carlos
IA: Confirmado. Reservación a nombre de Carlos
para 2 personas, sábado a las 21:00.
¿Quieren que les mande un recordatorio?
Cliente: Sí, por WhatsApp
IA: ¡Listo! Les envío la confirmación ahora.
¡Nos vemos el sábado!
```
---
## CTA Section
### ¿Listo para no perder más clientes?
**TODO:** [Agregar link al formulario de contacto o calendario de demo]
**O llama directamente:** TODO: [Número de teléfono]
---
## Pricing
**TODO:** [Agregar planes y precios]
- Plan Básico: $99/mes — 100 llamadas/mes
- Plan Profesional: $199/mes — Llamadas ilimitadas
- Plan Enterprise: Custom — Integraciones, múltiples ubicaciones
---
## Testimonios
**TODO:** [Agregar testimonios de clientes]
> "Desde que usamos el agente IA, nuestras reservaciones nocturnas aumentaron 40%. No perdemos ninguna llamada." — *TODO: Nombre del restaurante*
---
## FAQ
**¿Qué pasa si hay una situación que el IA no puede manejar?**
El agente transfiere la llamada a un humano y le proporciona el contexto completo.
**¿Se integra con mi sistema de reservas actual?**
Sí, tenemos integraciones con OpenTable, Resy, y sistemas personalizados.
**¿Necesito hardware nuevo?**
No. Solo conectamos tu número existente.
---
## Footer
**TODO:** [Links a otras páginas del sitio]
- Inicio
- Precios
- Contacto
- Blog
---
*Última actualización: 2026-02-19*
+114
View File
@@ -0,0 +1,114 @@
# SiteMente - Preguntas Frecuentes (FAQ)
*English version available below each answer*
---
## 📋 General
### ¿Qué es SiteMente y cómo puede ayudar a mi negocio?
**English:** SiteMente is an AI-powered platform that helps businesses automate customer service, improve response times, and increase conversions through intelligent chatbots and automation tools.
SiteMente es una plataforma impulsada por IA que ayuda a las empresas a automatizar el servicio al cliente, mejorar los tiempos de respuesta y aumentar las conversiones mediante chatbots inteligentes y herramientas de automatización.
### ¿Cuánto cuesta implementar SiteMente?
**English:** Pricing varies based on your business size and needs. We offer flexible plans starting from $99/month for small businesses. Contact us for a customized quote.
Los precios varían según el tamaño de tu empresa y tus necesidades. Ofrecemos planes flexibles desde $99/mes para pequeñas empresas. Contáctanos para una cotización personalizada.
### ¿Cuánto tiempo toma implementar la solución?
**English:** Most implementations are completed within 2-4 weeks, depending on complexity. Basic setup can be done in as little as 5 business days.
La mayoría de las implementaciones se completan en 2-4 semanas, dependiendo de la complejidad. La configuración básica puede realizarse en tan solo 5 días hábiles.
### ¿Necesito conocimientos técnicos para usar SiteMente?
**English:** No, SiteMente is designed to be user-friendly. Our no-code interface allows you to manage your AI assistant without any programming knowledge. We also provide full technical support.
No, SiteMente está diseñado para ser intuitivo. Nuestra interfaz sin código te permite gestionar tu asistente de IA sin conocimientos de programación. También brindamos soporte técnico completo.
### ¿Qué tipo de soporte ofrecen?
**English:** We offer 24/7 technical support, implementation assistance, and ongoing optimization. Our team is available via chat, email, and phone.
Ofrecemos soporte técnico 24/7, asistencia en implementación y optimización continua. Nuestro equipo está disponible via chat, correo y teléfono.
---
## 🍽️ Restaurantes
### ¿Puede SiteMente manejar reservas y pedidos en línea?
**English:** Yes, our AI can manage table reservations, take orders, and answer frequently asked questions about menus, hours, and special events.
Sí, nuestra IA puede gestionar reservas de mesas, tomar pedidos y responder preguntas frecuentes sobre menús, horarios y eventos especiales.
### ¿El chatbot puede integrarse con mi sistema de punto de venta (POS)?
**English:** Absolutely. We integrate with most POS systems including Toast, Square, and Clover, allowing seamless order management.
Por supuesto. Nos integramos con la mayoría de los sistemas POS incluyendo Toast, Square y Clover, permitiendo una gestión de pedidos sin complicaciones.
### ¿Puede manejar múltiples ubicaciones de restaurante?
**English:** Yes, our platform supports multi-location management, allowing you to centralize customer service while maintaining location-specific information.
Sí, nuestra plataforma soporta gestión multi-ubicación, permitiéndote centralizar el servicio al cliente mientras mantienes información específica por ubicación.
### ¿Cómo ayuda SiteMente a reducir costos operativos?
**English:** Our AI handles up to 80% of routine customer inquiries, reducing the need for staff while improving response times. This saves an average of 20+ hours per week.
Nuestra IA maneja hasta el 80% de las consultas rutinarias de clientes, reduciendo la necesidad de personal mientras mejora los tiempos de respuesta. Esto ahorra un promedio de 20+ horas por semana.
---
## 🏠 Inmobiliarias
### ¿Puede SiteMente calificar leads automáticamente?
**English:** Yes, our AI qualifies leads by asking relevant questions about budget, location preferences, timeline, and property needs, then routes qualified leads to your agents.
Sí, nuestra IA califica leads haciendo preguntas relevantes sobre presupuesto, preferencias de ubicación, cronograma y necesidades de propiedad, luego deriva los leads calificados a tus agentes.
### ¿Se integra con los portales inmobiliarios como Zillow o Realtor.com?
**English:** Yes, we integrate with major real estate platforms and your existing CRM to ensure seamless lead management and tracking.
Sí, nos integramos con las principales plataformas inmobiliarias y tu CRM existente para garantizar una gestión y seguimiento de leads sin complicaciones.
### ¿Puede mostrar propiedades disponibles en tiempo real?
**English:** Yes, our AI can access your property listings database and provide real-time information about available properties, prices, and features.
Sí, nuestra IA puede acceder a tu base de datos de propiedades y proporcionar información en tiempo real sobre propiedades disponibles, precios y características.
### ¿Cómo ayuda a aumentar las conversiones de leads?
**English:** Instant response (within seconds) combined with personalized property recommendations increases lead conversion rates by an average of 35%.
La respuesta instantánea (en segundos) combinada con recomendaciones personalizadas de propiedades aumenta las tasas de conversión de leads en un promedio del 35%.
---
## 🏥 Clínicas
### ¿Puede SiteMente gestionar citas y recordatorios?
**English:** Yes, our AI handles appointment scheduling, cancellations, rescheduling, and sends automated reminders to reduce no-shows by up to 40%.
Sí, nuestra IA gestiona programación de citas, cancelaciones, reagendamiento y envía recordatorios automatizados para reducir las ausencias hasta en un 40%.
### ¿Es compatible con sistemas de gestión de pacientes (PMS)?
**English:** Yes, we integrate with major Practice Management Systems like Dentrix, Eaglesoft, and medical EMRs to sync appointments and patient information.
Sí, nos integramos con los principales sistemas de gestión como Dentrix, Eaglesoft y historiales médicos electrónicos para sincronizar citas e información de pacientes.
### ¿Puede responder preguntas sobre seguros y cobertura?
**English:** Yes, our AI can answer common insurance questions, verify coverage details, and explain payment options to patients.
Sí, nuestra IA puede responder preguntas comunes de seguros, verificar detalles de cobertura y explicar opciones de pago a los pacientes.
### ¿Cómo protege la privacidad de los datos de los pacientes?
**English:** We are HIPAA compliant and use enterprise-grade encryption to ensure all patient data is protected and handled according to medical privacy regulations.
Somos compatibles con HIPAA y usamos encriptación de nivel empresarial para garantizar que todos los datos de los pacientes estén protegidos y manejados según las regulaciones de privacidad médica.
---
## 📞 ¿Tienes más preguntas?
Contáctanos en **[hola@sitmente.com](mailto:hola@sitmente.com)** o agenda una demostración gratuita.
---
*© 2024 SiteMente. Todos los derechos reservados.*
+304
View File
@@ -0,0 +1,304 @@
# SiteMente 7-Day Promotional Content Plan
## Overview
Promoting 4 demo pages (restaurant, real-estate, clinic, car-rental) across LinkedIn and email over 7 days.
---
## Day 1: Restaurant Vertical
### LinkedIn Post
**Hook:** "Is your restaurant losing guests to a slow website? 🍽️"
**Body:**
Your menu is amazing. Your reviews are stellar. But when hungry customers search for "best [cuisine] near me," they find your competitor first.
SiteMente builds lightning-fast demo pages for restaurants that:
- Showcase your menu with stunning visuals
- Integrate online ordering seamlessly
- Load in under 2 seconds (Google loves this)
- Work perfectly on mobile (where 60% of searches happen)
See how we do it → [Restaurant Demo]
**CTA:** Book your free demo consultation → [Book Now]
---
### Email
**Subject Line:** Your restaurant deserves a website that serves customers fast 🍽️
**Body:**
Hi [Name],
Imagine a customer Googling "best pizza in [city]" and finding your restaurant instantly—before your competitors even load.
That's what we built SiteMente for.
Our restaurant demo page shows you exactly how fast, beautiful, and conversion-focused your website could be.
→ See the Restaurant Demo
It's free to explore. No catch.
Best,
The SiteMente Team
---
## Day 2: Real Estate Vertical
### LinkedIn Post
**Hook:** "Your dream home deserves a dream website. 🏠"
**Body:**
Buyers scroll through 100+ listings a day. If your properties don't pop in the first 3 seconds, you're invisible.
SiteMente creates real estate demo pages that:
- Feature property galleries with immersive visuals
- Include instant mortgage calculators
- Load instantly on any device
- Turn browsers into buyers with smart CTAs
See the real estate experience → [Real Estate Demo]
**CTA:** Get your property showcase → [Book Demo]
---
### Email
**Subject Line:** Stop losing buyers to faster-loading listings 🏠
**Body:**
Hi [Name],
Here's a hard truth: buyers won't wait for slow pages. They'll swipe to the next listing.
Our real estate demo shows you how to capture attention instantly—with galleries, calculators, and CTAs that convert.
→ View the Real Estate Demo
See what's possible.
Best,
The SiteMente Team
---
## Day 3: Clinic Vertical
### LinkedIn Post
**Hook:** "Patients are booking elsewhere while your phone rings. 📅"
**Body:**
When someone needs a dentist, dermatologist, or doctor—now—they Google. And they book with the clinic that makes it easiest.
SiteMente builds clinic demo pages that:
- Showcase services and specialties beautifully
- Include one-click appointment booking
- Build trust with patient testimonials
- Load fast enough for impatient searchers
Check out the clinic experience → [Clinic Demo]
**CTA:** Transform patient bookings → [Book Demo]
---
### Email
**Subject Line:** Your patients are booking online. Are you ready? 📅
**Body:**
Hi [Name],
Modern patients don't want to call. They want to book in seconds—from their phone, at 11 PM, while researching options.
Our clinic demo shows you exactly how to capture those bookings.
→ See the Clinic Demo
No pressure. Just possibilities.
Best,
The SiteMente Team
---
## Day 4: Car Rental Vertical
### LinkedIn Post
**Hook:** "Why rent a car from a website that feels like 2005? 🚗"
**Body:**
Travelers have options. A lot of them. If your car rental site looks outdated or loads slowly, they'll choose the competitor who invested in their customer experience.
SiteMente creates car rental demo pages that:
- Showcase vehicles with high-impact visuals
- Offer instant availability checks
- Streamline the booking flow
- Work flawlessly on mobile (crucial for travelers)
See the car rental difference → [Car Rental Demo]
**CTA:** Upgrade your booking experience → [Book Demo]
---
### Email
**Subject Line:** Your cars are great. Is your website? 🚗
**Body:**
Hi [Name],
Travelers book cars in seconds. If your site takes 10 seconds to load, you've already lost them.
Our car rental demo shows you a faster, smarter way to convert visitors into renters.
→ View the Car Rental Demo
Take a look.
Best,
The SiteMente Team
---
## Day 5: Case Study / Social Proof
### LinkedIn Post
**Hook:** "We helped a local business 3x their bookings in 30 days. Here's how. 📈"
**Body:**
[Client Name] was struggling with a slow, outdated website. Traffic was down. Bookings were flat.
After we rebuilt their site with SiteMente:
- Page load time dropped from 8s to 1.5s
- Mobile bookings increased by 210%
- Overall conversions up 312%
The full story → [Case Study]
**CTA:** Want similar results? → [Book Your Demo]
---
### Email
**Subject Line:** 3x more bookings in 30 days. Here's the story. 📈
**Body:**
Hi [Name],
One of our clients went from struggling with a slow site to tripling their conversions in a month.
The secret? A faster, smarter website built for how customers actually search and book.
Read the full case study → [Case Study]
See what's possible for your business.
Best,
The SiteMente Team
---
## Day 6: FAQ / Objection Handling
### LinkedIn Post
**Hook:** "We heard your concerns about new websites. Let's address them. 💬"
**Body:**
We get it. Changing your website is a big deal. Here are the questions we hear most:
**"Will it disrupt my current site?"**
We build parallel. Launch when you're ready.
**"What about SEO?"**
We optimize for speed and search from day one.
**"Is it worth the investment?"**
Our clients see ROI in 30-90 days. We'll show you the math.
Still have questions? → [FAQ Page]
**CTA:** Let's talk. Book a no-pressure demo → [Book Now]
---
### Email
**Subject Line:** Questions about building a new website? We have answers. 💬
**Body:**
Hi [Name],
Thinking about a new website brings up a lot of questions. We've heard them all:
- "Will it hurt my SEO?"
- "How long does it take?"
- "What if I don't like the design?"
We've answered the most common ones here → [FAQ Page]
Prefer to talk it out? Book a quick demo and we'll walk you through everything—no pressure.
→ Book Your Demo
Talk soon,
The SiteMente Team
---
## Day 7: Final CTA - Book Your Demo
### LinkedIn Post
**Hook:** "Your website could be this good. Let's make it happen. 🎯"
**Body:**
We've shown you how SiteMente transforms websites for restaurants, real estate, clinics, and car rentals.
Now it's your turn.
Book a free 15-minute demo and we'll:
- Review your current site challenges
- Show you a customized demo for your industry
- Give you a clear roadmap and pricing
This week only: Free site audit with demo booking 🎁
→ Book Your Demo Now
Spaces are limited. See you there.
---
### Email
**Subject Line:** Last chance: Free demo + site audit this week 🎯
**Body:**
Hi [Name],
We've spent the week showing you what's possible—faster sites, more bookings, better experiences.
Now we want to show YOU what's possible for YOUR business.
Book your free demo this week and we'll include a complimentary site audit (value: $297).
→ Book Your Demo Now
Only a few spots left. Let's talk soon.
Best,
The SiteMente Team
---
## Content Calendar Summary
| Day | Platform | Content Type | Vertical |
|-----|----------|--------------|----------|
| 1 | LinkedIn + Email | Demo Showcase | Restaurant |
| 2 | LinkedIn + Email | Demo Showcase | Real Estate |
| 3 | LinkedIn + Email | Demo Showcase | Clinic |
| 4 | LinkedIn + Email | Demo Showcase | Car Rental |
| 5 | LinkedIn + Email | Case Study | Social Proof |
| 6 | LinkedIn + Email | FAQ/Objections | Trust Building |
| 7 | LinkedIn + Email | Final CTA | Book Demo |
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
apps: [{
name: 'sitemente',
script: 'npx',
args: 'next dev -- -p 1284',
cwd: '/root/.openclaw/workspace/SiteMente',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development',
PORT: 1284
}
}]
}
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
apps: [{
name: 'sitemente',
script: 'node_modules/.bin/next',
args: 'dev -- -p 1284',
cwd: '/root/.openclaw/workspace/SiteMente',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development',
PORT: 1284
}
}]
}
+33
View File
@@ -0,0 +1,33 @@
# Daily AI Research Prompts
Run these in morning brief:
## 1. New AI Services for SiteMente
```
Research the latest AI services and tools for small businesses in 2026. Find:
- New chatbot/AI features small businesses want
- Popular AI tools for restaurants, real estate, clinics
- Emerging AI capabilities we could offer (voice, video, automation)
- Pricing models for these services
Focus on what we can easily implement and sell to existing customers.
```
## 2. Competitor Analysis
```
Find SiteMente competitors in Spain/Costa del Sol. What AI services do they offer? What are their prices? What's missing that we could offer?
```
## 3. Customer Pain Points
```
What problems do restaurants, real estate agencies, clinics in Spain have with their current websites/booking systems? What AI solutions are they asking for?
```
## 4. Technology Trends
```
Latest AI trends for small business automation in 2026. Focus on: voice AI, appointment booking, CRM integration, multilingual support.
```
---
Save daily research to: `/leads/ai-research-YYYY-MM-DD.md`
+25
View File
@@ -0,0 +1,25 @@
# Benalmádena Restaurant Leads
Researched: 2026-02-16
## Lead List
| # | Name | Address | Phone | Website | Score | Notes |
|---|------|---------|-------|---------|-------|-------|
| 1 | Tex Mex Gringos | Calle La Fragata, s/n, 29630 | +34 951 777 848 | restaurantespuertomarina.com | 7/10 | Has site, could upgrade to AI |
| 2 | La Mar Chica | Calle Marbella, 1, 29639 | +34 951 634 708 | mar-chica.com | 7/10 | Has site, could add AI booking |
| 3 | Basil | Plaza Nueva Bonanza, 29630 | 631 971 592 | basilbenalmadena.com | 6/10 | Has WhatsApp, no booking system |
| 4 | SALU Grill & Wine | Calle San José, 6, Benalmádena Pueblo | +34 951 71 57 36 | salu-restaurant.com | 8/10 | Has site, opportunity for AI |
| 5 | Lime & Lemon Tapas | Benalmádena | - | limeandlemonbenalmadena.com | 5/10 | No phone listed, has website |
## Scoring Criteria
- No website = 10/10 (big opportunity)
- Has website, no booking/AI = 6-8/10
- Has website + booking = 3-5/10
## Next Steps
- Expand to more restaurants
- Add Real Estate agents
- Add Clinics
- Add Car Rental
- Create outreach sequence
+65
View File
@@ -0,0 +1,65 @@
# Car Rental Companies Research - Malaga Airport (AGP) & Costa del Sol
Research completed: February 2026
Serving: Malaga Airport (AGP), Costa del Sol
## Car Rental Companies with Phone Numbers
| Company Name | Phone | Website |
|--------------|-------|---------|
| Avis | +34 902 180 854 | avis.com |
| Budget | +34 902 109 384 | budget.com |
| Europcar | +34 902 503 010 | europcar.com |
| Goldcar | +34 902 119 726 | goldcar.es |
| Record & Go | +34 902 123 002 | recordrentacar.com |
| Enterprise | +34 917 821 011 | enterprise.com |
| Hertz | - | hertz.com |
| Sixt | - | sixt.com |
| Firefly | - | firefly.com |
| National Car Hire | - | nationalcar.com |
| Alamo | - | alamo.com |
| Thrifty | - | thrifty.com |
| Dollar | - | dollar.com |
| Maggiore | - | maggiore.it |
| Sicily by Car | - | sicilybycar.it |
| OK Mobility | - | okmobility.com |
| Auriga Crown | +34 952 175 095 | aurigacrown.com |
| Autolink | +34 952 173 520 | - |
| Cargest | +34 902 104 103 | - |
| Cargest Alt | +34 966 36 03 60 | - |
| Marbesol | - | marbesol.com |
| Zest Car Rental | +44 1362 852 288 | zestcarrental.com |
| All In Car Hire | - | allincarhire.com |
| Centauro | - | centauro.net |
| Wiber | - | wiber.es |
| Autos Lido | - | - |
| Fetajo | - | - |
| Autoclick | - | - |
| Global Car Hire | - | - |
| Pepecar | - | - |
| Top Rent-a-Car | - | - |
| Espacar | - | - |
| Prima Car Hire | - | - |
| Canny Car Hire | - | - |
| Miami Car Hire | - | - |
| Lidifur | - | - |
| DelPaso | - | - |
## Terminal Information
- Most companies have desks on the ground floor of **Terminal 2** and **Terminal 3**, just after baggage claim
- Additional offices along **Avenida García Morato**
- Some offsite companies offer free shuttle service
## Major Companies at Malaga Airport (Inside Terminal)
1. Avis & Budget
2. Sixt
3. Europcar
4. Record Go
5. Goldcar/Interent
6. Hertz & Firefly
7. Enterprise/Alamo/National
## Notes
- Companies WITH verified phone numbers prioritized
- Many international brands operate through aggregators like Auto Europe, Skyscanner
- Phone numbers may connect to central booking offices rather than local AGP desks
+57
View File
@@ -0,0 +1,57 @@
# Clinics Research - Costa del Sol
Research completed: February 2026
Areas: Benalmádena, Marbella
## Dental, Aesthetic & Medical Clinics with Phone Numbers
| Name | Phone | Address | Specialty |
|------|-------|---------|-----------|
| Las Palmeras Dental Clinic | - | Avda. Las Palmeras, Benalmar Beach, Block 6, 29630 Benalmádena | Dental |
| Grupo Dental Clinics Benalmádena | +34 601 44 28 13 | Av. Cdad. de Melilla 26, Block 8, Portal 1, Local 1, Benalmádena | Dental |
| Smart Dental | +34 635 61 82 42 | Av. Blas Infante 17, 29631 Benalmádena | Dental |
| NewYorkClinic (Dental) | +34 952 36 56 0 | Av. Bonanza s/n, Edificio Plaza Ibensa, Bajo 8-9, Benalmádena Costa | Dental & Aesthetic |
| NewYorkClinic (Esthetic) | +34 952 56 05 6 | Av. Bonanza s/n, Edificio Plaza Ibensa, Bajo 8-9, Benalmádena Costa | Aesthetic |
| Clinica Dental Idea | +34 952 19 76 41 | Centro Idea Shopping Center, Ctra. de Mijas Km 3.6, 29650 Mijas | Dental |
| Clinica Dental Estrella | - | Benalmádena | Dental |
| Rosasco Dental Clinic | - | Arroyo de la Miel, Benalmádena | Dental |
| R&H Dental Marbella | - | Av. Ricardo Soriano 54, Puerto Banús, Marbella | Dental |
| Noosa Dental | - | Av. Bulevar Príncipe Alfonso de Hohenlohe, Local 09, 29602 Marbella | Dental |
| ACE DNTL STUDIO | - | Avda. Pilar Calvo, Centro Comercial Iberico, Local 3, 29660 Nueva Andalucía, Marbella | Dental |
| Clinica Dental Crooke Marbella | - | Calle Mediterráneo 1, 29602 Marbella | Dental |
| Infinity Dental Marbella | - | Av. Ricardo Soriano 36, Edificio María III, Planta 1, Office 104, 29603 Marbella | Dental |
| Denticlinic | - | 12 Avenida Ricardo Soriano, 29601 Marbella | Dental |
| Clinica Dental Alemana Marbella | - | Marbella | Dental |
| VV07 Aesthetic Medicine Clinic | +34 635 38 52 38 | Marbella | Aesthetic Medicine, Dermatology |
| Skin Care & Beauty Clinic Marbella | +34 689 36 21 87 | Marbella | Skincare, Beauty |
| Arques Clinic | - | Benalmádena | Tattoo Removal, Laser |
| Clínica Doctora Matas | - | Sotogrande/Marbella area | Aesthetic Medicine |
| Edén Beauty Marbella | - | Marbella | Beauty Salon |
| Hident | - | Benalmádena | Dental |
| Clinica Cuevas Xanit Benalmádena | - | Benalmádena | Dental |
| Clínica Dental Dr. Abraham | - | Benalmádena | Dental |
| Centro (Benalmádena) | - | Benalmádena | Dental |
| Carlos Saiz Smile | - | Marbella | Dental |
| CLINICA VELEZ | - | Marbella | Dental |
## Notes
- Clinics WITH verified phone numbers prioritized
- Includes dental, aesthetic medicine, and medical clinics
- Many clinics serve English-speaking patients
- Some clinics without phone numbers included for reference
---
## Additional Clinics (Feb 17)
| Name | Full Address | Phone |
|------|--------------|-------|
| Smart Dental | Av. Blas Infante 17, 29631 Benalmádena | +34 911 98 04 65 / +34 635 61 82 42 |
| Grupo Dental Clinics | Av. Cdad. de Melilla 26, Block 8, 29639 Benalmádena | - |
| Las Palmeras Dental Clinic | Benalmádena Costa | - |
| Dental Narvaez | Av. de la Constitución, Benalmádena (near Renfe station) | - |
| Clínica Dental Dr. Abraham | Benalmádena | - |
| Rosasco Dental | Benalmádena | - |
| Hident | Benalmádena | - |
| Clínica Cuevas Xanit | Benalmádena | - |
| Clínica Dental Noruega | Benalmádena-Torremolinos | - |
+69
View File
@@ -0,0 +1,69 @@
# Costa del Sol Leads - SiteMente Outreach
Researched: 2026-02-16 via Perplexity (Prep)
---
## 🍽️ Restaurants (Benalmádena)
| # | Name | Address | Phone | Website | Score | Priority |
|---|------|---------|-------|---------|-------|----------|
| 1 | Tex Mex Gringos | Calle La Fragata, s/n, 29630 | +34 951 777 848 | restaurantespuertomarina.com | 7/10 | Medium |
| 2 | La Mar Chica | Calle Marbella, 1, 29639 | +34 951 634 708 | mar-chica.com | 7/10 | Medium |
| 3 | Basil | Plaza Nueva Bonanza, 29630 | 631 971 592 | basilbenalmadena.com | 6/10 | High |
| 4 | SALU Grill & Wine | Calle San José, 6, Benalmádena Pueblo | +34 951 715 736 | salu-restaurant.com | 8/10 | High |
| 5 | Lime & Lemon Tapas | Benalmádena | - | limeandlemonbenalmadena.com | 5/10 | Medium |
| 6 | Restaurant No7 | Benalmádena | +34 655 036 827 | TripAdvisor | 4/10 | Low |
| 7 | Capitan Bar & Restaurant | Benalmádena | +34 674 591 584 | TripAdvisor | 4/10 | Low |
---
## 🏠 Real Estate Agencies (Costa del Sol)
| # | Name | Areas | Website | Score |
|---|------|-------|---------|-------|
| 1 | Hernán Bustos Real Estate | Benalmádena, Torremolinos | hernanbustos.com | 9/10 |
| 2 | ViVi Real Estate | Benalmádena, Costa del Sol | vivi-realestate.com | 9/10 |
| 3 | Costa Listings | Benalmádena | costalistings.es | 8/10 |
| 4 | Benalsun Properties | Benalmádena, Fuengirola | Via SpainHouses.net | 7/10 |
---
## 🏥 Clinics (Benalmádena)
| # | Name | Phone | Website | Score |
|---|------|-------|---------|-------|
| 1 | Vithas Xanit International Hospital | +34 952 367 190 | vithas.es | 8/10 |
| 2 | Centro de Salud Arroyo de la Miel | - | juntadeandalucia.es | 10/10 |
---
## 🚗 Car Rental (Malaga/Benalmádena)
| # | Name | Website | Score |
|---|------|---------|-------|
| 1 | Malaga U Drive | malagaudrive.com | 8/10 |
| 2 | ALL IN Car Hire | allincarhire.com | 8/10 |
| 3 | Helle Hollis | hellehollis.com | 8/10 |
| 4 | Budget | budget.com | 6/10 |
| 5 | Avis | avis.com | 6/10 |
---
## Scoring System
- **10/10**: No website / opportunity for new site
- **7-9/10**: Has website, needs AI upgrade
- **5-6/10**: Has site, could add booking
- **1-4/10**: Has everything, hard sell
## Next Steps
1. ✅ Benalmádena restaurants - DONE
2. ⏳ Costa del Sol real estate - Need more phones
3. ⏳ Clinics - Need more data
4. ⏳ Car Rental - Done (mostly aggregators)
## Outreach Plan
1. Start with restaurants (high phone contact rate)
2. Real estate agents (big tickets)
3. Clinics (appointment booking)
4. Car rental (competitive, needs differentiation)
+25
View File
@@ -0,0 +1,25 @@
[
{"name": "Trocadero Benalmádena", "category": "restaurant", "phone": "+34 681 14 29 44", "email": "silviaduran@grupotrocadero.com", "address": "Avenida del Sol 121, 29631 Benalmádena", "score": 9},
{"name": "Restaurante Milan", "category": "restaurant", "phone": "+34 952 44 58 55", "email": "", "address": "Av. Federico Garcia Lorca 7, 29630 Benalmádena", "score": 8},
{"name": "Lime & Lemon", "category": "restaurant", "phone": "", "email": "info@limeandlemonbenalmadena.com", "address": "Av. Las Palmeras 1, Edificio Maite, 29630 Benalmádena", "score": 8},
{"name": "Tex Mex Gringos", "category": "restaurant", "phone": "", "email": "reservas@restaurantespuertomarina.com", "address": "Puerto Marina, Benalmádena", "score": 8},
{"name": "La Plaza Restaurant", "category": "restaurant", "phone": "+34 952 44 84 83", "email": "", "address": "Plaza de Espana 2, 29639 Benalmádena", "score": 8},
{"name": "El Parador", "category": "restaurant", "phone": "+34 952 44 92 93", "email": "", "address": "Av. Juan Luis Peralta 47, 29639 Benalmádena", "score": 8},
{"name": "Escorpio Restaurante", "category": "restaurant", "phone": "+34 952 569 047", "email": "", "address": "Santo Domingo de Guzmán 7, Benalmádena", "score": 8},
{"name": "The Bull Bar", "category": "restaurant", "phone": "+34 646 569 374", "email": "", "address": "Av del Chorrillo 15, Benalmádena", "score": 8},
{"name": "Restaurante La Fuente", "category": "restaurant", "phone": "+34 952 569 466", "email": "", "address": "Plaza de Espana 9, Benalmádena Pueblo", "score": 8},
{"name": "Caliu Restaurant", "category": "restaurant", "phone": "", "email": "caliu.torremolinos@gmail.com", "address": "Torremolinos", "score": 7},
{"name": "The Carvery", "category": "restaurant", "phone": "", "email": "info@thecarverycompany.com", "address": "Benalmádena", "score": 7},
{"name": "Smart Dental", "category": "clinic", "phone": "+34 911 98 04 65", "email": "", "address": "Av. Blas Infante 17, 29631 Benalmádena", "score": 9},
{"name": "Grupo Dental Clinics", "category": "clinic", "phone": "", "email": "", "address": "Av. Cdad. de Melilla 26, 29639 Benalmádena", "score": 7},
{"name": "Engel & Völkers Costa del Sol", "category": "realestate", "phone": "+34 952 650 234", "email": "", "address": "CC Diana Local 23, 29688 Estepona", "score": 8},
{"name": "Your Viva Marbella", "category": "realestate", "phone": "+34 951 27 27 43", "email": "", "address": "CC El Rosario, CN-340 Km 189, 29604 Marbella", "score": 8},
{"name": "Panorama Properties", "category": "realestate", "phone": "+34 952 774 266", "email": "", "address": "Hotel Local 23, 29602 Marbella", "score": 8},
{"name": "Marbella For Sale", "category": "realestate", "phone": "+34 952 907 386", "email": "", "address": "Edif. Marina Banús Bl.4 Local 8, 29660 Puerto Banús", "score": 8},
{"name": "Domus Venari", "category": "realestate", "phone": "+34 952 444 295", "email": "", "address": "Ctra. N340 KM189, 29604 Marbella", "score": 8},
{"name": "Diana Morales Properties", "category": "realestate", "phone": "+34 952 765 138", "email": "", "address": "Av. Cánovas del Castillo 4, 29601 Marbella", "score": 8},
{"name": "Hacienda Estates", "category": "realestate", "phone": "+34 952 850 154", "email": "", "address": "CC Pinogolf Local 2, 29604 Elviria", "score": 7},
{"name": "Sun Med Estates", "category": "realestate", "phone": "+34 952 493 372", "email": "", "address": "c/ Sedella 3, La Cala de Mijas", "score": 7},
{"name": "Marbesol Car Rental", "category": "carrental", "phone": "+34 952 93 44 12", "email": "", "address": "Málaga Airport", "score": 7},
{"name": "Enterprise Rent-A-Car", "category": "carrental", "phone": "", "email": "", "address": "Avda. Garcia Morato 22, 29004 Málaga", "score": 6}
]
+206
View File
@@ -0,0 +1,206 @@
# SiteMente Outreach Workflow
## Pipeline Stages
```
New → Contacted → Qualified → Proposal → Won/Lost
```
---
## 📞 CALL SCRIPTS
### Script 1: Cold Call - No Website (Score 10)
**Use for:** Restaurants/clinics with NO website
```
Hi [NAME], soy [YOUR NAME] de SiteMente, ¿te molesto un segundo?
Te llamo porque estamos ayudando a restaurantes/clínicas como el tuyo en Benalmádena a tener presencia online.
He visto que [BUSINESS NAME] no tiene web, ¿es correcto?
Perfecto, pues precisamente eso es lo que hacemos - creamos páginas web inteligentes con un asistente de IA que responde a tus clientes 24/7, incluso a las 3 de la mañana.
¿Te interesa que te envíe una demo de 2 minutos para que veas cómo funcionaría para [BUSINESS NAME]?
```
### Script 2: Warm Call - Has Website (Score 7-8)
**Use for:** Businesses with existing website
```
Hi [NAME], soy [YOUR NAME] de SiteMente.
Te llamo porque hemos estado ayudando a [restaurantes/inmobiliarias/clínicas] en la Costa del Sol a incrementar sus reservas con inteligencia artificial.
He visto que [BUSINESS NAME] tiene web, ¿estáis usando algún sistema de reservas o chat automático?
Vale, pues precisamente eso es lo que hacemos - un asistente IA que responde preguntas, toma reservas y cita clientes sin que tengas que contratar a nadie.
¿Te puedo enviar una demo rápida para que veas cómo funcionaría?
```
### Script 3: Follow-up Call
**Use for:** After sending email/WhatsApp
```
Hi [NAME], soy [YOUR NAME] de SiteMente.
Te escribí hace unos días sobre lo de la web + IA para [BUSINESS NAME].
¿Te dio tiempo a ver el video que te envié?
¿Qué te pareció? ¿Tiene sentido para vuestro negocio?
```
---
## 📧 EMAIL TEMPLATES
### Email 1: Cold Email - No Website (Spanish)
```
Subject: 🌐 [BUSINESS NAME] - ¿Por qué no tienes web en 2026?
Hola [NAME],
Soy [YOUR NAME] de SiteMente.
He buscado [BUSINESS NAME] en Google y no encuentro vuestra página web. En 2026, tener presencia online ya no es opcional - es supervivencia.
Vivimos en la Costa del Sol, donde el 70% de tus potenciales clientes buscan restaurants/servicios primero en Google. Si no te encuentran, van a la competencia.
En SiteMente creamos webs inteligentes con un asistente IA que:
- Responde preguntas 24/7
- Toma reservas automáticamente
- Funciona en español E inglés
Precio: desde 299€/mes (incluye web nueva + IA).
¿Te interesa que te haga una demo sin compromiso?
Un saludo,
[YOUR NAME]
SiteMente - IA para negocios locales
```
### Email 2: Cold Email - Has Website (Spanish)
```
Subject: 🤖 [BUSINESS NAME] - ¿Tu web trabaja mientras duermes?
Hola [NAME],
Soy [YOUR NAME] de SiteMente.
Tengo una pregunta: cuando un cliente busca "[tu tipo de negocio]" en Google y entra a tu web, ¿qué pasa si es a las 11 de la noche?
En la mayoría de los casos... nada. El cliente se va sin respuesta.
Pero no tiene por qué ser así. Nosotros añadimos un asistente de IA a tu web que:
- Responde INSTANTÁNEAMENTE a cualquier hora
- Toma reservas y citas sin intervención humana
- Habla español E inglés (perfecto para la Costa del Sol)
Esto no es un chatbot básico - es IA real que entiende contexto y cierra ventas.
¿Teecho una demo en 2 minutos?
Saludos,
[YOUR NAME]
SiteMente
```
### Email 3: Follow-up (Spanish)
```
Subject: [BUSINESS NAME] - ¿Te стало?
Hola [NAME],
Soy [YOUR NAME] de SiteMente.
Te escribí hace unos días sobre añadir IA a tu web.
Sé que estás ocupado, pero me gustaría saber si tiene sentido para [BUSINESS NAME].
¿Qué te parecería una llamada de 10 minutos para ver si podemos ayudar?
Sin compromiso.
Saludos,
[YOUR NAME]
```
---
## 📱 WHATSAPP TEMPLATES
### WhatsApp 1: First Contact
```
Hola [NAME], soy [YOUR NAME] de SiteMente (IA para negocios locales).
Vi que [BUSINESS NAME] no tiene web y me pareció interesante contactar contigo.
Te podemos ayudar a tener una web profesional + un asistente IA que responde a tus clientes 24/7.
¿Te interesa ver una demo? Es gratis y sin compromiso.
Un saludo!
```
### WhatsApp 2: Follow-up
```
Ey [NAME], soy [YOUR NAME] de SiteMente!
Te escribí hace unos días sobre lo de la web con IA para [BUSINESS NAME].
¿Tuviste oportunidad de verlo?
Si te interesa, me dices y te paso una demo en 2 minutos!
🙏
```
---
## 🎯 PRIORITY OUTREACH LIST
### TODAY - Call First (Score 10, Has Phone)
| # | Name | Category | Phone | Notes |
|---|------|----------|-------|-------|
| 1 | Trocadero Benalmádena | Restaurant | +34 681 142 944 | No website |
| 2 | Restaurante La Nina | Restaurant | +34 952 449 193 | No website |
| 3 | Restaurant No7 | Restaurant | +34 655 036 827 | No website |
| 4 | Capitan Bar & Restaurant | Restaurant | +34 674 591 584 | No website |
| 5 | Las Brisas | Restaurant | - | Beach, no phone |
| 6 | Vithas Xanit Hospital | Clinic | +34 952 367 190 | Big opportunity |
### THIS WEEK - Email (Score 9, No Phone)
| # | Name | Category | Website |
|---|------|----------|---------|
| 1 | Hernán Bustos RE | Real Estate | hernanbustos.com |
| 2 | ViVi Real Estate | Real Estate | vivi-realestate.com |
| 3 | Marbella Mundo | Real Estate | marbellamundo.es |
| 4 | Smart Dental | Clinic | - |
| 5 | Crooke & Laguna | Clinic | - |
---
## 📊 OUTREACH TRACKING
In CRM, update status:
- **New** → Lead just added
- **Contacted** → Called/email sent
- **Qualified** → Showed interest, needs demo
- **Proposal** → Demo done, sent proposal
- **Won** → Contract signed
- **Lost** → Not interested / no response
---
## 💡 TIPS
1. **Best time to call:** 10-11 AM or 5-6 PM
2. **Get WhatsApp:** Always ask for WhatsApp for follow-ups
3. **3 touch rule:** Call + Email + WhatsApp = 3 touches minimum
4. **Write notes:** After every call, write what was discussed
5. **Follow up fast:** If interested, send demo SAME DAY
+62
View File
@@ -0,0 +1,62 @@
# Real Estate Agencies Research - Costa del Sol
Research completed: February 2026
Areas: Benalmádena, Marbella, Torremolinos, Fuengirola
## Real Estate Agencies with Phone Numbers
| Agency Name | Phone | Website | Area |
|-------------|-------|---------|------|
| PropertyBenalmadena® | +34 951 778 130 | propertybenalmadena.com | Benalmádena |
| PropertyBenalmadena® Mobile | +34 669 769 794 | propertybenalmadena.com | Benalmádena |
| Spain Homes® | +34 683 45 86 86 | spainhomes.com | Benalmádena |
| Hernán Bustos Real Estate | - | hernanbustos.com | Benalmádena |
| INMOBILLIUM | - | inmobillium.com | Benalmádena |
| Navasol Real Estate | - | navasol.com | Benalmádena |
| Spain Houses - Arroyo de la Miel | - | spainhouses.net | Benalmádena |
| Panorama Properties | +34 952 774 266 | panorama.es | Marbella |
| Engel & Völkers Marbella | +34 952 868 406 | engelvoelkers.com | Marbella |
| DM Properties | +34 952 765 138 | dmproperties.com | Marbella |
| Marbella Unique Properties | +34 952 908 712 | marbellauniqueproperties.com | Marbella |
| Marbella Estates | +34 952 904 244 | marbella-estates.com | Marbella |
| Estate Agents Marbella | +34 952 887 724 | estateagentsmarbella.com | Marbella |
| Kristina Szekely Sotheby's International Realty | +34 952 772 000 | ksmarbella.com | Marbella |
| Nevado Realty | +34 952 825 517 | nevadomarbella.com | Marbella |
| Key Real Estate | +34 670 804 504 | keyrealestates.com | Marbella |
| Drumelia Estates | +34 952 766 950 | drumelia.com | Marbella |
| Marbella Hills Homes | +34 951 136 042 | marbellahillshomes.com | Marbella |
| Nvoga Marbella Realty | +34 952 813 333 | nvoga.com | Marbella |
| Marbella For Sale | +34 952 907 386 | marbellaforsale.com | Marbella |
| Bromley Estates Marbella | +34 952 939 460 | bromleyestatesmarbella.com | Marbella |
| Marlo Property | +34 687 748 741 | marloproperty.com | Marbella |
| Sidney George | +34 662 660 311 | sidneygeorge.com | Marbella |
| OneVila | +34 669 829 159 | onevilaproperties.com | Marbella |
| Homerun Brokers | +34 951 74 88 88 | homerunmarbella.com | Marbella |
| iad España - Jose Delgado | +34 651 03 51 31 | iadespana.es | Benalmádena |
| iad España - Aline Salgado | +34 660 96 05 XX | iadespana.es | Benalmádena Costa |
| 1Mast | +34 661 968 015 | 1mast.com | Fuengirola, Mijas |
| Svensk Fastighetsförmedling | +34 952 47 94 05 | svenskfast.se | Fuengirola |
| Engel & Völkers Fuengirola | +34 900 747 281 | engelvoelkers.com | Fuengirola |
| Fuengirola Estates | - | fuengirolaestates.com | Fuengirola |
## Notes
- Agencies WITH verified phone numbers prioritized
- Some agencies without phone numbers included for reference
- Many agencies operate across multiple Costa del Sol locations
---
## Additional Real Estate (Feb 17)
| Name | Full Address | Phone |
|------|--------------|-------|
| Engel & Völkers Costa del Sol | CC Diana Local 23, 29688 Estepona | +34 952 650 234 |
| Your Viva | CC El Rosario, CN-340 Km 189, 29604 Marbella | +34 951 27 27 43 |
| Panorama Properties | Hotel Local 23, 29602 Marbella | +34 952 774 266 |
| Marbella For Sale | Edif. Marina Banús Bl.4 Local 8, 29660 Puerto Banús | +34 952 907 386 |
| Domus Venari | Ctra. N340 KM189, 29604 Marbella | +34 952 444 295 |
| Diana Morales Properties | Av. Cánovas del Castillo 4, 29601 Marbella | +34 952 765 138 |
| Hacienda Estates | CC Pinogolf, Local 2, 29604 Elviria | +34 952 850 154 |
| Sun Med Estates | c/ Sedella 3, Local 1-A, La Cala de Mijas | +34 952 493 372 |
| Marbella Unique Properties | C/ Jesús Puente Local 25, 29660 Marbella | +34 952 908 712 |
| Sunshine Golf Properties | Calle Beamar 1, CC Campanario Local 3, 29649 Calahonda | +34 952 494 161 |
+85
View File
@@ -0,0 +1,85 @@
# Perplexity (Prep) Research Prompts
Use model: `sonar-pro` on Perplexity API
---
## 🍽️ Restaurants
### Benalmádena
```
Find 15 restaurants in Benalmádena, Málaga, Spain. For each provide: name, full address, phone number, whether they have a website (yes/no), and Google rating if available. Focus on independent restaurants, not chains.
```
### Marbella
```
Find 15 restaurants in Marbella, Spain. For each provide: name, full address, phone number, whether they have a website, and Google rating. Focus on independent restaurants.
```
### Torremolinos
```
Find 15 restaurants in Torremolinos, Málaga, Spain. For each provide: name, address, phone number, website status, and Google rating.
```
### Fuengirola
```
Find 15 restaurants in Fuengirola, Málaga, Spain. For each provide: name, address, phone number, website status, and Google rating.
```
---
## 🏠 Real Estate Agencies
### Costa del Sol
```
Find 15 real estate agencies in Benalmádena, Marbella, Torremolinos, and Fuengirola. For each provide: agency name, phone number, website URL, and areas they serve.
```
---
## 🏥 Clinics & Medical
### Benalmádena & Costa del Sol
```
Find 10 aesthetic clinics, dental clinics, and private medical centers in Benalmádena and Marbella. For each provide: name, address, phone number, website URL, and services offered.
```
---
## 🚗 Car Rental
### Malaga Airport & Costa del Sol
```
Find 10 car rental companies that serve Malaga Airport (AGP) and Benalmádena area. For each provide: company name, phone number, website URL, and whether they offer delivery to Benalmádena.
```
---
## Scoring Template
After getting results, score each lead:
| Score | Criteria |
|-------|----------|
| 10/10 | No website - big opportunity |
| 8-9/10 | Has website, needs AI upgrade |
| 6-7/10 | Has website, needs booking system |
| 4-5/10 | Has booking, could add AI |
| 1-3/10 | Has everything - hard sell |
---
## Output Format
Save results to:
- `/leads/restaurants-benalmadena.md`
- `/leads/restaurants-marbella.md`
- `/leads/real-estate-costa-del-sol.md`
- `/leads/clinics-costa-del-sol.md`
- `/leads/car-rental-costa-del-sol.md`
Format:
```
| # | Name | Address | Phone | Website | Score | Priority |
|---|------|---------|-------|---------|-------|----------|
```
+55
View File
@@ -0,0 +1,55 @@
# Restaurants Research - Costa del Sol
Research completed: February 2026
Areas: Benalmádena, Torremolinos, Fuengirola
## Restaurants with Phone Numbers
| Name | Full Address | Phone | Rating |
|------|--------------|-------|--------|
| Trocadero Benalmádena | Avenida del Sol 121, 29631 Benalmádena | +34 681 14 29 44 | 2.8/5 (TripAdvisor) |
| La Plaza Restaurant | Plaza de Espana 2, 29639 Benalmádena | +34 952 44 84 83 | 4.3/5 (TripAdvisor) |
| El Parador | Avenida Juan Luis Peralta 47, 29639 Benalmádena | +34 952 44 92 93 | 4.0/5 (TripAdvisor) |
| Escorpio Restaurante | Santo Domingo de Guzmán 7, Benalmádena | +34 952 569 047 | - |
| El Cordero | Plaza Bonanza, Benalmádena | +34 952 447 789 | - |
| The Bull Bar | Av del Chorrillo 15, Benalmádena | +34 646 569 374 | - |
| Restaurante La Fuente | Plaza de Espana 9, Benalmádena Pueblo | +34 952 569 466 | - |
| Taperia de Bodeguita | Av. Antonio Machado, Galeria la Roca 1A, Benalmádena | +34 952 577 441 | - |
| Restaurante Santa Ana Denis | Paseo Marítimo Local 4, Benalmádena Costa 29631 | 640 97 33 71 | - |
| Alamar | Av. Antonio Machado 57, Benalmádena | - | 9.6/10 |
| Restaurante Juan | C/ Mar 11, Paseo Maritimo 28, 29620 Torremolinos | +34 952 385 656 | - |
| Casa Juan | Calle San Ginés 20, La Carihuela, 29620 Torremolinos | +34 952 373 512 | - |
| La Reserva de Antonio | Plaza del Remo 6, La Carihuela, 29630 Torremolinos | +34 952 050 735 | - |
| Spanish Garden | Av. Salvador Allende 58, Local 6, 29620 Torremolinos | +34 952 388 798 | - |
| Frutos | Av. de la Riviera 80, Urb. Los Alamos, 29620 Torremolinos | +34 952 381 450 | - |
| El Figón de Montemar | Av de Carlota Alessandri 91, 29620 Torremolinos | +34 952 376 462 | - |
| Chiringuito Copacabana Playa | Paseo Maritimo Playamar 51, 29620 Torremolinos | +34 630 609 973 | - |
| Creperia bahia | Plaza de Los Tientos 29/30, Pueblo Blanco, 29630 Torremolinos | +34 600 398 644 | - |
| El Gato Lounge | Paseo Maritimo 1, 29620 Torremolinos | +34 951 251 509 | - |
| La Cacerola | Calle Dona Maria Barrabino 5, 29620 Torremolinos | +34 951 376 757 | - |
| Restaurante Bar España | Paseo Maritimo Rey de Espana 110, 29640 Fuengirola | +34 632 03 46 94 | - |
| La Subasta Benalmádena | C. Velázquez 32, Benalmádena | - | 8.9/10 |
| Restaurante Las Brisas | Paseo Marítimo Virgen del Carmen, Playa Santa Ana, Benalmádena | - | 4.5/5 |
| Coast To Coast | Benalmádena | - | 4.5/5 |
| The Steakhouse | Benalmádena | - | 4.9/5 |
## Notes
- Priority given to independent restaurants WITH verified phone numbers
- Ratings from TripAdvisor/Google where available
- Some highly-rated restaurants listed without phone numbers for reference
---
## Additional Restaurants (Feb 17)
| Name | Full Address | Phone | Notes |
|------|--------------|-------|-------|
| Restaurante Milan | Av. Federico Garcia Lorca 7, 29630 Benalmádena | +34 952 44 58 55 | Italian |
| Lime & Lemon | Av. Las Palmeras 1, Edificio Maite, 29630 Benalmádena | info@limeandlemonbenalmadena.com | Via email |
| La Subasta | C. Velázquez 32, Benalmádena | thefork.es | Tapas |
| Almarina Beach | Av. Juan Sebastian Elcano, Benalmádena | thefork.es | Beach seafood |
| SUD Social Pizza | Av. de la Constitución 31, Benalmádena | website | Pizza |
| ANGUS Puerto Marina | Puerto Marina Local B16, Benalmádena | website | Steakhouse |
| Restaurante Las Brisas | Paseo Marítimo Virgen del Carmen 10, Benalmádena | tripadvisor | Beach |
| La Mar Chica | Benalmádena Pueblo | tripadvisor | Seafood |
| Alamar | Av. Antonio Machado 57, Benalmádena | thefork | Mediterranean |
+79
View File
@@ -0,0 +1,79 @@
export type SiteMenteMessage = {
role: "user" | "assistant" | "system";
content: string;
};
export type SiteMenteSpeechResult = {
audioBytes: Uint8Array;
mimeType: string;
};
const TEXT_MODEL = "MiniMax-M2.5";
const API_KEY = process.env.MINIMAX_API_KEY || "sk-cp-q8FpnlRzqa2oHVrHYVKXgCDAltN85KdsiSqAzRZeP6zvSSauupHu4ffVPKLgVWFz534az2lgM39T6ReXUvfT-8sUlQd9faush2Kr3KNmykoJNNgE8IET73Q";
const MINIMAX_API_BASE = "https://api.minimax.io/v1";
const SYSTEM_PROMPT = `You are SiteMente, an AI site strategist that helps business owners make their websites think like a smart assistant.
You deeply understand landing pages, funnels, customer psychology, and automation.
You recommend specific AI Minds, suggest copy and UX changes, and ask focused clarifying questions before proposing changes.
Keep answers concise, practical, and business-oriented.`;
export const generateSiteMenteText = async (
messages: SiteMenteMessage[]
): Promise<string> => {
try {
// Build messages - add system prompt if not present
const hasSystem = messages.some((m) => m.role === "system");
// Filter and format messages
const contents = messages
.filter((m) => m.role !== "system")
.map((m) => ({
role: m.role === "assistant" ? "assistant" : "user",
content: m.content,
}));
// Add system instruction
if (!hasSystem) {
contents.unshift({ role: "system", content: SYSTEM_PROMPT });
}
const response = await fetch(`${MINIMAX_API_BASE}/text/chatcompletion_v2`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: TEXT_MODEL,
messages: contents,
max_tokens: 1024,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`MiniMax API error: ${response.status} - ${errorText}`);
}
const data = await response.json();
const text = data?.choices?.[0]?.message?.content;
if (!text) {
throw new Error("MiniMax returned an empty response.");
}
return text;
} catch (error) {
console.error("[SiteMente][MiniMax] Text generation failed", error);
throw error;
}
};
export const generateSiteMenteSpeech = async (
text: string
): Promise<SiteMenteSpeechResult> => {
// MiniMax doesn't have built-in TTS in their text API
// Vapi handles voice output on their end
throw new Error("TTS not implemented - Vapi handles voice output");
};
+4 -5
View File
@@ -1,8 +1,7 @@
import { import {
generateSiteMenteSpeech,
generateSiteMenteText, generateSiteMenteText,
type SiteMenteMessage, type SiteMenteMessage,
} from "./geminiClient"; } from "./minimaxClient";
export type SiteMenteTextRequest = { export type SiteMenteTextRequest = {
messages: SiteMenteMessage[]; messages: SiteMenteMessage[];
@@ -49,13 +48,13 @@ export const runSiteMenteVoiceTurn = async (
{ role: "user", content: request.transcript }, { role: "user", content: request.transcript },
]; ];
const reply = await generateSiteMenteText(withSystemPrompt(messages)); const reply = await generateSiteMenteText(withSystemPrompt(messages));
const audio = await generateSiteMenteSpeech(reply);
// Return text only - Vapi handles voice output on their end
return { return {
reply, reply,
audio: { audio: {
data: Buffer.from(audio.audioBytes).toString("base64"), data: "",
mimeType: audio.mimeType, mimeType: "audio/mp3",
}, },
}; };
}; };
+41 -9
View File
@@ -1,4 +1,4 @@
// Agent Types for Council // Agent Types for Council - SiteMente Ecosystem
export interface Agent { export interface Agent {
id: string; id: string;
@@ -20,42 +20,71 @@ export interface AgentTeam {
} }
export const defaultTeams: AgentTeam[] = [ export const defaultTeams: AgentTeam[] = [
// === HORUS - Master Orchestrator ===
{
id: "horus-team",
name: "Horus Command",
icon: "👁️",
description: "Lead orchestrator - delegates to all agents",
agents: [
{ id: "horus", name: "Horus", role: "Master Orchestrator", description: "Command center, delegates tasks, manages all agents", status: "idle", project: "horus" },
],
},
// === SiteMente Squad ===
{ {
id: "sitemente-squad", id: "sitemente-squad",
name: "SiteMente Squad", name: "SiteMente Squad",
icon: "🌐", icon: "🌐",
description: "Building and growing SiteMente B2B platform", description: "Building and growing SiteMente B2B platform",
agents: [ agents: [
{ id: "sm-architect", name: "Architect", role: "PM & Architecture", description: "Planning, architecture, task breakdown", status: "idle", project: "sitemente" }, { id: "thoth", name: "Thoth", role: "Strategy & Research", description: "SiteMente planning, research, analysis", status: "idle", project: "sitemente" },
{ id: "sm-frontend", name: "Frontend Dev", role: "UI/UX Implementation", description: "React, Next.js, components", status: "idle", project: "sitemente" }, { id: "ptah", name: "Ptah", role: "Dev \& Ops", description: "Development, deployment, technical implementation", status: "idle", project: "sitemente" },
{ id: "sm-ai", name: "AI Engineer", role: "LLM & Voice Integration", description: "Gemini, voice agents, widgets", status: "idle", project: "sitemente" }, { id: "seshat", name: "Seshat", role: "Content \& SEO", description: "Content strategy, SEO, marketing copy", status: "idle", project: "sitemente" },
{ id: "sm-seo", name: "SEO Specialist", role: "Content & Copy", description: "SEO, content, marketing copy", status: "idle", project: "sitemente" }, { id: "anubis", name: "Anubis", role: "Outreach \& Growth", description: "Lead generation, client acquisition", status: "idle", project: "sitemente" },
], ],
}, },
// === Trading Squad ===
{
id: "trading-squad",
name: "Trading Squad",
icon: "📈",
description: "Crypto market research and trade execution",
agents: [
{ id: "thoth-trading", name: "Thoth Trading", role: "Research \& Analysis", description: "Market research, chart analysis", status: "idle", project: "trading" },
{ id: "sekhmet", name: "Sekhmet", role: "Trade Executor", description: "Risk management, position sizing", status: "idle", project: "trading" },
],
},
// === HolaCompi Squad (Paused) ===
{ {
id: "holacompi-squad", id: "holacompi-squad",
name: "HolaCompi Squad", name: "HolaCompi Squad",
icon: "🤝", icon: "🤝",
description: "Consumer AI ally (paused until revenue)", description: "Consumer AI ally (paused until revenue)",
agents: [ agents: [
{ id: "hc-product", name: "Product Manager", role: "Flows & UX", description: "User flows, product decisions", status: "idle", project: "holacompi" }, { id: "hc-product", name: "Product Manager", role: "Flows \& UX", description: "User flows, product decisions", status: "idle", project: "holacompi" },
{ id: "hc telephony", name: "Telephony Engineer", role: "Voice & Telco", description: "Telnyx, Vapi, phone lines", status: "idle", project: "holacompi" }, { id: "hc-telephony", name: "Telephony Engineer", role: "Voice \& Telco", description: "Telnyx, Vapi, phone lines", status: "idle", project: "holacompi" },
{ id: "hc-voice", name: "Voice UX", role: "Conversation Design", description: "Dialogues, prompts, voice UX", status: "idle", project: "holacompi" }, { id: "hc-voice", name: "Voice UX", role: "Conversation Design", description: "Dialogues, prompts, voice UX", status: "idle", project: "holacompi" },
], ],
}, },
// === Infrastructure ===
{ {
id: "infrastructure-team", id: "infrastructure-team",
name: "Infrastructure", name: "Infrastructure",
icon: "🔧", icon: "🔧",
description: "Security, backups, and system ops", description: "Security, backups, and system ops",
agents: [ agents: [
{ id: "infra-sec", name: "Security Lead", role: "Hardening & Audits", description: "UFW, SSH, security audits", status: "idle", project: "infrastructure" }, { id: "infra-sec", name: "Security Lead", role: "Hardening \& Audits", description: "UFW, SSH, security audits", status: "idle", project: "infrastructure" },
{ id: "infra-backup", name: "Backup Manager", role: "Backup & Recovery", description: "Auto backups, cloud sync", status: "idle", project: "infrastructure" }, { id: "infra-ops", name: "Ops Manager", role: "DevOps \& Backups", description: "Deployments, backups, monitoring", status: "idle", project: "infrastructure" },
], ],
}, },
]; ];
export const agentRoles = [ export const agentRoles = [
{ id: "orchestrator", name: "Orchestrator", icon: "👁️" },
{ id: "architect", name: "Architect / PM", icon: "📋" }, { id: "architect", name: "Architect / PM", icon: "📋" },
{ id: "frontend", name: "Frontend Dev", icon: "🎨" }, { id: "frontend", name: "Frontend Dev", icon: "🎨" },
{ id: "backend", name: "Backend Dev", icon: "⚙️" }, { id: "backend", name: "Backend Dev", icon: "⚙️" },
@@ -64,4 +93,7 @@ export const agentRoles = [
{ id: "product", name: "Product Manager", icon: "📦" }, { id: "product", name: "Product Manager", icon: "📦" },
{ id: "security", name: "Security", icon: "🛡️" }, { id: "security", name: "Security", icon: "🛡️" },
{ id: "devops", name: "DevOps", icon: "🚀" }, { id: "devops", name: "DevOps", icon: "🚀" },
{ id: "trader", name: "Trader", icon: "📈" },
{ id: "researcher", name: "Researcher", icon: "🔍" },
{ id: "outreach", name: "Outreach", icon: "📣" },
]; ];
+47 -25
View File
@@ -1,7 +1,7 @@
// Mission Control Task Types // Mission Control Task Types
export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'blocked' | 'paused'; export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'blocked' | 'paused';
export type ProjectType = 'sitemente' | 'holacompi' | 'infrastructure'; export type ProjectType = 'sitemente' | 'holacompi' | 'infrastructure' | 'arabredox' | 'trading';
export interface Task { export interface Task {
id: string; id: string;
@@ -26,43 +26,65 @@ export interface Project {
color: string; color: string;
} }
// V1 SiteMente Checklist // SiteMente & Operations Task List
export const initialTasks: Task[] = [ export const initialTasks: Task[] = [
// === SiteMente v1 === // === SiteMente v1 ===
{ id: 't1', title: 'Pricing + Services section', description: '3 tiers + vertical packs + yearly toggle', status: 'done', priority: 'high', project: 'sitemente', order: 1, completedAt: '2026-02-16' }, { id: 't1', title: 'Pricing + Services section', description: '3 tiers + vertical packs + yearly toggle', status: 'done', priority: 'high', project: 'sitemente', order: 1, completedAt: '2026-02-16' },
{ id: 't2', title: 'Vertical pack cards', description: 'Real Estate, Restaurant, Clinic as distinct upsells', status: 'todo', priority: 'high', project: 'sitemente', order: 2 }, { id: 't2', title: 'Vertical pack cards', description: 'Real Estate, Restaurant, Clinic as distinct upsells', status: 'done', priority: 'high', project: 'sitemente', order: 2, completedAt: '2026-02-17' },
{ id: 't3', title: 'Contact/onboarding form', description: 'Lead capture: name, business type, phone, needs', status: 'todo', priority: 'high', project: 'sitemente', order: 3 }, { id: 't3', title: 'Contact/onboarding form', description: 'Lead capture: name, business type, phone, needs', status: 'todo', priority: 'high', project: 'sitemente', order: 3 },
{ id: 't4', title: 'Real Estate demo page', description: 'Polished vertical demo for realtors', status: 'todo', priority: 'high', project: 'sitemente', order: 4 }, { id: 't4', title: 'Real Estate demo page', description: 'Polished vertical demo for realtors', status: 'done', priority: 'high', project: 'sitemente', order: 4, completedAt: '2026-02-17' },
{ id: 't5', title: 'Restaurant demo page', description: 'Polished vertical demo for restaurants', status: 'todo', priority: 'medium', project: 'sitemente', order: 5 }, { id: 't5', title: 'Restaurant demo page', description: 'Polished vertical demo for restaurants', status: 'done', priority: 'medium', project: 'sitemente', order: 5, completedAt: '2026-02-17' },
{ id: 't6', title: 'Clinic demo page', description: 'Polished vertical demo for clinics', status: 'todo', priority: 'low', project: 'sitemente', order: 6 }, { id: 't6', title: 'Clinic demo page', description: 'Polished vertical demo for clinics', status: 'done', priority: 'low', project: 'sitemente', order: 6, completedAt: '2026-02-17' },
{ id: 't7', title: 'AI Widget live on landing', description: 'Embed widget on main landing page', status: 'todo', priority: 'high', project: 'sitemente', order: 7 }, { id: 't7', title: 'AI Voice Widget - Vapi integration', description: 'Embed voice widget with MiniMax brain', status: 'in_progress', priority: 'high', project: 'sitemente', order: 7 },
{ id: 't8', title: '"How it works" flow', description: '3-step visual flow showing the process', status: 'todo', priority: 'medium', project: 'sitemente', order: 8 }, { id: 't8', title: '"How it works" flow', description: '3-step visual flow showing the process', status: 'todo', priority: 'medium', project: 'sitemente', order: 8 },
{ id: 't9', title: 'FAQ accordion', description: '6-8 key questions for objection handling', status: 'todo', priority: 'medium', project: 'sitemente', order: 9 }, { id: 't9', title: 'FAQ accordion', description: '6-8 key questions for objection handling', status: 'todo', priority: 'medium', project: 'sitemente', order: 9 },
{ id: 't10', title: 'Mobile responsive pass', description: 'Ensure all components work on mobile', status: 'todo', priority: 'high', project: 'sitemente', order: 10 }, { id: 't10', title: 'Mobile responsive pass', description: 'Ensure all components work on mobile', status: 'todo', priority: 'high', project: 'sitemente', order: 10 },
{ id: 't11', title: 'Loading states / transitions', description: 'Add skeleton loaders and smooth transitions', status: 'todo', priority: 'low', project: 'sitemente', order: 11 }, { id: 't11', title: 'SSL + Domain setup', description: 'SiteMente.com with Lets Encrypt SSL', status: 'done', priority: 'critical', project: 'sitemente', order: 11, completedAt: '2026-02-20' },
{ id: 't12', title: 'Meta tags + SEO basics', description: 'Open Graph, Twitter cards, sitemap', status: 'todo', priority: 'medium', project: 'sitemente', order: 12 }, { id: 't12', title: 'Meta tags + SEO basics', description: 'Open Graph, Twitter cards, sitemap', status: 'done', priority: 'medium', project: 'sitemente', order: 12, completedAt: '2026-02-17' },
{ id: 't13', title: 'Identify 2-3 local businesses', description: 'Target list for first pitches', status: 'todo', priority: 'high', project: 'sitemente', order: 13 }, { id: 't13', title: 'Stripe payments', description: 'Payment processing for subscriptions', status: 'done', priority: 'high', project: 'sitemente', order: 13, completedAt: '2026-02-16' },
{ id: 't14', title: '1-pager PDF or demo link', description: 'Leave-behind for prospects', status: 'todo', priority: 'high', project: 'sitemente', order: 14 }, { id: 't14', title: 'CRM + Leads system', description: 'Lead capture and management', status: 'done', priority: 'high', project: 'sitemente', order: 14, completedAt: '2026-02-16' },
{ id: 't15', title: 'First paying client', description: 'Close the first deal', status: 'todo', priority: 'critical', project: 'sitemente', order: 15 }, { id: 't15', title: 'Identify local businesses', description: 'Target list for first pitches', status: 'todo', priority: 'high', project: 'sitemente', order: 15 },
{ id: 't16', title: '1-pager PDF or demo link', description: 'Leave-behind for prospects', status: 'todo', priority: 'high', project: 'sitemente', order: 16 },
{ id: 't17', title: 'First paying client', description: 'Close the first deal', status: 'todo', priority: 'critical', project: 'sitemente', order: 17 },
{ id: 't18', title: 'Voice AI - fix Vapi webhook', description: 'Get Vapi to call /api/site-mente/voice', status: 'todo', priority: 'critical', project: 'sitemente', order: 18 },
// === HolaCompi (paused) === // === HolaCompi (paused) ===
{ id: 'h1', title: 'HolaCompi core concept', description: 'AI ally for immigrants/consumers', status: 'paused', priority: 'medium', project: 'holacompi', order: 100 }, { id: 'h1', title: 'HolaCompi core concept', description: 'AI ally for immigrants/consumers', status: 'paused', priority: 'medium', project: 'holacompi', order: 100 },
{ id: 'h2', title: 'Cross-sell to SiteMente businesses', description: 'Route leads from HolaCompi to SiteMente clients', status: 'paused', priority: 'medium', project: 'holacompi', order: 101 }, { id: 'h2', title: 'Cross-sell to SiteMente businesses', description: 'Route leads to SiteMente clients', status: 'paused', priority: 'medium', project: 'holacompi', order: 101 },
// === Infrastructure & Security === // === Infrastructure ===
{ id: 'i1', title: 'Configure UFW firewall', description: 'Enable UFW, allow SSH (22) and web (80, 443)', status: 'todo', priority: 'high', project: 'infrastructure', order: 200 }, { id: 'i1', title: 'Configure UFW firewall', description: 'Enable UFW, allow SSH and web', status: 'done', priority: 'high', project: 'infrastructure', order: 200, completedAt: '2026-02-13' },
{ id: 'i2', title: 'Restrict port 3000', description: 'Only allow localhost for dev server', status: 'todo', priority: 'high', project: 'infrastructure', order: 201 }, { id: 'i2', title: 'Restrict port 3000', description: 'Only allow localhost for dev', status: 'done', priority: 'high', project: 'infrastructure', order: 201, completedAt: '2026-02-13' },
{ id: 'i3', title: 'Set up SSH key-only auth', description: 'Disable password login, use keys only', status: 'todo', priority: 'high', project: 'infrastructure', order: 202 }, { id: 'i3', title: 'Set up SSH key-only auth', description: 'Disable password login', status: 'todo', priority: 'high', project: 'infrastructure', order: 202 },
{ id: 'i4', title: 'Enable automatic security updates', description: 'Configure unattended-upgrades', status: 'todo', priority: 'medium', project: 'infrastructure', order: 203 }, { id: 'i4', title: 'Auto security updates', description: 'Configure unattended-upgrades', status: 'done', priority: 'medium', project: 'infrastructure', order: 203, completedAt: '2026-02-13' },
{ id: 'i5', title: 'Set up automated backups', description: 'Daily backup to local + cloud (Dropbox/Google Drive)', status: 'todo', priority: 'high', project: 'infrastructure', order: 204 }, { id: 'i5', title: 'Set up backups', description: 'Daily backup to local + cloud', status: 'todo', priority: 'high', project: 'infrastructure', order: 204 },
{ id: 'i6', title: 'Configure Brave Search API', description: 'Enable web research for AI-powered searches', status: 'todo', priority: 'medium', project: 'infrastructure', order: 205 }, { id: 'i6', title: 'Morning Brief cron', description: 'Daily 6am CET to Telegram', status: 'done', priority: 'high', project: 'infrastructure', order: 205, completedAt: '2026-02-20' },
{ id: 'i7', title: 'Configure Weather API', description: 'OpenWeatherMap for morning brief', status: 'todo', priority: 'medium', project: 'infrastructure', order: 206 }, { id: 'i7', title: 'Install trading skills', description: 'Coingecko, eth-readonly, x-twitter', status: 'done', priority: 'medium', project: 'infrastructure', order: 206, completedAt: '2026-02-20' },
{ id: 'i8', title: 'Configure News API', description: 'AI and market news for morning brief', status: 'todo', priority: 'medium', project: 'infrastructure', order: 207 }, { id: 'i8', title: 'Security health check', description: 'Run full audit and apply fixes', status: 'todo', priority: 'high', project: 'infrastructure', order: 207 },
{ id: 'i9', title: 'Connect Things 3 or Todoist', description: 'Import tasks for morning brief', status: 'todo', priority: 'medium', project: 'infrastructure', order: 208 },
{ id: 'i10', title: 'Hardening health check', description: 'Run full security audit and apply fixes', status: 'todo', priority: 'high', project: 'infrastructure', order: 209 }, // === ArabRedox ===
{ id: 'a1', title: 'Brand colors from logo', description: 'Extract HEX codes from logo', status: 'done', priority: 'critical', project: 'arabredox', order: 300, completedAt: '2026-02-17' },
{ id: 'a2', title: 'Upload logo to Hostinger', description: 'Upload to Horizons AI builder', status: 'done', priority: 'critical', project: 'arabredox', order: 301, completedAt: '2026-02-17' },
{ id: 'a3', title: 'Apply brand colors', description: 'Set in Hostinger Styles', status: 'done', priority: 'high', project: 'arabredox', order: 302, completedAt: '2026-02-17' },
{ id: 'a4', title: 'Hero section', description: 'Future of Health messaging', status: 'done', priority: 'high', project: 'arabredox', order: 303, completedAt: '2026-02-17' },
{ id: 'a5', title: 'Science section', description: 'Arabic/English Redox explanation', status: 'done', priority: 'high', project: 'arabredox', order: 304, completedAt: '2026-02-17' },
{ id: 'a6', title: 'Opportunity section', description: 'Financial Freedom messaging', status: 'done', priority: 'high', project: 'arabredox', order: 305, completedAt: '2026-02-17' },
{ id: 'a7', title: 'Products section', description: 'Display Redox products', status: 'done', priority: 'medium', project: 'arabredox', order: 306, completedAt: '2026-02-17' },
{ id: 'a8', title: 'Testimonials section', description: 'Customer stories', status: 'done', priority: 'medium', project: 'arabredox', order: 307, completedAt: '2026-02-17' },
{ id: 'a9', title: 'Genesis story section', description: 'People over pharma', status: 'done', priority: 'medium', project: 'arabredox', order: 308, completedAt: '2026-02-17' },
{ id: 'a10', title: 'Global reach section', description: '29+ countries', status: 'done', priority: 'medium', project: 'arabredox', order: 309, completedAt: '2026-02-17' },
{ id: 'a11', title: 'Contact/Join pages', description: 'Join Mission + Shop Now', status: 'done', priority: 'high', project: 'arabredox', order: 310, completedAt: '2026-02-17' },
{ id: 'a12', title: 'Point domain', description: 'ArabRedox.com to Hostinger', status: 'done', priority: 'high', project: 'arabredox', order: 311, completedAt: '2026-02-17' },
{ id: 'a13', title: 'Launch & test', description: 'Final QA and go live', status: 'done', priority: 'critical', project: 'arabredox', order: 312, completedAt: '2026-02-17' },
// === Trading ===
{ id: 'tr1', title: 'Trading infrastructure', description: 'API keys, monitoring, alerts', status: 'todo', priority: 'high', project: 'trading', order: 400 },
{ id: 'tr2', title: 'Risk parameters', description: 'Position size, stop loss, take profit', status: 'todo', priority: 'critical', project: 'trading', order: 401 },
{ id: 'tr3', title: 'Learn DopeTrades strategy', description: 'Watch videos, extract patterns', status: 'todo', priority: 'high', project: 'trading', order: 402 },
{ id: 'tr4', title: 'Paper trading phase', description: 'Test without real money', status: 'todo', priority: 'high', project: 'trading', order: 403 },
{ id: 'tr5', title: 'Live trading', description: 'Start with minimal SOL', status: 'todo', priority: 'medium', project: 'trading', order: 404 },
]; ];
// Lightweight project summary (used in UI, not stored)
export interface ProjectSummary { export interface ProjectSummary {
id: ProjectType; id: ProjectType;
name: string; name: string;
+15
View File
@@ -109,6 +109,21 @@ export function MorningBriefProvider({ children }: { children: ReactNode }) {
"Crypto Treasury Meeting (2:00 PM ET)", "Crypto Treasury Meeting (2:00 PM ET)",
], ],
}, },
// Load OpenClaw use cases from memory/cron
openclowUseCases: (() => {
try {
const oc = localStorage.getItem("sitemente:openclaw-usecases");
if (oc) return JSON.parse(oc);
} catch (e) {}
return {
topUseCases: ["PR Review Automation", "Browser Shopping", "Wine Cellar CSV"],
skillIdeas: [
{ name: "Auto-Shop", selected: false },
{ name: "Wine Cellar", selected: false },
{ name: "Health Dashboard", selected: false }
],
};
})(),
}; };
setBriefs((prev) => { setBriefs((prev) => {
+8 -1
View File
@@ -40,6 +40,12 @@ export interface MorningBrief {
topNews: string[]; topNews: string[];
events: string[]; events: string[];
}; };
// OpenClaw Use Cases (from daily research)
openclowUseCases?: {
topUseCases: string[];
skillIdeas: { name: string; selected: boolean }[];
};
} }
export const defaultBrief: MorningBrief = { export const defaultBrief: MorningBrief = {
@@ -55,5 +61,6 @@ export const defaultBrief: MorningBrief = {
sentiment: 'neutral', sentiment: 'neutral',
topNews: [], topNews: [],
events: [], events: [],
} },
openclowUseCases: { topUseCases: [], skillIdeas: [] },
}; };
+43
View File
@@ -8,6 +8,49 @@ const nextConfig: NextConfig = {
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
}, },
// Security headers
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
{
key: "Content-Security-Policy",
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; frame-src https://www.google.com https://www.youtube.com; media-src 'self' blob:;",
},
],
},
];
},
// Prevent exposure of sensitive files
async rewrites() {
return [
{
source: "/:path*",
destination: "/:path*",
},
];
},
}; };
export default nextConfig; export default nextConfig;
+484 -3
View File
@@ -10,15 +10,21 @@
"dependencies": { "dependencies": {
"@google/genai": "^1.39.0", "@google/genai": "^1.39.0",
"@google/generative-ai": "^0.24.1", "@google/generative-ai": "^0.24.1",
"@stripe/stripe-js": "^8.7.0",
"@vapi-ai/web": "^2.5.2",
"chartjs-plugin-zoom": "^2.2.0",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"lightweight-charts": "^5.1.0",
"next": "^15.5.3", "next": "^15.5.3",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1" "react-dom": "^19.1.1",
"stripe": "^17.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.1.12", "@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"dotenv": "^17.3.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.9.2" "typescript": "^5.9.2"
@@ -37,6 +43,31 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@daily-co/daily-js": {
"version": "0.85.0",
"resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.85.0.tgz",
"integrity": "sha512-lpl111ZWNTUWDnwYcPuNi9PGJPbLCeCw6LzmEY40nG0hv1jg5JLVW8Rq3Cj/+lOCP6W6h4PXm211ss0FFnxITQ==",
"license": "BSD-2-Clause",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@sentry/browser": "^8.33.1",
"bowser": "^2.8.1",
"dequal": "^2.0.3",
"events": "^3.1.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
@@ -600,6 +631,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT",
"peer": true
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.5.11", "version": "15.5.11",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.11.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.11.tgz",
@@ -846,6 +884,90 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@sentry-internal/browser-utils": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz",
"integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.0.tgz",
"integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.0.tgz",
"integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.55.0",
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz",
"integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "8.55.0",
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/browser": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.0.tgz",
"integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.55.0",
"@sentry-internal/feedback": "8.55.0",
"@sentry-internal/replay": "8.55.0",
"@sentry-internal/replay-canvas": "8.55.0",
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/core": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz",
"integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@stripe/stripe-js": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.7.0.tgz",
"integrity": "sha512-tNUerSstwNC1KuHgX4CASGO0Md3CB26IJzSXmVlSuFvhsBP4ZaEPpY4jxWOn9tfdDscuVT4Kqb8cZ2o9nLCgRQ==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -855,6 +977,12 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@types/hammerjs": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.2.0", "version": "25.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
@@ -884,6 +1012,19 @@
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@vapi-ai/web": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@vapi-ai/web/-/web-2.5.2.tgz",
"integrity": "sha512-mT4DjApi0/0+EK77h2xOLq3qVBa7rv3JNQ+gFWuhFE4YdGYxI51+fn8bQI9N535+zU/Z4jFKXotdBHQJ3filHA==",
"license": "MIT",
"dependencies": {
"@daily-co/daily-js": "^0.85.0",
"events": "^3.3.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/agent-base": { "node_modules/agent-base": {
"version": "7.1.4", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1040,6 +1181,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/bowser": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -1102,6 +1249,35 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camelcase-css": { "node_modules/camelcase-css": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -1132,6 +1308,32 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chartjs-plugin-zoom": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz",
"integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==",
"license": "MIT",
"dependencies": {
"@types/hammerjs": "^2.0.45",
"hammerjs": "^2.0.8"
},
"peerDependencies": {
"chart.js": ">=3.2.0"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -1264,6 +1466,15 @@
} }
} }
}, },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1288,6 +1499,33 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1316,6 +1554,36 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1326,12 +1594,27 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/extend": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/fancy-canvas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz",
"integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==",
"license": "MIT"
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -1496,7 +1779,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -1531,6 +1813,43 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "10.5.0", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -1591,6 +1910,18 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gtoken": { "node_modules/gtoken": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
@@ -1604,11 +1935,31 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -1762,6 +2113,15 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/lightweight-charts": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-5.1.0.tgz",
"integrity": "sha512-jEAYR4ODYeyNZcWUigsoLTl52rbPmgXnvd5FLIv/ZoA/2sSDw63YKnef8n4yhzum7W926yHeFwlm7ididKb7YQ==",
"license": "Apache-2.0",
"dependencies": {
"fancy-canvas": "2.1.0"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -1788,6 +2148,15 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2042,6 +2411,18 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -2312,6 +2693,21 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -2553,6 +2949,78 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -2670,6 +3138,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/stripe": {
"version": "17.5.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-17.5.0.tgz",
"integrity": "sha512-kcyeAkDFjGsVl17FqnG7q/+xIjt0ZjOo9Dm+q8deAvs2Xe4iAHrhxyoP4etUVFc+/LZJANjIPVR+ZOnt9hr/Ug==",
"license": "MIT",
"dependencies": {
"@types/node": ">=8.1.0",
"qs": "^6.11.0"
},
"engines": {
"node": ">=12.*"
}
},
"node_modules/styled-jsx": { "node_modules/styled-jsx": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+2
View File
@@ -14,7 +14,9 @@
"@google/generative-ai": "^0.24.1", "@google/generative-ai": "^0.24.1",
"@stripe/stripe-js": "^8.7.0", "@stripe/stripe-js": "^8.7.0",
"@vapi-ai/web": "^2.5.2", "@vapi-ai/web": "^2.5.2",
"chartjs-plugin-zoom": "^2.2.0",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"lightweight-charts": "^5.1.0",
"next": "^15.5.3", "next": "^15.5.3",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
+18
View File
@@ -0,0 +1,18 @@
[
{
"id": "1771652132836",
"task": "Restaurant demo page",
"command": "already done. so do nothing here",
"action": "continue-task",
"createdAt": "2026-02-21T05:35:32.836Z",
"status": "pending"
},
{
"id": "1771654840702",
"task": "Restaurant demo page",
"command": "we already finished that",
"action": "continue-task",
"createdAt": "2026-02-21T06:20:40.702Z",
"status": "pending"
}
]
+15
View File
@@ -0,0 +1,15 @@
# Robots.txt for SiteMente
User-agent: *
Allow: /
# Disallow admin and dashboard areas
Disallow: /dashboard
Disallow: /leads
Disallow: /mission-control
Disallow: /api/
Disallow: /admin
Disallow: /_
# Sitemap
Sitemap: https://sitemente.com/sitemap.xml
+70
View File
@@ -0,0 +1,70 @@
{
"BTC": {
"thought": "Waiting for pullback to support zone $65,500-$66,000",
"trend": "uptrend",
"phase": "distribution",
"key_level": 65750,
"bias": "bullish",
"confidence": 6,
"reason": "20 green candles — too extended, waiting for pop at support",
"next_action": "Wait for price to reach $65,500-$66,000 then watch for pop on 15min",
"updated_at": "2026-02-21T17:00:00Z",
"bias_history": [
{ "time": 1771632000000, "bias": "bullish", "price": 66461 },
{ "time": 1771200000000, "bias": "neutral", "price": 64000 }
],
"support_zones": [
{ "level": 66000, "strength": 3 },
{ "level": 65500, "strength": 2 },
{ "level": 64000, "strength": 2 }
],
"resistance_zones": [
{ "level": 68000, "strength": 3 },
{ "level": 70000, "strength": 2 }
]
},
"SOL": {
"thought": "Choppy price action, no clear trend",
"trend": "sideways",
"phase": "accumulation",
"key_level": 83,
"bias": "neutral",
"confidence": 4,
"reason": "Mixed candles, waiting for direction",
"next_action": "Wait for breakout above $88 or drop to $82 support",
"updated_at": "2026-02-21T17:00:00Z",
"bias_history": [
{ "time": 1771632000000, "bias": "neutral", "price": 84.35 }
],
"support_zones": [
{ "level": 82, "strength": 2 },
{ "level": 80, "strength": 3 }
],
"resistance_zones": [
{ "level": 88, "strength": 2 },
{ "level": 91, "strength": 3 }
]
},
"ETH": {
"thought": "Sideways near resistance, no entry yet",
"trend": "sideways",
"phase": "distribution",
"key_level": 1925,
"bias": "neutral",
"confidence": 4,
"reason": "Near resistance at $1,980, no pop setup visible",
"next_action": "Wait for pullback to $1,900-$1,950 support zone",
"updated_at": "2026-02-21T17:00:00Z",
"bias_history": [
{ "time": 1771632000000, "bias": "neutral", "price": 1959 }
],
"support_zones": [
{ "level": 1950, "strength": 3 },
{ "level": 1900, "strength": 2 }
],
"resistance_zones": [
{ "level": 2000, "strength": 3 },
{ "level": 2100, "strength": 2 }
]
}
}
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd /root/.openclaw/workspace/SiteMente
exec npx next dev -p 1284
+39
View File
@@ -0,0 +1,39 @@
[
{
"id": "1771654840335",
"task": "Restaurant demo page",
"command": "we already finished that",
"reply": "",
"createdAt": "2026-02-21T06:20:40.335Z",
"status": "completed"
},
{
"id": "1771710468432",
"task": "Quick message - trading",
"command": "what u think about the market now? ",
"project": "trading",
"reply": "",
"createdAt": "2026-02-21T21:47:48.432Z",
"status": "completed"
},
{
"id": "1771710773116",
"task": "Quick message - trading",
"command": "what u think about the market now?",
"project": "trading",
"reply": "",
"createdAt": "2026-02-21T21:52:53.116Z",
"status": "completed"
},
{
"id": "1771711674884",
"task": "Quick message - trading",
"command": "would u long SOL now?",
"project": "trading",
"reply": "",
"action": "quick-message",
"createdAt": "2026-02-21T22:07:54.884Z",
"status": "pending",
"notified": false
}
]
+13
View File
@@ -0,0 +1,13 @@
import { generateSiteMenteText } from './lib/ai/geminiClient';
async function test() {
try {
const result = await generateSiteMenteText([{role: 'user', content: 'Hello'}]);
console.log('Success:', JSON.stringify(result));
} catch (e: any) {
console.error('Error:', e.message);
console.error(e);
}
}
test();
+46
View File
@@ -0,0 +1,46 @@
[
{
"id": "dopetrades",
"name": "DopeTrades",
"status": "learning",
"framesAnalyzed": 382,
"patterns": [
"Double Top/Bottom",
"Head & Shoulders",
"Triangles",
"Flags",
"Wedges"
],
"entryRules": [
"Identify clear structure (swing highs/lows)",
"Wait for retest of level",
"Confirm momentum in desired direction",
"Higher timeframe alignment",
"Entry on break of structure or retest",
"Confirmation candle required"
],
"exitRules": [
"Stop below recent swing low (long)",
"Take profit minimum 2:1",
"Scale 50% at 1:1",
"Trailing stop after 1:1 achieved",
"Never move stop loss further"
],
"indicators": [
"9 EMA (short term)",
"20 EMA (medium term)",
"50 SMA (trend filter)",
"RSI 14 (momentum)",
"Volume profile"
],
"riskParams": [
"Max 2% risk per trade",
"Max 3 concurrent trades",
"6% daily max loss",
"10% weekly max loss",
"Stop after 3 losses"
],
"timeframe": "Multi: 4H/Daily trend, 1H structure, 15min entries",
"notes": "Frame analysis: 3% bullish, 7% bearish, 90% neutral. Dark charts confirmed."
}
]
File diff suppressed because one or more lines are too long