feat(nextjs): adds claude support in home and settings page
This commit is contained in:
parent
4eff9f4bdf
commit
ac5d278a9b
10 changed files with 295 additions and 15 deletions
|
|
@ -36,6 +36,8 @@ export async function POST(request: Request) {
|
|||
LLM: userConfig.LLM || existingConfig.LLM,
|
||||
OPENAI_API_KEY: userConfig.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY,
|
||||
GOOGLE_API_KEY: userConfig.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY,
|
||||
ANTHROPIC_API_KEY: userConfig.ANTHROPIC_API_KEY || existingConfig.ANTHROPIC_API_KEY,
|
||||
ANTHROPIC_MODEL: userConfig.ANTHROPIC_MODEL || existingConfig.ANTHROPIC_MODEL,
|
||||
OLLAMA_URL: userConfig.OLLAMA_URL || existingConfig.OLLAMA_URL,
|
||||
OLLAMA_MODEL: userConfig.OLLAMA_MODEL || existingConfig.OLLAMA_MODEL,
|
||||
CUSTOM_LLM_URL: userConfig.CUSTOM_LLM_URL || existingConfig.CUSTOM_LLM_URL,
|
||||
|
|
@ -50,6 +52,10 @@ export async function POST(request: Request) {
|
|||
userConfig.USE_CUSTOM_URL === undefined
|
||||
? existingConfig.USE_CUSTOM_URL
|
||||
: userConfig.USE_CUSTOM_URL,
|
||||
EXTENDED_REASONING:
|
||||
userConfig.EXTENDED_REASONING === undefined
|
||||
? existingConfig.EXTENDED_REASONING
|
||||
: userConfig.EXTENDED_REASONING,
|
||||
};
|
||||
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig));
|
||||
return NextResponse.json(mergedConfig);
|
||||
|
|
|
|||
230
servers/nextjs/components/AnthropicConfig.tsx
Normal file
230
servers/nextjs/components/AnthropicConfig.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "./ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
import { Switch } from "./ui/switch";
|
||||
|
||||
interface AnthropicConfigProps {
|
||||
anthropicApiKey: string;
|
||||
anthropicModel: string;
|
||||
extendedReasoning: boolean;
|
||||
onInputChange: (value: string | boolean, field: string) => void;
|
||||
}
|
||||
|
||||
|
||||
export default function AnthropicConfig({
|
||||
anthropicApiKey,
|
||||
anthropicModel,
|
||||
extendedReasoning,
|
||||
onInputChange,
|
||||
}: AnthropicConfigProps) {
|
||||
const [openModelSelect, setOpenModelSelect] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [modelsChecked, setModelsChecked] = useState(false);
|
||||
const [apiKey, setApiKey] = useState(anthropicApiKey);
|
||||
|
||||
useEffect(() => {
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(false);
|
||||
onInputChange("", "anthropic_model");
|
||||
}, [apiKey]);
|
||||
|
||||
const onApiKeyChange = (value: string) => {
|
||||
setApiKey(value);
|
||||
onInputChange(value, "anthropic_api_key");
|
||||
};
|
||||
|
||||
const fetchAvailableModels = async () => {
|
||||
if (!anthropicApiKey) return;
|
||||
|
||||
setModelsLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/ppt/anthropic/models/available', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api_key: anthropicApiKey
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAvailableModels(data);
|
||||
setModelsChecked(true);
|
||||
} else {
|
||||
console.error('Failed to fetch models');
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
toast.error('Error fetching models');
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(true);
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* API Key Input */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Anthropic API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={anthropicApiKey}
|
||||
onChange={(e) => onApiKeyChange(e.target.value)}
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
placeholder="Enter your Anthropic API key"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Your API key will be stored locally and never shared
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Extended Reasoning Toggle */}
|
||||
{/* <div>
|
||||
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Extended Reasoning
|
||||
</label>
|
||||
<Switch
|
||||
checked={extendedReasoning}
|
||||
onCheckedChange={(checked) => onInputChange(checked, "extended_reasoning")}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Enable extended reasoning for more detailed and thorough responses
|
||||
</p>
|
||||
</div> */}
|
||||
|
||||
{/* Check for available models button - show when no models checked or no models found */}
|
||||
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={fetchAvailableModels}
|
||||
disabled={modelsLoading || !anthropicApiKey}
|
||||
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 ${modelsLoading || !anthropicApiKey
|
||||
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
|
||||
: "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20"
|
||||
}`}
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Checking for models...
|
||||
</div>
|
||||
) : (
|
||||
"Check for available models"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show message if no models found */}
|
||||
{modelsChecked && availableModels.length === 0 && (
|
||||
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-sm text-yellow-800">
|
||||
No models found. Please make sure your API key is valid and has access to Anthropic models.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model Selection - only show if models are available */}
|
||||
{modelsChecked && availableModels.length > 0 ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Select Anthropic Model
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Popover
|
||||
open={openModelSelect}
|
||||
onOpenChange={setOpenModelSelect}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{anthropicModel
|
||||
? availableModels.find(model => model === anthropicModel) || anthropicModel
|
||||
: "Select a model"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search models..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableModels.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model}
|
||||
onSelect={(value) => {
|
||||
onInputChange(value, "anthropic_model");
|
||||
setOpenModelSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
anthropicModel === model
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{model}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ interface GoogleConfigProps {
|
|||
|
||||
export default function GoogleConfig({ googleApiKey, onInputChange }: GoogleConfigProps) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Google API Key
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|||
import { cn } from "@/lib/utils";
|
||||
import OpenAIConfig from "./OpenAIConfig";
|
||||
import GoogleConfig from "./GoogleConfig";
|
||||
import AnthropicConfig from "./AnthropicConfig";
|
||||
import OllamaConfig from "./OllamaConfig";
|
||||
import CustomConfig from "./CustomConfig";
|
||||
import {
|
||||
|
|
@ -69,11 +70,13 @@ export default function LLMProviderSelection({
|
|||
useEffect(() => {
|
||||
const needsModelSelection =
|
||||
(llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) ||
|
||||
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL);
|
||||
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) ||
|
||||
(llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_MODEL);
|
||||
|
||||
const needsApiKey =
|
||||
((llmConfig.IMAGE_PROVIDER === "dall-e-3" || llmConfig.LLM === "openai") && !llmConfig.OPENAI_API_KEY) ||
|
||||
((llmConfig.IMAGE_PROVIDER === "gemini_flash" || llmConfig.LLM === "google") && !llmConfig.GOOGLE_API_KEY) ||
|
||||
(llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_API_KEY) ||
|
||||
(llmConfig.IMAGE_PROVIDER === "pexels" && !llmConfig.PEXELS_API_KEY) ||
|
||||
(llmConfig.IMAGE_PROVIDER === "pixabay" && !llmConfig.PIXABAY_API_KEY);
|
||||
|
||||
|
|
@ -86,7 +89,7 @@ export default function LLMProviderSelection({
|
|||
|
||||
}, [llmConfig]);
|
||||
|
||||
const input_field_changed = (new_value: string, field: string) => {
|
||||
const input_field_changed = (new_value: string | boolean, field: string) => {
|
||||
const updatedConfig = updateLLMConfig(llmConfig, field, new_value);
|
||||
setLlmConfig(updatedConfig);
|
||||
};
|
||||
|
|
@ -177,9 +180,10 @@ export default function LLMProviderSelection({
|
|||
onValueChange={handleProviderChange}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-4 bg-transparent h-10">
|
||||
<TabsList className="grid w-full grid-cols-5 bg-transparent h-10">
|
||||
<TabsTrigger value="openai">OpenAI</TabsTrigger>
|
||||
<TabsTrigger value="google">Google</TabsTrigger>
|
||||
<TabsTrigger value="anthropic">Anthropic</TabsTrigger>
|
||||
<TabsTrigger value="ollama">Ollama</TabsTrigger>
|
||||
<TabsTrigger value="custom">Custom</TabsTrigger>
|
||||
</TabsList>
|
||||
|
|
@ -210,6 +214,16 @@ export default function LLMProviderSelection({
|
|||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Anthropic Content */}
|
||||
<TabsContent value="anthropic" className="mt-6">
|
||||
<AnthropicConfig
|
||||
anthropicApiKey={llmConfig.ANTHROPIC_API_KEY || ""}
|
||||
anthropicModel={llmConfig.ANTHROPIC_MODEL || ""}
|
||||
extendedReasoning={llmConfig.EXTENDED_REASONING || false}
|
||||
onInputChange={input_field_changed}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Ollama Content */}
|
||||
<TabsContent value="ollama" className="mt-6">
|
||||
<OllamaConfig
|
||||
|
|
@ -246,7 +260,7 @@ export default function LLMProviderSelection({
|
|||
</Tabs>
|
||||
|
||||
{/* Image Provider Selection */}
|
||||
<div className="mb-8">
|
||||
<div className="my-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Select Image Provider
|
||||
</label>
|
||||
|
|
@ -388,7 +402,9 @@ export default function LLMProviderSelection({
|
|||
? llmConfig.OLLAMA_MODEL ?? "xxxxx"
|
||||
: llmConfig.LLM === "custom"
|
||||
? llmConfig.CUSTOM_MODEL ?? "xxxxx"
|
||||
: LLM_PROVIDERS[llmConfig.LLM!]?.model_label || "xxxxx"}{" "}
|
||||
: llmConfig.LLM === "anthropic"
|
||||
? llmConfig.ANTHROPIC_MODEL ?? "xxxxx"
|
||||
: LLM_PROVIDERS[llmConfig.LLM!]?.model_label || "xxxxx"}{" "}
|
||||
for text generation and{" "}
|
||||
{llmConfig.IMAGE_PROVIDER &&
|
||||
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export default function OllamaConfig({
|
|||
}: OllamaConfigProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Choose a supported model
|
||||
</label>
|
||||
|
|
@ -185,7 +185,7 @@ export default function OllamaConfig({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Use custom Ollama URL
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ interface OpenAIConfigProps {
|
|||
|
||||
export default function OpenAIConfig({ openaiApiKey, onInputChange }: OpenAIConfigProps) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
OpenAI API Key
|
||||
</label>
|
||||
|
|
|
|||
7
servers/nextjs/types/global.d.ts
vendored
7
servers/nextjs/types/global.d.ts
vendored
|
|
@ -17,15 +17,22 @@ interface LLMConfig {
|
|||
LLM?: string;
|
||||
OPENAI_API_KEY?: string;
|
||||
GOOGLE_API_KEY?: string;
|
||||
ANTHROPIC_API_KEY?: string;
|
||||
ANTHROPIC_MODEL?: string;
|
||||
OLLAMA_URL?: string;
|
||||
OLLAMA_MODEL?: string;
|
||||
CUSTOM_LLM_URL?: string;
|
||||
CUSTOM_LLM_API_KEY?: string;
|
||||
CUSTOM_MODEL?: string;
|
||||
|
||||
// Image providers
|
||||
IMAGE_PROVIDER?: string;
|
||||
PIXABAY_API_KEY?: string;
|
||||
PEXELS_API_KEY?: string;
|
||||
|
||||
// Extended reasoning
|
||||
EXTENDED_REASONING?: boolean;
|
||||
|
||||
// Only used in UI settings
|
||||
USE_CUSTOM_URL?: boolean;
|
||||
}
|
||||
|
|
@ -78,6 +78,11 @@ export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
|
|||
model_value: "gemini-2.0-flash",
|
||||
model_label: "Gemini 2.0 Flash"
|
||||
},
|
||||
anthropic: {
|
||||
value: "anthropic",
|
||||
label: "Anthropic",
|
||||
description: "Anthropic's Claude models",
|
||||
},
|
||||
ollama: {
|
||||
value: "ollama",
|
||||
label: "Ollama",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export interface LLMConfig {
|
|||
LLM?: string;
|
||||
OPENAI_API_KEY?: string;
|
||||
GOOGLE_API_KEY?: string;
|
||||
ANTHROPIC_API_KEY?: string;
|
||||
ANTHROPIC_MODEL?: string;
|
||||
OLLAMA_URL?: string;
|
||||
OLLAMA_MODEL?: string;
|
||||
CUSTOM_LLM_URL?: string;
|
||||
|
|
@ -28,6 +30,7 @@ export interface LLMConfig {
|
|||
PEXELS_API_KEY?: string;
|
||||
PIXABAY_API_KEY?: string;
|
||||
IMAGE_PROVIDER?: string;
|
||||
EXTENDED_REASONING?: boolean;
|
||||
USE_CUSTOM_URL?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -42,11 +45,13 @@ export interface OllamaModelsResult {
|
|||
export const updateLLMConfig = (
|
||||
currentConfig: LLMConfig,
|
||||
field: string,
|
||||
value: string
|
||||
value: string | boolean
|
||||
): LLMConfig => {
|
||||
const fieldMappings: Record<string, keyof LLMConfig> = {
|
||||
openai_api_key: "OPENAI_API_KEY",
|
||||
google_api_key: "GOOGLE_API_KEY",
|
||||
anthropic_api_key: "ANTHROPIC_API_KEY",
|
||||
anthropic_model: "ANTHROPIC_MODEL",
|
||||
ollama_url: "OLLAMA_URL",
|
||||
ollama_model: "OLLAMA_MODEL",
|
||||
custom_llm_url: "CUSTOM_LLM_URL",
|
||||
|
|
@ -55,6 +60,7 @@ export const updateLLMConfig = (
|
|||
pexels_api_key: "PEXELS_API_KEY",
|
||||
pixabay_api_key: "PIXABAY_API_KEY",
|
||||
image_provider: "IMAGE_PROVIDER",
|
||||
extended_reasoning: "EXTENDED_REASONING",
|
||||
};
|
||||
|
||||
const configKey = fieldMappings[field];
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
|
|||
const OPENAI_API_KEY = llmConfig.OPENAI_API_KEY;
|
||||
const GOOGLE_API_KEY = llmConfig.GOOGLE_API_KEY;
|
||||
|
||||
const isAnthropicConfigValid =
|
||||
llmConfig.ANTHROPIC_MODEL !== "" &&
|
||||
llmConfig.ANTHROPIC_MODEL !== null &&
|
||||
llmConfig.ANTHROPIC_MODEL !== undefined &&
|
||||
llmConfig.ANTHROPIC_API_KEY !== "" &&
|
||||
llmConfig.ANTHROPIC_API_KEY !== null &&
|
||||
llmConfig.ANTHROPIC_API_KEY !== undefined;
|
||||
|
||||
const isOllamaConfigValid =
|
||||
llmConfig.OLLAMA_MODEL !== "" &&
|
||||
llmConfig.OLLAMA_MODEL !== null &&
|
||||
|
|
@ -59,11 +67,13 @@ export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
|
|||
? GOOGLE_API_KEY !== "" &&
|
||||
GOOGLE_API_KEY !== null &&
|
||||
GOOGLE_API_KEY !== undefined
|
||||
: llmConfig.LLM === "ollama"
|
||||
? isOllamaConfigValid
|
||||
: llmConfig.LLM === "custom"
|
||||
? isCustomConfigValid
|
||||
: false;
|
||||
: llmConfig.LLM === "anthropic"
|
||||
? isAnthropicConfigValid
|
||||
: llmConfig.LLM === "ollama"
|
||||
? isOllamaConfigValid
|
||||
: llmConfig.LLM === "custom"
|
||||
? isCustomConfigValid
|
||||
: false;
|
||||
|
||||
return isLLMConfigValid && isImageConfigValid();
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue