feat(mission-control): restore MC tabs - temple, office, memory, claude, pdf-viewer, resume, resume-upload, temple-3d, demos

Also added:
- Memory API endpoints
- Briefs API endpoints
- AnveVoice stats API
- Claude spawn API
- TTS proxy
- Cleopatra voice widget
- api-auth middleware
This commit is contained in:
2026-03-23 16:30:44 +01:00
parent d5575b58e3
commit 45af56d9cf
30 changed files with 5092 additions and 715 deletions
+263
View File
@@ -0,0 +1,263 @@
"use client";
import { useState, useEffect, useRef } from "react";
interface Message {
role: "user" | "assistant";
content: string;
timestamp: Date;
}
export default function ClaudeChatPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [apiKey, setApiKey] = useState("");
const [showSettings, setShowSettings] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const savedKey = localStorage.getItem("anthropic_api_key") || "";
setApiKey(savedKey);
if (!savedKey) {
setShowSettings(true);
}
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || !apiKey) return;
const userMessage: Message = {
role: "user",
content: input,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInput("");
setLoading(true);
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true",
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages: [
...messages.map(m => ({ role: m.role, content: m.content })),
{ role: "user", content: input }
],
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || "API Error");
}
const data = await response.json();
const assistantMessage: Message = {
role: "assistant",
content: data.content[0].text,
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error: any) {
const errorMessage: Message = {
role: "assistant",
content: `❌ Error: ${error.message}`,
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
}
setLoading(false);
};
const clearChat = () => {
setMessages([]);
};
const activeTab = messages.length === 0;
return (
<div className="min-h-screen bg-slate-950 flex flex-col">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex items-center justify-between flex-shrink-0">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2">
<span style={{ color: "#ff6154" }}></span>
Claude Code Chat
</h1>
<p className="text-slate-400 text-sm">Direct chat with Claude AI</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearChat}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
disabled={messages.length === 0}
>
🗑 Clear
</button>
<button
onClick={() => setShowSettings(true)}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
>
</button>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-4xl mx-auto space-y-4">
{messages.length === 0 && !loading && (
<div className="text-center py-12">
<div className="text-6xl mb-4">💬</div>
<h2 className="text-xl font-bold text-white mb-2">Chat with Claude</h2>
<p className="text-slate-400 max-w-md mx-auto">
Send a message to start a conversation. Claude will respond directly.
</p>
{!apiKey && (
<button
onClick={() => setShowSettings(true)}
className="mt-4 bg-[#ff6154] hover:bg-[#ff4f3a] text-white px-6 py-3 rounded-lg font-medium"
>
Add API Key to Start
</button>
)}
</div>
)}
{messages.map((msg, i) => (
<div
key={i}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
msg.role === "user"
? "bg-[#ff6154] text-white"
: "bg-slate-800 text-slate-100"
}`}
>
<div className="text-sm whitespace-pre-wrap font-mono leading-relaxed">
{msg.content}
</div>
<div className={`text-xs mt-1 ${msg.role === "user" ? "text-red-200" : "text-slate-500"}`}>
{msg.timestamp.toLocaleTimeString()}
</div>
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="bg-slate-800 rounded-2xl px-4 py-3">
<div className="flex items-center gap-2 text-slate-400">
<div className="animate-spin"></div>
<span>Claude is thinking...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Input */}
<div className="bg-slate-900 border-t border-slate-800 p-4 flex-shrink-0">
<div className="max-w-4xl mx-auto">
<div className="flex gap-3">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder={apiKey ? "Message Claude..." : "Add API key in settings to start"}
disabled={!apiKey}
className="flex-1 bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-white placeholder-slate-500 resize-none focus:outline-none focus:border-slate-500 disabled:opacity-50"
rows={1}
/>
<button
onClick={sendMessage}
disabled={!input.trim() || loading || !apiKey}
className="bg-[#ff6154] hover:bg-[#ff4f3a] disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-3 rounded-xl font-medium transition-colors"
>
Send
</button>
</div>
<p className="text-slate-500 text-xs mt-2 text-center">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</div>
{/* Settings Modal */}
{showSettings && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-xl p-6 w-full max-w-md border border-slate-700">
<h3 className="text-white font-bold text-lg mb-4"> Settings</h3>
<div className="mb-4">
<label className="block text-slate-400 text-sm mb-2">Anthropic API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-slate-500"
/>
<p className="text-slate-500 text-xs mt-1">
Get your key from <span className="text-blue-400">console.anthropic.com</span>
</p>
</div>
<div className="bg-slate-900 rounded-lg p-3 mb-4">
<p className="text-slate-400 text-xs">
<strong>Note:</strong> This chat uses your API key directly.
Your conversations are processed by Anthropic's Claude AI.
</p>
</div>
<div className="flex gap-2 justify-end">
<button
onClick={() => setShowSettings(false)}
className="px-4 py-2 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-600"
>
Cancel
</button>
<button
onClick={() => {
localStorage.setItem("anthropic_api_key", apiKey);
setShowSettings(false);
}}
className="px-4 py-2 bg-[#ff6154] text-white rounded-lg text-sm hover:bg-[#ff4f3a]"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}
+283
View File
@@ -0,0 +1,283 @@
"use client";
import { useState, useEffect } from "react";
interface Tab {
id: string;
name: string;
icon: string;
color: string;
projectPath?: string;
status?: "inactive" | "starting" | "active";
sessionKey?: string;
}
const TABS_STORAGE_KEY = "mc_claude_tabs";
export default function ClaudePage() {
const [tabs, setTabs] = useState<Tab[]>([
{ id: "main", name: "Claude Code", icon: "🤖", color: "#ff6154", status: "inactive" },
]);
const [activeTab, setActiveTab] = useState("main");
const [showSettings, setShowSettings] = useState(false);
const [apiKey, setApiKey] = useState("");
const [message, setMessage] = useState("");
useEffect(() => {
const saved = localStorage.getItem(TABS_STORAGE_KEY);
if (saved) {
try {
setTabs(JSON.parse(saved));
} catch (e) {}
}
const savedKey = localStorage.getItem("anthropic_api_key") || "";
setApiKey(savedKey);
}, []);
const saveTabs = (newTabs: Tab[]) => {
localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(newTabs));
setTabs(newTabs);
};
const spawnSession = async (tabId: string) => {
const tab = tabs.find(t => t.id === tabId);
if (!tab) return;
// Update status to starting
const newTabs = tabs.map(t =>
t.id === tabId ? { ...t, status: "starting" as const } : t
);
saveTabs(newTabs);
try {
const response = await fetch("/api/claude/spawn", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tabId, tabName: tab.name, apiKey }),
});
const data = await response.json();
if (data.success) {
// Update with session info
const finalTabs = tabs.map(t =>
t.id === tabId
? { ...t, status: "active" as const, sessionKey: data.sessionKey }
: t
);
saveTabs(finalTabs);
setMessage(`Session spawned! Ask Horus to connect.`);
} else {
setMessage(`Error: ${data.error}`);
}
} catch (e) {
setMessage(`Connection failed. Try asking Horus directly.`);
}
};
const addTab = () => {
const name = prompt("Project name:");
if (!name) return;
const colors = ["#ff6154", "#3b82f6", "#22c55e", "#f59e0b", "#8b5cf6", "#ec4899"];
const newTab: Tab = {
id: `tab-${Date.now()}`,
name,
icon: "📁",
color: colors[Math.floor(Math.random() * colors.length)],
status: "inactive",
};
saveTabs([...tabs, newTab]);
setActiveTab(newTab.id);
};
const removeTab = (tabId: string) => {
if (tabs.length <= 1) return;
const newTabs = tabs.filter(t => t.id !== tabId);
saveTabs(newTabs);
if (activeTab === tabId) {
setActiveTab(newTabs[0].id);
}
};
const activeTabData = tabs.find(t => t.id === activeTab);
return (
<div className="min-h-screen bg-slate-950 flex flex-col">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">🤖 Claude Code Sessions</h1>
<p className="text-slate-400 text-sm">Spawn coding agents powered by Claude</p>
</div>
<button
onClick={() => setShowSettings(true)}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
>
Settings
</button>
</div>
{/* Tabs Bar */}
<div className="bg-slate-900/50 border-b border-slate-800 px-4 py-2 flex items-center gap-2 overflow-x-auto">
{tabs.map((tab) => (
<div
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg cursor-pointer transition-all text-sm font-medium whitespace-nowrap
${activeTab === tab.id
? "bg-slate-800 text-white border border-slate-600"
: "bg-slate-800/50 text-slate-400 border border-transparent hover:bg-slate-800 hover:text-white"
}
`}
>
<span>{tab.icon}</span>
<span>{tab.name}</span>
{tab.status === "active" && (
<span className="w-2 h-2 rounded-full bg-green-500" title="Active" />
)}
{tab.status === "starting" && (
<span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" title="Starting..." />
)}
{tabs.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }}
className="ml-1 text-slate-500 hover:text-red-400 text-xs"
>
</button>
)}
</div>
))}
<button
onClick={addTab}
className="bg-slate-800/50 hover:bg-slate-800 text-slate-400 hover:text-white px-3 py-2 rounded-lg text-sm border border-dashed border-slate-600"
>
+ Add Project
</button>
</div>
{/* Content */}
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center max-w-lg">
{activeTabData?.status === "inactive" && (
<>
<div className="text-6xl mb-6" style={{ color: activeTabData.color }}>
{activeTabData.icon}
</div>
<h2 className="text-2xl font-bold text-white mb-2">{activeTabData.name}</h2>
<p className="text-slate-400 mb-6">
Start a Claude Code session for this project
</p>
<button
onClick={() => spawnSession(activeTab)}
className="bg-[#ff6154] hover:bg-[#ff4f3a] text-white px-8 py-4 rounded-xl font-bold text-lg transition-all transform hover:scale-105 shadow-lg"
style={{ boxShadow: "0 4px 20px rgba(255, 97, 84, 0.4)" }}
>
🚀 Start Claude Session
</button>
</>
)}
{activeTabData?.status === "starting" && (
<>
<div className="text-6xl mb-6 animate-pulse"></div>
<h2 className="text-2xl font-bold text-white mb-2">Starting Session...</h2>
<p className="text-slate-400">Connecting to Claude Code</p>
</>
)}
{activeTabData?.status === "active" && (
<>
<div className="text-6xl mb-6"></div>
<h2 className="text-2xl font-bold text-white mb-2">Session Active!</h2>
<p className="text-slate-400 mb-4">
Session ID: <code className="text-amber-400">{activeTabData.sessionKey?.slice(0, 12)}...</code>
</p>
<div className="bg-slate-800 rounded-lg p-4 text-left text-sm">
<p className="text-slate-300 mb-2">
💡 <strong>To use this session:</strong>
</p>
<p className="text-slate-400">
Ask Horus to connect to this session for coding tasks.
</p>
</div>
<button
onClick={() => spawnSession(activeTab)}
className="mt-4 bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm"
>
Restart Session
</button>
</>
)}
{message && (
<div className="mt-6 bg-slate-800 rounded-lg p-4 text-left">
<p className="text-slate-300 text-sm">{message}</p>
</div>
)}
</div>
</div>
{/* Instructions */}
<div className="bg-slate-900/50 border-t border-slate-800 px-6 py-4">
<div className="flex items-center justify-between text-sm">
<div className="text-slate-500">
💡 Claude Code sessions run as separate agents. Ask Horus to delegate coding tasks to them.
</div>
<div className="flex items-center gap-2">
{tabs.filter(t => t.status === "active").length > 0 && (
<span className="text-green-400">
{tabs.filter(t => t.status === "active").length} active
</span>
)}
</div>
</div>
</div>
{/* Settings Modal */}
{showSettings && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-slate-800 rounded-xl p-6 w-96 border border-slate-700">
<h3 className="text-white font-bold text-lg mb-4"> Settings</h3>
<div className="mb-4">
<label className="block text-slate-400 text-sm mb-2">Anthropic API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-..."
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm"
/>
<p className="text-slate-500 text-xs mt-1">
Get key from console.anthropic.com
</p>
</div>
<div className="flex gap-2 justify-end">
<button
onClick={() => setShowSettings(false)}
className="px-4 py-2 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-600"
>
Cancel
</button>
<button
onClick={() => {
localStorage.setItem("anthropic_api_key", apiKey);
setShowSettings(false);
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}
+520
View File
@@ -0,0 +1,520 @@
"use client";
import { useState, useEffect } from "react";
// Only 3 main agents show as dots
const MAIN_AGENTS = [
{ id: "horus", name: "Horus", icon: "👁️", color: "#3b82f6" },
{ id: "cleopatra", name: "Cleopatra", icon: "👸", color: "#a855f7" },
{ id: "amun", name: "Amun", icon: "👑", color: "#f59e0b" },
];
// All agents for tag detection
const ALL_AGENTS = [
...MAIN_AGENTS,
{ id: "anubis", name: "Anubis", icon: "🐕", color: "#22c55e" },
{ id: "thoth", name: "Thoth", icon: "📚", color: "#06b6d4" },
{ id: "ptah", name: "Ptah", icon: "🎨", color: "#f97316" },
{ id: "seshat", name: "Seshat", icon: "📝", color: "#ec4899" },
{ id: "hathor", name: "Hathor", icon: "💕", color: "#ef4444" },
{ id: "sekhmet", name: "Sekhmet", icon: "⚔️", color: "#94a3b8" },
{ id: "maat", name: "Maat", icon: "⚖️", color: "#64748b" },
];
const INACTIVE_COLOR = "#4b5563";
interface DayEntry {
agent: string;
agentIcon: string;
agentColor: string;
time: string;
content: string;
tags: string[];
isMainAgent: boolean;
}
interface MemoryDay {
date: string;
entries: DayEntry[];
hasBriefs: { morning: boolean; eod: boolean };
}
interface CalendarDay {
date: string;
day: number;
isCurrentMonth: boolean;
isToday: boolean;
activeMainAgents: string[];
entries: DayEntry[];
hasBriefs: { morning: boolean; eod: boolean };
}
export default function MemoryPage() {
const [memoryDays, setMemoryDays] = useState<MemoryDay[]>([]);
const [selectedDay, setSelectedDay] = useState<MemoryDay | null>(null);
const [currentMonth, setCurrentMonth] = useState(new Date());
const [loading, setLoading] = useState(true);
const [filterAgent, setFilterAgent] = useState<string | null>(null);
const [filterTag, setFilterTag] = useState<string | null>(null);
const [allTags, setAllTags] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [editingEntry, setEditingEntry] = useState<DayEntry | null>(null);
const [editTags, setEditTags] = useState<string>("");
useEffect(() => {
fetchMemory();
}, []);
const fetchMemory = async () => {
setLoading(true);
try {
const res = await fetch("/api/memory/list", {
headers: { 'Authorization': 'Bearer horus-mc-secret-2026' }
});
const data = await res.json();
setMemoryDays(data.days || []);
setAllTags(data.allTags || []);
} catch (e) {
console.error("Failed to fetch memory:", e);
}
setLoading(false);
};
const getAgentInfo = (agentName: string) => {
const lower = agentName.toLowerCase();
const agent = ALL_AGENTS.find(a => lower.includes(a.id) || lower.includes(a.name.toLowerCase()));
return agent || { name: agentName, icon: "🤖", color: INACTIVE_COLOR };
};
const isMainAgent = (agentName: string) => {
const lower = agentName.toLowerCase();
return MAIN_AGENTS.some(a => lower.includes(a.id) || lower.includes(a.name.toLowerCase()));
};
const getCalendarDays = (): CalendarDay[] => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startPad = firstDay.getDay();
const days: CalendarDay[] = [];
const today = new Date().toISOString().split("T")[0];
const monthDays = memoryDays.filter((d) => d.date.startsWith(`${year}-${String(month + 1).padStart(2, "0")}`));
for (let i = startPad - 1; i >= 0; i--) {
const d = new Date(year, month, -i);
days.push({
date: d.toISOString().split("T")[0],
day: d.getDate(),
isCurrentMonth: false,
isToday: false,
activeMainAgents: [],
entries: [],
hasBriefs: { morning: false, eod: false },
});
}
for (let i = 1; i <= lastDay.getDate(); i++) {
const date = `${year}-${String(month + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`;
const memDay = monthDays.find((d) => d.date === date);
const mainAgents = memDay?.entries
.filter((e) => e.isMainAgent)
.map((e) => e.agent.toLowerCase()) || [];
days.push({
date,
day: i,
isCurrentMonth: true,
isToday: date === today,
activeMainAgents: [...new Set(mainAgents)],
entries: memDay?.entries || [],
hasBriefs: memDay?.hasBriefs || { morning: false, eod: false },
});
}
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
const d = new Date(year, month + 1, i);
days.push({
date: d.toISOString().split("T")[0],
day: d.getDate(),
isCurrentMonth: false,
isToday: false,
activeMainAgents: [],
entries: [],
hasBriefs: { morning: false, eod: false },
});
}
return days;
};
const getFilteredEntries = () => {
if (!selectedDay) return [];
let entries = selectedDay.entries;
if (filterAgent) {
entries = entries.filter((e) => e.agent.toLowerCase() === filterAgent.toLowerCase());
}
if (filterTag) {
entries = entries.filter((e) => e.tags.includes(filterTag));
}
if (searchQuery) {
entries = entries.filter((e) =>
e.content.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return entries;
};
const calendarDays = getCalendarDays();
const filteredEntries = getFilteredEntries();
const getMainAgentDots = (activeMainAgents: string[]) => {
return MAIN_AGENTS.map((agent) => ({
agent,
active: activeMainAgents.some((a) => a.includes(agent.id) || a.includes(agent.name.toLowerCase())),
}));
};
const handleEditTags = (entry: DayEntry) => {
setEditingEntry(entry);
setEditTags(entry.tags.join(", "));
};
const saveTags = async () => {
if (!editingEntry || !selectedDay) return;
// In a real app, this would POST to an API to update the file
// For now, just update the local state
const newTags = editTags.split(",").map((t) => t.trim().replace(/^#/, "")).filter(Boolean);
setSelectedDay({
...selectedDay,
entries: selectedDay.entries.map((e) =>
e === editingEntry ? { ...e, tags: newTags } : e
),
});
setEditingEntry(null);
setEditTags("");
// TODO: POST to /api/memory/tags to save
};
return (
<div className="min-h-screen bg-slate-950 flex">
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex-shrink-0">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">📅 Memory Calendar</h1>
<p className="text-slate-400 text-sm">Our unified memory system</p>
</div>
<button
onClick={fetchMemory}
className="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded"
>
Refresh
</button>
</div>
</div>
{/* Calendar */}
<div className="flex-1 overflow-auto p-6">
{/* Month Navigation */}
<div className="flex items-center justify-between mb-4">
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded"
>
</button>
<h2 className="text-xl font-bold text-white">
{currentMonth.toLocaleDateString("en-US", { month: "long", year: "numeric" })}
</h2>
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded"
>
</button>
</div>
{/* Day Headers */}
<div className="grid grid-cols-7 gap-1 mb-1">
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((d) => (
<div key={d} className="text-center text-slate-500 text-sm py-2">
{d}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-1">
{calendarDays.map((day, idx) => {
const dots = getMainAgentDots(day.activeMainAgents);
const isSelected = selectedDay?.date === day.date;
const hasEntries = day.entries.length > 0;
return (
<button
key={idx}
onClick={() => setSelectedDay(day)}
className={`
relative p-2 min-h-[80px] rounded-lg border transition-all
${day.isCurrentMonth ? "bg-slate-800/50 border-slate-700 hover:bg-slate-800" : "bg-slate-900/30 border-slate-800 opacity-50"}
${day.isToday ? "border-amber-500" : ""}
${isSelected ? "border-brand-pink bg-slate-800" : ""}
`}
>
<span className={`text-sm ${day.isCurrentMonth ? "text-white" : "text-slate-600"} ${day.isToday ? "font-bold text-amber-400" : ""}`}>
{day.day}
</span>
{/* Briefs Indicators */}
<div className="flex gap-1 mt-1">
{day.hasBriefs.morning && <span className="text-xs"></span>}
{day.hasBriefs.eod && <span className="text-xs">🌙</span>}
</div>
{/* Main Agent Dots (only 3) */}
<div className="flex gap-1 mt-1 justify-center">
{dots.map(({ agent, active }) => (
<span
key={agent.id}
className="w-3 h-3 rounded-full"
style={{ backgroundColor: active ? agent.color : INACTIVE_COLOR }}
title={`${agent.name}: ${active ? "Active" : "Inactive"}`}
/>
))}
</div>
{/* Tag indicator if has entries */}
{hasEntries && !day.activeMainAgents.length && (
<div className="flex gap-1 mt-1 justify-center">
<span className="text-xs bg-slate-600 text-white px-1 rounded">📝</span>
</div>
)}
</button>
);
})}
</div>
{/* Legend */}
<div className="mt-4 p-4 bg-slate-800/50 rounded-lg border border-slate-700">
<h3 className="text-white font-medium mb-2">Dots = Active Agents</h3>
<div className="flex flex-wrap gap-4">
{MAIN_AGENTS.map((agent) => (
<div key={agent.id} className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: agent.color }} />
<span className="text-slate-400 text-xs">{agent.icon} {agent.name}</span>
</div>
))}
<div className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: INACTIVE_COLOR }} />
<span className="text-slate-400 text-xs">Inactive</span>
</div>
<div className="flex items-center gap-1">
<span className="text-slate-400 text-xs">📝 = Has entries (other agents)</span>
</div>
</div>
</div>
</div>
</div>
{/* Right Sidebar - Day Detail */}
<div className="w-[28rem] bg-slate-900 border-l border-slate-800 flex flex-col overflow-hidden">
{selectedDay ? (
<>
{/* Day Header */}
<div className="p-4 border-b border-slate-800 flex-shrink-0">
<h2 className="text-xl font-bold text-white">
{new Date(selectedDay.date).toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
})}
</h2>
<div className="flex gap-2 mt-2">
{selectedDay.hasBriefs.morning && (
<a
href={`/api/briefs/morning/${selectedDay.date}`}
target="_blank"
className="text-xs bg-amber-500/20 text-amber-400 px-2 py-1 rounded hover:bg-amber-500/30"
>
Morning Brief
</a>
)}
{selectedDay.hasBriefs.eod && (
<a
href={`/api/briefs/eod/${selectedDay.date}`}
target="_blank"
className="text-xs bg-purple-500/20 text-purple-400 px-2 py-1 rounded hover:bg-purple-500/30"
>
🌙 EOD Brief
</a>
)}
</div>
</div>
{/* Filters */}
<div className="p-4 border-b border-slate-800 flex-shrink-0">
<input
type="text"
placeholder="Search entries..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm placeholder-slate-500"
/>
<div className="flex gap-2 mt-2 flex-wrap">
<button
onClick={() => setFilterAgent(null)}
className={`px-2 py-1 text-xs rounded ${!filterAgent ? "bg-brand-pink text-white" : "bg-slate-800 text-slate-400"}`}
>
All
</button>
{MAIN_AGENTS.map((agent) => (
<button
key={agent.id}
onClick={() => setFilterAgent(filterAgent === agent.name ? null : agent.name)}
className={`px-2 py-1 text-xs rounded flex items-center gap-1 ${
filterAgent === agent.name ? "bg-brand-pink text-white" : "bg-slate-800 text-slate-400"
}`}
>
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: agent.color }} />
{agent.icon}
</button>
))}
</div>
{allTags.length > 0 && (
<div className="flex gap-1 mt-2 flex-wrap">
{allTags.slice(0, 8).map((tag) => (
<button
key={tag}
onClick={() => setFilterTag(filterTag === tag ? null : tag)}
className={`px-2 py-0.5 text-xs rounded ${
filterTag === tag ? "bg-amber-500 text-black" : "bg-slate-700 text-slate-400 hover:bg-slate-600"
}`}
>
#{tag}
</button>
))}
</div>
)}
</div>
{/* Entries */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{loading ? (
<div className="text-slate-500 text-center">Loading...</div>
) : filteredEntries.length === 0 ? (
<div className="text-slate-500 text-center">No entries for this day</div>
) : (
filteredEntries.map((entry, idx) => (
<div key={idx} className="bg-slate-800/50 rounded-lg p-3 border border-slate-700">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span
className="w-6 h-6 rounded-full flex items-center justify-center text-sm"
style={{ backgroundColor: entry.agentColor + "33" }}
>
{entry.agentIcon}
</span>
<span className="text-white font-medium text-sm">{entry.agent}</span>
<span className="text-slate-500 text-xs">{entry.time}</span>
</div>
<button
onClick={() => handleEditTags(entry)}
className="text-slate-500 hover:text-white text-xs"
title="Edit tags"
>
🏷
</button>
</div>
<pre className="text-slate-300 text-xs whitespace-pre-wrap font-mono">
{entry.content.length > 250
? entry.content.slice(0, 250) + "..."
: entry.content}
</pre>
{/* Tags */}
<div className="flex gap-1 mt-2 flex-wrap">
{entry.tags.map((tag) => (
<span key={tag} className="px-2 py-0.5 bg-amber-500/20 text-amber-400 text-xs rounded">
#{tag}
</span>
))}
</div>
</div>
))
)}
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-5xl mb-4">📅</div>
<p className="text-slate-500">Select a day to view entries</p>
</div>
</div>
)}
{/* Who's Active */}
<div className="p-4 border-t border-slate-800 flex-shrink-0">
<h3 className="text-slate-400 text-xs uppercase mb-2">Today</h3>
<div className="flex gap-2">
{MAIN_AGENTS.map((agent) => {
const isActive = memoryDays[0]?.entries.some((e) =>
e.agent.toLowerCase().includes(agent.id)
);
return (
<div key={agent.id} className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: isActive ? agent.color : INACTIVE_COLOR }} />
<span className="text-slate-400 text-xs">{agent.icon}</span>
</div>
);
})}
</div>
</div>
</div>
{/* Edit Tags Modal */}
{editingEntry && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-slate-800 rounded-xl p-6 w-96 border border-slate-600">
<h3 className="text-white font-bold mb-4">Edit Tags</h3>
<div className="flex items-center gap-2 mb-2">
<span className="text-xl">{editingEntry.agentIcon}</span>
<span className="text-white">{editingEntry.agent}</span>
</div>
<input
type="text"
value={editTags}
onChange={(e) => setEditTags(e.target.value)}
placeholder="tag1, tag2, tag3"
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white text-sm placeholder-slate-400 mb-4"
/>
<div className="flex gap-2 justify-end">
<button
onClick={() => setEditingEntry(null)}
className="px-4 py-2 bg-slate-700 text-white rounded hover:bg-slate-600"
>
Cancel
</button>
<button
onClick={saveTags}
className="px-4 py-2 bg-brand-pink text-white rounded hover:bg-[#ff7bc0]"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
"use client";
export default function OfficePage() {
return (
<div className="min-h-screen bg-slate-950 flex flex-col">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex-shrink-0">
<h1 className="text-2xl font-bold text-white">🏢 Claw3D Office</h1>
<p className="text-slate-400 text-sm">3D workspace for AI agents</p>
</div>
{/* Claw3D Embed - via Apache proxy over HTTPS */}
<div className="flex-1 relative">
<iframe
src="https://sitemente.com/claw3d/office"
className="absolute inset-0 w-full h-full border-0"
title="Claw3D Office"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
/>
</div>
</div>
);
}
+154
View File
@@ -0,0 +1,154 @@
"use client";
import { useState } from "react";
export default function PDFViewerPage() {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pdfBase64, setPdfBase64] = useState<string | null>(null);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.type !== "application/pdf") {
setError("Please select a PDF file");
return;
}
setLoading(true);
setError(null);
// Read file as base64
const reader = new FileReader();
reader.onload = (event) => {
const base64 = event.target?.result as string;
setPdfBase64(base64);
setPdfUrl(URL.createObjectURL(file));
setLoading(false);
};
reader.onerror = () => {
setError("Failed to read file");
setLoading(false);
};
reader.readAsDataURL(file);
};
const handleUrlSubmit = async () => {
const input = prompt("Enter PDF URL:");
if (!input) return;
setLoading(true);
setError(null);
try {
new URL(input);
setPdfUrl(input);
setPdfBase64(null);
} catch {
setError("Invalid URL");
}
setLoading(false);
};
return (
<div className="min-h-screen bg-slate-950 flex flex-col">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex-shrink-0">
<h1 className="text-2xl font-bold text-white">📄 PDF Viewer</h1>
<p className="text-slate-400 text-sm">View and analyze PDF documents</p>
</div>
{/* Toolbar */}
<div className="bg-slate-900/50 border-b border-slate-800 px-6 py-3 flex-shrink-0">
<div className="flex items-center gap-4">
<label className="cursor-pointer bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">
📁 Upload PDF
<input
type="file"
accept="application/pdf"
onChange={handleFileUpload}
className="hidden"
/>
</label>
<button
onClick={handleUrlSubmit}
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
🔗 Load from URL
</button>
{pdfUrl && (
<a
href={pdfUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Open Original
</a>
)}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{error && (
<div className="bg-red-900/50 border border-red-700 rounded-lg p-4 m-4 max-w-md">
<h3 className="text-red-400 font-bold mb-2">Error</h3>
<p className="text-red-300">{error}</p>
<button
onClick={() => setError(null)}
className="mt-2 bg-red-700 hover:bg-red-600 text-white px-4 py-1 rounded text-sm"
>
Dismiss
</button>
</div>
)}
{!pdfUrl && !loading && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-8xl mb-6 opacity-50">📄</div>
<h2 className="text-xl font-bold text-white mb-2">No PDF Loaded</h2>
<p className="text-slate-400 mb-6">Upload a PDF or enter a URL to view it</p>
<div className="text-slate-500 text-sm space-y-1">
<p> Upload your resume to let Horus analyze it</p>
<p> Supports PDF files up to 50MB</p>
<p> Or load from any public PDF URL</p>
</div>
</div>
</div>
)}
{loading && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin text-6xl mb-4"></div>
<p className="text-slate-400">Loading PDF...</p>
</div>
</div>
)}
{pdfUrl && !loading && (
<div className="flex-1 overflow-auto bg-slate-800 p-4 flex justify-center">
<iframe
src={pdfUrl}
className="w-full h-full min-h-[600px] bg-white shadow-2xl rounded"
title="PDF Viewer"
/>
</div>
)}
</div>
{/* Instructions */}
<div className="bg-slate-900/50 border-t border-slate-800 px-6 py-2 flex-shrink-0">
<p className="text-slate-500 text-xs">
💡 Tip: Upload your resume PDF and Horus can analyze the design to recreate it
</p>
</div>
</div>
);
}
@@ -0,0 +1,81 @@
"use client";
import { useState } from "react";
export default function ResumeUploadPage() {
const [uploading, setUploading] = useState(false);
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
setUploading(true);
const newImages: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const formData = new FormData();
formData.append("file", file);
try {
const res = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const data = await res.json();
if (data.url) {
newImages.push(data.url);
}
} catch (err) {
console.error("Upload failed:", err);
}
}
setUploadedImages([...uploadedImages, ...newImages]);
setUploading(false);
};
return (
<div className="min-h-screen bg-slate-950 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-white mb-6">📤 Upload Resume Images</h1>
<p className="text-slate-400 mb-6">Upload images of your resume design to let Horus analyze and recreate it.</p>
<div className="border-2 border-dashed border-slate-600 rounded-xl p-12 text-center hover:border-slate-500 transition-colors">
<input
type="file"
accept="image/*"
multiple
onChange={handleUpload}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload" className="cursor-pointer">
<div className="text-6xl mb-4">📁</div>
<div className="text-white text-lg mb-2">Click to upload images</div>
<div className="text-slate-500 text-sm">PNG, JPG, GIF up to 10MB</div>
</label>
</div>
{uploading && (
<div className="mt-6 text-center text-slate-400">Uploading...</div>
)}
{uploadedImages.length > 0 && (
<div className="mt-8">
<h2 className="text-white font-bold mb-4">Uploaded Images:</h2>
<div className="space-y-4">
{uploadedImages.map((url, i) => (
<div key={i} className="bg-slate-800 rounded-lg p-4">
<div className="text-slate-400 text-sm mb-2">Image {i + 1}</div>
<div className="bg-slate-700 rounded p-2 text-slate-300 text-sm font-mono break-all">{url}</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
+519
View File
@@ -0,0 +1,519 @@
"use client";
import { useState, useEffect } from "react";
interface PersonalInfo {
name: string;
title: string;
email: string;
phone: string;
location: string;
linkedin: string;
website: string;
}
interface Experience {
id: string;
title: string;
company: string;
location: string;
startDate: string;
endDate: string;
bullets: string[];
}
interface Education {
id: string;
degree: string;
school: string;
location: string;
graduationDate: string;
gpa: string;
honors: string;
}
export default function ResumePage() {
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
const [personal, setPersonal] = useState<PersonalInfo>({
name: "Your Name",
title: "Your Professional Title",
email: "your.email@email.com",
phone: "+1 (555) 000-0000",
location: "City, State",
linkedin: "linkedin.com/in/yourprofile",
website: "yourwebsite.com"
});
const [experiences, setExperiences] = useState<Experience[]>([
{
id: "1",
title: "Job Title",
company: "Company Name",
location: "City, State",
startDate: "Jan 2020",
endDate: "Present",
bullets: [
"Key achievement or responsibility with measurable impact",
"Another accomplishment that demonstrates your skills",
"Additional relevant contribution"
]
}
]);
const [education, setEducation] = useState<Education[]>([
{
id: "1",
degree: "Bachelor of Science in Computer Science",
school: "University Name",
location: "City, State",
graduationDate: "May 2016",
gpa: "3.8/4.0",
honors: "Magna Cum Laude"
}
]);
const [skills, setSkills] = useState<string[]>([
"JavaScript/TypeScript", "React/Next.js", "Node.js", "Python",
"AWS/Cloud Services", "Database Management", "Agile/Scrum", "Git"
]);
const [languages, setLanguages] = useState<{name: string, level: string}[]>([
{ name: "English", level: "Native" },
{ name: "Spanish", level: "Professional" },
{ name: "Arabic", level: "Native" }
]);
const [certifications, setCertifications] = useState<string[]>([
"AWS Solutions Architect",
"Google Cloud Professional"
]);
useEffect(() => {
const saved = localStorage.getItem("haitham_resume_v2");
if (saved) {
try {
const data = JSON.parse(saved);
if (data.personal) setPersonal(data.personal);
if (data.experiences) setExperiences(data.experiences);
if (data.education) setEducation(data.education);
if (data.skills) setSkills(data.skills);
if (data.languages) setLanguages(data.languages);
if (data.certifications) setCertifications(data.certifications);
} catch (e) {}
}
}, []);
useEffect(() => {
localStorage.setItem("haitham_resume_v2", JSON.stringify({
personal, experiences, education, skills, languages, certifications
}));
}, [personal, experiences, education, skills, languages, certifications]);
const addExperience = () => {
setExperiences([...experiences, {
id: Date.now().toString(),
title: "", company: "", location: "", startDate: "", endDate: "", bullets: [""]
}]);
};
const updateExperience = (id: string, field: keyof Experience, value: any) => {
setExperiences(experiences.map(e => e.id === id ? { ...e, [field]: value } : e));
};
const addBullet = (id: string) => {
setExperiences(experiences.map(e =>
e.id === id ? { ...e, bullets: [...e.bullets, ""] } : e
));
};
const updateBullet = (expId: string, index: number, value: string) => {
setExperiences(experiences.map(e =>
e.id === expId ? { ...e, bullets: e.bullets.map((b, i) => i === index ? value : b) } : e
));
};
const removeBullet = (expId: string, index: number) => {
setExperiences(experiences.map(e =>
e.id === expId ? { ...e, bullets: e.bullets.filter((_, i) => i !== index) } : e
));
};
const addEducation = () => {
setEducation([...education, {
id: Date.now().toString(), degree: "", school: "", location: "",
graduationDate: "", gpa: "", honors: ""
}]);
};
const updateEducation = (id: string, field: keyof Education, value: string) => {
setEducation(education.map(e => e.id === id ? { ...e, [field]: value } : e));
};
const handlePrint = () => {
setActiveTab("preview");
setTimeout(() => window.print(), 100);
};
const inputClass = "w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500";
const labelClass = "block text-slate-400 text-xs mb-1 font-medium";
return (
<div className="min-h-screen bg-slate-950 flex flex-col">
{/* Header */}
<div className="bg-slate-900 border-b border-slate-800 px-6 py-4 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">📝 Professional Resume</h1>
<p className="text-slate-400 text-sm">Edit Preview Download PDF</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setActiveTab(activeTab === "edit" ? "preview" : "edit")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === "edit" ? "bg-slate-700 text-white" : "bg-blue-600 text-white"
}`}
>
{activeTab === "edit" ? "👁️ Preview" : "✏️ Edit"}
</button>
<button
onClick={handlePrint}
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
📄 Download PDF
</button>
</div>
</div>
{activeTab === "edit" ? (
<div className="flex-1 overflow-auto p-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* Personal Info */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">👤 Personal Information</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Full Name</label>
<input type="text" value={personal.name} onChange={e => setPersonal({...personal, name: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Professional Title</label>
<input type="text" value={personal.title} onChange={e => setPersonal({...personal, title: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Email</label>
<input type="email" value={personal.email} onChange={e => setPersonal({...personal, email: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Phone</label>
<input type="tel" value={personal.phone} onChange={e => setPersonal({...personal, phone: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>Location</label>
<input type="text" value={personal.location} onChange={e => setPersonal({...personal, location: e.target.value})} className={inputClass} />
</div>
<div>
<label className={labelClass}>LinkedIn</label>
<input type="text" value={personal.linkedin} onChange={e => setPersonal({...personal, linkedin: e.target.value})} className={inputClass} />
</div>
</div>
</div>
{/* Experience */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-bold">💼 Work Experience</h2>
<button onClick={addExperience} className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm">+ Add Position</button>
</div>
{experiences.map((exp) => (
<div key={exp.id} className="bg-slate-900 rounded-lg p-4 mb-4 border border-slate-700">
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<label className={labelClass}>Job Title</label>
<input type="text" value={exp.title} onChange={e => updateExperience(exp.id, "title", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Company</label>
<input type="text" value={exp.company} onChange={e => updateExperience(exp.id, "company", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Location</label>
<input type="text" value={exp.location} onChange={e => updateExperience(exp.id, "location", e.target.value)} className={inputClass} />
</div>
<div className="flex gap-2">
<div className="flex-1">
<label className={labelClass}>Start</label>
<input type="text" value={exp.startDate} onChange={e => updateExperience(exp.id, "startDate", e.target.value)} className={inputClass} placeholder="Jan 2020" />
</div>
<div className="flex-1">
<label className={labelClass}>End</label>
<input type="text" value={exp.endDate} onChange={e => updateExperience(exp.id, "endDate", e.target.value)} className={inputClass} placeholder="Present" />
</div>
</div>
</div>
<div className="mb-2">
<label className={labelClass}>Key Achievements</label>
{exp.bullets.map((bullet, i) => (
<div key={i} className="flex gap-2 mb-2">
<input type="text" value={bullet} onChange={e => updateBullet(exp.id, i, e.target.value)} className={inputClass} placeholder={`Achievement ${i + 1}`} />
{exp.bullets.length > 1 && (
<button onClick={() => removeBullet(exp.id, i)} className="text-red-400 hover:text-red-300 text-sm px-2"></button>
)}
</div>
))}
<button onClick={() => addBullet(exp.id)} className="text-blue-400 hover:text-blue-300 text-xs">+ Add achievement</button>
</div>
</div>
))}
</div>
{/* Education */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-bold">🎓 Education</h2>
<button onClick={addEducation} className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded text-sm">+ Add</button>
</div>
{education.map((edu) => (
<div key={edu.id} className="bg-slate-900 rounded-lg p-4 mb-4 border border-slate-700">
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelClass}>Degree</label>
<input type="text" value={edu.degree} onChange={e => updateEducation(edu.id, "degree", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>School</label>
<input type="text" value={edu.school} onChange={e => updateEducation(edu.id, "school", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Location</label>
<input type="text" value={edu.location} onChange={e => updateEducation(edu.id, "location", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Graduation Date</label>
<input type="text" value={edu.graduationDate} onChange={e => updateEducation(edu.id, "graduationDate", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>GPA (optional)</label>
<input type="text" value={edu.gpa} onChange={e => updateEducation(edu.id, "gpa", e.target.value)} className={inputClass} />
</div>
<div>
<label className={labelClass}>Honors (optional)</label>
<input type="text" value={edu.honors} onChange={e => updateEducation(edu.id, "honors", e.target.value)} className={inputClass} placeholder="Magna Cum Laude" />
</div>
</div>
</div>
))}
</div>
{/* Skills */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">🛠 Technical Skills</h2>
<div className="flex flex-wrap gap-2">
{skills.map((skill, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1">
<input
type="text"
value={skill}
onChange={e => {
const newSkills = [...skills];
newSkills[i] = e.target.value;
setSkills(newSkills);
}}
className="bg-transparent text-white text-sm w-24 focus:outline-none"
/>
<button onClick={() => setSkills(skills.filter((_, j) => j !== i))} className="text-slate-500 hover:text-red-400 text-xs"></button>
</div>
))}
<button onClick={() => setSkills([...skills, ""])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1 rounded-lg">+ Add</button>
</div>
</div>
{/* Languages */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">🌍 Languages</h2>
<div className="flex flex-wrap gap-3">
{languages.map((lang, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-2">
<input
type="text"
value={lang.name}
onChange={e => {
const newLangs = [...languages];
newLangs[i].name = e.target.value;
setLanguages(newLangs);
}}
className="bg-transparent text-white text-sm w-24 focus:outline-none"
/>
<span className="text-slate-500 text-xs"></span>
<input
type="text"
value={lang.level}
onChange={e => {
const newLangs = [...languages];
newLangs[i].level = e.target.value;
setLanguages(newLangs);
}}
className="bg-transparent text-slate-400 text-sm w-20 focus:outline-none"
/>
</div>
))}
<button onClick={() => setLanguages([...languages, {name: "", level: ""}])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-2 rounded-lg">+ Add</button>
</div>
</div>
{/* Certifications */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
<h2 className="text-white font-bold mb-4">📜 Certifications</h2>
<div className="flex flex-wrap gap-2">
{certifications.map((cert, i) => (
<div key={i} className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1">
<input
type="text"
value={cert}
onChange={e => {
const newCerts = [...certifications];
newCerts[i] = e.target.value;
setCertifications(newCerts);
}}
className="bg-transparent text-white text-sm w-40 focus:outline-none"
/>
<button onClick={() => setCertifications(certifications.filter((_, j) => j !== i))} className="text-slate-500 hover:text-red-400 text-xs"></button>
</div>
))}
<button onClick={() => setCertifications([...certifications, ""])} className="bg-slate-700 hover:bg-slate-600 text-white text-sm px-3 py-1 rounded-lg">+ Add</button>
</div>
</div>
</div>
</div>
) : (
/* PREVIEW - Professional Resume Template */
<div className="flex-1 overflow-auto bg-slate-100 p-8">
<div className="max-w-850px mx-auto bg-white shadow-xl rounded-lg overflow-hidden" style={{maxWidth: "850px"}}>
{/* Header - Dark Blue Professional */}
<div className="bg-gradient-to-r from-slate-800 to-slate-700 text-white px-8 py-10">
<div className="text-center mb-4">
<h1 className="text-3xl font-bold tracking-wide mb-2">{personal.name || "Your Name"}</h1>
<div className="text-slate-300 text-lg font-light">{personal.title || "Professional Title"}</div>
</div>
<div className="flex justify-center flex-wrap gap-4 text-sm text-slate-300">
{personal.email && <span> {personal.email}</span>}
{personal.phone && <span>📱 {personal.phone}</span>}
{personal.location && <span>📍 {personal.location}</span>}
{personal.linkedin && <span>💼 {personal.linkedin}</span>}
</div>
</div>
{/* Body */}
<div className="px-8 py-6 space-y-6">
{/* Summary */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Professional Summary</span>
</div>
<p className="text-slate-600 text-sm leading-relaxed">
Results-driven professional with proven expertise in delivering high-impact solutions. Skilled in modern technologies and best practices. Demonstrated ability to lead projects, optimize processes, and achieve organizational goals.
</p>
</div>
{/* Experience */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Work Experience</span>
</div>
{experiences.filter(e => e.title || e.company).map((exp) => (
<div key={exp.id} className="mb-4">
<div className="flex justify-between items-baseline mb-1">
<div>
<span className="font-bold text-slate-800">{exp.title || "Job Title"}</span>
<span className="text-slate-600"> | {exp.company || "Company"}</span>
</div>
<span className="text-slate-500 text-sm">{exp.startDate} - {exp.endDate}</span>
</div>
{exp.location && <div className="text-slate-500 text-sm mb-2">{exp.location}</div>}
<ul className="space-y-1">
{exp.bullets.filter(b => b).map((bullet, i) => (
<li key={i} className="text-slate-600 text-sm flex items-start gap-2">
<span className="text-slate-400"></span>
<span>{bullet}</span>
</li>
))}
</ul>
</div>
))}
</div>
{/* Education */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Education</span>
</div>
{education.filter(e => e.degree || e.school).map((edu) => (
<div key={edu.id} className="mb-3">
<div className="flex justify-between items-baseline">
<div>
<span className="font-bold text-slate-800">{edu.degree || "Degree"}</span>
{edu.honors && <span className="text-slate-500"> ({edu.honors})</span>}
</div>
<span className="text-slate-500 text-sm">{edu.graduationDate}</span>
</div>
<div className="text-slate-600 text-sm">{edu.school}{edu.location && `, ${edu.location}`}</div>
{edu.gpa && <div className="text-slate-500 text-sm">GPA: {edu.gpa}</div>}
</div>
))}
</div>
{/* Skills */}
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Technical Skills</span>
</div>
<div className="flex flex-wrap gap-2">
{skills.filter(s => s).map((skill, i) => (
<span key={i} className="bg-slate-100 text-slate-700 px-3 py-1 rounded text-sm">{skill}</span>
))}
</div>
</div>
{/* Languages & Certs */}
<div className="grid grid-cols-2 gap-6">
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Languages</span>
</div>
<div className="space-y-1">
{languages.filter(l => l.name).map((lang, i) => (
<div key={i} className="text-sm">
<span className="text-slate-800 font-medium">{lang.name}</span>
<span className="text-slate-500"> - {lang.level}</span>
</div>
))}
</div>
</div>
<div>
<div className="border-b-2 border-slate-800 pb-1 mb-3">
<span className="text-sm font-bold text-slate-800 uppercase tracking-wider">Certifications</span>
</div>
<div className="space-y-1">
{certifications.filter(c => c).map((cert, i) => (
<div key={i} className="text-slate-700 text-sm">{cert}</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
)}
<style jsx global>{`
@media print {
body { background: white !important; }
.bg-slate-100 { background: white !important; }
}
`}</style>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
import TempleOfAIDashboard from "@/components/temple-of-ai/TempleOfAIDashboard";
export default function TemplePage() {
return <TempleOfAIDashboard />;
}