Add LICENSE, README, and Docs tab to Mission Control
This commit is contained in:
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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*
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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* 👁️
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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'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'll receive a confirmation email</li>
|
||||||
|
<li>✓ Our team will contact you within 24 hours</li>
|
||||||
|
<li>✓ We'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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 tú 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>
|
||||||
|
|||||||
@@ -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'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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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.*
|
||||||
@@ -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 |
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 | - |
|
||||||
@@ -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)
|
||||||
@@ -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}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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 |
|
||||||
@@ -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 |
|
||||||
|
|---|------|---------|-------|---------|-------|----------|
|
||||||
|
```
|
||||||
@@ -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 |
|
||||||
@@ -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");
|
||||||
|
};
|
||||||
@@ -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
@@ -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: "📣" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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: [] },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Generated
+484
-3
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /root/.openclaw/workspace/SiteMente
|
||||||
|
exec npx next dev -p 1284
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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();
|
||||||
@@ -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
Reference in New Issue
Block a user