2026-01-04 21:58:59 -05:00
"use client" ;
import { useState , useEffect } from "react" ;
2026-01-14 03:42:38 -05:00
import { Card , Button , ModelSelectModal , ManualConfigModal } from "@/shared/components" ;
2026-01-04 21:58:59 -05:00
import Image from "next/image" ;
const CLOUD _URL = process . env . NEXT _PUBLIC _CLOUD _URL ;
export default function ClaudeToolCard ( {
tool ,
isExpanded ,
onToggle ,
activeProviders ,
modelMappings ,
onModelMappingChange ,
baseUrl ,
hasActiveProviders ,
apiKeys ,
cloudEnabled ,
} ) {
const [ claudeStatus , setClaudeStatus ] = useState ( null ) ;
const [ checkingClaude , setCheckingClaude ] = useState ( false ) ;
const [ applying , setApplying ] = useState ( false ) ;
const [ restoring , setRestoring ] = useState ( false ) ;
const [ message , setMessage ] = useState ( null ) ;
const [ showInstallGuide , setShowInstallGuide ] = useState ( false ) ;
const [ modalOpen , setModalOpen ] = useState ( false ) ;
const [ currentEditingAlias , setCurrentEditingAlias ] = useState ( null ) ;
const [ selectedApiKey , setSelectedApiKey ] = useState ( "" ) ;
const [ modelAliases , setModelAliases ] = useState ( { } ) ;
2026-01-14 03:42:38 -05:00
const [ showManualConfigModal , setShowManualConfigModal ] = useState ( false ) ;
2026-01-04 21:58:59 -05:00
const getConfigStatus = ( ) => {
if ( ! claudeStatus ? . installed ) return null ;
const currentUrl = claudeStatus . settings ? . env ? . ANTHROPIC _BASE _URL ;
if ( ! currentUrl ) return "not_configured" ;
const localMatch = currentUrl . includes ( "localhost" ) || currentUrl . includes ( "127.0.0.1" ) ;
const cloudMatch = cloudEnabled && CLOUD _URL && currentUrl . startsWith ( CLOUD _URL ) ;
if ( localMatch || cloudMatch ) return "configured" ;
return "other" ;
} ;
const configStatus = getConfigStatus ( ) ;
useEffect ( ( ) => {
if ( apiKeys ? . length > 0 && ! selectedApiKey ) {
setSelectedApiKey ( apiKeys [ 0 ] . key ) ;
}
} , [ apiKeys ] ) ;
useEffect ( ( ) => {
if ( isExpanded && ! claudeStatus ) {
checkClaudeStatus ( ) ;
fetchModelAliases ( ) ;
}
} , [ isExpanded ] ) ;
const fetchModelAliases = async ( ) => {
try {
const res = await fetch ( "/api/models/alias" ) ;
const data = await res . json ( ) ;
if ( res . ok ) setModelAliases ( data . aliases || { } ) ;
} catch ( error ) {
console . log ( "Error fetching model aliases:" , error ) ;
}
} ;
useEffect ( ( ) => {
if ( claudeStatus ? . installed ) {
const env = claudeStatus . settings ? . env || { } ;
tool . defaultModels . forEach ( ( model ) => {
if ( model . envKey ) {
const value = env [ model . envKey ] || model . defaultValue || "" ;
if ( value ) onModelMappingChange ( model . alias , value ) ;
}
} ) ;
// Only set selectedApiKey if it exists in apiKeys list
const tokenFromFile = env . ANTHROPIC _AUTH _TOKEN ;
if ( tokenFromFile && apiKeys ? . some ( k => k . key === tokenFromFile ) ) {
setSelectedApiKey ( tokenFromFile ) ;
}
}
} , [ claudeStatus , apiKeys ] ) ;
const checkClaudeStatus = async ( ) => {
setCheckingClaude ( true ) ;
try {
const res = await fetch ( "/api/cli-tools/claude-settings" ) ;
const data = await res . json ( ) ;
setClaudeStatus ( data ) ;
} catch ( error ) {
setClaudeStatus ( { installed : false , error : error . message } ) ;
} finally {
setCheckingClaude ( false ) ;
}
} ;
const handleApplySettings = async ( ) => {
setApplying ( true ) ;
setMessage ( null ) ;
try {
const env = { ANTHROPIC _BASE _URL : baseUrl } ;
// Get key from dropdown, fallback to first key or sk_9router for localhost
const keyToUse = selectedApiKey ? . trim ( )
|| ( apiKeys ? . length > 0 ? apiKeys [ 0 ] . key : null )
|| ( ! cloudEnabled ? "sk_9router" : null ) ;
if ( keyToUse ) {
env . ANTHROPIC _AUTH _TOKEN = keyToUse ;
}
tool . defaultModels . forEach ( ( model ) => {
const targetModel = modelMappings [ model . alias ] ;
if ( targetModel && model . envKey ) env [ model . envKey ] = targetModel ;
} ) ;
const res = await fetch ( "/api/cli-tools/claude-settings" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { env } ) ,
} ) ;
const data = await res . json ( ) ;
if ( res . ok ) {
setMessage ( { type : "success" , text : "Settings applied successfully!" } ) ;
setClaudeStatus ( prev => ( { ... prev , hasBackup : true , settings : { ... prev ? . settings , env } } ) ) ;
} else {
setMessage ( { type : "error" , text : data . error || "Failed to apply settings" } ) ;
}
} catch ( error ) {
setMessage ( { type : "error" , text : error . message } ) ;
} finally {
setApplying ( false ) ;
}
} ;
const handleResetSettings = async ( ) => {
setRestoring ( true ) ;
setMessage ( null ) ;
try {
const res = await fetch ( "/api/cli-tools/claude-settings" , { method : "DELETE" } ) ;
const data = await res . json ( ) ;
if ( res . ok ) {
setMessage ( { type : "success" , text : "Settings reset successfully!" } ) ;
tool . defaultModels . forEach ( ( model ) => onModelMappingChange ( model . alias , model . defaultValue || "" ) ) ;
setSelectedApiKey ( "" ) ;
} else {
setMessage ( { type : "error" , text : data . error || "Failed to reset settings" } ) ;
}
} catch ( error ) {
setMessage ( { type : "error" , text : error . message } ) ;
} finally {
setRestoring ( false ) ;
}
} ;
const openModelSelector = ( alias ) => {
setCurrentEditingAlias ( alias ) ;
setModalOpen ( true ) ;
} ;
const handleModelSelect = ( model ) => {
if ( currentEditingAlias ) onModelMappingChange ( currentEditingAlias , model . value ) ;
} ;
// Generate settings.json content for manual copy
2026-01-14 03:42:38 -05:00
const getManualConfigs = ( ) => {
2026-01-04 21:58:59 -05:00
const keyToUse = ( selectedApiKey && selectedApiKey . trim ( ) )
? selectedApiKey
: ( ! cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>" ) ;
const env = { ANTHROPIC _BASE _URL : baseUrl , ANTHROPIC _AUTH _TOKEN : keyToUse } ;
tool . defaultModels . forEach ( ( model ) => {
const targetModel = modelMappings [ model . alias ] ;
if ( targetModel && model . envKey ) env [ model . envKey ] = targetModel ;
} ) ;
2026-01-14 03:42:38 -05:00
return [
{
filename : "~/.claude/settings.json" ,
content : JSON . stringify ( { env } , null , 2 ) ,
} ,
] ;
2026-01-04 21:58:59 -05:00
} ;
return (
< Card className = "overflow-hidden" >
< div className = "flex items-center justify-between hover:cursor-pointer" onClick = { onToggle } >
< div className = "flex items-center gap-4" >
2026-01-18 22:39:30 -05:00
< div className = "size-12 flex items-center justify-center" >
< Image src = "/providers/claude.png" alt = { tool . name } width = { 40 } height = { 40 } className = "size-12 object-contain rounded-xl" onError = { ( e ) => { e . target . style . display = "none" ; } } / >
2026-01-04 21:58:59 -05:00
< / d i v >
< div >
< div className = "flex items-center gap-2" >
< h3 className = "font-semibold text-lg" > { tool . name } < / h 3 >
{ configStatus === "configured" && < span className = "px-2 py-0.5 text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full" > Connected < / s p a n > }
{ configStatus === "not_configured" && < span className = "px-2 py-0.5 text-xs font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full" > Not configured < / s p a n > }
{ configStatus === "other" && < span className = "px-2 py-0.5 text-xs font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full" > Other endpoint < / s p a n > }
< / d i v >
< p className = "text-sm text-text-muted" > { tool . description } < / p >
< / d i v >
< / d i v >
< span className = { ` material-symbols-outlined text-text-muted transition-transform ${ isExpanded ? "rotate-180" : "" } ` } > expand _more < / s p a n >
< / d i v >
{ isExpanded && (
< div className = "mt-4 pt-4 border-t border-border flex flex-col gap-4" >
{ checkingClaude && (
< div className = "flex items-center gap-2 text-text-muted" >
< span className = "material-symbols-outlined animate-spin" > progress _activity < / s p a n >
< span > Checking Claude CLI ... < / s p a n >
< / d i v >
) }
{ ! checkingClaude && claudeStatus && ! claudeStatus . installed && (
< div className = "flex flex-col gap-4" >
< div className = "flex items-center gap-3 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg" >
< span className = "material-symbols-outlined text-yellow-500" > warning < / s p a n >
< div className = "flex-1" >
< p className = "font-medium text-yellow-600 dark:text-yellow-400" > Claude CLI not installed < / p >
< p className = "text-sm text-text-muted" > Please install Claude CLI to use this feature . < / p >
< / d i v >
< Button variant = "outline" size = "sm" onClick = { ( ) => setShowInstallGuide ( ! showInstallGuide ) } >
< span className = "material-symbols-outlined text-[18px] mr-1" > { showInstallGuide ? "expand_less" : "help" } < / s p a n >
{ showInstallGuide ? "Hide" : "How to Install" }
< / B u t t o n >
< / d i v >
{ showInstallGuide && (
< div className = "p-4 bg-surface border border-border rounded-lg" >
< h4 className = "font-medium mb-3" > Installation Guide < / h 4 >
< div className = "space-y-3 text-sm" >
< div >
< p className = "text-text-muted mb-1" > macOS / Linux / Windows : < / p >
< code className = "block px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs" > npm install - g @ anthropic - ai / claude - code < / c o d e >
< / d i v >
< p className = "text-text-muted" > After installation , run < code className = "px-1 bg-black/5 dark:bg-white/5 rounded" > claude < / c o d e > t o v e r i f y . < / p >
< / d i v >
< / d i v >
) }
< / d i v >
) }
{ ! checkingClaude && claudeStatus ? . installed && (
< >
< div className = "flex items-center gap-2" >
< span className = "material-symbols-outlined text-green-500 text-[16px]" > check _circle < / s p a n >
< span className = "text-xs text-text-muted shrink-0" > URL : < / s p a n >
< code className = "text-xs font-mono text-text-main truncate" > { baseUrl } < / c o d e >
< / d i v >
< div className = "flex items-center gap-2" >
< span className = "text-xs text-text-muted shrink-0" > Key : < / s p a n >
{ apiKeys . length > 0 ? (
< select value = { selectedApiKey } onChange = { ( e ) => setSelectedApiKey ( e . target . value ) } className = "flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" >
{ apiKeys . map ( ( key ) => < option key = { key . id } value = { key . key } > { key . key } < / o p t i o n > ) }
< / s e l e c t >
) : (
< span className = "text-xs text-text-muted" >
{ cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router" }
< / s p a n >
) }
< / d i v >
< div className = "flex flex-col gap-2" >
{ tool . defaultModels . map ( ( model ) => (
< div key = { model . alias } className = "flex items-center gap-2" >
< span className = "w-32 shrink-0 text-sm font-semibold text-text-main text-right" > { model . name } < / s p a n >
< span className = "material-symbols-outlined text-text-muted text-[14px]" > arrow _forward < / s p a n >
< input type = "text" value = { modelMappings [ model . alias ] || "" } onChange = { ( e ) => onModelMappingChange ( model . alias , e . target . value ) } placeholder = "provider/model-id" className = "flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" / >
< button onClick = { ( ) => openModelSelector ( model . alias ) } disabled = { ! hasActiveProviders } className = { ` px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${ hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border" } ` } > Select Model < / b u t t o n >
{ modelMappings [ model . alias ] && < button onClick = { ( ) => onModelMappingChange ( model . alias , "" ) } className = "p-1 text-text-muted hover:text-red-500 rounded transition-colors" title = "Clear" > < span className = "material-symbols-outlined text-[14px]" > close < / s p a n > < / b u t t o n > }
< / d i v >
) ) }
< / d i v >
{ message && (
< div className = { ` flex items-center gap-2 px-2 py-1.5 rounded text-xs ${ message . type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600" } ` } >
< span className = "material-symbols-outlined text-[14px]" > { message . type === "success" ? "check_circle" : "error" } < / s p a n >
< span > { message . text } < / s p a n >
< / d i v >
) }
< div className = "flex items-center gap-2" >
< Button variant = "primary" size = "sm" onClick = { handleApplySettings } disabled = { ! hasActiveProviders } loading = { applying } >
< span className = "material-symbols-outlined text-[14px] mr-1" > save < / s p a n > A p p l y
< / B u t t o n >
< Button variant = "outline" size = "sm" onClick = { handleResetSettings } disabled = { ! claudeStatus ? . has9Router } loading = { restoring } >
< span className = "material-symbols-outlined text-[14px] mr-1" > restore < / s p a n > R e s e t
< / B u t t o n >
2026-01-14 03:42:38 -05:00
< Button variant = "ghost" size = "sm" onClick = { ( ) => setShowManualConfigModal ( true ) } >
< span className = "material-symbols-outlined text-[14px] mr-1" > content _copy < / s p a n > M a n u a l C o n f i g
2026-01-04 21:58:59 -05:00
< / B u t t o n >
< / d i v >
< / >
) }
< / d i v >
) }
< ModelSelectModal isOpen = { modalOpen } onClose = { ( ) => setModalOpen ( false ) } onSelect = { handleModelSelect } selectedModel = { currentEditingAlias ? modelMappings [ currentEditingAlias ] : null } activeProviders = { activeProviders } modelAliases = { modelAliases } title = { ` Select model for ${ currentEditingAlias } ` } / >
2026-01-14 03:42:38 -05:00
< ManualConfigModal
isOpen = { showManualConfigModal }
onClose = { ( ) => setShowManualConfigModal ( false ) }
title = "Claude CLI - Manual Configuration"
configs = { getManualConfigs ( ) }
/ >
2026-01-04 21:58:59 -05:00
< / C a r d >
) ;
}