SiteMente - AI-Powered Lead Generation Platform
Features: - Mission Control dashboard - HP Submissions tracking - AI Agents integration - Lead management CRM - Marketing email templates - Chrome extension support Tech: Next.js, TypeScript, Tailwind CSS, MySQL
This commit is contained in:
+19
-16
@@ -1,31 +1,34 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Next.js
|
|
||||||
.next/
|
|
||||||
out/
|
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.*.local
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Vite
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Build
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# Debug
|
# System
|
||||||
npm-debug.log*
|
.DS_Store
|
||||||
yarn-debug.log*
|
Thumbs.db
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
# OS
|
# Secrets - NEVER COMMIT
|
||||||
.DS_Store
|
*.pem
|
||||||
Thumbs.db
|
*.key
|
||||||
|
credentials.json
|
||||||
|
secrets.json
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { text } = body;
|
||||||
|
|
||||||
|
const prompt = `You are Cleopatra, a professional Spanish-speaking sales agent. Keep responses SHORT (1-2 sentences), friendly, in Spanish.
|
||||||
|
|
||||||
|
User: ${text}
|
||||||
|
|
||||||
|
Response in Spanish:`;
|
||||||
|
|
||||||
|
const res = await fetch("http://127.0.0.1:11434/api/generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
signal: AbortSignal.timeout(60000),
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "phi3:mini",
|
||||||
|
prompt: prompt,
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const reply = (data.response || "¡Hola! Estoy aquí.").substring(0, 200);
|
||||||
|
|
||||||
|
return NextResponse.json({ response: reply });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({
|
||||||
|
response: "Tengo problemas para conectar. Intenta de nuevo."
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
// Get all Hookd items
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const action = url.searchParams.get('action') || 'list'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const possiblePaths = [
|
||||||
|
path.join(process.cwd(), '..', 'chrome-extensions', 'hookd', 'data.json'),
|
||||||
|
'/var/www/hostpioneers.com/public_html/contacts.json',
|
||||||
|
]
|
||||||
|
|
||||||
|
let data: any[] = []
|
||||||
|
|
||||||
|
// Try to read from Hookd storage
|
||||||
|
for (const filePath of possiblePaths) {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
data = JSON.parse(content)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'stats') {
|
||||||
|
return NextResponse.json({
|
||||||
|
total: data.length,
|
||||||
|
byCategory: data.reduce((acc: any, item) => {
|
||||||
|
const cat = item.category || 'Uncategorized'
|
||||||
|
acc[cat] = (acc[cat] || 0) + 1
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
aiProcessed: data.filter((i: any) => i.aiProcessed).length,
|
||||||
|
saved: data.filter((i: any) => i.saved).length,
|
||||||
|
recent: data.slice(0, 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ items: data })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Hookd API error:', error)
|
||||||
|
return NextResponse.json({ items: [], error: 'Failed to fetch' }, { status: 200 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process items with AI
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { items, instruction } = body
|
||||||
|
|
||||||
|
// This would call MiniMax API to process items
|
||||||
|
// For now, return mock AI analysis
|
||||||
|
|
||||||
|
const results = items.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
category: guessCategory(item),
|
||||||
|
score: guessScore(item),
|
||||||
|
tags: guessTags(item),
|
||||||
|
summary: item.content?.slice(0, 100) + '...'
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({ results })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI processing error:', error)
|
||||||
|
return NextResponse.json({ error: 'Processing failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessCategory(item: any): string {
|
||||||
|
const content = (item.content || '').toLowerCase()
|
||||||
|
const link = (item.link || '').toLowerCase()
|
||||||
|
|
||||||
|
if (content.includes('ai') || content.includes('gpt') || content.includes('claude') ||
|
||||||
|
content.includes('openai') || content.includes('anthropic') ||
|
||||||
|
link.includes('github.com') || link.includes('huggingface')) {
|
||||||
|
return 'AI'
|
||||||
|
}
|
||||||
|
if (content.includes('news') || content.includes('breaking') || content.includes('just in')) {
|
||||||
|
return 'News'
|
||||||
|
}
|
||||||
|
if (content.includes('idea') || content.includes('thought') || content.includes('what if')) {
|
||||||
|
return 'Ideas'
|
||||||
|
}
|
||||||
|
if (content.includes('lol') || content.includes('meme') || content.includes('funny') || content.includes('😂')) {
|
||||||
|
return 'Memes'
|
||||||
|
}
|
||||||
|
return 'Other'
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessScore(item: any): number {
|
||||||
|
const content = (item.content || '').toLowerCase()
|
||||||
|
let score = 5
|
||||||
|
|
||||||
|
if (item.link) score += 1
|
||||||
|
if (content.length > 100) score += 1
|
||||||
|
if (content.includes('github')) score += 1
|
||||||
|
if (content.includes('ai') || content.includes('gpt')) score += 2
|
||||||
|
|
||||||
|
return Math.min(10, score)
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessTags(item: any): string[] {
|
||||||
|
const tags: string[] = []
|
||||||
|
const content = (item.content || '').toLowerCase()
|
||||||
|
|
||||||
|
if (content.includes('ai')) tags.push('AI')
|
||||||
|
if (content.includes('tool')) tags.push('Tools')
|
||||||
|
if (content.includes('news')) tags.push('News')
|
||||||
|
if (content.includes('tutorial')) tags.push('Tutorial')
|
||||||
|
if (content.includes('github')) tags.push('Code')
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Try multiple possible paths
|
||||||
|
const possiblePaths = [
|
||||||
|
path.join(process.cwd(), '..', 'hostpioneers.com', 'public_html', 'contacts.json'),
|
||||||
|
path.join(process.cwd(), 'public', '..', 'contacts.json'),
|
||||||
|
'/var/www/hostpioneers.com/public_html/contacts.json',
|
||||||
|
]
|
||||||
|
|
||||||
|
let data = '[]'
|
||||||
|
for (const filePath of possiblePaths) {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
data = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissions = JSON.parse(data || '[]')
|
||||||
|
return NextResponse.json({ submissions: submissions.reverse() })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
return NextResponse.json({ submissions: [], error: 'Failed to fetch' }, { status: 200 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
async function getPool() {
|
||||||
|
return mysql.createPool({
|
||||||
|
host: 'localhost',
|
||||||
|
user: 'sitemente',
|
||||||
|
password: 'pa0ndoQRPDYBHzTXX3rbWd6E',
|
||||||
|
database: 'sitemente',
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const segment = searchParams.get('segment')
|
||||||
|
const dormancy = searchParams.get('dormancy')
|
||||||
|
const search = searchParams.get('search')
|
||||||
|
const sortBy = searchParams.get('sortBy') || 'total_spent'
|
||||||
|
const sortOrder = searchParams.get('sortOrder') || 'DESC'
|
||||||
|
const page = parseInt(searchParams.get('page') || '1')
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '100')
|
||||||
|
const offset = (page - 1) * limit
|
||||||
|
|
||||||
|
// Validate sortBy to prevent SQL injection
|
||||||
|
const allowedSortColumns = ['firstname', 'lastname', 'email', 'company', 'country', 'total_spent', 'segment', 'dormancy_flag', 'id']
|
||||||
|
const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'total_spent'
|
||||||
|
const safeSortOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pool = await getPool()
|
||||||
|
|
||||||
|
let countQuery = 'SELECT COUNT(*) as total FROM hostpioneers_leads WHERE 1=1'
|
||||||
|
let query = 'SELECT * FROM hostpioneers_leads WHERE 1=1'
|
||||||
|
const params: any[] = []
|
||||||
|
|
||||||
|
if (segment && segment !== 'all') {
|
||||||
|
countQuery += ' AND segment = ?'
|
||||||
|
query += ' AND segment = ?'
|
||||||
|
params.push(segment)
|
||||||
|
}
|
||||||
|
if (dormancy && dormancy !== 'all') {
|
||||||
|
countQuery += ' AND dormancy_flag = ?'
|
||||||
|
query += ' AND dormancy_flag = ?'
|
||||||
|
params.push(dormancy)
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
const searchClause = ' AND (firstname LIKE ? OR lastname LIKE ? OR email LIKE ? OR company LIKE ? OR phone LIKE ? OR country LIKE ?)'
|
||||||
|
countQuery += searchClause
|
||||||
|
query += searchClause
|
||||||
|
const searchTerm = `%${search}%`
|
||||||
|
params.push(searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [countResult] = await pool.query(countQuery, params)
|
||||||
|
const total = (countResult as any[])[0].total
|
||||||
|
|
||||||
|
query += ` ORDER BY ${safeSortBy} ${safeSortOrder} LIMIT ? OFFSET ?`
|
||||||
|
const [rows] = await pool.query(query, [...params, limit, offset])
|
||||||
|
|
||||||
|
await pool.end()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
leads: rows,
|
||||||
|
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
|
||||||
|
const dbConfig = {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
user: process.env.DB_USER || 'sitemente',
|
||||||
|
password: process.env.DB_PASS || 'SiteMente2026!',
|
||||||
|
database: process.env.DB_NAME || 'sitemente'
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
conn = await mysql.createConnection(dbConfig);
|
||||||
|
const [rows] = await conn.execute('SELECT * FROM research ORDER BY created_at DESC');
|
||||||
|
return NextResponse.json(rows);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json([]);
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
conn = await mysql.createConnection(dbConfig);
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
'INSERT INTO research (name, query, results_count, summary, results, created_at) VALUES (?, ?, ?, ?, ?, NOW())',
|
||||||
|
[body.name, body.query, body.results_count || 0, body.summary || '', JSON.stringify(body.results || [])]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [rows] = await conn.execute('SELECT * FROM research ORDER BY created_at DESC');
|
||||||
|
return NextResponse.json(rows);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ error: 'Failed to save research' }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
let conn;
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const id = searchParams.get('id');
|
||||||
|
|
||||||
|
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
||||||
|
|
||||||
|
conn = await mysql.createConnection(dbConfig);
|
||||||
|
await conn.execute('DELETE FROM research WHERE id = ?', [id]);
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return NextResponse.json({ error: 'Failed to delete' }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
if (conn) conn.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export default function VoiceWidget() {
|
||||||
|
const [isListening, setIsListening] = useState(false);
|
||||||
|
const [transcript, setTranscript] = useState("");
|
||||||
|
const [response, setResponse] = useState("");
|
||||||
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const recognitionRef = useRef<any>(null);
|
||||||
|
const synthRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
||||||
|
const synth = window.speechSynthesis;
|
||||||
|
|
||||||
|
if (SpeechRecognition) {
|
||||||
|
recognitionRef.current = new SpeechRecognition();
|
||||||
|
recognitionRef.current.continuous = false;
|
||||||
|
recognitionRef.current.interimResults = true;
|
||||||
|
recognitionRef.current.lang = "es-ES";
|
||||||
|
|
||||||
|
recognitionRef.current.onresult = (event: any) => {
|
||||||
|
const last = event.results.length - 1;
|
||||||
|
const text = event.results[last][0].transcript;
|
||||||
|
setTranscript(text);
|
||||||
|
|
||||||
|
if (event.results[last].isFinal) {
|
||||||
|
handleSend(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognitionRef.current.onend = () => {
|
||||||
|
setIsListening(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (synth) {
|
||||||
|
synthRef.current = synth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (recognitionRef.current) recognitionRef.current.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startListening = () => {
|
||||||
|
if (recognitionRef.current && !isListening) {
|
||||||
|
setTranscript("");
|
||||||
|
recognitionRef.current.start();
|
||||||
|
setIsListening(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopListening = () => {
|
||||||
|
if (recognitionRef.current && isListening) {
|
||||||
|
recognitionRef.current.stop();
|
||||||
|
setIsListening(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = async (text: string) => {
|
||||||
|
if (!text.trim()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setResponse("Pensando...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ text })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const reply = (data.response || "¡Hola! Estoy aquí. ¿En qué puedo ayudarte?").substring(0, 200);
|
||||||
|
|
||||||
|
setResponse(reply);
|
||||||
|
speak(reply);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setResponse("Tengo problemas para conectar. Intenta de nuevo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const speak = (text: string) => {
|
||||||
|
if (!synthRef.current) return;
|
||||||
|
|
||||||
|
synthRef.current.cancel();
|
||||||
|
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
utterance.lang = "es-ES";
|
||||||
|
utterance.rate = 0.85;
|
||||||
|
utterance.pitch = 0.9;
|
||||||
|
|
||||||
|
const voices = synthRef.current.getVoices();
|
||||||
|
const spanishVoice = voices.find((v: any) => v.lang.includes("es"));
|
||||||
|
if (spanishVoice) utterance.voice = spanishVoice;
|
||||||
|
|
||||||
|
utterance.onstart = () => setIsSpeaking(true);
|
||||||
|
utterance.onend = () => setIsSpeaking(false);
|
||||||
|
utterance.onerror = () => setIsSpeaking(false);
|
||||||
|
|
||||||
|
synthRef.current.speak(utterance);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-purple-900 to-indigo-900 p-6 rounded-2xl shadow-2xl max-w-md mx-auto">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="text-4xl mb-2">👑</div>
|
||||||
|
<h3 className="text-xl font-bold text-white">Cleopatra Voice</h3>
|
||||||
|
<p className="text-purple-200 text-sm">Asistente de Ventas IA</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className={`px-4 py-2 rounded-full text-sm ${
|
||||||
|
isListening ? "bg-red-500 animate-pulse" :
|
||||||
|
isSpeaking ? "bg-green-500 animate-pulse" :
|
||||||
|
isLoading ? "bg-yellow-500" :
|
||||||
|
"bg-gray-600"
|
||||||
|
} text-white`}>
|
||||||
|
{isListening ? "🎤 Escuchando..." :
|
||||||
|
isSpeaking ? "🔊 Hablando..." :
|
||||||
|
isLoading ? "⏳ Pensando..." :
|
||||||
|
"💬 Lista"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-16 flex items-center justify-center mb-4 gap-1">
|
||||||
|
{[...Array(12)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`w-1 bg-purple-400 rounded-full transition-all duration-100 ${
|
||||||
|
isListening || isSpeaking ? "animate-pulse" : ""
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
height: isListening || isSpeaking ? `${20 + Math.random() * 40}px` : "8px",
|
||||||
|
animationDelay: `${i * 50}ms`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{transcript && (
|
||||||
|
<div className="bg-black/30 rounded-lg p-3 mb-3">
|
||||||
|
<p className="text-xs text-purple-300">Dijiste:</p>
|
||||||
|
<p className="text-white">{transcript}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{response && (
|
||||||
|
<div className="bg-purple-800/50 rounded-lg p-3 mb-4">
|
||||||
|
<p className="text-xs text-green-300">Cleopatra:</p>
|
||||||
|
<p className="text-white">{response}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={isListening ? stopListening : startListening}
|
||||||
|
className={`w-16 h-16 rounded-full flex items-center justify-center text-2xl transition-all ${
|
||||||
|
isListening
|
||||||
|
? "bg-red-500 hover:bg-red-600 animate-pulse"
|
||||||
|
: "bg-purple-500 hover:bg-purple-600"
|
||||||
|
} text-white shadow-lg`}
|
||||||
|
>
|
||||||
|
{isListening ? "⏹" : "🎤"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => speak(response || "¡Hola! Soy Cleopatra. ¿En qué puedo ayudarte?")}
|
||||||
|
disabled={isSpeaking || !response}
|
||||||
|
className="w-12 h-12 rounded-full bg-blue-500 hover:bg-blue-600 disabled:opacity-50 flex items-center justify-center text-xl"
|
||||||
|
>
|
||||||
|
🔊
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-xs text-purple-300 mt-4">
|
||||||
|
{isListening ? "Habla ahora..." : "Toca el micrófono para empezar"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import VoiceWidget from "../../components/VoiceWidget";
|
||||||
|
|
||||||
|
export default function DemoPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-black flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">
|
||||||
|
🏆 SiteMente AI Demo
|
||||||
|
</h1>
|
||||||
|
<p className="text-purple-200">
|
||||||
|
Meet Cleopatra - Your AI Voice Assistant
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VoiceWidget />
|
||||||
|
|
||||||
|
<div className="mt-8 text-center">
|
||||||
|
<p className="text-sm text-purple-300">
|
||||||
|
Powered by Ollama (local AI)
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-purple-400 mt-2">
|
||||||
|
Voice processing happens locally - no external APIs!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
export default function HookdPage() {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
|
||||||
|
color: '#fff',
|
||||||
|
padding: '2rem',
|
||||||
|
fontFamily: 'system-ui, sans-serif'
|
||||||
|
}}>
|
||||||
|
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
||||||
|
<div style={{ fontSize: '64px', marginBottom: '1rem' }}>📌</div>
|
||||||
|
<h1 style={{ fontSize: '2.5rem', marginBottom: '0.5rem' }}>Hookd</h1>
|
||||||
|
<p style={{ color: '#94a3b8', fontSize: '1.1rem' }}>
|
||||||
|
Save, organize and AI-sort your X content
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(79, 70, 229, 0.1)',
|
||||||
|
border: '1px solid rgba(79, 70, 229, 0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
marginBottom: '2rem',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<p style={{ marginBottom: '1rem' }}>Download the Hookd Chrome Extension</p>
|
||||||
|
<a
|
||||||
|
href="/hookd.zip"
|
||||||
|
download="hookd.zip"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
background: 'linear-gradient(135deg, #4F46E5, #7C3AED)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '12px 32px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '1rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⬇️ Download Hookd.zip
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{ marginBottom: '1rem' }}>Features</h2>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1rem' }}>
|
||||||
|
{[
|
||||||
|
{ icon: '📥', title: 'DM Organizer', desc: 'Extract and categorize your X DMs' },
|
||||||
|
{ icon: '🐦', title: 'Feed Saver', desc: 'Checkmark posts to save in bulk' },
|
||||||
|
{ icon: '🤖', title: 'AI Processing', desc: 'Connect to OpenClaw for auto-categorization' },
|
||||||
|
{ icon: '👥', title: 'Contact Lists', desc: 'Share to Dev Friends, Meme Buddies & more' },
|
||||||
|
].map((f, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1rem'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '1.5rem', marginBottom: '0.5rem' }}>{f.icon}</div>
|
||||||
|
<h3 style={{ fontSize: '0.9rem', marginBottom: '0.25rem' }}>{f.title}</h3>
|
||||||
|
<p style={{ color: '#94a3b8', fontSize: '0.8rem' }}>{f.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{ marginTop: '2rem', marginBottom: '1rem' }}>Installation</h2>
|
||||||
|
<ol style={{ color: '#94a3b8', paddingLeft: '1.5rem', lineHeight: '1.8' }}>
|
||||||
|
<li>Download the extension (button above)</li>
|
||||||
|
<li>Unzip the downloaded file</li>
|
||||||
|
<li>Open Chrome → <code style={{ background: '#334155', padding: '2px 6px', borderRadius: '4px' }}>chrome://extensions/</code></li>
|
||||||
|
<li>Enable <strong>Developer mode</strong> (top right toggle)</li>
|
||||||
|
<li>Click <strong>Load unpacked</strong></li>
|
||||||
|
<li>Select the <code>hookd</code> folder (the folder, not individual files)</li>
|
||||||
|
<li>Pin 📌 Hookd to your toolbar!</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '2rem', padding: '1rem', background: 'rgba(239, 68, 68, 0.1)', borderRadius: '8px' }}>
|
||||||
|
<p style={{ fontSize: '0.9rem', color: '#94a3b8' }}>
|
||||||
|
<strong>Note:</strong> Version 1.0.5 - CSP compliant, requires Chrome 88+
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
async function getPool() {
|
||||||
|
return mysql.createPool({
|
||||||
|
host: 'localhost',
|
||||||
|
user: 'sitemente',
|
||||||
|
password: 'pa0ndoQRPDYBHzTXX3rbWd6E',
|
||||||
|
database: 'sitemente',
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const pool = await getPool()
|
||||||
|
const [rows] = await pool.query('SELECT * FROM hostpioneers_leads ORDER BY total_spent DESC LIMIT 100')
|
||||||
|
await pool.end()
|
||||||
|
return NextResponse.json({ leads: rows })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface Submission {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
whatsapp: string;
|
||||||
|
message: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HPSubmissionsPage() {
|
||||||
|
const [submissions, setSubmissions] = useState<Submission[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/hp-submissions')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
setSubmissions(data.submissions || []);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', color: '#fff' }}>
|
||||||
|
<h1>HP Submissions</h1>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', color: '#fff' }}>
|
||||||
|
<h1 style={{ marginBottom: '2rem' }}>📬 HP Contact Submissions</h1>
|
||||||
|
|
||||||
|
{submissions.length === 0 ? (
|
||||||
|
<p style={{ color: '#888' }}>No submissions yet</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
|
{submissions.map((sub, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
background: '#1e293b',
|
||||||
|
padding: '1.5rem',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid #334155'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||||
|
<strong style={{ color: '#4169e1', fontSize: '1.1rem' }}>{sub.name}</strong>
|
||||||
|
<span style={{ color: '#64748b', fontSize: '0.85rem' }}>{sub.date}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ color: '#94a3b8', marginBottom: '0.5rem' }}>
|
||||||
|
📧 {sub.email}
|
||||||
|
</div>
|
||||||
|
{sub.whatsapp && (
|
||||||
|
<div style={{ color: '#94a3b8', marginBottom: '0.5rem' }}>
|
||||||
|
📱 {sub.whatsapp}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ color: '#cbd5e1', marginTop: '0.75rem', padding: '0.75rem', background: '#0f172a', borderRadius: '8px' }}>
|
||||||
|
{sub.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
interface Lead {
|
||||||
|
id: number
|
||||||
|
firstname: string
|
||||||
|
lastname: string
|
||||||
|
company: string
|
||||||
|
email: string
|
||||||
|
phone: string
|
||||||
|
country: string
|
||||||
|
status: string
|
||||||
|
total_spent: string
|
||||||
|
segment: string
|
||||||
|
dormancy_flag: string
|
||||||
|
email_status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Pagination {
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
total: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HPLeadsPage() {
|
||||||
|
const [leads, setLeads] = useState<Lead[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [filter, setFilter] = useState('all')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pagination, setPagination] = useState<Pagination>({ page: 1, limit: 100, total: 0, totalPages: 0 })
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [showSearch, setShowSearch] = useState(false)
|
||||||
|
const [jumpPage, setJumpPage] = useState('')
|
||||||
|
const [sortBy, setSortBy] = useState('total_spent')
|
||||||
|
const [sortOrder, setSortOrder] = useState('DESC')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLeads()
|
||||||
|
}, [filter, page, sortBy, sortOrder])
|
||||||
|
|
||||||
|
const fetchLeads = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
const segmentParam = filter !== 'all' ? `&segment=${filter}` : ''
|
||||||
|
const res = await fetch(`/api/mission-control/hp-leads?page=${page}&limit=100${segmentParam}&sortBy=${sortBy}&sortOrder=${sortOrder}`)
|
||||||
|
const data = await res.json()
|
||||||
|
setLeads(data.leads || [])
|
||||||
|
setPagination(data.pagination || { page: 1, limit: 100, total: 0, totalPages: 0 })
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSort = (column: string) => {
|
||||||
|
if (sortBy === column) {
|
||||||
|
setSortOrder(sortOrder === 'ASC' ? 'DESC' : 'ASC')
|
||||||
|
} else {
|
||||||
|
setSortBy(column)
|
||||||
|
setSortOrder('DESC')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJump = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const pageNum = parseInt(jumpPage)
|
||||||
|
if (pageNum > 0 && pageNum <= pagination.totalPages) {
|
||||||
|
setPage(pageNum)
|
||||||
|
setJumpPage('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setPage(1)
|
||||||
|
fetchSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSearch = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
const segmentParam = filter !== 'all' ? `&segment=${filter}` : ''
|
||||||
|
const searchParam = search ? `&search=${encodeURIComponent(search)}` : ''
|
||||||
|
const res = await fetch(`/api/mission-control/hp-leads?page=1&limit=100${segmentParam}${searchParam}&sortBy=${sortBy}&sortOrder=${sortOrder}`)
|
||||||
|
const data = await res.json()
|
||||||
|
setLeads(data.leads || [])
|
||||||
|
setPagination(data.pagination || { page: 1, limit: 100, total: 0, totalPages: 0 })
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
setSearch('')
|
||||||
|
setFilter('all')
|
||||||
|
setPage(1)
|
||||||
|
fetchLeads()
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortIcon = ({ column }: { column: string }) => (
|
||||||
|
<span style={{ marginLeft: '0.5rem', opacity: sortBy === column ? 1 : 0.3 }}>
|
||||||
|
{sortBy === column && sortOrder === 'ASC' ? '↑' : '↓'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', background: '#0f172a', minHeight: '100vh', color: '#fff' }}>
|
||||||
|
<div style={{ marginBottom: '2rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', margin: 0 }}>HP Leads</h1>
|
||||||
|
<p style={{ color: '#64748b', margin: '0.5rem 0 0 0' }}>{pagination.total.toLocaleString()} total records</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSearch(!showSearch)}
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem 1.5rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: 'none',
|
||||||
|
background: showSearch ? '#4169e1' : '#1e293b',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔍 {showSearch ? 'Hide Search' : 'Search'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSearch && (
|
||||||
|
<form onSubmit={handleSearch} style={{ marginBottom: '1.5rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Search name, email, company, phone..."
|
||||||
|
style={{ flex: 1, padding: '0.75rem 1rem', borderRadius: '0.5rem', border: '1px solid #334155', background: '#1e293b', color: '#fff', minWidth: '200px' }}
|
||||||
|
/>
|
||||||
|
<button type="submit" style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: '#4169e1', color: '#fff', cursor: 'pointer' }}>Search</button>
|
||||||
|
<button type="button" onClick={clearSearch} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: '#334155', color: '#fff', cursor: 'pointer' }}>Clear</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.5rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: '#64748b' }}>Filter:</span>
|
||||||
|
{['all', 'VIP', 'Standard', 'Re-engage'].map(f => (
|
||||||
|
<button
|
||||||
|
key={f}
|
||||||
|
onClick={() => { setFilter(f); setPage(1); }}
|
||||||
|
style={{ padding: '0.5rem 1rem', borderRadius: '0.5rem', border: 'none', background: filter === f ? '#4169e1' : '#1e293b', color: '#fff', cursor: 'pointer', fontWeight: filter === f ? 'bold' : 'normal' }}
|
||||||
|
>
|
||||||
|
{f === 'all' ? 'All' : f}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ background: '#1e293b', borderRadius: '1rem', overflow: 'hidden' }}>
|
||||||
|
<div style={{ overflowX: 'auto' }}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '900px' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#0f172a' }}>
|
||||||
|
{['firstname', 'lastname', 'company', 'email', 'phone', 'country', 'total_spent', 'segment', 'dormancy_flag'].map(col => (
|
||||||
|
<th
|
||||||
|
key={col}
|
||||||
|
onClick={() => handleSort(col)}
|
||||||
|
style={{ padding: '1rem', textAlign: 'left', borderBottom: '1px solid #334155', cursor: 'pointer', userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
<span style={{ color: '#64748b', fontSize: '0.85rem', fontWeight: 'bold', textTransform: 'uppercase' }}>
|
||||||
|
{col === 'firstname' ? 'First Name' : col === 'lastname' ? 'Last Name' : col === 'company' ? 'Company' : col === 'email' ? 'Email' : col === 'phone' ? 'Phone' : col === 'country' ? 'Country' : col === 'total_spent' ? 'Spent' : col === 'segment' ? 'Segment' : col === 'dormancy_flag' ? 'Status' : col}
|
||||||
|
<SortIcon column={col} />
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loading ? (
|
||||||
|
<tr><td colSpan={9} style={{ padding: '3rem', textAlign: 'center', color: '#64748b' }}>Loading...</td></tr>
|
||||||
|
) : leads.length === 0 ? (
|
||||||
|
<tr><td colSpan={9} style={{ padding: '3rem', textAlign: 'center', color: '#64748b' }}>No results found</td></tr>
|
||||||
|
) : leads.map(lead => (
|
||||||
|
<tr key={lead.id} style={{ borderBottom: '1px solid #334155' }}>
|
||||||
|
<td style={{ padding: '1rem' }}>{lead.firstname}</td>
|
||||||
|
<td style={{ padding: '1rem' }}>{lead.lastname}</td>
|
||||||
|
<td style={{ padding: '1rem' }}>{lead.company || '-'}</td>
|
||||||
|
<td style={{ padding: '1rem', color: '#5bc0de' }}>{lead.email}</td>
|
||||||
|
<td style={{ padding: '1rem', fontSize: '0.9rem' }}>{lead.phone || '-'}</td>
|
||||||
|
<td style={{ padding: '1rem' }}>{lead.country}</td>
|
||||||
|
<td style={{ padding: '1rem', color: '#10b981', fontWeight: 'bold' }}>${Number(lead.total_spent).toFixed(2)}</td>
|
||||||
|
<td style={{ padding: '1rem' }}>
|
||||||
|
<span style={{ padding: '0.25rem 0.75rem', borderRadius: '1rem', fontSize: '0.75rem', background: lead.segment === 'VIP' ? '#dc2626' : lead.segment === 'Standard' ? '#ca8a04' : '#16a34a', color: '#fff' }}>{lead.segment}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '1rem' }}>
|
||||||
|
<span style={{ padding: '0.25rem 0.75rem', borderRadius: '1rem', fontSize: '0.75rem', background: lead.dormancy_flag === 'dormant_2y' ? '#7c2d12' : '#064e3b', color: '#fff' }}>{lead.dormancy_flag}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: page === 1 ? '#334155' : '#4169e1', color: '#fff', cursor: page === 1 ? 'not-allowed' : 'pointer', opacity: page === 1 ? 0.5 : 1 }}>← Previous</button>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<form onSubmit={handleJump} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<span style={{ color: '#64748b' }}>Go to:</span>
|
||||||
|
<input type="number" value={jumpPage} onChange={(e) => setJumpPage(e.target.value)} placeholder="#" min={1} max={pagination.totalPages} style={{ width: '60px', padding: '0.5rem', borderRadius: '0.25rem', border: '1px solid #334155', background: '#1e293b', color: '#fff', textAlign: 'center' }} />
|
||||||
|
<button type="submit" style={{ padding: '0.5rem 1rem', borderRadius: '0.25rem', border: 'none', background: '#334155', color: '#fff', cursor: 'pointer' }}>Go</button>
|
||||||
|
</form>
|
||||||
|
<span style={{ padding: '0.5rem 1rem', background: '#1e293b', borderRadius: '0.5rem' }}><strong>{page}</strong> / {pagination.totalPages}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
||||||
|
{[1, Math.floor(pagination.totalPages/4), Math.floor(pagination.totalPages/2), Math.floor(pagination.totalPages*3/4), pagination.totalPages].filter((p, i, arr) => arr.indexOf(p) === i && p > 0 && p <= pagination.totalPages).map(p => (
|
||||||
|
<button key={p} onClick={() => setPage(p)} style={{ padding: '0.5rem 0.75rem', borderRadius: '0.25rem', border: 'none', background: page === p ? '#4169e1' : '#1e293b', color: '#fff', cursor: 'pointer', fontSize: '0.85rem' }}>{p}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={() => setPage(p => Math.min(pagination.totalPages, p + 1))} disabled={page >= pagination.totalPages} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: 'none', background: page >= pagination.totalPages ? '#334155' : '#4169e1', color: '#fff', cursor: page >= pagination.totalPages ? 'not-allowed' : 'pointer', opacity: page >= pagination.totalPages ? 0.5 : 1 }}>Next →</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ marginTop: '1rem', color: '#64748b', textAlign: 'center' }}>Showing {leads.length} of {pagination.total.toLocaleString()} records</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface ResearchResult {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
score: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Research {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
query: string;
|
||||||
|
results_count: number;
|
||||||
|
created_at: string;
|
||||||
|
saved: boolean;
|
||||||
|
summary: string;
|
||||||
|
results?: ResearchResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MissionResearchPage() {
|
||||||
|
const [research, setResearch] = useState<Research[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [newResearch, setNewResearch] = useState({ name: "", query: "" });
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [expanded, setExpanded] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchResearch();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchResearch = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/research");
|
||||||
|
const data = await res.json();
|
||||||
|
setResearch(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!newResearch.name || !newResearch.query) return;
|
||||||
|
|
||||||
|
setSearching(true);
|
||||||
|
try {
|
||||||
|
const tavilyRes = await fetch('https://api.tavily.com/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
api_key: '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',
|
||||||
|
query: newResearch.query,
|
||||||
|
max_results: 20,
|
||||||
|
search_depth: 'advanced'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const tavilyData = await tavilyRes.json();
|
||||||
|
|
||||||
|
const results = (tavilyData.results || []).map((r: any) => ({
|
||||||
|
title: r.title || 'No title',
|
||||||
|
url: r.url || '',
|
||||||
|
score: r.score || 0,
|
||||||
|
content: (r.content || '').substring(0, 400)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const summary = results.slice(0, 5).map((r: any) => r.title).join('; ') || 'No results';
|
||||||
|
|
||||||
|
const res = await fetch("/api/research", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newResearch.name,
|
||||||
|
query: newResearch.query,
|
||||||
|
results_count: results.length,
|
||||||
|
summary: summary.substring(0, 150),
|
||||||
|
results: results
|
||||||
|
})
|
||||||
|
});
|
||||||
|
await res.json();
|
||||||
|
setNewResearch({ name: "", query: "" });
|
||||||
|
fetchResearch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
setSearching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteResearch = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/research?id=${id}`, { method: "DELETE" });
|
||||||
|
fetchResearch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-red-900/30 border border-red-600 rounded-lg p-3 mb-4">
|
||||||
|
<p className="text-red-400 text-sm">
|
||||||
|
🔒 <strong>Security Rule:</strong> Never install skills from the web. Read the content, understand what it does, then BUILD IT yourself.
|
||||||
|
We build, we don't install. One compromised skill can wipe everything.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-2xl font-bold mb-6">🔍 Deep Research</h1>
|
||||||
|
|
||||||
|
{/* Search Form */}
|
||||||
|
<div className="bg-gray-800 p-4 rounded-lg mb-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Research Name (e.g., OpenClaw Skills)"
|
||||||
|
value={newResearch.name}
|
||||||
|
onChange={(e) => setNewResearch({ ...newResearch, name: e.target.value })}
|
||||||
|
className="bg-gray-700 text-white px-4 py-2 rounded"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Query"
|
||||||
|
value={newResearch.query}
|
||||||
|
onChange={(e) => setNewResearch({ ...newResearch, query: e.target.value })}
|
||||||
|
className="bg-gray-700 text-white px-4 py-2 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={searching || !newResearch.name || !newResearch.query}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{searching ? "Researching..." : "Run Research"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Research List */}
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-gray-400">Loading...</p>
|
||||||
|
) : research.length === 0 ? (
|
||||||
|
<p className="text-gray-400">No research yet. Run your first search!</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{research.map((r) => (
|
||||||
|
<div key={r.id} className="bg-gray-800 rounded-lg overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-lg">{r.name}</h3>
|
||||||
|
<p className="text-gray-400 text-sm">{r.query}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(expanded === r.id ? null : r.id)}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
{expanded === r.id ? "Hide Table" : "View Table"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteResearch(r.id)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table View */}
|
||||||
|
{expanded === r.id && r.results && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-gray-900">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-gray-400 font-medium">✓</th>
|
||||||
|
<th className="px-4 py-3 text-left text-gray-400 font-medium">Skill / Tool</th>
|
||||||
|
<th className="px-4 py-3 text-left text-gray-400 font-medium">What It Does</th>
|
||||||
|
<th className="px-4 py-3 text-center text-gray-400 font-medium">Score</th>
|
||||||
|
<th className="px-4 py-3 text-center text-gray-400 font-medium">Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-700">
|
||||||
|
{r.results.map((result, i) => (
|
||||||
|
<tr key={i} className="hover:bg-gray-750">
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<input type="checkbox" className="w-5 h-5 accent-green-500" />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 font-medium text-white max-w-xs truncate">
|
||||||
|
{result.title}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-300 max-w-md">
|
||||||
|
{result.content.substring(0, 150)}...
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<span className={`px-2 py-1 rounded text-xs ${
|
||||||
|
result.score > 0.9 ? 'bg-green-900 text-green-400' :
|
||||||
|
result.score > 0.7 ? 'bg-yellow-900 text-yellow-400' :
|
||||||
|
'bg-gray-700 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{(result.score * 100).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<a
|
||||||
|
href={result.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-400 hover:text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Open →
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ const sidebarCategories: SidebarCategory[] = [
|
|||||||
]},
|
]},
|
||||||
{ id: "leads", name: "Leads", icon: "📈", items: [
|
{ id: "leads", name: "Leads", icon: "📈", items: [
|
||||||
{ id: "leads-crm", name: "CRM", icon: "📊", category: "leads" },
|
{ id: "leads-crm", name: "CRM", icon: "📊", category: "leads" },
|
||||||
|
{ id: "hp-submissions", name: "HP Submissions", icon: "📬", category: "hp-submissions" },
|
||||||
{ id: "dashboard", name: "Client Dashboard", icon: "🏢", category: "dashboard" },
|
{ id: "dashboard", name: "Client Dashboard", icon: "🏢", category: "dashboard" },
|
||||||
]},
|
]},
|
||||||
{ id: "projects", name: "Projects", icon: "🎯", items: [
|
{ id: "projects", name: "Projects", icon: "🎯", items: [
|
||||||
@@ -46,6 +47,7 @@ const sidebarCategories: SidebarCategory[] = [
|
|||||||
{ 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: "hookd", name: "Hookd", icon: "📌", color: "#4F46E5", category: "hookd" },
|
||||||
{ id: "arabredox", name: "Arabredox", icon: "💚", color: "#22c55e", category: "projects" },
|
{ id: "arabredox", name: "Arabredox", icon: "💚", color: "#22c55e", category: "projects" },
|
||||||
{ id: "infrastructure", name: "Infra", icon: "⚙️", color: "#10b981", category: "projects" },
|
{ id: "infrastructure", name: "Infra", icon: "⚙️", color: "#10b981", category: "projects" },
|
||||||
]},
|
]},
|
||||||
@@ -77,6 +79,7 @@ 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: "snapshots", name: "Snapshots", icon: "📸", category: "snapshots" },
|
{ id: "snapshots", name: "Snapshots", icon: "📸", category: "snapshots" },
|
||||||
|
{ id: "research", name: "Deep Research", icon: "🔍", category: "research" },
|
||||||
]},
|
]},
|
||||||
{ id: "docs", name: "Docs", icon: "📚", items: [
|
{ id: "docs", name: "Docs", icon: "📚", items: [
|
||||||
{ id: "docs-index", name: "Documentation", icon: "📚", category: "docs" },
|
{ id: "docs-index", name: "Documentation", icon: "📚", category: "docs" },
|
||||||
@@ -436,6 +439,16 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentItem?.category === "research" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">🔍 Deep Research</h3>
|
||||||
|
<p className="text-white/60 mb-4">AI-powered research using Tavily API</p>
|
||||||
|
<a href="/mission-control/research" className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 rounded-lg text-sm font-medium hover:bg-blue-700 transition">🔍 Open Research</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* BMHQ Automation Panels */}
|
{/* BMHQ Automation Panels */}
|
||||||
{currentItem?.category === "autorun" && (
|
{currentItem?.category === "autorun" && (
|
||||||
<div className="rounded-xl border border-green-500/30 bg-gradient-to-br from-[#1a2a1a] to-[#1a2a2a] p-6">
|
<div className="rounded-xl border border-green-500/30 bg-gradient-to-br from-[#1a2a1a] to-[#1a2a2a] p-6">
|
||||||
@@ -568,6 +581,19 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentItem?.category === "hp-submissions" && (
|
||||||
|
<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">HP Submissions</h3>
|
||||||
|
<p className="text-white/60 mb-6">HostPioneers contact form submissions</p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-3">
|
||||||
|
<a href="/mission-control/hp-submissions" target="_blank" className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">📬 View Submissions</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{currentItem?.category === "dashboard" && (
|
{currentItem?.category === "dashboard" && (
|
||||||
<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">
|
||||||
@@ -612,6 +638,7 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
|
|||||||
<option value="holacompi">HolaCompi</option>
|
<option value="holacompi">HolaCompi</option>
|
||||||
<option value="arabredox">Arabredox</option>
|
<option value="arabredox">Arabredox</option>
|
||||||
<option value="infrastructure">Infrastructure</option>
|
<option value="infrastructure">Infrastructure</option>
|
||||||
|
<option value="hookd">Hookd</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>
|
||||||
<button onClick={() => setShowAddTask(false)} className="px-3 py-1.5 text-sm text-white/60 hover:text-white">Cancel</button>
|
<button onClick={() => setShowAddTask(false)} className="px-3 py-1.5 text-sm text-white/60 hover:text-white">Cancel</button>
|
||||||
@@ -655,6 +682,38 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentItem?.category === "hookd" && (
|
||||||
|
<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">Hookd</h3>
|
||||||
|
<p className="text-white/60 mb-6">Save, organize and AI-sort your X content</p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-3 mb-4">
|
||||||
|
<a href="/extensions/hookd" target="_blank" className="px-4 py-2 bg-indigo-600 rounded-lg text-sm font-medium hover:bg-indigo-700 transition">📥 Download Extension</a>
|
||||||
|
<a href="https://github.com/HaithamEKhalifa/hookd" target="_blank" className="px-4 py-2 bg-white/10 rounded-lg text-sm hover:bg-white/20 transition">🐙 GitHub</a>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto mt-6 text-left">
|
||||||
|
<div className="bg-white/5 rounded-lg p-3">
|
||||||
|
<span className="text-lg">📥</span>
|
||||||
|
<p className="text-xs text-white/60 mt-1">DM Organizer</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/5 rounded-lg p-3">
|
||||||
|
<span className="text-lg">🐦</span>
|
||||||
|
<p className="text-xs text-white/60 mt-1">Feed Saver</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/5 rounded-lg p-3">
|
||||||
|
<span className="text-lg">🤖</span>
|
||||||
|
<p className="text-xs text-white/60 mt-1">AI Processing</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/5 rounded-lg p-3">
|
||||||
|
<span className="text-lg">🏷️</span>
|
||||||
|
<p className="text-xs text-white/60 mt-1">Custom Tags</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+226
-1
@@ -1 +1,226 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"id": "eod-1773864002102",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-18T20:00:02.102Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773777601641",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-17T20:00:01.641Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773691202704",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-16T20:00:02.704Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773604802284",
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-15T20:00:02.284Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773518402124",
|
||||||
|
"date": "2026-03-14",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-14T20:00:02.124Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773432001747",
|
||||||
|
"date": "2026-03-13",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-13T20:00:01.747Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773345602107",
|
||||||
|
"date": "2026-03-12",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-12T20:00:02.107Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773259202293",
|
||||||
|
"date": "2026-03-11",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-11T20:00:02.293Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773172801469",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-10T20:00:01.469Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773086402559",
|
||||||
|
"date": "2026-03-09",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-09T20:00:02.559Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1773000002206",
|
||||||
|
"date": "2026-03-08",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-08T20:00:02.206Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772913601820",
|
||||||
|
"date": "2026-03-07",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-07T20:00:01.820Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772827201835",
|
||||||
|
"date": "2026-03-06",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-06T20:00:01.835Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772740802338",
|
||||||
|
"date": "2026-03-05",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-05T20:00:02.338Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772654402583",
|
||||||
|
"date": "2026-03-04",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-04T20:00:02.583Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772568002013",
|
||||||
|
"date": "2026-03-03",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-03T20:00:02.013Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772481602906",
|
||||||
|
"date": "2026-03-02",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-03-02T20:00:02.906Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772308802958",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"completed": [
|
||||||
|
"See Mission Control for details"
|
||||||
|
],
|
||||||
|
"progress": {},
|
||||||
|
"council": {},
|
||||||
|
"tomorrow": [],
|
||||||
|
"created_at": "2026-02-28T20:00:02.958Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "eod-1772228668001",
|
||||||
|
"date": "2026-02-27",
|
||||||
|
"completed": [
|
||||||
|
"Council to top of MC",
|
||||||
|
"Agent modal with tasks/chat",
|
||||||
|
"Voice widget added (disabled - API key issue)",
|
||||||
|
"Grid layout for agents"
|
||||||
|
],
|
||||||
|
"progress": {
|
||||||
|
"MC": "80%",
|
||||||
|
"Voice": "50%",
|
||||||
|
"Briefs": "90%"
|
||||||
|
},
|
||||||
|
"council": {
|
||||||
|
"Horus": "Orchestrating all agents",
|
||||||
|
"Thoth": "Research ready",
|
||||||
|
"Ptah": "Dev ops ready"
|
||||||
|
},
|
||||||
|
"tomorrow": [
|
||||||
|
"Fix MiniMax API key",
|
||||||
|
"Enable voice widget",
|
||||||
|
"Test agent automation"
|
||||||
|
],
|
||||||
|
"created_at": "2026-02-27T21:44:28.001Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,4 +1,22 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"id": "log-1772229860745",
|
||||||
|
"templateId": "anubis-outreach-scan",
|
||||||
|
"agent": "anubis",
|
||||||
|
"taskTitle": "Weekly Outreach Scan",
|
||||||
|
"startedAt": "2026-02-27T22:04:20.745Z",
|
||||||
|
"status": "running",
|
||||||
|
"output": "Agent executing..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log-1772229850446",
|
||||||
|
"templateId": "ptah-infra-monitor",
|
||||||
|
"agent": "ptah",
|
||||||
|
"taskTitle": "Infra Health Check",
|
||||||
|
"startedAt": "2026-02-27T22:04:10.446Z",
|
||||||
|
"status": "running",
|
||||||
|
"output": "Agent executing..."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "log-1772215845028",
|
"id": "log-1772215845028",
|
||||||
"templateId": "thoth-daily-research",
|
"templateId": "thoth-daily-research",
|
||||||
|
|||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"category": "restaurants",
|
||||||
|
"location": "Málaga/Costa del Sol",
|
||||||
|
"businesses": [
|
||||||
|
{"name": "El Tapeo de Cervantes", "address": "Calle Cister, Málaga", "phone": "", "type": "tapas restaurant"},
|
||||||
|
{"name": "Casa Lola", "address": "Calle Granada, Málaga centro", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "La Farola de Orellana", "address": "Málaga", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Arrebato Gastrotaberna", "address": "Málaga", "phone": "", "type": "gastro bar"},
|
||||||
|
{"name": "El Lagar de Verum", "address": "Málaga", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Casa Aranda", "address": "Centro, Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Uvedoble Taberna", "address": "Calle Cister 1, 29015 Málaga", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Mesón de Cervantes", "address": "Granada Street 36, 29015 Málaga", "phone": "+34 951 55 00 55", "type": "restaurant"},
|
||||||
|
{"name": "Taberna El Mentidero", "address": "Málaga", "phone": "", "type": "tavern"},
|
||||||
|
{"name": "La Cabrera", "address": "Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Arco Tapas Bar", "address": "Marbella", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Bistro Paloma", "address": "Marbella", "phone": "", "type": "bistro"},
|
||||||
|
{"name": "El Candil de San Pedro", "address": "San Pedro de Alcántara, Marbella", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "La Bodega", "address": "Marbella", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Taberna La Niña del Pisto", "address": "Marbella Old Town", "phone": "", "type": "tavern"},
|
||||||
|
{"name": "La Sala", "address": "Marbella", "phone": "", "type": "restaurant/bar"},
|
||||||
|
{"name": "Wot You Like", "address": "Torremolinos", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Casero La Comida Mediterránea", "address": "Torremolinos", "phone": "", "type": "Mediterranean restaurant"},
|
||||||
|
{"name": "The Dizzy Donkey", "address": "Benalmádena", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Azure Terraza", "address": "Benalmádena", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Coast To Coast", "address": "Benalmádena", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Il Boccaccio", "address": "Benalmádena", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Taperia La Bodeguita", "address": "Benalmádena", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "La Cepa", "address": "Benalmádena", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Sabores Gourmet", "address": "Estepona", "phone": "", "type": "gourmet restaurant"},
|
||||||
|
{"name": "The Boab Tree Restaurante", "address": "Estepona", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "La Carbonara", "address": "Estepona", "phone": "", "type": "Italian restaurant"},
|
||||||
|
{"name": "La Bulla Gastrobar", "address": "Estepona", "phone": "", "type": "gastro bar"},
|
||||||
|
{"name": "La Casa del Rey", "address": "Estepona", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "La Escollera", "address": "Estepona", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Tipi Tapa", "address": "Calle Malaga 4, 29640 Fuengirola", "phone": "951 311 630", "type": "tapas restaurant"},
|
||||||
|
{"name": "Lotus Steak & Burger House", "address": "Fuengirola", "phone": "", "type": "steakhouse"},
|
||||||
|
{"name": "La Mamounia", "address": "Fuengirola", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Platos Gastrobar", "address": "Fuengirola", "phone": "", "type": "gastro bar"},
|
||||||
|
{"name": "O Mamma Mia", "address": "Fuengirola", "phone": "", "type": "Italian restaurant"},
|
||||||
|
{"name": "Alberto's Restaurante", "address": "Matalascañas", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Costaluz Taberna", "address": "Matalascañas", "phone": "", "type": "tavern"},
|
||||||
|
{"name": "Restaurante Los Pepes", "address": "Matalascañas", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Misuto", "address": "Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Araboka Centro", "address": "Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Arrozeando Pedregalejo", "address": "Pedregalejo, Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Las Merchanas", "address": "Calle Meryland, Málaga", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "La Cosmo", "address": "Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Casa Lola Strachan", "address": "Pl. de las Flores 2, 29005 Málaga", "phone": "", "type": "tapas bar"},
|
||||||
|
{"name": "Malavida", "address": "Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "No Piqui", "address": "Málaga", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "San Sabor A Nápoles", "address": "Victoria, Málaga", "phone": "", "type": "Italian restaurant"},
|
||||||
|
{"name": "Nintai", "address": "Marbella", "phone": "", "type": "Japanese restaurant"},
|
||||||
|
{"name": "Erre & Urrechu", "address": "Marbella", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "La Milla Marbella", "address": "Marbella", "phone": "", "type": "restaurant"},
|
||||||
|
{"name": "Leña Marbella", "address": "Puente Romano, Marbella", "phone": "", "type": "restaurant"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "clinics",
|
||||||
|
"location": "Málaga/Costa del Sol",
|
||||||
|
"businesses": [
|
||||||
|
{"name": "Hospital Dr. Gálvez", "address": "Málaga", "phone": "", "type": "private hospital"},
|
||||||
|
{"name": "Vithas Málaga Hospital", "address": "Avenida Pintor Sorolla, Málaga", "phone": "", "type": "private hospital"},
|
||||||
|
{"name": "Hospital Vithas Parque San Antonio", "address": "Málaga", "phone": "", "type": "private hospital"},
|
||||||
|
{"name": "CHIP Hospital (Centro Hospitalario Integral Privado)", "address": "Málaga", "phone": "", "type": "private hospital"},
|
||||||
|
{"name": "Quirónsalud Málaga Hospital", "address": "Málaga", "phone": "", "type": "private hospital"},
|
||||||
|
{"name": "HM Málaga Hospital", "address": "Málaga", "phone": "", "type": "private hospital"},
|
||||||
|
{"name": "Hospital Regional Universitario de Málaga", "address": "Málaga", "phone": "", "type": "public hospital"},
|
||||||
|
{"name": "Hospital Universitario Virgen de la Victoria", "address": "Málaga", "phone": "", "type": "public hospital"},
|
||||||
|
{"name": "Vithas Xanit International Hospital", "address": "Málaga/Costa del Sol", "phone": "", "type": "international hospital"},
|
||||||
|
{"name": "The Budwig Center", "address": "Málaga", "phone": "", "type": "alternative medicine clinic"},
|
||||||
|
{"name": "Perez Bryan Gynaecology Clinic", "address": "Málaga centro", "phone": "", "type": "gynecology clinic"},
|
||||||
|
{"name": "Costa Medical Services", "address": "Calle Fernando Camino 10, 29016 Málaga", "phone": "", "type": "medical center"},
|
||||||
|
{"name": "Fios Clinic", "address": "Plaza Uncibay 3, 29008 Málaga", "phone": "951 431 794", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Phybone", "address": "29010 Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Qicenter", "address": "Málaga", "phone": "", "type": "medical center"},
|
||||||
|
{"name": "Fisitania Fisioterapia", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Centro Ankarena", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Fisioterapia Águeda Rodríguez", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Fisioterapia 2002", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Auriafisio", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Jorge Espada Fisioterapia", "address": "Marbella", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Solaris Clinic", "address": "Málaga", "phone": "", "type": "rehabilitation center"},
|
||||||
|
{"name": "Osteopathy Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "osteopathy/physiotherapy"},
|
||||||
|
{"name": "English Physio Málaga", "address": "Málaga", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Physio Wave Marbella", "address": "Marbella", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "Corporis Fisioterapia", "address": "Marbella", "phone": "", "type": "physiotherapy clinic"},
|
||||||
|
{"name": "In One Clinic", "address": "Marbella", "phone": "", "type": "multispecialty clinic"},
|
||||||
|
{"name": "Clinica Premium", "address": "Málaga", "phone": "952 865 856", "type": "medical clinic"},
|
||||||
|
{"name": "AMS Centro Médico", "address": "Costa del Sol", "phone": "", "type": "medical center"},
|
||||||
|
{"name": "Grupo Dental Clinics Málaga Centro", "address": "Málaga", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Grupo Dental Clinics Torremolinos", "address": "Benyamina Avenue 25, Torremolinos", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Gross Dentists", "address": "Málaga y Torremolinos", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Clínica Omega", "address": "Marbella y Torremolinos", "phone": "952 073 602 / 951 681 508", "type": "dental clinic"},
|
||||||
|
{"name": "Marbella Dental Arts", "address": "Marbella", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Scandinavian Dental Clinic", "address": "Torremolinos", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Clínica Medident", "address": "Torremolinos", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Clinica Dental Soriano", "address": "Marbella centro", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "R&H Dental Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Málaga", "address": "Málaga", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Fuengirola", "address": "Fuengirola", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Torremolinos", "address": "Torremolinos", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Estepona", "address": "Estepona", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Crooke Laguna Dental Clinic", "address": "Calle Mediterráneo 1, Marbella", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "International Dental Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Dentist Marbella", "address": "Marbella", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Antequera", "address": "Antequera", "phone": "", "type": "dental clinic"},
|
||||||
|
{"name": "Adeslas Dental Ronda", "address": "Ronda", "phone": "", "type": "dental clinic"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "real_estate_agencies",
|
||||||
|
"location": "Málaga/Costa del Sol",
|
||||||
|
"businesses": [
|
||||||
|
{"name": "DM Properties (Diana Morales Properties)", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Marbella Estates", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Marbella Living", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "The Agency Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Bromley Estates Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Key Real Estates", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "CDS Property", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Christie's International Real Estate Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "luxury real estate"},
|
||||||
|
{"name": "Panorama Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Cima Real Estate", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Terra Meridiana", "address": "Estepona", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Marbella24 (Ralf Michael)", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "MPDunne & Hamptons International", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Engel & Völkers Marbella", "address": "Marbella", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Marco Properties", "address": "Estepona", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Orangestate", "address": "Marbella & Fuengirola", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Spain Homes", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Lucas Fox Estepona", "address": "C/ del Naranjo, Estepona", "phone": "+34 933 562 989", "type": "real estate agency"},
|
||||||
|
{"name": "Cabanillas Real Estate", "address": "Estepona", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Costa Dream House", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "VIVA Costa del Sol", "address": "Calahonda", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Olsen Property Brokers", "address": "Estepona", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "BEST HOME", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Fuengirola Estates", "address": "Fuengirola", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Navasol Real Estate", "address": "Benalmádena Costa", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Mojo Estates", "address": "Estepona Old Town", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "ERA Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Costa del Sol Property", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Real Estepona", "address": "Estepona", "phone": "", "type": "real estate portal"},
|
||||||
|
{"name": "Spain Homes Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Idealista (Marbella)", "address": "Marbella", "phone": "", "type": "property portal"},
|
||||||
|
{"name": "Costa del Sol Real Estate", "address": "Costa del Sol", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Luxury Properties Marbella", "address": "Marbella", "phone": "", "type": "luxury real estate"},
|
||||||
|
{"name": "Estepona Property Agents", "address": "Estepona", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Benalmádena Real Estate", "address": "Benalmádena", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Mijas Real Estate", "address": "Mijas", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Torremolinos Properties", "address": "Torremolinos", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Fuengirola Property", "address": "Fuengirola", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Malaga City Properties", "address": "Málaga", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Ronda Properties", "address": "Ronda", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Benahavis Real Estate", "address": "Benahavís", "phone": "", "type": "real estate agency"},
|
||||||
|
{"name": "Nueva Alcuzcurra Real Estate", "address": "Costa del Sol", "phone": "", "type": "real estate agency"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "car_rental",
|
||||||
|
"location": "Málaga/Costa del Sol",
|
||||||
|
"businesses": [
|
||||||
|
{"name": "Marbesol", "address": "Málaga Airport & Marbella", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Hertz Málaga Airport", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "National Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Enterprise Rent-A-Car", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Avis Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Budget Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Alamo Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "SIXT Costa del Sol", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Europcar Málaga Airport", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Fetajo Rent a Car", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "MalagaCar.com", "address": "Pol. Ind. Aeropuerto de Málaga", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Enterprise Marbella", "address": "Marbella", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Yellow Car Marbella", "address": "Marbella & Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Malaga U Drive", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Rahul Rent a Car", "address": "Torremolinos & Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Europcar Torremolinos", "address": "Torremolinos", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Caramba Car", "address": "Torremolinos", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Rent a Car Patry", "address": "Torremolinos", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "CAFISA Rent a Car", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Albacars Benalmádena", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Localrent Benalmádena", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "DoYouSpain Benalmádena", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Finauto Rent a Car", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Miami Car Hire", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Montemar Rent a Car", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Autos Remo", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Good Morning Rent a Car", "address": "Benalmádena", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Clickrent", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "DelPaso", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Fox Rent A Car", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Flexways", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Ace Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Thrifty Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Dollar Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Payless Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Gold Car Rental", "address": "Málaga", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Record Car Rental", "address": "Málaga", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Firefly Car Rental", "address": "Málaga Airport", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Kimi Car Rental", "address": "Málaga", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Orlando Car Rental", "address": "Costa del Sol", "phone": "", "type": "car rental"},
|
||||||
|
{"name": "Beta Car Rental", "address": "Málaga", "phone": "", "type": "car rental"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "hotels",
|
||||||
|
"location": "Málaga/Costa del Sol",
|
||||||
|
"businesses": [
|
||||||
|
{"name": "Hotel MS Maestranza", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Catalonia Puerta del Mar", "address": "Málaga centro", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Coeo Hernan Ruíz", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hostal Larios", "address": "Calle Larios, Málaga", "phone": "", "type": "hostal"},
|
||||||
|
{"name": "Hotel Boutique Teatro Romano", "address": "Málaga", "phone": "", "type": "boutique hotel"},
|
||||||
|
{"name": "LD Convento Cister", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Carlos V Málaga", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Vincci Seleccion Posada", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Gran Hotel Miramar GL", "address": "Málaga", "phone": "", "type": "luxury hotel"},
|
||||||
|
{"name": "Marbella Club Hotel Golf Resort & Spa", "address": "Marbella", "phone": "", "type": "luxury resort"},
|
||||||
|
{"name": "Hotel Don Pepe Gran Meliá", "address": "Marbella", "phone": "", "type": "luxury hotel"},
|
||||||
|
{"name": "Hotel El Fuerte Marbella", "address": "Marbella", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "ME Marbella", "address": "Marbella", "phone": "", "type": "luxury hotel"},
|
||||||
|
{"name": "Amare Beach Hotel Marbella", "address": "Marbella", "phone": "", "type": "beach hotel"},
|
||||||
|
{"name": "Palacio Solecio", "address": "Marbella", "phone": "", "type": "boutique hotel"},
|
||||||
|
{"name": "Elba Estepona Gran Hotel & Thalasso", "address": "Estepona", "phone": "", "type": "hotel & spa"},
|
||||||
|
{"name": "H10 Estepona Palace", "address": "Estepona", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Exe Estepona Thalasso & Spa", "address": "Estepona", "phone": "", "type": "adults only hotel"},
|
||||||
|
{"name": "Sol Marbella Estepona Atalaya Park", "address": "Estepona", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Senator Banús", "address": "Estepona", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "THE FLAG Hotel Marbella", "address": "Marbella", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Benalma Costa del Sol", "address": "Benalmádena", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Costa Azul", "address": "Benalmádena", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Sunset Beach Club", "address": "Benalmádena", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Best Siroco", "address": "Benalmádena", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Elsereins", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Ritual", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Ocean House", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Medplaya Hotel Bali", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Los Pearlos", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Meliá Costa del Sol", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Sol Principe", "address": "Torremolinos", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Allegro Granada", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Villa Ligero", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Del Pintor", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Luzzy", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Málaga Centro", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Sercotel Costa de la Luz", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Parador de Málaga Gibralfaro", "address": "Málaga", "phone": "", "type": "parador hotel"},
|
||||||
|
{"name": "Hotel ILunion Málaga", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel AXO Little Hight", "address": "Málaga", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Malak",
|
||||||
|
"address": "Málaga",
|
||||||
|
"phone": "",
|
||||||
|
"type": "hotel"},
|
||||||
|
{"name": "Hotel Mariposa", "address": "Fuengirola", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel IPV Palace", "address": "Fuengirola", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Las Farolas", "address": "Fuengirola", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Florida", "address": "Fuengirola", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Monarque Torreblanca", "address": "Fuengirola", "phone": "", "type": "hotel"},
|
||||||
|
{"name": "Hotel Ondine", "address": "Estepona", "phone": "", "type": "hotel"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+450
-1
@@ -1 +1,450 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"id": "morning-1773896519933",
|
||||||
|
"date": "2026-03-19",
|
||||||
|
"weather": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"temp": 13,
|
||||||
|
"feels_like": 13,
|
||||||
|
"wind": "8km/h SW",
|
||||||
|
"humidity": "76%",
|
||||||
|
"precipitation": "0.0mm"
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-19T05:01:59.933Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773892802810",
|
||||||
|
"date": "2026-03-19",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 71246,
|
||||||
|
"ETH": 2212,
|
||||||
|
"SOL": 90
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-19T04:00:02.810Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773810085974",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"weather": {
|
||||||
|
"location": "Benalmádena",
|
||||||
|
"temp": 14,
|
||||||
|
"condition": "Overcast",
|
||||||
|
"humidity": 54,
|
||||||
|
"wind": 9,
|
||||||
|
"forecast": "Rain expected later today, clearing tomorrow"
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-18T05:01:25.974Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773806402251",
|
||||||
|
"date": "2026-03-18",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 74515,
|
||||||
|
"ETH": 2341,
|
||||||
|
"SOL": 95
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-18T04:00:02.251Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773723629261",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"weather": {
|
||||||
|
"location": "Benalmádena, Spain",
|
||||||
|
"current": "Clear",
|
||||||
|
"temp": 11,
|
||||||
|
"feelsLike": 10,
|
||||||
|
"humidity": 68,
|
||||||
|
"wind": "12 km/h NE",
|
||||||
|
"sunrise": "07:27",
|
||||||
|
"sunset": "19:28",
|
||||||
|
"today_high": 16,
|
||||||
|
"today_low": 11,
|
||||||
|
"tomorrow": "Rain expected morning, clearing PM - high 15°C"
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-17T05:00:29.261Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773720002267",
|
||||||
|
"date": "2026-03-17",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 74410,
|
||||||
|
"ETH": 2317,
|
||||||
|
"SOL": 93
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-17T04:00:02.267Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773637240086",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"weather": {
|
||||||
|
"location": "Benalmádena, Málaga",
|
||||||
|
"current": {
|
||||||
|
"temp": 13,
|
||||||
|
"condition": "Clear",
|
||||||
|
"humidity": 66,
|
||||||
|
"wind": 9
|
||||||
|
},
|
||||||
|
"today": {
|
||||||
|
"min": 13,
|
||||||
|
"max": 15,
|
||||||
|
"condition": "Sunny"
|
||||||
|
},
|
||||||
|
"tomorrow": {
|
||||||
|
"min": 12,
|
||||||
|
"max": 16,
|
||||||
|
"condition": "Sunny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-16T05:00:40.086Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773633602782",
|
||||||
|
"date": "2026-03-16",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 73668,
|
||||||
|
"ETH": 2237,
|
||||||
|
"SOL": 93
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-16T04:00:02.782Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773550855351",
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"weather": {
|
||||||
|
"current": {
|
||||||
|
"temp": 4,
|
||||||
|
"feelsLike": 2,
|
||||||
|
"condition": "Cloudy",
|
||||||
|
"humidity": 75
|
||||||
|
},
|
||||||
|
"today": {
|
||||||
|
"high": 14,
|
||||||
|
"low": 6,
|
||||||
|
"condition": "Partly cloudy, rain early"
|
||||||
|
},
|
||||||
|
"tomorrow": {
|
||||||
|
"high": 18,
|
||||||
|
"low": 5,
|
||||||
|
"condition": "Sunny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-15T05:00:55.351Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773547202622",
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 71328,
|
||||||
|
"ETH": 2095,
|
||||||
|
"SOL": 87
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-15T04:00:02.622Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773460802169",
|
||||||
|
"date": "2026-03-14",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 71075,
|
||||||
|
"ETH": 2101,
|
||||||
|
"SOL": 88
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-14T04:00:02.169Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773374402444",
|
||||||
|
"date": "2026-03-13",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 71398,
|
||||||
|
"ETH": 2118,
|
||||||
|
"SOL": 89
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-13T04:00:02.444Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773288001561",
|
||||||
|
"date": "2026-03-12",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 69443,
|
||||||
|
"ETH": 2023,
|
||||||
|
"SOL": 84
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-12T04:00:01.561Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773201601829",
|
||||||
|
"date": "2026-03-11",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 69558,
|
||||||
|
"ETH": 2020,
|
||||||
|
"SOL": 85
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-11T04:00:01.829Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773115201870",
|
||||||
|
"date": "2026-03-10",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 69489,
|
||||||
|
"ETH": 2027,
|
||||||
|
"SOL": 85
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-10T04:00:01.870Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1773028802619",
|
||||||
|
"date": "2026-03-09",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 67179,
|
||||||
|
"ETH": 1976,
|
||||||
|
"SOL": 83
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-09T04:00:02.620Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772942402314",
|
||||||
|
"date": "2026-03-08",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 67052,
|
||||||
|
"ETH": 1948,
|
||||||
|
"SOL": 82
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-08T04:00:02.314Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772856002342",
|
||||||
|
"date": "2026-03-07",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 68137,
|
||||||
|
"ETH": 1980,
|
||||||
|
"SOL": 84
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-07T04:00:02.342Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772769602040",
|
||||||
|
"date": "2026-03-06",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 71054,
|
||||||
|
"ETH": 2081,
|
||||||
|
"SOL": 88
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-06T04:00:02.040Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772683202243",
|
||||||
|
"date": "2026-03-05",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 72494,
|
||||||
|
"ETH": 2120,
|
||||||
|
"SOL": 90
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-05T04:00:02.243Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772596802681",
|
||||||
|
"date": "2026-03-04",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 67617,
|
||||||
|
"ETH": 1956,
|
||||||
|
"SOL": 85
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-04T04:00:02.681Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772514120505",
|
||||||
|
"date": "2026-03-03",
|
||||||
|
"weather": {
|
||||||
|
"temp": 9.4,
|
||||||
|
"condition": "light rain",
|
||||||
|
"wind": 6.4
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-03T05:02:00.505Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772510401919",
|
||||||
|
"date": "2026-03-03",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 68359,
|
||||||
|
"ETH": 2008,
|
||||||
|
"SOL": 86
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-03-03T04:00:01.919Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772254807319",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"weather": {
|
||||||
|
"temp": 12,
|
||||||
|
"condition": "clear"
|
||||||
|
},
|
||||||
|
"priorities": [],
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-02-28T05:00:07.319Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "morning-1772251202213",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"weather": "Check weather",
|
||||||
|
"market": {
|
||||||
|
"BTC": 65787,
|
||||||
|
"ETH": 1923,
|
||||||
|
"SOL": 81
|
||||||
|
},
|
||||||
|
"priorities": [
|
||||||
|
"Focus on SiteMente revenue",
|
||||||
|
"Fix voice widget API",
|
||||||
|
"Get first client"
|
||||||
|
],
|
||||||
|
"goal": "$3,000/month",
|
||||||
|
"leads": [],
|
||||||
|
"created_at": "2026-02-28T04:00:02.213Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -36,9 +36,9 @@
|
|||||||
"priority": "critical"
|
"priority": "critical"
|
||||||
},
|
},
|
||||||
"preInstructions": "Report any issues immediately to Horus",
|
"preInstructions": "Report any issues immediately to Horus",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"runCount": 2,
|
"runCount": 3,
|
||||||
"lastRun": "2026-02-27T17:53:03.836Z"
|
"lastRun": "2026-02-27T22:04:10.446Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "seshat-content-pipeline",
|
"id": "seshat-content-pipeline",
|
||||||
@@ -80,7 +80,8 @@
|
|||||||
},
|
},
|
||||||
"preInstructions": "Focus on Benalmádena and Costa del Sol area",
|
"preInstructions": "Focus on Benalmádena and Costa del Sol area",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"runCount": 0
|
"runCount": 1,
|
||||||
|
"lastRun": "2026-02-27T22:04:20.745Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "thoth-trading-market",
|
"id": "thoth-trading-market",
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "research-001",
|
||||||
|
"name": "OpenClaw Top Skills",
|
||||||
|
"query": "OpenClaw best skills 2024 trending github",
|
||||||
|
"results_count": 20,
|
||||||
|
"created_at": "2026-03-02T15:30:00Z",
|
||||||
|
"saved": true,
|
||||||
|
"summary": "Top: awesome-openclaw-skills, VoltAgent, BankrBot skills, security skills",
|
||||||
|
"results": [
|
||||||
|
{"title": "awesome-openclaw-skills (913 skills)", "url": "https://github.com/sundial-org/awesome-openclaw-skills", "score": 0.9999, "content": "A curated collection of the top agent skills from OpenClaw, regularly updated with the most popular and useful skills."},
|
||||||
|
{"title": "VoltAgent/awesome-openclaw-skills (5400+ skills)", "url": "https://github.com/VoltAgent/awesome-openclaw-skills", "score": 0.9998, "content": "The awesome collection of OpenClaw skills. 5400+ skills filtered and categorized from the official OpenClaw Skills Registry."},
|
||||||
|
{"title": "OpenClaw Skills Directory (10,000+ skills)", "url": "https://openclawskills.best/", "score": 0.9989, "content": "OpenClaw Skills Directory - 10,000+ Skills, Updated Daily."},
|
||||||
|
{"title": "BankrBot/openclaw-skills", "url": "https://github.com/BankrBot/openclaw-skills", "score": 0.9987, "content": "Bankr Skills equip builders with plug-and-play tools to build more powerful agents."},
|
||||||
|
{"title": "Security Skills - ClawSec", "url": "https://github.com/clawsec", "score": 0.9800, "content": "Security skill suite for OpenClaw - drift detection, audits, integrity verification."}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "research-002",
|
||||||
|
"name": "Ollama Free Use Cases",
|
||||||
|
"query": "Ollama free local AI use cases business productivity",
|
||||||
|
"results_count": 20,
|
||||||
|
"created_at": "2026-03-02T15:31:00Z",
|
||||||
|
"saved": true,
|
||||||
|
"summary": "Business automation, customer support, coding assistants, data analysis",
|
||||||
|
"results": [
|
||||||
|
{"title": "Revolutionizing Small Business with Ollama", "url": "https://www.raiaai.com/blogs/revolutionizing-small-business-operations-with-ollama-and-ai-agents", "score": 0.9999, "content": "How small businesses use Ollama for automation, customer service, and productivity."},
|
||||||
|
{"title": "Complete Guide to Local AI Models", "url": "https://www.thundercompute.com/blog/what-is-ollama-run-ai-models-locally", "score": 0.9999, "content": "Run Llama, Mistral, Codestral locally - zero API costs."},
|
||||||
|
{"title": "Build Your Free Personal AI Agent", "url": "https://atalupadhyay.wordpress.com/2026/03/01/build-your-own-free-personal-ai-agent-using-qwen-3-5-ollama/", "score": 0.9997, "content": "Build AI agents locally with Ollama - no cloud fees."},
|
||||||
|
{"title": "Your Machine Your AI - Local Productivity Stack", "url": "https://blog.dataengineerthings.org/your-machine-your-ai-the-ultimate-local-productivity-stack-with-ollama-7a118f271479", "score": 0.9998, "content": "The ultimate local productivity stack with Ollama."},
|
||||||
|
{"title": "Ollama Advanced Use Cases", "url": "https://www.cohorte.co/blog/ollama-advanced-use-cases-and-integrations", "score": 0.9998, "content": "Advanced use cases and integrations for Ollama."}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "research-003",
|
||||||
|
"name": "AI Agent Tools Alternatives",
|
||||||
|
"query": "AI agent tools integration API Tavily alternative web search automation 2024",
|
||||||
|
"results_count": 20,
|
||||||
|
"created_at": "2026-03-02T15:32:00Z",
|
||||||
|
"saved": true,
|
||||||
|
"summary": "Tavily, Brave, Exa, Serper, Firecrawl, Apify, Scrapeless",
|
||||||
|
"results": [
|
||||||
|
{"title": "Beyond Tavily - Complete Guide to AI Search APIs", "url": "https://websearchapi.ai/blog/tavily-alternatives", "score": 0.6714, "content": "Complete comparison of AI search APIs for agents."},
|
||||||
|
{"title": "Best Tavily Alternatives: Scrapeless", "url": "https://www.scrapeless.com/en/wiki/tavily-alternatives", "score": 0.6514, "content": "Web scraping and data extraction alternatives."},
|
||||||
|
{"title": "7 Free Web Search APIs for AI Agents", "url": "https://www.kdnuggets.com/7-free-web-search-apis-for-ai-agents", "score": 0.5863, "content": "Free options for AI agent web search."},
|
||||||
|
{"title": "Best APIs for Autonomous Agents 2025", "url": "https://fast.io/resources/best-apis-autonomous-agents/", "score": 0.5217, "content": "Top tool recommendations for autonomous agents."},
|
||||||
|
{"title": "Firecrawl - AI Data Extraction", "url": "https://www.firecrawl.dev/blog/tavily-alternatives", "score": 0.3870, "content": "Alternative for web scraping and content extraction."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<h1>Your competitors are using AI. Here's proof.</h1>
|
||||||
|
|
||||||
|
<p>Hi {{ contact.FIRSTNAME }},</p>
|
||||||
|
|
||||||
|
<p>It's been a while since we last reached out. We've been busy building AI agents that are transforming how local businesses handle calls, leads, and websites.</p>
|
||||||
|
|
||||||
|
<p>Here's what businesses like yours and your competitors are doing online right now:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>✅ AI agents answering every call - 24/7, never miss a lead</li>
|
||||||
|
<li>✅ Smart chatbots qualifying website visitors - instant responses</li>
|
||||||
|
<li>✅ Competitor monitoring - know what rivals are doing before they do it</li>
|
||||||
|
<li>✅ Automated lead follow-up - never let a prospect go cold</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>We're not sharing this to alarm you.</p>
|
||||||
|
|
||||||
|
<p>We're sharing it because this is now the baseline. This is what average looks like in 2026.</p>
|
||||||
|
|
||||||
|
<h2>Our offer:</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Single Agent:</strong> $1,997 setup + $797/month</li>
|
||||||
|
<li><strong>Multi-Agent:</strong> $4,997 setup + $1,797/month</li>
|
||||||
|
<li><strong>Founding Client Bonus:</strong> Reputation Manager ($297/mo value) FREE</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><strong>60-Day Guarantee:</strong> If you don't see results, next month is free.</p>
|
||||||
|
|
||||||
|
<p><strong>Founding Client Pricing closes April 30, 2026.</strong></p>
|
||||||
|
|
||||||
|
<!-- Button-style CTA -->
|
||||||
|
<table border="0" cellspacing="0" cellpadding="0" style="margin-top:16px; margin-bottom:24px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" bgcolor="#4F46E5" style="border-radius:4px;">
|
||||||
|
<a href="https://hostpioneers.com/?utm_source=brevo&utm_campaign=G-LBL267Q2RW&utm_medium=email#pricing"
|
||||||
|
style="display:inline-block; padding:12px 24px; font-family:Arial, Helvetica, sans-serif; font-size:16px; color:#FFFFFF; text-decoration:none;">
|
||||||
|
See Pricing
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>If you're curious what this would look like for your business, just hit reply and tell us what you do—we'll recommend the best setup in plain English.</p>
|
||||||
|
|
||||||
|
<p><strong>The HostPioneers Team</strong></p>
|
||||||
|
|
||||||
|
<p>💡 <strong>Need a new website?</strong> We build sites with AI agents included.</p>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<br><br><br><br><br><br>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#FFFFFF; border-top:1px solid #E5E5E5;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" style="padding-top:20px; padding-bottom:20px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" style="color:#606060; font-family:Helvetica, Arial, sans-serif; font-size:11px; line-height:150%; padding-right:20px; padding-bottom:5px; padding-left:20px; text-align:center;">
|
||||||
|
This email was sent to {{ contact.EMAIL }}<br>
|
||||||
|
<a href="{{ unsubscribe }}" style="color:#404040 !important;">unsubscribe from this list</a>
|
||||||
|
|
||||||
|
<a href="{{ update_profile }}" style="color:#404040 !important;">update subscription preferences</a>
|
||||||
|
<br><br>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width: 480px){
|
||||||
|
table td{font-size:14px !important;}
|
||||||
|
table td a{display:block !important; margin-top:10px !important;}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
# Hookd - Chrome Extension
|
||||||
|
|
||||||
|
Save, organize and AI-sort your X (Twitter) content.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📥 **DM Organizer** - Extract and categorize DMs
|
||||||
|
- 🐦 **Feed Saver** - Checkmark posts to save in bulk
|
||||||
|
- 🏷️ **Custom Categories** - AI, News, Ideas, Memes, Other
|
||||||
|
- 🤖 **AI Processing Ready** - Connect to OpenClaw for AI analysis
|
||||||
|
- 👥 **Contact Lists** - Quick share to friends/family lists
|
||||||
|
- 💾 **Export** - Save your data anytime
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Open Chrome and go to `chrome://extensions/`
|
||||||
|
2. Enable "Developer mode" (toggle in top right)
|
||||||
|
3. Click "Load unpacked"
|
||||||
|
4. Select the `hookd` folder
|
||||||
|
5. Click the extension icon and pin it to toolbar
|
||||||
|
|
||||||
|
## OpenClaw Integration
|
||||||
|
|
||||||
|
To enable AI processing:
|
||||||
|
|
||||||
|
1. Install OpenClaw Browser Relay extension
|
||||||
|
2. Configure connection in extension settings (⚙️ tab)
|
||||||
|
3. AI will automatically:
|
||||||
|
- Read saved content
|
||||||
|
- Suggest categories
|
||||||
|
- Score by relevance
|
||||||
|
- Delete spam
|
||||||
|
|
||||||
|
## Categories
|
||||||
|
|
||||||
|
- 💡 **AI** - AI tools, news, developments
|
||||||
|
- 📰 **News** - Breaking news, trends
|
||||||
|
- 💡 **Ideas** - Inspiration, ideas, concepts
|
||||||
|
- 😂 **Memes** - Fun content
|
||||||
|
- 📁 **Other** - Uncategorized
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Saving DMs
|
||||||
|
1. Open X DMs
|
||||||
|
2. Hover over a message
|
||||||
|
3. Click 📌 to save
|
||||||
|
|
||||||
|
### Saving Feed Posts
|
||||||
|
1. Open X home/timeline
|
||||||
|
2. Hover over any tweet
|
||||||
|
3. Click the ○ checkbox on left
|
||||||
|
4. Select multiple tweets
|
||||||
|
5. Click "Save to Hookd" in floating bar
|
||||||
|
|
||||||
|
### Viewing Saved
|
||||||
|
1. Click Hookd extension icon
|
||||||
|
2. Go to 💾 Saved tab
|
||||||
|
|
||||||
|
### AI Processing
|
||||||
|
1. Go to 🤖 AI tab
|
||||||
|
2. Click "Process All with AI"
|
||||||
|
3. AI will analyze and categorize
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `manifest.json` - Extension config
|
||||||
|
- `popup.html/js` - Popup UI
|
||||||
|
- `background.js` - Service worker
|
||||||
|
- `content.js/css` - X.com page interaction
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] Connect to OpenClaw Browser Relay
|
||||||
|
- [ ] Add X API integration (alternative to scraping)
|
||||||
|
- [ ] FB/IG support
|
||||||
|
- [ ] Cloud sync
|
||||||
|
- [ ] Mobile companion
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Service Worker - Hookd Extension
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'hookd_items';
|
||||||
|
const SETTINGS_KEY = 'hookd_settings';
|
||||||
|
const LISTS_KEY = 'hookd_lists';
|
||||||
|
|
||||||
|
const defaultSettings = {
|
||||||
|
categories: ['AI', 'News', 'Ideas', 'Memes', 'Other'],
|
||||||
|
userHandle: 'HaithamEKhalifa',
|
||||||
|
relayEnabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultLists = [
|
||||||
|
{ id: 'dev', name: 'Dev Friends', emoji: '👨💻', contacts: [] },
|
||||||
|
{ id: 'memes', name: 'Meme Buddies', emoji: '😂', contacts: [] },
|
||||||
|
{ id: 'business', name: 'Business', emoji: '💼', contacts: [] },
|
||||||
|
{ id: 'family', name: 'Family', emoji: '👨👩👧', contacts: [] }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log('Hookd installed');
|
||||||
|
chrome.storage.local.set({
|
||||||
|
[STORAGE_KEY]: [],
|
||||||
|
[SETTINGS_KEY]: defaultSettings,
|
||||||
|
[LISTS_KEY]: defaultLists
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
// Get items
|
||||||
|
if (message.type === 'GET_ITEMS') {
|
||||||
|
chrome.storage.local.get([STORAGE_KEY], (r) => sendResponse(r[STORAGE_KEY] || []));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get lists
|
||||||
|
if (message.type === 'GET_LISTS') {
|
||||||
|
chrome.storage.local.get([LISTS_KEY], (r) => sendResponse(r[LISTS_KEY] || defaultLists));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add contact to list
|
||||||
|
if (message.type === 'ADD_CONTACT') {
|
||||||
|
chrome.storage.local.get([LISTS_KEY], (r) => {
|
||||||
|
const lists = r[LISTS_KEY] || defaultLists;
|
||||||
|
const list = lists.find(l => l.id === message.listId);
|
||||||
|
if (list && !list.contacts.find(c => c.username === message.contact.username)) {
|
||||||
|
list.contacts.push(message.contact);
|
||||||
|
chrome.storage.local.set({ [LISTS_KEY]: lists });
|
||||||
|
}
|
||||||
|
sendResponse({ success: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove contact from list
|
||||||
|
if (message.type === 'REMOVE_CONTACT') {
|
||||||
|
chrome.storage.local.get([LISTS_KEY], (r) => {
|
||||||
|
const lists = r[LISTS_KEY] || defaultLists;
|
||||||
|
const list = lists.find(l => l.id === message.listId);
|
||||||
|
if (list) {
|
||||||
|
list.contacts = list.contacts.filter(c => c.username !== message.username);
|
||||||
|
chrome.storage.local.set({ [LISTS_KEY]: lists });
|
||||||
|
}
|
||||||
|
sendResponse({ success: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new list
|
||||||
|
if (message.type === 'CREATE_LIST') {
|
||||||
|
chrome.storage.local.get([LISTS_KEY], (r) => {
|
||||||
|
const lists = r[LISTS_KEY] || defaultLists;
|
||||||
|
lists.push({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: message.name,
|
||||||
|
emoji: message.emoji || '📌',
|
||||||
|
contacts: []
|
||||||
|
});
|
||||||
|
chrome.storage.local.set({ [LISTS_KEY]: lists });
|
||||||
|
sendResponse({ success: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete list
|
||||||
|
if (message.type === 'DELETE_LIST') {
|
||||||
|
chrome.storage.local.get([LISTS_KEY], (r) => {
|
||||||
|
let lists = r[LISTS_KEY] || defaultLists;
|
||||||
|
lists = lists.filter(l => l.id !== message.listId);
|
||||||
|
chrome.storage.local.set({ [LISTS_KEY]: lists });
|
||||||
|
sendResponse({ success: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share item to list
|
||||||
|
if (message.type === 'SHARE_TO_LIST') {
|
||||||
|
chrome.storage.local.get([STORAGE_KEY], (r) => {
|
||||||
|
const items = r[STORAGE_KEY] || [];
|
||||||
|
// Mark item as shared
|
||||||
|
const item = items.find(i => i.id === message.itemId);
|
||||||
|
if (item) {
|
||||||
|
item.sharedTo = item.sharedTo || [];
|
||||||
|
item.sharedTo.push(message.listId);
|
||||||
|
chrome.storage.local.set({ [STORAGE_KEY]: items });
|
||||||
|
}
|
||||||
|
sendResponse({ success: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save item
|
||||||
|
if (message.type === 'SAVE_ITEM') {
|
||||||
|
chrome.storage.local.get([STORAGE_KEY], (r) => {
|
||||||
|
const items = r[STORAGE_KEY] || [];
|
||||||
|
const newItem = {
|
||||||
|
id: Date.now(),
|
||||||
|
...message.data,
|
||||||
|
type: message.data.source || 'dm',
|
||||||
|
saved: false,
|
||||||
|
aiProcessed: false,
|
||||||
|
sharedTo: [],
|
||||||
|
time: new Date().toISOString()
|
||||||
|
};
|
||||||
|
items.unshift(newItem);
|
||||||
|
chrome.storage.local.set({ [STORAGE_KEY]: items });
|
||||||
|
sendResponse({ success: true, item: newItem });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete items
|
||||||
|
if (message.type === 'DELETE_ITEMS') {
|
||||||
|
chrome.storage.local.get([STORAGE_KEY], (r) => {
|
||||||
|
const items = r[STORAGE_KEY] || [];
|
||||||
|
const remaining = items.filter(i => !message.ids.includes(i.id));
|
||||||
|
chrome.storage.local.set({ [STORAGE_KEY]: remaining });
|
||||||
|
sendResponse({ success: true, deleted: message.ids.length });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update item
|
||||||
|
if (message.type === 'UPDATE_ITEM') {
|
||||||
|
chrome.storage.local.get([STORAGE_KEY], (r) => {
|
||||||
|
const items = r[STORAGE_KEY] || [];
|
||||||
|
const idx = items.findIndex(i => i.id === message.data.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
items[idx] = { ...items[idx], ...message.data };
|
||||||
|
chrome.storage.local.set({ [STORAGE_KEY]: items });
|
||||||
|
}
|
||||||
|
sendResponse({ success: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* Content script styles for X.com */
|
||||||
|
|
||||||
|
/* Hover effect on tweets */
|
||||||
|
[data-testid="tweet"]:hover .hookd-checkbox {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save button hover */
|
||||||
|
.hookd-save-btn:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification animation */
|
||||||
|
@keyframes hookd-slide-in {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#hookd-floating-bar {
|
||||||
|
animation: hookd-slide-in 0.3s ease-out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Content Script - runs on X.com
|
||||||
|
// Adds "Save to Hookd" option to tweet/post menu
|
||||||
|
|
||||||
|
console.log('Hookd content script loaded');
|
||||||
|
|
||||||
|
// Listen for messages from background
|
||||||
|
chrome.runtime.onMessage.addListener((msg, sender, response) => {
|
||||||
|
if (msg.type === 'GET_SAVED') {
|
||||||
|
response([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add "Save to Hookd" to X's menu
|
||||||
|
function addHookdOption() {
|
||||||
|
// Find all "more" buttons (the ... button on tweets/posts)
|
||||||
|
document.querySelectorAll('[data-testid="tweet"] [role="button"], [data-testid="tweet"] [aria-label*="more"], [data-testid="tweet"] [aria-label*="More"]').forEach(menuBtn => {
|
||||||
|
// Check if we already added our option
|
||||||
|
if (menuBtn.closest('[data-testid="tweet"]')?.querySelector('.hookd-menu-option')) continue;
|
||||||
|
|
||||||
|
const tweet = menuBtn.closest('[data-testid="tweet"]');
|
||||||
|
if (!tweet) return;
|
||||||
|
|
||||||
|
// Create save button
|
||||||
|
const saveBtn = document.createElement('div');
|
||||||
|
saveBtn.className = 'hookd-menu-option';
|
||||||
|
saveBtn.style.cssText = `
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #fff;
|
||||||
|
`;
|
||||||
|
saveBtn.innerHTML = '<span style="font-size:18px;">📌</span><span>Save to Hookd</span>';
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
saveTweet(tweet);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the dropdown menu and append
|
||||||
|
const dropdown = menuBtn.closest('[role="menu"]') || menuBtn.closest('div[aria-labelledby]');
|
||||||
|
if (dropdown && !dropdown.querySelector('.hookd-menu-option')) {
|
||||||
|
dropdown.appendChild(saveBtn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also try to find DM more options
|
||||||
|
document.querySelectorAll('[data-testid="DMMessage"] [role="button"]').forEach(btn => {
|
||||||
|
if (btn.textContent.includes('more') || btn.getAttribute('aria-label')?.includes('more')) {
|
||||||
|
const dm = btn.closest('[data-testid="DMMessage"]');
|
||||||
|
if (dm && !dm.querySelector('.hookd-menu-option')) {
|
||||||
|
const saveBtn = document.createElement('div');
|
||||||
|
saveBtn.className = 'hookd-menu-option';
|
||||||
|
saveBtn.style.cssText = `
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
`;
|
||||||
|
saveBtn.innerHTML = '📌 Save to Hookd';
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
saveDM(dm);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert after the clickable area
|
||||||
|
btn.parentElement?.appendChild(saveBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save tweet
|
||||||
|
function saveTweet(tweetElement) {
|
||||||
|
try {
|
||||||
|
// Extract author
|
||||||
|
const authorEl = tweetElement.querySelector('[data-testid="User-Name"] span a[role="link"]') ||
|
||||||
|
tweetElement.querySelector('a[tabindex="-1"]');
|
||||||
|
const author = authorEl?.textContent?.trim()?.replace('@', '') || 'unknown';
|
||||||
|
|
||||||
|
// Extract content
|
||||||
|
const contentEl = tweetElement.querySelector('[data-testid="tweetText"]');
|
||||||
|
const content = contentEl?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
// Extract link
|
||||||
|
const linkEl = tweetElement.querySelector('a[href*="/status/"]');
|
||||||
|
const link = linkEl?.href || '';
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: Date.now(),
|
||||||
|
author: author,
|
||||||
|
content: content.substring(0, 500),
|
||||||
|
link: link,
|
||||||
|
source: 'tweet',
|
||||||
|
time: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to storage via background
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'SAVE_ITEM',
|
||||||
|
data: item
|
||||||
|
}, (response) => {
|
||||||
|
if (response?.success) {
|
||||||
|
showNotification('Saved to Hookd!');
|
||||||
|
} else {
|
||||||
|
showNotification('Failed to save');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Hookd save error:', err);
|
||||||
|
showNotification('Error saving');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save DM
|
||||||
|
function saveDM(dmElement) {
|
||||||
|
try {
|
||||||
|
const contentEl = dmElement.querySelector('[data-testid="dmMessageContent"]');
|
||||||
|
const content = contentEl?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: Date.now(),
|
||||||
|
author: 'DM',
|
||||||
|
content: content.substring(0, 500),
|
||||||
|
source: 'dm',
|
||||||
|
time: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'SAVE_ITEM',
|
||||||
|
data: item
|
||||||
|
}, (response) => {
|
||||||
|
if (response?.success) {
|
||||||
|
showNotification('Saved to Hookd!');
|
||||||
|
} else {
|
||||||
|
showNotification('Failed to save');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Hookd save error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
function showNotification(message) {
|
||||||
|
const notif = document.createElement('div');
|
||||||
|
notif.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #4F46E5;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 99999;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
`;
|
||||||
|
notif.textContent = message;
|
||||||
|
document.body.appendChild(notif);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notif.style.opacity = '0';
|
||||||
|
notif.style.transition = 'opacity 0.3s';
|
||||||
|
setTimeout(() => notif.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for new elements
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
addHookdOption();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start observing
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial scan
|
||||||
|
setTimeout(addHookdOption, 1000);
|
||||||
|
setTimeout(addHookdOption, 3000);
|
||||||
|
|
||||||
|
console.log('Hookd content script initialized');
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 552 B |
Binary file not shown.
|
After Width: | Height: | Size: 121 B |
Binary file not shown.
|
After Width: | Height: | Size: 272 B |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Hookd",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "Save, organize and AI-sort your X content",
|
||||||
|
"permissions": ["storage"],
|
||||||
|
"host_permissions": ["https://x.com/*", "https://twitter.com/*"],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Hookd</title>
|
||||||
|
<style>
|
||||||
|
body { width: 320px; min-height: 450px; font-family: Arial, sans-serif; background: #0f172a; color: #fff; margin: 0; padding: 0; }
|
||||||
|
.header { background: #4F46E5; padding: 14px; text-align: center; }
|
||||||
|
.header h1 { margin: 0; font-size: 18px; }
|
||||||
|
.tabs { display: flex; background: #1e293b; }
|
||||||
|
.tab { flex: 1; padding: 12px 8px; text-align: center; cursor: pointer; font-size: 11px; border-bottom: 2px solid #334155; }
|
||||||
|
.tab:hover { background: #334155; }
|
||||||
|
.tab.active { border-bottom-color: #4F46E5; background: #1e293b; }
|
||||||
|
.content { padding: 14px; }
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
h3 { margin: 0 0 12px 0; font-size: 13px; color: #94a3b8; }
|
||||||
|
.box { background: #1e293b; border-radius: 8px; padding: 12px; margin-bottom: 8px; }
|
||||||
|
.box-list { background: #1e293b; border-radius: 8px; padding: 10px; margin-bottom: 6px; display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.btn { background: #4F46E5; color: white; border: none; padding: 10px 16px; border-radius: 6px; cursor: pointer; width: 100%; margin-top: 8px; font-size: 12px; }
|
||||||
|
.btn-sm { padding: 6px 12px; width: auto; }
|
||||||
|
.btn:hover { background: #5a82f0; }
|
||||||
|
.btn-danger { background: #dc2626; }
|
||||||
|
.btn-danger:hover { background: #ef4444; }
|
||||||
|
input { width: 100%; padding: 10px; background: #0f172a; border: 1px solid #334155; border-radius: 6px; color: white; margin-bottom: 8px; font-size: 12px; box-sizing: border-box; }
|
||||||
|
input::placeholder { color: #64748b; }
|
||||||
|
.delete-btn { background: none; border: none; color: #dc2626; cursor: pointer; font-size: 16px; padding: 4px 8px; }
|
||||||
|
.empty { text-align: center; padding: 30px; color: #64748b; font-size: 12px; }
|
||||||
|
.add-form { display: flex; gap: 6px; margin-top: 10px; }
|
||||||
|
.add-form input { margin: 0; flex: 1; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header"><h1>Hookd</h1></div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" id="tab-inbox">INBOX</div>
|
||||||
|
<div class="tab" id="tab-lists">LISTS</div>
|
||||||
|
<div class="tab" id="tab-settings">SET</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div id="inbox" class="tab-content active">
|
||||||
|
<h3>INBOX <span id="inbox-count">(0)</span></h3>
|
||||||
|
<div id="inbox-list"></div>
|
||||||
|
<div class="empty" id="inbox-empty">No items.</div>
|
||||||
|
<button class="btn btn-sm" id="btn-test-add">+ Test Add Item</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="lists" class="tab-content">
|
||||||
|
<h3>LISTS</h3>
|
||||||
|
<div id="lists-container"></div>
|
||||||
|
<div class="add-form">
|
||||||
|
<input type="text" id="new-list-name" placeholder="New list name...">
|
||||||
|
<button class="btn btn-sm" id="btn-create-list">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings" class="tab-content">
|
||||||
|
<h3>SETTINGS</h3>
|
||||||
|
<div class="box">
|
||||||
|
<input type="text" id="setting-handle" placeholder="@username">
|
||||||
|
<button class="btn" id="btn-save-handle">SAVE HANDLE</button>
|
||||||
|
</div>
|
||||||
|
<div class="box" style="margin-top:12px;">
|
||||||
|
<button class="btn btn-danger" id="btn-clear-all">CLEAR ALL DATA</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// State
|
||||||
|
var items = [];
|
||||||
|
var lists = [
|
||||||
|
{ id: '1', name: 'Dev Friends', contacts: [] },
|
||||||
|
{ id: '2', name: 'Meme Buddies', contacts: [] },
|
||||||
|
{ id: '3', name: 'Business', contacts: [] }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
function loadData() {
|
||||||
|
chrome.storage.local.get(['hookd_items', 'hookd_lists'], function(result) {
|
||||||
|
items = result.hookd_items || [];
|
||||||
|
lists = result.hookd_lists || [
|
||||||
|
{ id: '1', name: 'Dev Friends', contacts: [] },
|
||||||
|
{ id: '2', name: 'Meme Buddies', contacts: [] },
|
||||||
|
{ id: '3', name: 'Business', contacts: [] }
|
||||||
|
];
|
||||||
|
renderAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save items
|
||||||
|
function saveItems() {
|
||||||
|
chrome.storage.local.set({ hookd_items: items });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render all
|
||||||
|
function renderAll() {
|
||||||
|
renderInbox();
|
||||||
|
renderLists();
|
||||||
|
loadHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
function switchTab(tabId) {
|
||||||
|
var contents = document.querySelectorAll('.tab-content');
|
||||||
|
for (var i = 0; i < contents.length; i++) {
|
||||||
|
contents[i].classList.remove('active');
|
||||||
|
}
|
||||||
|
var tabs = document.querySelectorAll('.tab');
|
||||||
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
|
tabs[i].classList.remove('active');
|
||||||
|
}
|
||||||
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
document.getElementById('tab-' + tabId).classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render inbox
|
||||||
|
function renderInbox() {
|
||||||
|
document.getElementById('inbox-count').textContent = '(' + items.length + ')';
|
||||||
|
var list = document.getElementById('inbox-list');
|
||||||
|
var empty = document.getElementById('inbox-empty');
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
list.innerHTML = '';
|
||||||
|
empty.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
empty.style.display = 'none';
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
html += '<div class="box" style="position:relative;">' +
|
||||||
|
'<div style="font-size:10px;color:#64748b;margin-bottom:4px;">@' + (items[i].author || 'unknown') + '</div>' +
|
||||||
|
'<div style="font-size:12px;">' + ((items[i].content || 'No content').substring(0, 60)) + '</div>' +
|
||||||
|
'<button class="delete-btn" data-index="' + i + '">x</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
list.innerHTML = html;
|
||||||
|
|
||||||
|
// Add delete handlers
|
||||||
|
var deleteBtns = list.querySelectorAll('.delete-btn');
|
||||||
|
for (var i = 0; i < deleteBtns.length; i++) {
|
||||||
|
deleteBtns[i].addEventListener('click', function() {
|
||||||
|
var idx = parseInt(this.getAttribute('data-index'));
|
||||||
|
items.splice(idx, 1);
|
||||||
|
saveItems();
|
||||||
|
renderInbox();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test add item
|
||||||
|
function testAddItem() {
|
||||||
|
items.unshift({
|
||||||
|
id: Date.now(),
|
||||||
|
author: 'TestUser',
|
||||||
|
content: 'Test item saved!',
|
||||||
|
time: new Date().toISOString()
|
||||||
|
});
|
||||||
|
saveItems();
|
||||||
|
renderInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render lists
|
||||||
|
function renderLists() {
|
||||||
|
var container = document.getElementById('lists-container');
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < lists.length; i++) {
|
||||||
|
html += '<div class="box-list">' +
|
||||||
|
'<span>' + lists[i].name + ' (' + lists[i].contacts.length + ')</span>' +
|
||||||
|
'<button class="delete-btn" data-list-index="' + i + '">x</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Add delete handlers
|
||||||
|
var deleteBtns = container.querySelectorAll('.delete-btn');
|
||||||
|
for (var i = 0; i < deleteBtns.length; i++) {
|
||||||
|
deleteBtns[i].addEventListener('click', function() {
|
||||||
|
var idx = parseInt(this.getAttribute('data-list-index'));
|
||||||
|
lists.splice(idx, 1);
|
||||||
|
chrome.storage.local.set({ hookd_lists: lists });
|
||||||
|
renderLists();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create list
|
||||||
|
function createList() {
|
||||||
|
var input = document.getElementById('new-list-name');
|
||||||
|
var name = input.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
lists.push({
|
||||||
|
id: String(Date.now()),
|
||||||
|
name: name,
|
||||||
|
contacts: []
|
||||||
|
});
|
||||||
|
chrome.storage.local.set({ hookd_lists: lists });
|
||||||
|
input.value = '';
|
||||||
|
renderLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load handle
|
||||||
|
function loadHandle() {
|
||||||
|
chrome.storage.local.get(['hookd_handle'], function(result) {
|
||||||
|
if (result.hookd_handle) {
|
||||||
|
document.getElementById('setting-handle').value = result.hookd_handle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save handle
|
||||||
|
function saveHandle() {
|
||||||
|
var handle = document.getElementById('setting-handle').value.trim();
|
||||||
|
if (!handle) return;
|
||||||
|
chrome.storage.local.set({ hookd_handle: handle });
|
||||||
|
alert('Saved: ' + handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all
|
||||||
|
function clearAll() {
|
||||||
|
if (!confirm('Delete ALL data?')) return;
|
||||||
|
items = [];
|
||||||
|
lists = [
|
||||||
|
{ id: '1', name: 'Dev Friends', contacts: [] },
|
||||||
|
{ id: '2', name: 'Meme Buddies', contacts: [] },
|
||||||
|
{ id: '3', name: 'Business', contacts: [] }
|
||||||
|
];
|
||||||
|
chrome.storage.local.set({ hookd_items: [], hookd_lists: lists });
|
||||||
|
renderAll();
|
||||||
|
alert('All data cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Tab clicks
|
||||||
|
document.getElementById('tab-inbox').addEventListener('click', function() { switchTab('inbox'); });
|
||||||
|
document.getElementById('tab-lists').addEventListener('click', function() { switchTab('lists'); });
|
||||||
|
document.getElementById('tab-settings').addEventListener('click', function() { switchTab('settings'); });
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
document.getElementById('btn-test-add').addEventListener('click', testAddItem);
|
||||||
|
document.getElementById('btn-create-list').addEventListener('click', createList);
|
||||||
|
document.getElementById('btn-save-handle').addEventListener('click', saveHandle);
|
||||||
|
document.getElementById('btn-clear-all').addEventListener('click', clearAll);
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user