All files / src/components/Settings General.tsx

0% Statements 0/169
0% Branches 0/1
0% Functions 0/1
0% Lines 0/169

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200                                                                                                                                                                                                                                                                                                                                                                                                               
import { useState, useEffect } from 'react';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { useChatStore } from '../../store/chatStore';
import { getStoredGatewayToken, setStoredGatewayToken } from '../../lib/gateway-client';
import { silentErrorHandler } from '../../lib/error-utils';
 
export function General() {
  const connectionState = useConnectionStore((s) => s.connectionState);
  const gatewayVersion = useConnectionStore((s) => s.gatewayVersion);
  const error = useConnectionStore((s) => s.error);
  const connect = useConnectionStore((s) => s.connect);
  const disconnect = useConnectionStore((s) => s.disconnect);
  const quickConfig = useConfigStore((s) => s.quickConfig);
  const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
  const currentModel = useChatStore((s) => s.currentModel);
  const [theme, setTheme] = useState<'light' | 'dark'>(quickConfig.theme || 'light');
  const [autoStart, setAutoStart] = useState(quickConfig.autoStart ?? false);
  const [showToolCalls, setShowToolCalls] = useState(quickConfig.showToolCalls ?? false);
  const [gatewayToken, setGatewayToken] = useState(getStoredGatewayToken());
  const [isSaving, setIsSaving] = useState(false);
 
  const connected = connectionState === 'connected';
  const connecting = connectionState === 'connecting' || connectionState === 'reconnecting';
 
  // 同步主题设置
  useEffect(() => {
    if (quickConfig.theme) {
      setTheme(quickConfig.theme);
    }
    if (quickConfig.autoStart !== undefined) {
      setAutoStart(quickConfig.autoStart);
    }
    if (quickConfig.showToolCalls !== undefined) {
      setShowToolCalls(quickConfig.showToolCalls);
    }
  }, [quickConfig.theme, quickConfig.autoStart, quickConfig.showToolCalls]);
 
  // 应用主题到文档
  useEffect(() => {
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
    } else {
      document.documentElement.classList.remove('dark');
    }
  }, [theme]);
 
  const handleThemeChange = async (newTheme: 'light' | 'dark') => {
    setTheme(newTheme);
    setIsSaving(true);
    try {
      await saveQuickConfig({ theme: newTheme });
    } finally {
      setIsSaving(false);
    }
  };
 
  const handleAutoStartChange = async (value: boolean) => {
    setAutoStart(value);
    setIsSaving(true);
    try {
      await saveQuickConfig({ autoStart: value });
    } finally {
      setIsSaving(false);
    }
  };
 
  const handleShowToolCallsChange = async (value: boolean) => {
    setShowToolCalls(value);
    setIsSaving(true);
    try {
      await saveQuickConfig({ showToolCalls: value });
    } finally {
      setIsSaving(false);
    }
  };
 
  const handleConnect = () => {
    connect(undefined, gatewayToken || undefined).catch(silentErrorHandler('General'));
  };
  const handleDisconnect = () => { disconnect(); };
 
  return (
    <div>
      <h1 className="text-2xl font-bold text-gray-900 mb-8">通用设置</h1>
 
      <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">Gateway 连接</h2>
      <div className="bg-gray-50 rounded-xl p-5 mb-6 space-y-4">
        <div className="flex justify-between items-center">
          <span className="text-sm text-gray-700">状态</span>
          <div className="flex items-center gap-2">
            <span className={`w-2 h-2 rounded-full ${connected ? 'bg-green-400' : connecting ? 'bg-yellow-400 animate-pulse' : 'bg-gray-300'}`} />
            <span className={`text-sm font-medium ${connected ? 'text-green-600' : connecting ? 'text-yellow-600' : 'text-gray-400'}`}>
              {connected ? '已连接' : connecting ? '连接中...' : connectionState === 'handshaking' ? '握手中...' : '未连接'}
            </span>
          </div>
        </div>
        <div className="flex justify-between items-center">
          <span className="text-sm text-gray-700">地址</span>
          <span className="text-sm text-gray-500 font-mono">ws://127.0.0.1:50051</span>
        </div>
        <div className="flex justify-between items-center">
          <span className="text-sm text-gray-700">Token</span>
          <input
            type="password"
            value={gatewayToken}
            onChange={(e) => {
              setGatewayToken(e.target.value);
              setStoredGatewayToken(e.target.value);
            }}
            placeholder="可选:Gateway auth token"
            className="w-72 px-3 py-1.5 border border-gray-200 rounded-lg text-sm bg-white focus:outline-none text-gray-500 font-mono"
          />
        </div>
        {gatewayVersion && (
          <div className="flex justify-between items-center">
            <span className="text-sm text-gray-700">版本</span>
            <span className="text-sm text-gray-500">{gatewayVersion}</span>
          </div>
        )}
        <div className="flex justify-between items-center">
          <span className="text-sm text-gray-700">当前模型</span>
          <span className="text-sm text-orange-600 font-medium">{currentModel}</span>
        </div>
        {error && (
          <div className="text-xs text-red-500 bg-red-50 rounded-lg p-2">{error}</div>
        )}
        <div className="flex gap-2 pt-1">
          {connected ? (
            <button
              onClick={handleDisconnect}
              className="text-sm border border-gray-300 rounded-lg px-4 py-1.5 hover:bg-gray-100 text-gray-600"
            >
              断开连接
            </button>
          ) : (
            <button
              onClick={handleConnect}
              disabled={connecting}
              className="text-sm bg-orange-500 text-white rounded-lg px-4 py-1.5 hover:bg-orange-600 disabled:opacity-50"
            >
              {connecting ? '连接中...' : '连接 Gateway'}
            </button>
          )}
        </div>
      </div>
 
      <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">外观与行为</h2>
      <div className="bg-gray-50 rounded-xl p-5 space-y-5">
        <div className="flex justify-between items-center">
          <div>
            <div className="text-sm font-medium text-gray-900">主题模式</div>
            <div className="text-xs text-gray-500 mt-0.5">选择浅色或深色模式。</div>
          </div>
          <div className="flex gap-2">
            <button
              onClick={() => handleThemeChange('light')}
              disabled={isSaving}
              className={`w-8 h-8 rounded-full border-2 transition-all ${theme === 'light' ? 'border-orange-500 ring-2 ring-orange-200' : 'border-gray-300'} bg-white disabled:opacity-50`}
            />
            <button
              onClick={() => handleThemeChange('dark')}
              disabled={isSaving}
              className={`w-8 h-8 rounded-full border-2 transition-all ${theme === 'dark' ? 'border-orange-500 ring-2 ring-orange-200' : 'border-gray-300'} bg-gray-900 disabled:opacity-50`}
            />
          </div>
        </div>
 
        <div className="flex justify-between items-center">
          <div>
            <div className="text-sm font-medium text-gray-900">开机自启</div>
            <div className="text-xs text-gray-500 mt-0.5">登录时自动启动 ZCLAW。</div>
          </div>
          <Toggle checked={autoStart} onChange={handleAutoStartChange} disabled={isSaving} />
        </div>
 
        <div className="flex justify-between items-center">
          <div>
            <div className="text-sm font-medium text-gray-900">显示工具调用</div>
            <div className="text-xs text-gray-500 mt-0.5">在对话消息中显示模型的工具调用详情块。</div>
          </div>
          <Toggle checked={showToolCalls} onChange={handleShowToolCallsChange} disabled={isSaving} />
        </div>
      </div>
    </div>
  );
}
 
function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: (v: boolean) => void; disabled?: boolean }) {
  return (
    <button
      onClick={() => !disabled && onChange(!checked)}
      disabled={disabled}
      className={`w-11 h-6 rounded-full transition-colors relative flex-shrink-0 ${checked ? 'bg-orange-500' : 'bg-gray-300'} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
    >
      <span className={`block w-5 h-5 bg-white rounded-full shadow absolute top-0.5 transition-all ${checked ? 'left-[22px]' : 'left-0.5'}`} />
    </button>
  );
}