diff --git a/app/amun/page.tsx b/app/amun/page.tsx new file mode 100644 index 0000000..4c035f5 --- /dev/null +++ b/app/amun/page.tsx @@ -0,0 +1,440 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Card from "@/components/ui/Card"; + +const today = () => new Date().toISOString().split("T")[0]; + +type Severity = "low" | "medium" | "high" | "critical"; +type BugStatus = "open" | "closed"; + +interface Bug { + description: string; + severity: Severity; + status: BugStatus; +} + +interface FormState { + date: string; + testsRun: string[]; + bugsFound: Bug[]; + codeQualityScore: number; + recommendations: string[]; +} + +const defaultForm: FormState = { + date: today(), + testsRun: [""], + bugsFound: [], + codeQualityScore: 0, + recommendations: [""], +}; + +const severityColors: Record = { + low: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30", + medium: "text-amber-400 bg-amber-500/10 border-amber-500/30", + high: "text-orange-400 bg-orange-500/10 border-orange-500/30", + critical: "text-red-400 bg-red-500/10 border-red-500/30", +}; + +export default function AmunPage() { + const [form, setForm] = useState(defaultForm); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [errors, setErrors] = useState>({}); + const [loadingDate, setLoadingDate] = useState(false); + const [history, setHistory] = useState([]); + + const loadSessionForDate = useCallback(async (date: string) => { + setLoadingDate(true); + try { + const res = await fetch(`/api/amun?date=${date}`); + const data = await res.json(); + if (data) { + setForm({ + date: data.date, + testsRun: typeof data.testsRun === "string" ? JSON.parse(data.testsRun) : (data.testsRun || [""]), + bugsFound: typeof data.bugsFound === "string" ? JSON.parse(data.bugsFound) : (data.bugsFound || []), + codeQualityScore: data.codeQualityScore || 0, + recommendations: typeof data.recommendations === "string" ? JSON.parse(data.recommendations) : (data.recommendations || [""]), + }); + } else { + setForm({ ...defaultForm, date }); + } + } catch { + setForm({ ...defaultForm, date }); + } finally { + setLoadingDate(false); + } + }, []); + + const loadHistory = useCallback(async () => { + try { + const res = await fetch("/api/amun?limit=10"); + const data = await res.json(); + if (Array.isArray(data)) { + setHistory( + data.map((s) => ({ + date: s.date, + testsRun: typeof s.testsRun === "string" ? JSON.parse(s.testsRun) : (s.testsRun || []), + bugsFound: typeof s.bugsFound === "string" ? JSON.parse(s.bugsFound) : (s.bugsFound || []), + codeQualityScore: s.codeQualityScore || 0, + recommendations: typeof s.recommendations === "string" ? JSON.parse(s.recommendations) : (s.recommendations || []), + })) + ); + } + } catch { + // ignore + } + }, []); + + useEffect(() => { + loadSessionForDate(today()); + loadHistory(); + }, [loadSessionForDate, loadHistory]); + + const validate = (): boolean => { + const errs: Record = {}; + if (form.codeQualityScore < 0 || form.codeQualityScore > 100) { + errs.codeQualityScore = "Score must be between 0 and 100"; + } + setErrors(errs); + return Object.keys(errs).length === 0; + }; + + const handleSave = async () => { + if (!validate()) return; + setSaving(true); + try { + const res = await fetch("/api/amun", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + if (!res.ok) { + const err = await res.json(); + setErrors({ submit: err.error || "Failed to save" }); + } else { + setSaved(true); + setTimeout(() => setSaved(false), 3000); + loadHistory(); + } + } catch { + setErrors({ submit: "Network error. Please try again." }); + } finally { + setSaving(false); + } + }; + + const updateArrayItem = (field: "testsRun" | "recommendations", index: number, value: string) => { + setForm((prev) => { + const arr = [...prev[field]]; + arr[index] = value; + return { ...prev, [field]: arr }; + }); + }; + + const addArrayItem = (field: "testsRun" | "recommendations") => { + setForm((prev) => ({ ...prev, [field]: [...prev[field], ""] })); + }; + + const removeArrayItem = (field: "testsRun" | "recommendations", index: number) => { + setForm((prev) => ({ ...prev, [field]: prev[field].filter((_, i) => i !== index) })); + }; + + const addBug = () => { + setForm((prev) => ({ + ...prev, + bugsFound: [...prev.bugsFound, { description: "", severity: "medium", status: "open" }], + })); + }; + + const updateBug = (index: number, field: keyof Bug, value: string) => { + setForm((prev) => { + const bugs = [...prev.bugsFound]; + bugs[index] = { ...bugs[index], [field]: value as never }; + return { ...prev, bugsFound: bugs }; + }); + }; + + const removeBug = (index: number) => { + setForm((prev) => ({ ...prev, bugsFound: prev.bugsFound.filter((_, i) => i !== index) })); + }; + + const scoreColor = (score: number) => { + if (score >= 80) return "text-emerald-400"; + if (score >= 60) return "text-amber-400"; + if (score >= 40) return "text-orange-400"; + return "text-red-400"; + }; + + const scoreBarColor = (score: number) => { + if (score >= 80) return "bg-emerald-500"; + if (score >= 60) return "bg-amber-500"; + if (score >= 40) return "bg-orange-500"; + return "bg-red-500"; + }; + + const openBugs = form.bugsFound.filter((b) => b.status === "open"); + const closedBugs = form.bugsFound.filter((b) => b.status === "closed"); + + return ( +
+ {/* Header */} +
+
+

+ βš™οΈ Amun Dev Agent +

+

Development testing & code quality dashboard

+
+
+ { + setForm((prev) => ({ ...prev, date: e.target.value })); + loadSessionForDate(e.target.value); + }} + className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-purple-500/50" + /> + +
+
+ + {errors.submit && ( +
+ {errors.submit} +
+ )} + + {/* Stats Row */} +
+
+

Tests Run

+

{form.testsRun.filter((t) => t.trim()).length}

+
+
+

Open Bugs

+

0 ? "text-red-400" : "text-emerald-400"}`}> + {openBugs.length} +

+
+
+

Code Quality

+

+ {form.codeQualityScore}% +

+
+
+ +
+ {/* Code Quality Score */} + + {errors.codeQualityScore && ( +

{errors.codeQualityScore}

+ )} +
+ setForm((p) => ({ ...p, codeQualityScore: parseInt(e.target.value) }))} + className="flex-1 accent-purple-500" + /> +
+ + setForm((p) => ({ + ...p, + codeQualityScore: Math.min(100, Math.max(0, parseInt(e.target.value) || 0)), + })) + } + className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-2 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50 text-center" + /> + % +
+
+
+
+
+ + + {/* Tests Run */} + +
+ {form.testsRun.map((t, i) => ( +
+ β–Έ + updateArrayItem("testsRun", i, e.target.value)} + placeholder={`Test ${i + 1} (e.g. unit:auth, e2e:checkout)`} + className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 font-mono" + /> + {form.testsRun.length > 1 && ( + + )} +
+ ))} + +
+
+ + {/* Bugs Found */} + +
+ {form.bugsFound.length === 0 && ( +

No bugs logged yet.

+ )} + {form.bugsFound.map((bug, i) => ( +
+
+ updateBug(i, "description", e.target.value)} + placeholder="Bug description" + className="flex-1 bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50" + /> + +
+
+ + +
+
+ ))} + +
+
+ + {/* Recommendations */} + +
+ {form.recommendations.map((r, i) => ( +
+ β†’ + updateArrayItem("recommendations", i, e.target.value)} + placeholder={`Recommendation ${i + 1}`} + className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50" + /> + {form.recommendations.length > 1 && ( + + )} +
+ ))} + +
+
+ + {/* Session History */} + {history.length > 0 && ( + +
+ {history.map((s) => ( + + ))} +
+
+ )} + + {/* Save Button */} +
+ +
+
+
+ ); +} diff --git a/app/api/amun/route.ts b/app/api/amun/route.ts new file mode 100644 index 0000000..c261938 --- /dev/null +++ b/app/api/amun/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabase } from "@/lib/supabase"; + +const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc"; +const GENERAL_CHANNEL = "1476261655955378401"; + +function formatAmunBrief(data: any): string { + return `πŸ‘‘ AMUN QA REPORT - ${data.date || new Date().toISOString().split('T')[0]} + +πŸ” TESTS RUN +${(data.tests || []).map((t: string) => `- ${t}`).join('\n')} + +πŸ› BUGS FOUND +${(data.bugs || []).map((b: string) => `- ${b}`).join('\n')} + +πŸ“Š QUALITY SCORE: ${data.quality || 'N/A'} + +πŸ’‘ RECOMMENDATIONS +${(data.recommendations || []).map((r: string) => `- ${r}`).join('\n')}`; +} + +export async function GET() { + const { data, error } = await supabase + .from("amun_sessions") + .select("*") + .order("created_at", { ascending: false }) + .limit(20); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json(data || []); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const { data: insertData, error } = await supabase + .from("amun_sessions") + .insert([{ + date: body.date || new Date().toISOString().split('T')[0], + tests: body.tests, + bugs: body.bugs, + quality: body.quality, + recommendations: body.recommendations + }]); + + if (error) { + console.error("Supabase error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + const discordMessage = formatAmunBrief(body); + await fetch(`https://discord.com/api/v10/channels/${GENERAL_CHANNEL}/messages`, { + method: "POST", + headers: { + "Authorization": `Bot ${DISCORD_BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ content: discordMessage }), + }); + + return NextResponse.json({ success: true, message: "Amun brief saved and sent to Discord" }); + } catch (error) { + return NextResponse.json({ error: "Failed" }, { status: 500 }); + } +} diff --git a/app/api/dashboard/route.ts b/app/api/dashboard/route.ts new file mode 100644 index 0000000..6f59599 --- /dev/null +++ b/app/api/dashboard/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/db"; +import { morningBriefs, eodBriefs, amunSessions } from "@/db/schema"; +import { desc } from "drizzle-orm"; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const days = parseInt(searchParams.get("days") || "30"); + + const [mornings, eods, amuns] = await Promise.all([ + db.select().from(morningBriefs).orderBy(desc(morningBriefs.date)).limit(days), + db.select().from(eodBriefs).orderBy(desc(eodBriefs.date)).limit(days), + db.select().from(amunSessions).orderBy(desc(amunSessions.date)).limit(days), + ]); + + // Parse JSON fields for dashboard consumption + const parsedMornings = mornings.map((b) => ({ + ...b, + marketData: safeJson(b.marketData, {}), + aiNewsHeadlines: safeJson(b.aiNewsHeadlines, []), + skillsToLearn: safeJson(b.skillsToLearn, []), + sitementeStatus: safeJson(b.sitementeStatus, {}), + warmLeads: safeJson(b.warmLeads, []), + dayPriorities: safeJson(b.dayPriorities, []), + skippedTasks: safeJson(b.skippedTasks, []), + })); + + const parsedEods = eods.map((b) => ({ + ...b, + marketUpdate: safeJson(b.marketUpdate, {}), + completedTasks: safeJson(b.completedTasks, []), + tomorrowPriorities: safeJson(b.tomorrowPriorities, []), + councilFeedback: safeJson(b.councilFeedback, {}), + projectProgress: safeJson(b.projectProgress, []), + })); + + const parsedAmuns = amuns.map((s) => ({ + ...s, + testsRun: safeJson(s.testsRun, []), + bugsFound: safeJson(s.bugsFound, []), + recommendations: safeJson(s.recommendations, []), + })); + + return NextResponse.json({ + mornings: parsedMornings, + eods: parsedEods, + amuns: parsedAmuns, + }); + } catch (error) { + console.error("GET /api/dashboard error:", error); + return NextResponse.json({ error: "Failed to fetch dashboard data" }, { status: 500 }); + } +} + +function safeJson(str: string, fallback: T): T { + try { + return JSON.parse(str) as T; + } catch { + return fallback; + } +} diff --git a/app/api/discord/route.ts b/app/api/discord/route.ts new file mode 100644 index 0000000..4cb4462 --- /dev/null +++ b/app/api/discord/route.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * POST /api/discord + * + * Receives a formatted message and forwards it to a Discord webhook. + * The DISCORD_WEBHOOK_URL environment variable must be set for messages to be sent. + * + * Body: + * { + * channel: "morning" | "eod" | "amun", + * source: "horus" | "amun", + * date: "YYYY-MM-DD", + * formatted_text: "Markdown-style message string" + * } + * + * Example curl: + * curl -X POST http://localhost:3000/api/discord \ + * -H "Content-Type: application/json" \ + * -d '{ + * "channel": "morning", + * "source": "horus", + * "date": "2026-02-27", + * "formatted_text": "β˜€ **Morning Brief β€” 27 Feb 2026**\nπŸ“ BenalmΓ‘dena, 22Β°C Sunny\nπŸ“ˆ BTC: $68,000 (+2.5%)\n⚑ Priorities: 5 set" + * }' + * + * Note: Add Authorization: Bearer header when auth is implemented. + */ + +interface DiscordPayload { + channel: "morning" | "eod" | "amun"; + source: "horus" | "amun"; + date: string; + formatted_text: string; +} + +const channelEmoji: Record = { + morning: "β˜€", + eod: "πŸŒ™", + amun: "βš™οΈ", +}; + +const sourceEmoji: Record = { + horus: "πŸ¦…", + amun: "βš™οΈ", +}; + +export async function POST(request: NextRequest) { + try { + const body: DiscordPayload = await request.json(); + + // Validate required fields + if (!body.channel || !body.source || !body.date || !body.formatted_text) { + return NextResponse.json( + { error: "Missing required fields: channel, source, date, formatted_text" }, + { status: 400 } + ); + } + + if (!["morning", "eod", "amun"].includes(body.channel)) { + return NextResponse.json( + { error: "channel must be one of: morning, eod, amun" }, + { status: 400 } + ); + } + + if (!["horus", "amun"].includes(body.source)) { + return NextResponse.json( + { error: "source must be one of: horus, amun" }, + { status: 400 } + ); + } + + const webhookUrl = process.env.DISCORD_WEBHOOK_URL; + + if (!webhookUrl) { + // Webhook not configured β€” log and return success (non-blocking) + console.log( + `[Discord] Webhook not configured. Would have sent to #${body.channel}:`, + body.formatted_text.slice(0, 100) + ); + return NextResponse.json({ + ok: true, + sent: false, + reason: "DISCORD_WEBHOOK_URL not configured", + }); + } + + // Build Discord embed + const embed = { + title: `${channelEmoji[body.channel]} ${body.channel.charAt(0).toUpperCase() + body.channel.slice(1)} Brief β€” ${body.date}`, + description: body.formatted_text, + color: + body.channel === "morning" + ? 0xf59e0b // amber + : body.channel === "eod" + ? 0x3b82f6 // blue + : 0x8b5cf6, // purple + footer: { + text: `${sourceEmoji[body.source]} Sent by ${body.source.charAt(0).toUpperCase() + body.source.slice(1)}`, + }, + timestamp: new Date().toISOString(), + }; + + const discordRes = await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: "Horus & Amun", + avatar_url: "https://cdn.discordapp.com/embed/avatars/0.png", + embeds: [embed], + }), + }); + + if (!discordRes.ok) { + const errText = await discordRes.text(); + console.error("[Discord] Webhook failed:", discordRes.status, errText); + return NextResponse.json( + { error: "Discord webhook failed", status: discordRes.status }, + { status: 502 } + ); + } + + return NextResponse.json({ ok: true, sent: true }); + } catch (error) { + console.error("POST /api/discord error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/api/eod/route.ts b/app/api/eod/route.ts new file mode 100644 index 0000000..089c4a0 --- /dev/null +++ b/app/api/eod/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabase } from "@/lib/supabase"; + +const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc"; +const EOD_CHANNEL = "1476344633406656785"; + +function formatEODBrief(data: any): string { + return `πŸŒ™ END OF DAY - ${data.date || new Date().toISOString().split('T')[0]} + +βœ… COMPLETED TODAY +${(data.completed || []).map((c: string) => `- ${c}`).join('\n')} + +πŸ“Š PROGRESS +${Object.entries(data.progress || {}).map(([k, v]) => `${k}: ${v}`).join('\n')} + +🧠 COUNCIL +${Object.entries(data.council || {}).map(([k, v]) => `- ${k}: ${v}`).join('\n')} + +🎯 TOMORROW +${(data.tomorrow || []).map((t: string) => `- ${t}`).join('\n')}`; +} + +export async function GET() { + const { data, error } = await supabase + .from("eod_briefs") + .select("*") + .order("created_at", { ascending: false }) + .limit(20); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json(data || []); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const { data: insertData, error } = await supabase + .from("eod_briefs") + .insert([{ + date: body.date || new Date().toISOString().split('T')[0], + completed: body.completed, + progress: body.progress, + council: body.council, + tomorrow: body.tomorrow + }]); + + if (error) { + console.error("Supabase error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + const discordMessage = formatEODBrief(body); + await fetch(`https://discord.com/api/v10/channels/${EOD_CHANNEL}/messages`, { + method: "POST", + headers: { + "Authorization": `Bot ${DISCORD_BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ content: discordMessage }), + }); + + return NextResponse.json({ success: true, message: "EOD brief saved and sent to Discord" }); + } catch (error) { + return NextResponse.json({ error: "Failed" }, { status: 500 }); + } +} diff --git a/app/api/morning/route.ts b/app/api/morning/route.ts new file mode 100644 index 0000000..8ccd6bc --- /dev/null +++ b/app/api/morning/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabase } from "@/lib/supabase"; + +const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc"; +const MORNING_CHANNEL = "1476344610493042698"; + +function formatMorningBrief(data: any): string { + return `β˜€οΈ MORNING BRIEF - ${data.date || new Date().toISOString().split('T')[0]} + +🌀️ WEATHER +${data.weather || 'N/A'} + +πŸ“Š MARKET +BTC: $${data.market?.BTC || 'N/A'} (${data.market?.btcChange || '0%'}) +ETH: $${data.market?.ETH || 'N/A'} (${data.market?.ethChange || '0%'}) +SOL: $${data.market?.SOL || 'N/A'} (${data.market?.solChange || '0%'}) + +🎯 PRIORITIES +${(data.priorities || []).map((p: string, i: number) => `${i + 1}. ${p}`).join('\n')} + +πŸ’° GOAL: ${data.goal || 'N/A'} + +πŸ”₯ HOT LEADS +${(data.leads || []).map((l: string) => `- ${l}`).join('\n')}`; +} + +export async function GET() { + const { data, error } = await supabase + .from("morning_briefs") + .select("*") + .order("created_at", { ascending: false }) + .limit(20); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json(data || []); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + // Save to Supabase + const { data, error } = await supabase + .from("morning_briefs") + .insert([{ + date: body.date || new Date().toISOString().split('T')[0], + weather: body.weather, + market: body.market, + priorities: body.priorities, + goal: body.goal, + leads: body.leads + }]); + + if (error) { + console.error("Supabase error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + // Send to Discord + const discordMessage = formatMorningBrief(body); + await fetch(`https://discord.com/api/v10/channels/${MORNING_CHANNEL}/messages`, { + method: "POST", + headers: { + "Authorization": `Bot ${DISCORD_BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ content: discordMessage }), + }); + + return NextResponse.json({ success: true, message: "Morning brief saved and sent to Discord" }); + } catch (error) { + console.error("Error:", error); + return NextResponse.json({ error: "Failed to save brief" }, { status: 500 }); + } +} diff --git a/app/api/youtube-transcript/route.ts b/app/api/youtube-transcript/route.ts new file mode 100644 index 0000000..8dc6e1b --- /dev/null +++ b/app/api/youtube-transcript/route.ts @@ -0,0 +1,108 @@ +import { NextRequest, NextResponse } from "next/server"; +import { spawn } from "child_process"; +import fs from "fs"; +import path from "path"; + +const STORAGE_FILE = "/root/.openclaw/workspace/SiteMente/data/transcripts.json"; + +function getTranscripts() { + if (fs.existsSync(STORAGE_FILE)) { + return JSON.parse(fs.readFileSync(STORAGE_FILE, "utf-8")); + } + return []; +} + +function saveTranscript(data) { + const transcripts = getTranscripts(); + // Check if already exists + const existing = transcripts.findIndex(t => t.videoId === data.videoId); + if (existing >= 0) { + transcripts[existing] = data; + } else { + transcripts.unshift(data); + } + fs.writeFileSync(STORAGE_FILE, JSON.stringify(transcripts, null, 2)); + return transcripts; +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const videoId = searchParams.get("videoId"); + + const transcripts = getTranscripts(); + + if (videoId) { + const found = transcripts.find(t => t.videoId === videoId); + return NextResponse.json(found || { error: "Not found" }); + } + + return NextResponse.json(transcripts); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { action, videoUrl, videoId, transcript, title, categories } = body; + + // Extract video ID if not provided + let vid = videoId; + if (!vid && videoUrl) { + const match = videoUrl.match(/(?:v=|\/)([0-9A-Za-z_-]{11})/); + vid = match ? match[1] : videoUrl; + } + + if (action === "fetch") { + // Run Python script to get transcript + return new Promise((resolve) => { + const scriptPath = "/root/.openclaw/workspace/scripts/youtube_transcript.py"; + const outputFile = `/tmp/transcript_${vid}.txt`; + + const proc = spawn("python3", [scriptPath, vid], { + cwd: "/root/.openclaw/workspace/scripts" + }); + + let output = ""; + let error = ""; + + proc.stdout.on("data", (data) => { output += data.toString(); }); + proc.stderr.on("data", (data) => { error += data.toString(); }); + + proc.on("close", (code) => { + if (fs.existsSync(outputFile)) { + const transcriptText = fs.readFileSync(outputFile, "utf-8"); + resolve(NextResponse.json({ + success: true, + videoId: vid, + transcript: transcriptText + })); + } else { + resolve(NextResponse.json({ + success: false, + error: error || "Could not fetch transcript" + }, { status: 500 })); + } + }); + }); + } + + if (action === "save") { + // Save transcript with metadata + const data = { + videoId: vid, + videoUrl: videoUrl || `https://www.youtube.com/watch?v=${vid}`, + title: title || `Video ${vid}`, + transcript: transcript || "", + categories: categories || [], + savedAt: new Date().toISOString() + }; + + const updated = saveTranscript(data); + return NextResponse.json({ success: true, transcripts: updated }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); + + } catch (error) { + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/demos/page.tsx b/app/demos/page.tsx index 877d895..a528d1a 100644 --- a/app/demos/page.tsx +++ b/app/demos/page.tsx @@ -5,7 +5,7 @@ import { useSearchParams, useRouter, usePathname } from "next/navigation"; import Image from "next/image"; import { motion } from "framer-motion"; import PaymentButton from "@/components/stripe/PaymentButton"; -import SiteMenteVoiceWidget from "@/components/SiteMenteVoiceWidget"; +// import SiteMenteVoiceWidget from "@/components/SiteMenteVoiceWidget"; type Language = "es" | "en"; type Vertical = "real-estate" | "restaurant" | "clinic" | "home-services"; @@ -532,11 +532,11 @@ function DemosContent() {

{t.footer}

- {/* Voice AI Widget */} - + /> */}
); } diff --git a/app/eod/page.tsx b/app/eod/page.tsx new file mode 100644 index 0000000..f595ebf --- /dev/null +++ b/app/eod/page.tsx @@ -0,0 +1,444 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Card from "@/components/ui/Card"; + +const today = () => new Date().toISOString().split("T")[0]; + +interface MarketUpdate { + btc: string; + eth: string; + sol: string; + btcChange: string; + ethChange: string; + solChange: string; +} + +interface CompletedTask { + task: string; + completed: boolean; +} + +interface ProjectProgress { + project: string; + progress: number; +} + +interface CouncilFeedback { + horus: string; + amun: string; + [key: string]: string; +} + +interface FormState { + date: string; + marketUpdate: MarketUpdate; + completedTasks: CompletedTask[]; + tomorrowPriorities: string[]; + councilFeedback: CouncilFeedback; + projectProgress: ProjectProgress[]; +} + +const defaultProjects = ["SiteMente", "HolaCompi", "WandVoice"]; + +const defaultForm: FormState = { + date: today(), + marketUpdate: { btc: "", eth: "", sol: "", btcChange: "", ethChange: "", solChange: "" }, + completedTasks: [{ task: "", completed: true }], + tomorrowPriorities: ["", "", "", "", ""], + councilFeedback: { horus: "", amun: "" }, + projectProgress: defaultProjects.map((p) => ({ project: p, progress: 0 })), +}; + +export default function EodBriefPage() { + const [form, setForm] = useState(defaultForm); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [errors, setErrors] = useState>({}); + const [loadingDate, setLoadingDate] = useState(false); + const [extraAgents, setExtraAgents] = useState([]); + + const loadBriefForDate = useCallback(async (date: string) => { + setLoadingDate(true); + try { + const res = await fetch(`/api/eod?date=${date}`); + const data = await res.json(); + if (data) { + const mu = typeof data.marketUpdate === "string" ? JSON.parse(data.marketUpdate) : (data.marketUpdate || defaultForm.marketUpdate); + const ct = typeof data.completedTasks === "string" ? JSON.parse(data.completedTasks) : (data.completedTasks || defaultForm.completedTasks); + const tp = typeof data.tomorrowPriorities === "string" ? JSON.parse(data.tomorrowPriorities) : (data.tomorrowPriorities || defaultForm.tomorrowPriorities); + const cf = typeof data.councilFeedback === "string" ? JSON.parse(data.councilFeedback) : (data.councilFeedback || defaultForm.councilFeedback); + const pp = typeof data.projectProgress === "string" ? JSON.parse(data.projectProgress) : (data.projectProgress || defaultForm.projectProgress); + + setForm({ date: data.date, marketUpdate: mu, completedTasks: ct, tomorrowPriorities: tp, councilFeedback: cf, projectProgress: pp }); + + // Detect extra agents + const extras = Object.keys(cf).filter((k) => k !== "horus" && k !== "amun"); + setExtraAgents(extras); + } else { + setForm({ ...defaultForm, date }); + setExtraAgents([]); + } + } catch { + setForm({ ...defaultForm, date }); + } finally { + setLoadingDate(false); + } + }, []); + + useEffect(() => { + loadBriefForDate(today()); + }, [loadBriefForDate]); + + const validate = (): boolean => { + const errs: Record = {}; + if (!form.completedTasks.some((t) => t.task.trim())) errs.completedTasks = "At least one completed task is required"; + if (!form.tomorrowPriorities.some((p) => p.trim())) errs.tomorrowPriorities = "At least one priority for tomorrow is required"; + setErrors(errs); + return Object.keys(errs).length === 0; + }; + + const handleSave = async () => { + if (!validate()) return; + setSaving(true); + try { + const res = await fetch("/api/eod", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + if (!res.ok) { + const err = await res.json(); + setErrors({ submit: err.error || "Failed to save" }); + } else { + setSaved(true); + setTimeout(() => setSaved(false), 3000); + } + } catch { + setErrors({ submit: "Network error. Please try again." }); + } finally { + setSaving(false); + } + }; + + const updateTask = (index: number, field: keyof CompletedTask, value: string | boolean) => { + setForm((prev) => { + const tasks = [...prev.completedTasks]; + tasks[index] = { ...tasks[index], [field]: value }; + return { ...prev, completedTasks: tasks }; + }); + }; + + const addTask = () => { + setForm((prev) => ({ ...prev, completedTasks: [...prev.completedTasks, { task: "", completed: true }] })); + }; + + const removeTask = (index: number) => { + setForm((prev) => ({ ...prev, completedTasks: prev.completedTasks.filter((_, i) => i !== index) })); + }; + + const updatePriority = (index: number, value: string) => { + setForm((prev) => { + const arr = [...prev.tomorrowPriorities]; + arr[index] = value; + return { ...prev, tomorrowPriorities: arr }; + }); + }; + + const addPriority = () => { + setForm((prev) => ({ ...prev, tomorrowPriorities: [...prev.tomorrowPriorities, ""] })); + }; + + const removePriority = (index: number) => { + setForm((prev) => ({ ...prev, tomorrowPriorities: prev.tomorrowPriorities.filter((_, i) => i !== index) })); + }; + + const updateProgress = (index: number, value: number) => { + setForm((prev) => { + const pp = [...prev.projectProgress]; + pp[index] = { ...pp[index], progress: value }; + return { ...prev, projectProgress: pp }; + }); + }; + + const addProject = () => { + setForm((prev) => ({ ...prev, projectProgress: [...prev.projectProgress, { project: "", progress: 0 }] })); + }; + + const updateProjectName = (index: number, name: string) => { + setForm((prev) => { + const pp = [...prev.projectProgress]; + pp[index] = { ...pp[index], project: name }; + return { ...prev, projectProgress: pp }; + }); + }; + + const updateCouncil = (agent: string, value: string) => { + setForm((prev) => ({ ...prev, councilFeedback: { ...prev.councilFeedback, [agent]: value } })); + }; + + const addAgent = () => { + const name = prompt("Agent name:"); + if (name?.trim()) { + setExtraAgents((prev) => [...prev, name.trim()]); + setForm((prev) => ({ ...prev, councilFeedback: { ...prev.councilFeedback, [name.trim()]: "" } })); + } + }; + + const changeColor = (val: string) => { + const n = parseFloat(val); + if (isNaN(n)) return "text-slate-400"; + return n >= 0 ? "text-emerald-400" : "text-red-400"; + }; + + const progressColor = (p: number) => { + if (p >= 80) return "bg-emerald-500"; + if (p >= 50) return "bg-amber-500"; + if (p >= 25) return "bg-orange-500"; + return "bg-red-500"; + }; + + return ( +
+ {/* Header */} +
+
+

+ πŸŒ™ End-of-Day Brief +

+

Horus + Amun daily debrief

+
+
+ { + setForm((prev) => ({ ...prev, date: e.target.value })); + loadBriefForDate(e.target.value); + }} + className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-blue-500/50" + /> + +
+
+ + {errors.submit && ( +
+ {errors.submit} +
+ )} + + {loadingDate && ( +
+ Loading brief for selected date… +
+ )} + +
+ {/* Market Update */} + +
+ {(["btc", "eth", "sol"] as const).map((coin) => ( +
+ + + setForm((p) => ({ ...p, marketUpdate: { ...p.marketUpdate, [coin]: e.target.value } })) + } + placeholder="$0.00" + className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 mb-2" + /> + + setForm((p) => ({ + ...p, + marketUpdate: { ...p.marketUpdate, [`${coin}Change`]: e.target.value }, + })) + } + placeholder="24h % (e.g. +2.5)" + className={`w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500/50 ${changeColor(form.marketUpdate[`${coin}Change` as keyof MarketUpdate])}`} + /> +
+ ))} +
+
+ + {/* Completed Tasks */} + + {errors.completedTasks && ( +

{errors.completedTasks}

+ )} +
+ {form.completedTasks.map((task, i) => ( +
+ + updateTask(i, "task", e.target.value)} + placeholder={`Task ${i + 1}`} + className={`flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-emerald-500/50 ${ + task.completed ? "text-slate-400 line-through" : "text-slate-200" + }`} + /> + {form.completedTasks.length > 1 && ( + + )} +
+ ))} + +
+
+ + {/* Tomorrow's Priorities */} + + {errors.tomorrowPriorities && ( +

{errors.tomorrowPriorities}

+ )} +
+ {form.tomorrowPriorities.map((p, i) => ( +
+ {i + 1}. + updatePriority(i, e.target.value)} + placeholder={`Priority ${i + 1}`} + className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50" + /> + {form.tomorrowPriorities.length > 3 && ( + + )} +
+ ))} + {form.tomorrowPriorities.length < 7 && ( + + )} +
+
+ + {/* Council Feedback */} + +
+ {(["horus", "amun", ...extraAgents] as string[]).map((agent) => ( +
+ + updateCouncil(agent, e.target.value)} + placeholder={`${agent.charAt(0).toUpperCase() + agent.slice(1)}'s one-sentence feedback`} + className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50" + /> +
+ ))} + +
+
+ + {/* Project Progress */} + +
+ {form.projectProgress.map((proj, i) => ( +
+
+ updateProjectName(i, e.target.value)} + placeholder="Project name" + className="bg-transparent text-sm font-medium text-slate-300 focus:outline-none border-b border-transparent focus:border-blue-500/50 pb-0.5 w-40" + /> +
+ {proj.progress}% + updateProgress(i, Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))} + className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded px-2 py-1 text-xs text-slate-300 focus:outline-none focus:border-blue-500/50 text-center" + /> +
+
+
+
+
+
+ ))} + +
+ + + {/* Save Button */} +
+ +
+
+
+ ); +} diff --git a/app/mission-control/briefs/page.tsx b/app/mission-control/briefs/page.tsx new file mode 100644 index 0000000..97243ad --- /dev/null +++ b/app/mission-control/briefs/page.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export default function MissionBriefsPage() { + const [morningBriefs, setMorningBriefs] = useState([]); + const [eodBriefs, setEodBriefs] = useState([]); + const [amunBriefs, setAmunBriefs] = useState([]); + const [loading, setLoading] = useState(true); + const [activeView, setActiveView] = useState<"mission" | "morning" | "eod" | "amun">("mission"); + + useEffect(() => { + fetchAllBriefs(); + }, []); + + const fetchAllBriefs = async () => { + setLoading(true); + try { + const [mRes, eRes, aRes] = await Promise.all([ + fetch("/api/morning"), + fetch("/api/eod"), + fetch("/api/amun") + ]); + const [mData, eData, aData] = await Promise.all([ + mRes.json(), + eRes.json(), + aRes.json() + ]); + setMorningBriefs(Array.isArray(mData) ? mData : []); + setEodBriefs(Array.isArray(eData) ? eData : []); + setAmunBriefs(Array.isArray(aData) ? aData : []); + } catch (e) { + console.error(e); + } + setLoading(false); + }; + + const formatMorning = (data: any) => `β˜€οΈ MORNING - ${data.date} + +🌀️ ${data.weather || ''} + +πŸ“Š BTC: $${data.market?.BTC} (${data.market?.btcChange}) + ETH: $${data.market?.ETH} (${data.market?.ethChange}) + SOL: $${data.market?.SOL} (${data.market?.solChange}) + +🎯 ${data.priorities?.map((p: string, i: number) => `${i + 1}. ${p}`).join('\n ') || ''} + +πŸ’° ${data.goal || ''}`; + + const formatEOD = (data: any) => `πŸŒ™ EOD - ${data.date} + +βœ… ${data.completed?.map((c: string) => `- ${c}`).join('\n') || ''} + +πŸ“Š ${Object.entries(data.progress || {}).map(([k, v]) => `${k}: ${v}`).join('\n') || ''} + +🧠 ${Object.entries(data.council || {}).map(([k, v]) => `- ${k}: ${v}`).join('\n') || ''}`; + + const formatAmun = (data: any) => `πŸ‘‘ AMUN - ${data.date} + +πŸ” ${data.tests?.map((t: string) => `- ${t}`).join('\n') || ''} + +πŸ› ${data.bugs?.map((b: string) => `- ${b}`).join('\n') || ''} + +πŸ“Š ${data.quality || ''} + +πŸ’‘ ${data.recommendations?.map((r: string) => `- ${r}`).join('\n') || ''}`; + + const getLatest = () => { + const latestMorning = morningBriefs[0]; + const latestEOD = eodBriefs[0]; + const latestAmun = amunBriefs[0]; + + return { latestMorning, latestEOD, latestAmun }; + }; + + const { latestMorning, latestEOD, latestAmun } = getLatest(); + + return ( +
+

πŸ“‹ Mission Control Briefs

+ + {/* Navigation */} +
+ + + + +
+ + {loading ? ( +
Loading...
+ ) : ( + <> + {/* Mission View - Latest from all */} + {activeView === "mission" && ( +
+

🎯 Latest Overview

+ + {latestMorning && ( +
+

β˜€οΈ Latest Morning

+
{formatMorning(latestMorning)}
+
+ )} + + {latestEOD && ( +
+

πŸŒ™ Latest EOD

+
{formatEOD(latestEOD)}
+
+ )} + + {latestAmun && ( +
+

πŸ‘‘ Latest Amun

+
{formatAmun(latestAmun)}
+
+ )} + + {!latestMorning && !latestEOD && !latestAmun && ( +
No briefs yet. POST to /api/morning, /api/eod, or /api/amun
+ )} +
+ )} + + {/* Morning Briefs */} + {activeView === "morning" && ( +
+

β˜€οΈ Morning Briefs

+ {morningBriefs.length === 0 ? ( +
No morning briefs yet
+ ) : ( + morningBriefs.map((brief: any, i: number) => ( +
+
{formatMorning(brief)}
+
+ )) + )} +
+ )} + + {/* EOD Briefs */} + {activeView === "eod" && ( +
+

πŸŒ™ End of Day Briefs

+ {eodBriefs.length === 0 ? ( +
No EOD briefs yet
+ ) : ( + eodBriefs.map((brief: any, i: number) => ( +
+
{formatEOD(brief)}
+
+ )) + )} +
+ )} + + {/* Amun Sessions */} + {activeView === "amun" && ( +
+

πŸ‘‘ Amun QA Sessions

+ {amunBriefs.length === 0 ? ( +
No Amun sessions yet
+ ) : ( + amunBriefs.map((brief: any, i: number) => ( +
+
{formatAmun(brief)}
+
+ )) + )} +
+ )} + + )} +
+ ); +} diff --git a/app/mission-control/transcripts/page.tsx b/app/mission-control/transcripts/page.tsx new file mode 100644 index 0000000..f7333ff --- /dev/null +++ b/app/mission-control/transcripts/page.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useState, useEffect } from "react"; +import fs from "fs"; + +const AVAILABLE_CATEGORIES = [ + "SiteMente", + "OpenClaw", + "Trading", + "Marketing", + "Technical", + "Voice AI", + "Business", + "Inspiration", + "Personal Growth" +]; + +const STORAGE_FILE = "/root/.openclaw/workspace/SiteMente/data/transcripts.json"; + +function getTranscripts() { + try { + if (fs.existsSync(STORAGE_FILE)) { + return JSON.parse(fs.readFileSync(STORAGE_FILE, "utf-8")); + } + } catch (e) {} + return []; +} + +export default function TranscriptsPage() { + const [transcripts, setTranscripts] = useState([]); + const [videoUrl, setVideoUrl] = useState(""); + const [transcriptText, setTranscriptText] = useState(""); + const [title, setTitle] = useState(""); + const [selectedCategories, setSelectedCategories] = useState([]); + const [filterCategory, setFilterCategory] = useState("all"); + const [saving, setSaving] = useState(false); + + useEffect(() => { + setTranscripts(getTranscripts()); + }, []); + + const toggleCategory = (cat: string) => { + setSelectedCategories(prev => + prev.includes(cat) + ? prev.filter(c => c !== cat) + : [...prev, cat] + ); + }; + + const saveTranscript = async () => { + if (!videoUrl || !transcriptText) return; + setSaving(true); + + // Extract video ID + const match = videoUrl.match(/(?:v=|\/)([0-9A-Za-z_-]{11})/); + const videoId = match ? match[1] : ""; + + const data = { + videoId, + videoUrl, + title: title || `Video ${videoId}`, + transcript: transcriptText, + categories: selectedCategories, + savedAt: new Date().toISOString() + }; + + // Save directly + const current = getTranscripts(); + const existing = current.findIndex(t => t.videoId === videoId); + if (existing >= 0) { + current[existing] = data; + } else { + current.unshift(data); + } + + fs.writeFileSync(STORAGE_FILE, JSON.stringify(current, null, 2)); + setTranscripts(current); + setSaving(false); + setVideoUrl(""); + setTranscriptText(""); + setTitle(""); + setSelectedCategories([]); + }; + + const filteredTranscripts = filterCategory === "all" + ? transcripts + : transcripts.filter(t => t.categories?.includes(filterCategory)); + + const allCategories = [...new Set(transcripts.flatMap(t => t.categories || []))]; + + return ( +
+

🎬 YouTube Transcripts

+ + {/* Add New Transcript */} +
+

Add Transcript

+ + setVideoUrl(e.target.value)} + placeholder="YouTube URL..." + className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3" + /> + + setTitle(e.target.value)} + placeholder="Title..." + className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3" + /> + +
+

Categories:

+
+ {AVAILABLE_CATEGORIES.map(cat => ( + + ))} +
+
+ +