45af56d9cf
Also added: - Memory API endpoints - Briefs API endpoints - AnveVoice stats API - Claude spawn API - TTS proxy - Cleopatra voice widget - api-auth middleware
264 lines
8.9 KiB
TypeScript
264 lines
8.9 KiB
TypeScript
"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>
|
||
);
|
||
}
|