feat: integrate Z.AI, Ollama Cloud, and OpenCode Zen free models
Added comprehensive AI model integrations: Z.AI Integration: - Client with Anthropic-compatible API (GLM Coding Plan) - Routes for config, testing, and streaming chat - Settings UI component with API key management OpenCode Zen Integration: - Free models client using 'public' API key - Dynamic model fetching from models.dev - Supports GPT-5 Nano, Big Pickle, Grok Code Fast 1, MiniMax M2.1 - No API key required for free tier! UI Enhancements: - Added Free Models tab (first position) in Advanced Settings - Z.AI tab with GLM Coding Plan info - OpenCode Zen settings with model cards and status All integrations work standalone without opencode.exe dependency.
This commit is contained in:
@@ -4,6 +4,8 @@ import OpenCodeBinarySelector from "./opencode-binary-selector"
|
||||
import EnvironmentVariablesEditor from "./environment-variables-editor"
|
||||
import OllamaCloudSettings from "./settings/OllamaCloudSettings"
|
||||
import QwenCodeSettings from "./settings/QwenCodeSettings"
|
||||
import ZAISettings from "./settings/ZAISettings"
|
||||
import OpenCodeZenSettings from "./settings/OpenCodeZenSettings"
|
||||
|
||||
interface AdvancedSettingsModalProps {
|
||||
open: boolean
|
||||
@@ -27,41 +29,60 @@ const AdvancedSettingsModal: Component<AdvancedSettingsModalProps> = (props) =>
|
||||
</header>
|
||||
|
||||
<div class="border-b" style={{ "border-color": "var(--border-base)" }}>
|
||||
<div class="flex w-full px-6">
|
||||
<div class="flex w-full px-6 overflow-x-auto">
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 border-transparent hover:border-gray-300 ${
|
||||
activeTab() === "general"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: ""
|
||||
}`}
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "zen"
|
||||
? "border-orange-500 text-orange-400"
|
||||
: "border-transparent hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setActiveTab("zen")}
|
||||
>
|
||||
🆓 Free Models
|
||||
</button>
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "general"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: "border-transparent hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setActiveTab("general")}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 border-transparent hover:border-gray-300 ${
|
||||
activeTab() === "ollama"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: ""
|
||||
}`}
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "ollama"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: "border-transparent hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setActiveTab("ollama")}
|
||||
>
|
||||
Ollama Cloud
|
||||
</button>
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 border-transparent hover:border-gray-300 ${
|
||||
activeTab() === "qwen"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: ""
|
||||
}`}
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "qwen"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: "border-transparent hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setActiveTab("qwen")}
|
||||
>
|
||||
Qwen Code
|
||||
</button>
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap ${activeTab() === "zai"
|
||||
? "border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
: "border-transparent hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => setActiveTab("zai")}
|
||||
>
|
||||
Z.AI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<Show when={activeTab() === "zen"}>
|
||||
<OpenCodeZenSettings />
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === "general"}>
|
||||
<div class="p-6 space-y-6">
|
||||
<OpenCodeBinarySelector
|
||||
@@ -90,6 +111,10 @@ const AdvancedSettingsModal: Component<AdvancedSettingsModalProps> = (props) =>
|
||||
<Show when={activeTab() === "qwen"}>
|
||||
<QwenCodeSettings />
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === "zai"}>
|
||||
<ZAISettings />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t flex justify-end" style={{ "border-color": "var(--border-base)" }}>
|
||||
|
||||
222
packages/ui/src/components/settings/OpenCodeZenSettings.tsx
Normal file
222
packages/ui/src/components/settings/OpenCodeZenSettings.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Component, createSignal, onMount, For, Show } from 'solid-js'
|
||||
import { Zap, CheckCircle, XCircle, Loader, Sparkles } from 'lucide-solid'
|
||||
|
||||
interface ZenModel {
|
||||
id: string
|
||||
name: string
|
||||
family?: string
|
||||
free: boolean
|
||||
reasoning?: boolean
|
||||
tool_call?: boolean
|
||||
limit?: {
|
||||
context: number
|
||||
output: number
|
||||
}
|
||||
}
|
||||
|
||||
const OpenCodeZenSettings: Component = () => {
|
||||
const [models, setModels] = createSignal<ZenModel[]>([])
|
||||
const [isLoading, setIsLoading] = createSignal(true)
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<'idle' | 'testing' | 'connected' | 'failed'>('idle')
|
||||
const [error, setError] = createSignal<string | null>(null)
|
||||
|
||||
// Load models on mount
|
||||
onMount(async () => {
|
||||
await loadModels()
|
||||
await testConnection()
|
||||
})
|
||||
|
||||
const loadModels = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/opencode-zen/models')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setModels(data.models || [])
|
||||
setError(null)
|
||||
} else {
|
||||
throw new Error('Failed to load models')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load OpenCode Zen models:', err)
|
||||
setError('Failed to load models')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const testConnection = async () => {
|
||||
setConnectionStatus('testing')
|
||||
try {
|
||||
const response = await fetch('/api/opencode-zen/test')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setConnectionStatus(data.connected ? 'connected' : 'failed')
|
||||
} else {
|
||||
setConnectionStatus('failed')
|
||||
}
|
||||
} catch (err) {
|
||||
setConnectionStatus('failed')
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-gradient-to-br from-orange-500/20 to-yellow-500/20 rounded-lg">
|
||||
<Zap class="w-6 h-6 text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white">OpenCode Zen</h2>
|
||||
<p class="text-sm text-zinc-400">Free AI models - No API key required!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
{connectionStatus() === 'testing' && (
|
||||
<span class="flex items-center gap-2 text-sm text-zinc-400">
|
||||
<Loader class="w-4 h-4 animate-spin" />
|
||||
Testing...
|
||||
</span>
|
||||
)}
|
||||
{connectionStatus() === 'connected' && (
|
||||
<span class="flex items-center gap-2 text-sm text-emerald-400">
|
||||
<CheckCircle class="w-4 h-4" />
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
{connectionStatus() === 'failed' && (
|
||||
<span class="flex items-center gap-2 text-sm text-red-400">
|
||||
<XCircle class="w-4 h-4" />
|
||||
Offline
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<div class="bg-gradient-to-r from-orange-500/10 via-yellow-500/10 to-orange-500/10 border border-orange-500/20 rounded-xl p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<Sparkles class="w-5 h-5 text-orange-400 mt-0.5" />
|
||||
<div>
|
||||
<h3 class="font-semibold text-orange-300 mb-1">Free Models Available!</h3>
|
||||
<p class="text-sm text-zinc-300">
|
||||
OpenCode Zen provides access to powerful AI models completely free of charge.
|
||||
These models are ready to use immediately - no API keys or authentication required!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Models Grid */}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-white">Available Free Models</h3>
|
||||
<button
|
||||
onClick={loadModels}
|
||||
disabled={isLoading()}
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors"
|
||||
>
|
||||
{isLoading() ? <Loader class="w-4 h-4 animate-spin" /> : null}
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={error()}>
|
||||
<div class="p-4 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||||
{error()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isLoading()}>
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="flex items-center gap-3 text-zinc-400">
|
||||
<Loader class="w-6 h-6 animate-spin" />
|
||||
<span>Loading models...</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isLoading() && models().length > 0}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<For each={models()}>
|
||||
{(model) => (
|
||||
<div class="group bg-zinc-900/50 border border-zinc-800 hover:border-orange-500/50 rounded-xl p-4 transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h4 class="font-semibold text-white group-hover:text-orange-300 transition-colors">
|
||||
{model.name}
|
||||
</h4>
|
||||
<p class="text-xs text-zinc-500 font-mono">{model.id}</p>
|
||||
</div>
|
||||
<span class="px-2 py-0.5 text-[10px] font-bold uppercase bg-emerald-500/20 text-emerald-400 rounded">
|
||||
FREE
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
{model.reasoning && (
|
||||
<span class="px-2 py-0.5 text-[10px] bg-purple-500/20 text-purple-400 rounded">
|
||||
Reasoning
|
||||
</span>
|
||||
)}
|
||||
{model.tool_call && (
|
||||
<span class="px-2 py-0.5 text-[10px] bg-blue-500/20 text-blue-400 rounded">
|
||||
Tool Use
|
||||
</span>
|
||||
)}
|
||||
{model.family && (
|
||||
<span class="px-2 py-0.5 text-[10px] bg-zinc-700 text-zinc-400 rounded">
|
||||
{model.family}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{model.limit && (
|
||||
<div class="flex items-center gap-4 text-xs text-zinc-500">
|
||||
<span>Context: {formatNumber(model.limit.context)}</span>
|
||||
<span>Output: {formatNumber(model.limit.output)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isLoading() && models().length === 0 && !error()}>
|
||||
<div class="text-center py-12 text-zinc-500">
|
||||
<p>No free models available at this time.</p>
|
||||
<button
|
||||
onClick={loadModels}
|
||||
class="mt-4 px-4 py-2 text-sm bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 rounded-lg transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Usage Info */}
|
||||
<div class="bg-zinc-900/50 border border-zinc-800 rounded-xl p-4">
|
||||
<h4 class="font-medium text-white mb-2">How to Use</h4>
|
||||
<ul class="text-sm text-zinc-400 space-y-1">
|
||||
<li>• Select any Zen model from the model picker in chat</li>
|
||||
<li>• No API key configuration needed - just start chatting!</li>
|
||||
<li>• Models support streaming, reasoning, and tool use</li>
|
||||
<li>• Rate limits may apply during high demand periods</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenCodeZenSettings
|
||||
249
packages/ui/src/components/settings/ZAISettings.tsx
Normal file
249
packages/ui/src/components/settings/ZAISettings.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { Component, createSignal, onMount, Show } from 'solid-js'
|
||||
import toast from 'solid-toast'
|
||||
import { Button } from '@suid/material'
|
||||
import { Cpu, CheckCircle, XCircle, Loader, Key, ExternalLink } from 'lucide-solid'
|
||||
|
||||
interface ZAIConfig {
|
||||
enabled: boolean
|
||||
apiKey?: string
|
||||
endpoint?: string
|
||||
}
|
||||
|
||||
const ZAISettings: Component = () => {
|
||||
const [config, setConfig] = createSignal<ZAIConfig>({ enabled: false })
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [isTesting, setIsTesting] = createSignal(false)
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<'idle' | 'testing' | 'connected' | 'failed'>('idle')
|
||||
const [models, setModels] = createSignal<string[]>([])
|
||||
|
||||
// Load config on mount
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/zai/config')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setConfig(data.config)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Z.AI config:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const handleConfigChange = (field: keyof ZAIConfig, value: any) => {
|
||||
setConfig(prev => ({ ...prev, [field]: value }))
|
||||
setConnectionStatus('idle')
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/zai/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config())
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Z.AI configuration saved', {
|
||||
duration: 3000,
|
||||
icon: <CheckCircle class="w-4 h-4 text-green-500" />
|
||||
})
|
||||
} else {
|
||||
throw new Error('Failed to save config')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to save Z.AI configuration', {
|
||||
duration: 5000,
|
||||
icon: <XCircle class="w-4 h-4 text-red-500" />
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsTesting(true)
|
||||
setConnectionStatus('testing')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/zai/test', {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setConnectionStatus(data.connected ? 'connected' : 'failed')
|
||||
|
||||
if (data.connected) {
|
||||
toast.success('Successfully connected to Z.AI', {
|
||||
duration: 3000,
|
||||
icon: <CheckCircle class="w-4 h-4 text-green-500" />
|
||||
})
|
||||
|
||||
// Load models after successful connection
|
||||
loadModels()
|
||||
} else {
|
||||
toast.error('Failed to connect to Z.AI', {
|
||||
duration: 3000,
|
||||
icon: <XCircle class="w-4 h-4 text-red-500" />
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw new Error('Connection test failed')
|
||||
}
|
||||
} catch (error) {
|
||||
setConnectionStatus('failed')
|
||||
toast.error('Connection test failed', {
|
||||
duration: 3000,
|
||||
icon: <XCircle class="w-4 h-4 text-red-500" />
|
||||
})
|
||||
} finally {
|
||||
setIsTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/zai/models')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setModels(data.models.map((m: any) => m.name))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load models:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (connectionStatus()) {
|
||||
case 'testing':
|
||||
return <Loader class="w-4 h-4 animate-spin" />
|
||||
case 'connected':
|
||||
return <CheckCircle class="w-4 h-4 text-green-500" />
|
||||
case 'failed':
|
||||
return <XCircle class="w-4 h-4 text-red-500" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="space-y-6 p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Cpu class="w-6 h-6 text-blue-500" />
|
||||
<h2 class="text-xl font-semibold">Z.AI Integration</h2>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4">
|
||||
<h3 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">GLM Coding Plan</h3>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
Z.AI provides access to Claude models through their GLM Coding Plan. Get your API key from the{' '}
|
||||
<a
|
||||
href="https://z.ai/manage-apikey/apikey-list"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:no-underline inline-flex items-center gap-1"
|
||||
>
|
||||
Z.AI Platform <ExternalLink class="w-3 h-3" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{/* Enable/Disable Toggle */}
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="font-medium">Enable Z.AI</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config().enabled}
|
||||
onChange={(e) => handleConfigChange('enabled', e.target.checked)}
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label class="block font-medium mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Key class="w-4 h-4" />
|
||||
API Key
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter your Z.AI API key"
|
||||
value={config().apiKey || ''}
|
||||
onChange={(e) => handleConfigChange('apiKey', e.target.value)}
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800"
|
||||
disabled={!config().enabled}
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Get your key from <a href="https://z.ai/manage-apikey/apikey-list" target="_blank" class="text-blue-500 hover:underline">z.ai/manage-apikey</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Endpoint */}
|
||||
<div>
|
||||
<label class="block font-medium mb-2">Endpoint</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://api.z.ai/api/anthropic"
|
||||
value={config().endpoint || ''}
|
||||
onChange={(e) => handleConfigChange('endpoint', e.target.value)}
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800"
|
||||
disabled={!config().enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Test Connection */}
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={testConnection}
|
||||
disabled={!config().enabled || isTesting()}
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
{getStatusIcon()}
|
||||
{isTesting() ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
|
||||
<Show when={connectionStatus() === 'connected'}>
|
||||
<span class="text-green-600 text-sm">Connected successfully</span>
|
||||
</Show>
|
||||
<Show when={connectionStatus() === 'failed'}>
|
||||
<span class="text-red-600 text-sm">Connection failed</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Available Models */}
|
||||
<Show when={models().length > 0}>
|
||||
<div>
|
||||
<label class="block font-medium mb-2">Available Models</label>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
{models().map(model => (
|
||||
<div class="p-3 border border-gray-200 dark:border-gray-700 rounded-md bg-gray-50 dark:bg-gray-800">
|
||||
<code class="text-sm font-mono">{model}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Save Configuration */}
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={saveConfig}
|
||||
disabled={isLoading()}
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
{isLoading() ? <Loader class="w-4 h-4 animate-spin" /> : null}
|
||||
Save Configuration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ZAISettings
|
||||
Reference in New Issue
Block a user