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:
@@ -19,7 +19,13 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const [model, setModel] = useState("llama3.1"); // Default local model
|
const [model, setModel] = useState("llama3.1"); // Default local model
|
||||||
const [enableTools, setEnableTools] = useState(true);
|
const [enableTools, setEnableTools] = useState(true);
|
||||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||||
|
const [claudeModels] = useState<string[]>([
|
||||||
|
"claude-3-5-sonnet-20241022",
|
||||||
|
"claude-3-5-haiku-20241022",
|
||||||
|
]);
|
||||||
const [streamingContent, setStreamingContent] = useState("");
|
const [streamingContent, setStreamingContent] = useState("");
|
||||||
|
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
|
||||||
|
const [apiKeyInput, setApiKeyInput] = useState("");
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -33,6 +39,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getContextWindowSize = (modelName: string): number => {
|
const getContextWindowSize = (modelName: string): number => {
|
||||||
|
if (modelName.startsWith("claude-")) return 200000;
|
||||||
if (modelName.includes("llama3")) return 8192;
|
if (modelName.includes("llama3")) return 8192;
|
||||||
if (modelName.includes("qwen2.5")) return 32768;
|
if (modelName.includes("qwen2.5")) return 32768;
|
||||||
if (modelName.includes("deepseek")) return 16384;
|
if (modelName.includes("deepseek")) return 16384;
|
||||||
@@ -190,6 +197,15 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!input.trim() || loading) return;
|
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 userMsg: Message = { role: "user", content: input };
|
||||||
const newHistory = [...messages, userMsg];
|
const newHistory = [...messages, userMsg];
|
||||||
|
|
||||||
@@ -200,7 +216,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const config: ProviderConfig = {
|
const config: ProviderConfig = {
|
||||||
provider: "ollama",
|
provider: model.startsWith("claude-") ? "anthropic" : "ollama",
|
||||||
model: model,
|
model: model,
|
||||||
base_url: "http://localhost:11434",
|
base_url: "http://localhost:11434",
|
||||||
enable_tools: enableTools,
|
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 clearSession = async () => {
|
||||||
const confirmed = await ask(
|
const confirmed = await ask(
|
||||||
"Are you sure? This will clear all messages and reset the conversation context.",
|
"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
|
🔄 New Session
|
||||||
</button>
|
</button>
|
||||||
{availableModels.length > 0 ? (
|
{availableModels.length > 0 || claudeModels.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -407,11 +438,24 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
backgroundSize: "10px",
|
backgroundSize: "10px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{availableModels.map((m) => (
|
{claudeModels.length > 0 && (
|
||||||
<option key={m} value={m}>
|
<optgroup label="Anthropic">
|
||||||
{m}
|
{claudeModels.map((m) => (
|
||||||
</option>
|
<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>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
@@ -766,6 +810,105 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user