Frontend: Add Claude integration UI

- Add Claude models to dropdown with optgroup sections
- Update context window calculation for Claude (200k tokens)
- Add API key dialog modal for first-time Claude use
- Check for API key existence before sending Claude requests
- Auto-detect provider from model name (claude-*)
- Update sendMessage to handle Claude provider
- Store and retrieve API key via backend commands
- Add visual separation between Anthropic and Ollama models
This commit is contained in:
Dave
2025-12-27 19:43:00 +00:00
parent 1529ca77e7
commit e3f4f92c54

View File

@@ -19,7 +19,13 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [model, setModel] = useState("llama3.1"); // Default local model
const [enableTools, setEnableTools] = useState(true);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [claudeModels] = useState<string[]>([
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022",
]);
const [streamingContent, setStreamingContent] = useState("");
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
const [apiKeyInput, setApiKeyInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -33,6 +39,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
};
const getContextWindowSize = (modelName: string): number => {
if (modelName.startsWith("claude-")) return 200000;
if (modelName.includes("llama3")) return 8192;
if (modelName.includes("qwen2.5")) return 32768;
if (modelName.includes("deepseek")) return 16384;
@@ -190,6 +197,15 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const sendMessage = async () => {
if (!input.trim() || loading) return;
// Check if using Claude and API key is required
if (model.startsWith("claude-")) {
const hasKey = await invoke<boolean>("get_anthropic_api_key_exists");
if (!hasKey) {
setShowApiKeyDialog(true);
return;
}
}
const userMsg: Message = { role: "user", content: input };
const newHistory = [...messages, userMsg];
@@ -200,7 +216,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
try {
const config: ProviderConfig = {
provider: "ollama",
provider: model.startsWith("claude-") ? "anthropic" : "ollama",
model: model,
base_url: "http://localhost:11434",
enable_tools: enableTools,
@@ -227,6 +243,21 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
}
};
const handleSaveApiKey = async () => {
if (!apiKeyInput.trim()) return;
try {
await invoke("set_anthropic_api_key", { apiKey: apiKeyInput });
setShowApiKeyDialog(false);
setApiKeyInput("");
// Retry sending the message
sendMessage();
} catch (e) {
console.error("Failed to save API key:", e);
alert(`Failed to save API key: ${e}`);
}
};
const clearSession = async () => {
const confirmed = await ask(
"Are you sure? This will clear all messages and reset the conversation context.",
@@ -380,7 +411,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
>
🔄 New Session
</button>
{availableModels.length > 0 ? (
{availableModels.length > 0 || claudeModels.length > 0 ? (
<select
value={model}
onChange={(e) => {
@@ -407,11 +438,24 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
backgroundSize: "10px",
}}
>
{claudeModels.length > 0 && (
<optgroup label="Anthropic">
{claudeModels.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</optgroup>
)}
{availableModels.length > 0 && (
<optgroup label="Ollama">
{availableModels.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</optgroup>
)}
</select>
) : (
<input
@@ -766,6 +810,105 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
</button>
</div>
</div>
{/* API Key Dialog */}
{showApiKeyDialog && (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.7)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
}}
>
<div
style={{
backgroundColor: "#2f2f2f",
padding: "32px",
borderRadius: "12px",
maxWidth: "500px",
width: "90%",
border: "1px solid #444",
}}
>
<h2 style={{ marginTop: 0, color: "#ececec" }}>
Enter Anthropic API Key
</h2>
<p
style={{ color: "#aaa", fontSize: "0.9em", marginBottom: "20px" }}
>
To use Claude models, please enter your Anthropic API key. Your
key will be stored securely in your system keychain.
</p>
<input
type="password"
value={apiKeyInput}
onChange={(e) => setApiKeyInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSaveApiKey()}
placeholder="sk-ant-..."
style={{
width: "100%",
padding: "12px",
borderRadius: "8px",
border: "1px solid #555",
backgroundColor: "#1a1a1a",
color: "#ececec",
fontSize: "1em",
marginBottom: "20px",
outline: "none",
}}
/>
<div
style={{
display: "flex",
gap: "12px",
justifyContent: "flex-end",
}}
>
<button
type="button"
onClick={() => {
setShowApiKeyDialog(false);
setApiKeyInput("");
}}
style={{
padding: "10px 20px",
borderRadius: "8px",
border: "1px solid #555",
backgroundColor: "transparent",
color: "#aaa",
cursor: "pointer",
fontSize: "0.9em",
}}
>
Cancel
</button>
<button
type="button"
onClick={handleSaveApiKey}
disabled={!apiKeyInput.trim()}
style={{
padding: "10px 20px",
borderRadius: "8px",
border: "none",
backgroundColor: apiKeyInput.trim() ? "#ececec" : "#555",
color: apiKeyInput.trim() ? "#000" : "#888",
cursor: apiKeyInput.trim() ? "pointer" : "not-allowed",
fontSize: "0.9em",
}}
>
Save Key
</button>
</div>
</div>
</div>
)}
</div>
);
}