commons-proxy 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,1327 @@
1
+ <div x-data="{
2
+ activeTab: 'ui'
3
+ }" class="view-container">
4
+ <!-- Header & Tabs -->
5
+ <div class="view-card !p-0 flex flex-col overflow-hidden">
6
+ <div class="bg-space-900/50 border-b border-space-border px-8 pt-8 pb-0 shrink-0">
7
+ <div class="flex items-center justify-between mb-6">
8
+ <h3 class="text-xl font-bold text-white flex items-center gap-2">
9
+ <svg class="w-5 h-5 text-neon-purple" fill="none" stroke="currentColor" viewBox="0 0 24 24">
10
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
11
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
13
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
14
+ </svg>
15
+ <span x-text="$store.global.t('systemConfig')">System Configuration</span>
16
+ </h3>
17
+ </div>
18
+
19
+ <div class="flex gap-6 overflow-x-auto">
20
+ <button @click="activeTab = 'ui'"
21
+ class="pb-3 border-b-2 transition-colors font-medium text-sm flex items-center gap-2 whitespace-nowrap"
22
+ :class="activeTab === 'ui' ? 'border-neon-purple text-white' : 'border-transparent text-gray-500 hover:text-gray-300'">
23
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
24
+ stroke="currentColor">
25
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
26
+ d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
27
+ </svg>
28
+ <span x-text="$store.global.t('tabInterface')">Interface</span>
29
+ </button>
30
+ <button @click="activeTab = 'claude'"
31
+ class="pb-3 border-b-2 transition-colors font-medium text-sm flex items-center gap-2 whitespace-nowrap"
32
+ :class="activeTab === 'claude' ? 'border-neon-purple text-white' : 'border-transparent text-gray-500 hover:text-gray-300'">
33
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
34
+ stroke="currentColor">
35
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
36
+ d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
37
+ </svg>
38
+ <span x-text="$store.global.t('tabClaude')">Claude CLI</span>
39
+ </button>
40
+ <button @click="activeTab = 'models'"
41
+ class="pb-3 border-b-2 transition-colors font-medium text-sm flex items-center gap-2 whitespace-nowrap"
42
+ :class="activeTab === 'models' ? 'border-neon-purple text-white' : 'border-transparent text-gray-500 hover:text-gray-300'">
43
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
44
+ stroke="currentColor">
45
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
46
+ d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
47
+ </svg>
48
+ <span x-text="$store.global.t('tabModels')">Models</span>
49
+ </button>
50
+ <button @click="activeTab = 'server'"
51
+ class="pb-3 border-b-2 transition-colors font-medium text-sm flex items-center gap-2 whitespace-nowrap"
52
+ :class="activeTab === 'server' ? 'border-neon-purple text-white' : 'border-transparent text-gray-500 hover:text-gray-300'">
53
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
54
+ stroke="currentColor">
55
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
56
+ d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
57
+ </svg>
58
+ <span x-text="$store.global.t('tabServer')">Server</span>
59
+ </button>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Scrollable Content -->
64
+ <div class="p-8 overflow-y-auto flex-1 custom-scrollbar">
65
+
66
+ <!-- Tab 1: UI Preferences -->
67
+ <div x-show="activeTab === 'ui'" class="space-y-8 max-w-2xl animate-fade-in">
68
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
69
+ <!-- Language -->
70
+ <div class="form-control">
71
+ <label class="label">
72
+ <span class="label-text text-gray-300" x-text="$store.global.t('language')">Language</span>
73
+ </label>
74
+ <select
75
+ class="select select-bordered select-sm w-full bg-space-800 border-space-border/50 text-gray-300 focus:border-neon-purple focus:ring-1 focus:ring-neon-purple/50 font-medium transition-all !py-0 leading-tight"
76
+ :value="$store.global.lang"
77
+ @change="$store.global.setLang($event.target.value)">
78
+ <option value="en">English</option>
79
+ <option value="zh">中文</option>
80
+ <option value="tr">Türkçe</option>
81
+ <option value="id">Bahasa Indonesia</option>
82
+ <option value="pt">Português (BR)</option>
83
+ </select>
84
+ </div>
85
+
86
+ <!-- Polling Interval -->
87
+ <div class="form-control">
88
+ <label class="label">
89
+ <span class="label-text text-gray-300" x-text="$store.global.t('pollingInterval')">Polling
90
+ Interval</span>
91
+ <span class="label-text-alt font-mono text-neon-purple"
92
+ x-text="$store.settings.refreshInterval + 's'"></span>
93
+ </label>
94
+ <div class="flex gap-3 items-center">
95
+ <input type="range" min="10" max="300" class="custom-range custom-range-purple flex-1"
96
+ x-model.number="$store.settings.refreshInterval"
97
+ :style="`background-size: ${($store.settings.refreshInterval - 10) / 2.9}% 100%`"
98
+ @change="$store.settings.saveSettings(true)"
99
+ aria-label="Polling interval slider">
100
+ <input type="number" min="10" max="300"
101
+ class="input input-sm input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
102
+ x-model.number="$store.settings.refreshInterval"
103
+ @change="$store.settings.saveSettings(true)"
104
+ aria-label="Polling interval value">
105
+ </div>
106
+ <div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
107
+ <span>10s</span>
108
+ <span>300s</span>
109
+ </div>
110
+ </div>
111
+
112
+ <!-- Log Buffer -->
113
+ <div class="form-control col-span-full">
114
+ <label class="label">
115
+ <span class="label-text text-gray-300" x-text="$store.global.t('maxDisplayLogs')">Log Buffer
116
+ Size</span>
117
+ <span class="label-text-alt font-mono text-neon-purple"
118
+ x-text="$store.settings.logLimit + ' ' + $store.global.t('lines')"></span>
119
+ </label>
120
+ <div class="flex gap-3 items-center">
121
+ <input type="range" min="500" max="5000" step="500" class="custom-range custom-range-purple flex-1"
122
+ x-model.number="$store.settings.logLimit"
123
+ :style="`background-size: ${($store.settings.logLimit - 500) / 45}% 100%`"
124
+ @change="$store.settings.saveSettings(true)"
125
+ aria-label="Log buffer size slider">
126
+ <input type="number" min="500" max="5000" step="500"
127
+ class="input input-sm input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
128
+ x-model.number="$store.settings.logLimit"
129
+ @change="$store.settings.saveSettings(true)"
130
+ aria-label="Log buffer size value">
131
+ </div>
132
+ <div class="w-full flex justify-between text-xs px-2 mt-2 text-gray-600 font-mono">
133
+ <span>500</span>
134
+ <span>5000</span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="divider border-space-border/50"></div>
140
+
141
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
142
+ <div class="form-control bg-space-900/50 p-4 rounded-lg border transition-all duration-300 hover:border-neon-purple/50"
143
+ :class="$store.settings.showExhausted ? 'border-neon-purple/50 bg-neon-purple/5 shadow-[0_0_15px_rgba(168,85,247,0.1)]' : 'border-space-border/50'">
144
+ <div class="flex items-center justify-between">
145
+ <div class="flex flex-col gap-1">
146
+ <span class="label-text font-medium transition-colors"
147
+ :class="$store.settings.showExhausted ? 'text-neon-purple' : 'text-gray-300'"
148
+ x-text="$store.global.t('showExhausted')">Show Exhausted Models</span>
149
+ <span class="text-xs text-gray-500"
150
+ x-text="$store.global.t('showExhaustedDesc')">Display models even if they have 0%
151
+ remaining quota.</span>
152
+ </div>
153
+ <label class="relative inline-flex items-center cursor-pointer">
154
+ <input type="checkbox" class="sr-only peer"
155
+ :checked="$store.settings.showExhausted === true"
156
+ @change="$store.settings.showExhausted = $event.target.checked; $store.settings.saveSettings(true)"
157
+ aria-label="Show exhausted models toggle">
158
+ <div
159
+ class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-purple peer-checked:after:bg-white">
160
+ </div>
161
+ </label>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="form-control bg-space-900/50 p-4 rounded-lg border transition-all duration-300 hover:border-neon-green/50"
166
+ :class="$store.settings.compact ? 'border-neon-green/50 bg-neon-green/5 shadow-[0_0_15px_rgba(34,197,94,0.1)]' : 'border-space-border/50'">
167
+ <div class="flex items-center justify-between">
168
+ <div class="flex flex-col gap-1">
169
+ <span class="label-text font-medium transition-colors"
170
+ :class="$store.settings.compact ? 'text-neon-green' : 'text-gray-300'"
171
+ x-text="$store.global.t('compactMode')">Compact Mode</span>
172
+ <span class="text-xs text-gray-500" x-text="$store.global.t('compactModeDesc')">Reduce
173
+ padding in tables for higher information density.</span>
174
+ </div>
175
+ <label class="relative inline-flex items-center cursor-pointer">
176
+ <input type="checkbox" class="sr-only peer" :checked="$store.settings.compact === true"
177
+ @change="$store.settings.compact = $event.target.checked; $store.settings.saveSettings(true)"
178
+ aria-label="Compact mode toggle">
179
+ <div
180
+ class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white">
181
+ </div>
182
+ </label>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Tab 2: Claude CLI Configuration -->
189
+ <div x-show="activeTab === 'claude'" x-data="claudeConfig" class="space-y-6 max-w-3xl animate-fade-in">
190
+ <div class="alert bg-space-900/50 border-space-border text-sm shadow-none">
191
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
192
+ class="stroke-info shrink-0 w-6 h-6">
193
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
194
+ d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
195
+ </svg>
196
+ <span class="text-gray-400">
197
+ <span x-text="$store.global.t('claudeSettingsAlertPrefix')">Settings below directly modify</span>
198
+ <code class="text-neon-cyan font-mono" x-text="configPath">~/.claude/settings.json</code>.
199
+ <span x-text="$store.global.t('claudeSettingsAlertSuffix')">Restart Claude CLI to apply.</span>
200
+ </span>
201
+ </div>
202
+
203
+ <!-- Configuration Presets -->
204
+ <div class="card bg-space-900/30 border border-neon-cyan/30 p-5">
205
+ <div class="flex items-center justify-between mb-4">
206
+ <div class="flex items-center gap-2">
207
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-neon-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor">
208
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
209
+ </svg>
210
+ <label class="text-xs uppercase text-neon-cyan font-semibold" x-text="$store.global.t('configPresets') || 'Configuration Presets'">Configuration Presets</label>
211
+ </div>
212
+ <button class="btn btn-xs btn-ghost text-neon-cyan hover:bg-neon-cyan/10 gap-1"
213
+ @click="saveCurrentAsPreset()"
214
+ :disabled="savingPreset">
215
+ <svg x-show="!savingPreset" xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
216
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
217
+ </svg>
218
+ <span x-show="!savingPreset" x-text="$store.global.t('saveAsPreset') || 'Save as Preset'">Save as Preset</span>
219
+ <span x-show="savingPreset" class="loading loading-spinner loading-xs"></span>
220
+ </button>
221
+ </div>
222
+ <div class="flex gap-2">
223
+ <select
224
+ class="select select-sm bg-space-800 border-space-border text-white flex-1 font-mono text-xs"
225
+ :disabled="presets.length === 0"
226
+ :value="selectedPresetName"
227
+ @change="onPresetSelect($event.target.value)"
228
+ aria-label="Select preset">
229
+ <option value="" disabled x-show="presets.length === 0">No presets available</option>
230
+ <template x-for="preset in presets" :key="preset.name">
231
+ <option :value="preset.name" x-text="preset.name" :selected="preset.name === selectedPresetName"></option>
232
+ </template>
233
+ </select>
234
+ <button class="btn btn-sm btn-ghost text-red-400 hover:bg-red-500/10"
235
+ @click="deleteSelectedPreset()"
236
+ :disabled="!selectedPresetName || presets.length === 0 || deletingPreset"
237
+ :title="$store.global.t('deletePreset') || 'Delete preset'">
238
+ <span x-show="!deletingPreset">
239
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
240
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
241
+ </svg>
242
+ </span>
243
+ <span x-show="deletingPreset" class="loading loading-spinner loading-xs"></span>
244
+ </button>
245
+ </div>
246
+ <p class="text-[10px] text-gray-600 mt-2" x-text="$store.global.t('presetHint') || 'Select a preset to load it. Click \"Apply to Claude CLI\" to save changes.'">Select a preset to load it. Click "Apply to Claude CLI" to save changes.</p>
247
+ </div>
248
+
249
+ <!-- Base URL -->
250
+ <div class="card bg-space-900/30 border border-space-border/50 p-5">
251
+ <label class="label text-xs uppercase text-gray-500 font-semibold mb-2"
252
+ x-text="$store.global.t('proxyConnection')">Proxy Connection</label>
253
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
254
+ <div>
255
+ <div class="text-[11px] text-gray-400 mb-1 font-mono">ANTHROPIC_BASE_URL</div>
256
+ <input type="text" x-model="config.env.ANTHROPIC_BASE_URL"
257
+ placeholder="http://localhost:8080"
258
+ class="input input-sm input-bordered !bg-space-800 w-full font-mono text-sm !border-space-border focus:!border-neon-purple !text-white placeholder:!text-gray-600">
259
+ </div>
260
+ <div>
261
+ <div class="text-[11px] text-gray-400 mb-1 font-mono">ANTHROPIC_AUTH_TOKEN</div>
262
+ <input type="password" x-model="config.env.ANTHROPIC_AUTH_TOKEN" placeholder="any-string"
263
+ class="input input-sm input-bordered !bg-space-800 w-full font-mono text-sm !border-space-border focus:!border-neon-purple !text-white placeholder:!text-gray-600">
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- Models Selection -->
269
+ <div class="card bg-space-900/30 border border-space-border/50 p-5">
270
+ <label class="label text-xs uppercase text-gray-500 font-semibold mb-2"
271
+ x-text="$store.global.t('modelSelection')">Model Selection</label>
272
+
273
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
274
+ <!-- Primary -->
275
+ <div class="form-control">
276
+ <label class="label pt-0 pb-1 text-[11px] text-gray-400 font-bold tracking-wider"
277
+ x-text="$store.global.t('primaryModel')">Primary Model</label>
278
+ <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
279
+ <input type="text"
280
+ :value="open ? searchTerm : config.env.ANTHROPIC_MODEL"
281
+ @input="searchTerm = $event.target.value"
282
+ @focus="open = true; searchTerm = ''"
283
+ @click.away="open = false; searchTerm = ''"
284
+ class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
285
+ :placeholder="open ? $store.global.t('typeToSearch') : ''"
286
+ :class="{ '!text-gray-500': !open && !config.env.ANTHROPIC_MODEL }"
287
+ aria-label="Primary model selection">
288
+ <div class="absolute right-2 top-1.5 cursor-pointer text-gray-500 hover:text-white transition-colors"
289
+ @click="open = !open; if(open) { searchTerm = ''; $el.previousElementSibling.focus() }"
290
+ @mousedown.prevent>▼</div>
291
+
292
+ <ul x-show="open" x-transition:enter="transition ease-out duration-100"
293
+ x-transition:enter-start="opacity-0 scale-95"
294
+ x-transition:enter-end="opacity-100 scale-100"
295
+ class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
296
+ <template
297
+ x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
298
+ :key="modelId">
299
+ <li>
300
+ <a @mousedown.prevent="selectModel('ANTHROPIC_MODEL', modelId); open = false; searchTerm = ''"
301
+ class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
302
+ :class="config.env.ANTHROPIC_MODEL === modelId || config.env.ANTHROPIC_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
303
+ <div class="flex items-center gap-2">
304
+ <span class="w-1.5 h-1.5 rounded-full"
305
+ :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
306
+ <span x-text="modelId"></span>
307
+ </div>
308
+ <template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
309
+ <span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
310
+ </template>
311
+ </a>
312
+ </li>
313
+ </template>
314
+ <li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
315
+ <span class="text-xs text-gray-500 italic py-2"
316
+ x-text="$store.global.t('noMatchingModels')">No matching models</span>
317
+ </li>
318
+ </ul>
319
+ </div>
320
+ <span class="text-[10px] text-gray-600 mt-1 font-mono">ANTHROPIC_MODEL</span>
321
+ </div>
322
+
323
+ <!-- Sub-agent -->
324
+ <div class="form-control">
325
+ <label class="label pt-0 pb-1 text-[11px] text-gray-400 font-bold tracking-wider"
326
+ x-text="$store.global.t('subAgentModel')">Sub-agent Model</label>
327
+ <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
328
+ <input type="text"
329
+ :value="open ? searchTerm : config.env.CLAUDE_CODE_SUBAGENT_MODEL"
330
+ @input="searchTerm = $event.target.value"
331
+ @focus="open = true; searchTerm = ''"
332
+ @click.away="open = false; searchTerm = ''"
333
+ class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-purple pr-8 placeholder:!text-gray-600"
334
+ :placeholder="open ? $store.global.t('typeToSearch') : ''"
335
+ :class="{ '!text-gray-500': !open && !config.env.CLAUDE_CODE_SUBAGENT_MODEL }"
336
+ aria-label="Sub-agent model selection">
337
+ <div class="absolute right-2 top-1.5 cursor-pointer text-gray-500 hover:text-white transition-colors"
338
+ @click="open = !open; if(open) { searchTerm = ''; $el.previousElementSibling.focus() }"
339
+ @mousedown.prevent>▼</div>
340
+
341
+ <ul x-show="open" x-transition:enter="transition ease-out duration-100"
342
+ x-transition:enter-start="opacity-0 scale-95"
343
+ x-transition:enter-end="opacity-100 scale-100"
344
+ class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
345
+ <template
346
+ x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
347
+ :key="modelId">
348
+ <li>
349
+ <a @mousedown.prevent="selectModel('CLAUDE_CODE_SUBAGENT_MODEL', modelId); open = false; searchTerm = ''"
350
+ class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
351
+ :class="config.env.CLAUDE_CODE_SUBAGENT_MODEL === modelId || config.env.CLAUDE_CODE_SUBAGENT_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
352
+ <div class="flex items-center gap-2">
353
+ <span class="w-1.5 h-1.5 rounded-full"
354
+ :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
355
+ <span x-text="modelId"></span>
356
+ </div>
357
+ <template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
358
+ <span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
359
+ </template>
360
+ </a>
361
+ </li>
362
+ </template>
363
+ <li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
364
+ <span class="text-xs text-gray-500 italic py-2"
365
+ x-text="$store.global.t('noMatchingModels')">No matching models</span>
366
+ </li>
367
+ </ul>
368
+ </div>
369
+ <span class="text-[10px] text-gray-600 mt-1 font-mono">CLAUDE_CODE_SUBAGENT_MODEL</span>
370
+ </div>
371
+ </div>
372
+
373
+ <div class="divider text-xs font-mono text-gray-600 my-2"
374
+ x-text="$store.global.t('defaultModelAliases')">DEFAULT MODEL ALIASES</div>
375
+
376
+ <!-- Overrides -->
377
+ <div class="space-y-4">
378
+ <!-- Opus -->
379
+ <div class="form-control">
380
+ <label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold"
381
+ x-text="$store.global.t('opusAlias')">Opus Alias</label>
382
+ <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
383
+ <input type="text"
384
+ :value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_OPUS_MODEL"
385
+ @input="searchTerm = $event.target.value"
386
+ @focus="open = true; searchTerm = ''"
387
+ @click.away="open = false; searchTerm = ''"
388
+ class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
389
+ :placeholder="open ? $store.global.t('searchPlaceholder') : ''"
390
+ :class="{ '!text-gray-500': !open && !config.env.ANTHROPIC_DEFAULT_OPUS_MODEL }"
391
+ aria-label="Opus model alias selection">
392
+ <div class="absolute right-2 top-1.5 cursor-pointer text-gray-500 hover:text-white transition-colors"
393
+ @click="open = !open; if(open) { searchTerm = ''; $el.previousElementSibling.focus() }"
394
+ @mousedown.prevent>▼</div>
395
+ <ul x-show="open" x-transition:enter="transition ease-out duration-100"
396
+ x-transition:enter-start="opacity-0 scale-95"
397
+ x-transition:enter-end="opacity-100 scale-100"
398
+ class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
399
+ <template
400
+ x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
401
+ :key="modelId">
402
+ <li>
403
+ <a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_OPUS_MODEL', modelId); open = false; searchTerm = ''"
404
+ class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
405
+ :class="config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
406
+ <div class="flex items-center gap-2">
407
+ <span class="w-1.5 h-1.5 rounded-full"
408
+ :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
409
+ <span x-text="modelId"></span>
410
+ </div>
411
+ <template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
412
+ <span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
413
+ </template>
414
+ </a>
415
+ </li>
416
+ </template>
417
+ <li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
418
+ <span class="text-xs text-gray-500 italic py-2"
419
+ x-text="$store.global.t('noMatchingModels')">No matching models</span>
420
+ </li>
421
+ </ul>
422
+ </div>
423
+ </div>
424
+ <!-- Sonnet -->
425
+ <div class="form-control">
426
+ <label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold"
427
+ x-text="$store.global.t('sonnetAlias')">Sonnet Alias</label>
428
+ <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
429
+ <input type="text"
430
+ :value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_SONNET_MODEL"
431
+ @input="searchTerm = $event.target.value"
432
+ @focus="open = true; searchTerm = ''"
433
+ @click.away="open = false; searchTerm = ''"
434
+ class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
435
+ :placeholder="open ? $store.global.t('searchPlaceholder') : ''"
436
+ :class="{ '!text-gray-500': !open && !config.env.ANTHROPIC_DEFAULT_SONNET_MODEL }"
437
+ aria-label="Sonnet model alias selection">
438
+ <div class="absolute right-2 top-1.5 cursor-pointer text-gray-500 hover:text-white transition-colors"
439
+ @click="open = !open; if(open) { searchTerm = ''; $el.previousElementSibling.focus() }"
440
+ @mousedown.prevent>▼</div>
441
+ <ul x-show="open" x-transition:enter="transition ease-out duration-100"
442
+ x-transition:enter-start="opacity-0 scale-95"
443
+ x-transition:enter-end="opacity-100 scale-100"
444
+ class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
445
+ <template
446
+ x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
447
+ :key="modelId">
448
+ <li>
449
+ <a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_SONNET_MODEL', modelId); open = false; searchTerm = ''"
450
+ class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
451
+ :class="config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
452
+ <div class="flex items-center gap-2">
453
+ <span class="w-1.5 h-1.5 rounded-full"
454
+ :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
455
+ <span x-text="modelId"></span>
456
+ </div>
457
+ <template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
458
+ <span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
459
+ </template>
460
+ </a>
461
+ </li>
462
+ </template>
463
+ <li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
464
+ <span class="text-xs text-gray-500 italic py-2"
465
+ x-text="$store.global.t('noMatchingModels')">No matching models</span>
466
+ </li>
467
+ </ul>
468
+ </div>
469
+ </div>
470
+ <!-- Haiku -->
471
+ <div class="form-control">
472
+ <label class="label pt-0 pb-1 text-[10px] text-gray-500 uppercase font-bold"
473
+ x-text="$store.global.t('haikuAlias')">Haiku Alias</label>
474
+ <div class="relative w-full" x-data="{ open: false, searchTerm: '' }">
475
+ <input type="text"
476
+ :value="open ? searchTerm : config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL"
477
+ @input="searchTerm = $event.target.value"
478
+ @focus="open = true; searchTerm = ''"
479
+ @click.away="open = false; searchTerm = ''"
480
+ class="input input-sm w-full font-mono text-xs !bg-space-800 !border-space-border !text-white focus:!bg-space-800 focus:!border-neon-cyan pr-8 placeholder:!text-gray-600"
481
+ :placeholder="open ? $store.global.t('searchPlaceholder') : ''"
482
+ :class="{ '!text-gray-500': !open && !config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL }"
483
+ aria-label="Haiku model alias selection">
484
+ <div class="absolute right-2 top-1.5 cursor-pointer text-gray-500 hover:text-white transition-colors"
485
+ @click="open = !open; if(open) { searchTerm = ''; $el.previousElementSibling.focus() }"
486
+ @mousedown.prevent>▼</div>
487
+ <ul x-show="open" x-transition:enter="transition ease-out duration-100"
488
+ x-transition:enter-start="opacity-0 scale-95"
489
+ x-transition:enter-end="opacity-100 scale-100"
490
+ class="absolute left-0 right-0 top-full mt-1 menu p-2 shadow-2xl bg-space-900 border border-space-border rounded-lg max-h-60 overflow-y-auto z-[100] custom-scrollbar">
491
+ <template
492
+ x-for="modelId in $store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase()))"
493
+ :key="modelId">
494
+ <li>
495
+ <a @mousedown.prevent="selectModel('ANTHROPIC_DEFAULT_HAIKU_MODEL', modelId); open = false; searchTerm = ''"
496
+ class="font-mono text-xs py-2 hover:bg-space-800 border-b border-space-border/30 last:border-0 flex items-center justify-between gap-2"
497
+ :class="config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === modelId || config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === modelId + '[1m]' ? 'text-neon-cyan bg-space-800/50' : 'text-gray-300'">
498
+ <div class="flex items-center gap-2">
499
+ <span class="w-1.5 h-1.5 rounded-full"
500
+ :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
501
+ <span x-text="modelId"></span>
502
+ </div>
503
+ <template x-if="gemini1mSuffix && $store.data.getModelFamily(modelId) === 'gemini'">
504
+ <span class="text-[10px] bg-neon-green/10 text-neon-green px-1.5 py-0.5 rounded border border-neon-green/20 font-bold uppercase tracking-tighter">[1M]</span>
505
+ </template>
506
+ </a>
507
+ </li>
508
+ </template>
509
+ <li x-show="$store.data.models.filter(m => m.toLowerCase().includes(searchTerm.toLowerCase())).length === 0">
510
+ <span class="text-xs text-gray-500 italic py-2"
511
+ x-text="$store.global.t('noMatchingModels')">No matching models</span>
512
+ </li>
513
+ </ul>
514
+ </div>
515
+ </div>
516
+ </div>
517
+ </div>
518
+
519
+ <!-- MCP CLI Experimental Mode -->
520
+ <div class="card bg-space-900/30 border border-space-border/50 p-5">
521
+ <div class="flex items-center justify-between">
522
+ <div class="flex flex-col gap-1">
523
+ <span class="text-sm font-medium transition-colors"
524
+ :class="config.env.ENABLE_EXPERIMENTAL_MCP_CLI === 'true' ? 'text-neon-green' : 'text-gray-300'"
525
+ x-text="$store.global.t('mcpCliExperimental')">Experimental MCP CLI</span>
526
+ <span class="text-[11px] text-gray-500" x-text="$store.global.t('mcpCliDesc')">
527
+ Enables experimental MCP integration for reliable tool usage with reduced context consumption.
528
+ </span>
529
+ </div>
530
+ <label class="relative inline-flex items-center cursor-pointer">
531
+ <input type="checkbox" class="sr-only peer"
532
+ :checked="config.env.ENABLE_EXPERIMENTAL_MCP_CLI === 'true'"
533
+ @change="config.env.ENABLE_EXPERIMENTAL_MCP_CLI = $event.target.checked ? 'true' : 'false'"
534
+ aria-label="Experimental MCP CLI toggle">
535
+ <div
536
+ class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white">
537
+ </div>
538
+ </label>
539
+ </div>
540
+ </div>
541
+
542
+ <!-- Gemini 1M Context Suffix Toggle -->
543
+ <div class="card bg-space-900/30 border border-space-border/50 p-5">
544
+ <div class="flex items-center justify-between">
545
+ <div class="flex flex-col gap-1">
546
+ <span class="text-sm font-medium transition-colors"
547
+ :class="gemini1mSuffix ? 'text-neon-green' : 'text-gray-300'"
548
+ x-text="$store.global.t('gemini1mMode')">Gemini 1M Context Mode</span>
549
+ <span class="text-[11px] text-gray-500" x-text="$store.global.t('gemini1mDesc')">
550
+ Appends [1m] suffix to Gemini models for 1M context window support.
551
+ </span>
552
+ <span class="text-[10px] text-yellow-500/80" x-text="$store.global.t('gemini1mWarning')">
553
+ ⚠ Large context may reduce Gemini-3-Pro performance.
554
+ </span>
555
+ </div>
556
+ <label class="relative inline-flex items-center cursor-pointer">
557
+ <input type="checkbox" class="sr-only peer"
558
+ :checked="gemini1mSuffix"
559
+ @change="toggleGemini1mSuffix($event.target.checked)"
560
+ aria-label="Gemini 1M context mode toggle">
561
+ <div
562
+ class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white">
563
+ </div>
564
+ </label>
565
+ </div>
566
+ </div>
567
+
568
+ <div class="flex justify-end pt-2 gap-3">
569
+ <button class="btn btn-sm btn-ghost border border-space-border/50 hover:border-red-500/30 hover:bg-red-500/5 text-gray-400 hover:text-red-400 px-6 gap-2"
570
+ @click="restoreDefaultClaudeConfig" :disabled="restoring">
571
+ <svg x-show="!restoring" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
572
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
573
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
574
+ </svg>
575
+ <span x-show="!restoring" x-text="$store.global.t('restoreDefault')">Restore Default</span>
576
+ <span x-show="restoring" class="loading loading-spinner loading-xs"></span>
577
+ </button>
578
+ <button class="btn btn-sm bg-neon-purple hover:bg-purple-600 border-none text-white px-6 gap-2"
579
+ @click="saveClaudeConfig" :disabled="loading">
580
+ <svg x-show="!loading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
581
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
582
+ d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
583
+ </svg>
584
+ <span x-show="!loading" x-text="$store.global.t('applyToClaude')">Apply to Claude CLI</span>
585
+ <span x-show="loading" class="loading loading-spinner loading-xs"></span>
586
+ </button>
587
+ </div>
588
+
589
+ <!-- Restore Defaults Confirmation Modal -->
590
+ <dialog id="restore_defaults_modal" class="modal">
591
+ <div class="modal-box bg-space-900 border-2 border-red-500/50">
592
+ <h3 class="font-bold text-lg text-red-400 flex items-center gap-2">
593
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
594
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
595
+ </svg>
596
+ <span x-text="$store.global.t('confirmRestoreTitle')">Confirm Restore</span>
597
+ </h3>
598
+ <p class="py-4 text-gray-300" x-text="$store.global.t('confirmRestoreMessage')">
599
+ Are you sure you want to restore Claude CLI to default settings? This will remove proxy configuration.
600
+ </p>
601
+ <div class="modal-action">
602
+ <button class="btn btn-ghost text-gray-400" onclick="document.getElementById('restore_defaults_modal').close()"
603
+ x-text="$store.global.t('cancel')">Cancel</button>
604
+ <button class="btn bg-red-500 hover:bg-red-600 border-none text-white" @click="executeRestore()"
605
+ :disabled="restoring"
606
+ :class="{ 'loading': restoring }">
607
+ <span x-text="$store.global.t('confirmRestore')" x-show="!restoring">Confirm Restore</span>
608
+ </button>
609
+ </div>
610
+ </div>
611
+ <form method="dialog" class="modal-backdrop">
612
+ <button>close</button>
613
+ </form>
614
+ </dialog>
615
+
616
+ <!-- Unsaved Changes Confirmation Modal -->
617
+ <dialog id="unsaved_changes_modal" class="modal">
618
+ <div class="modal-box bg-space-900 border-2 border-yellow-500/50">
619
+ <h3 class="font-bold text-lg text-yellow-400 flex items-center gap-2">
620
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
621
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
622
+ </svg>
623
+ <span x-text="$store.global.t('unsavedChangesTitle') || 'Unsaved Changes'">Unsaved Changes</span>
624
+ </h3>
625
+ <p class="py-4 text-gray-300">
626
+ <span x-text="$store.global.t('unsavedChangesMessage') || 'Your current configuration doesn\'t match any saved preset.'">Your current configuration doesn't match any saved preset.</span>
627
+ <br><br>
628
+ <span class="text-yellow-400/80" x-text="'Load \"' + pendingPresetName + '\" and lose current changes?'"></span>
629
+ </p>
630
+ <div class="modal-action">
631
+ <button class="btn btn-ghost text-gray-400" @click="cancelLoadPreset()"
632
+ x-text="$store.global.t('cancel')">Cancel</button>
633
+ <button class="btn bg-yellow-500 hover:bg-yellow-600 border-none text-black" @click="confirmLoadPreset()">
634
+ <span x-text="$store.global.t('loadAnyway') || 'Load Anyway'">Load Anyway</span>
635
+ </button>
636
+ </div>
637
+ </div>
638
+ <form method="dialog" class="modal-backdrop">
639
+ <button @click="cancelLoadPreset()">close</button>
640
+ </form>
641
+ </dialog>
642
+
643
+ <!-- Save Preset Modal -->
644
+ <dialog id="save_preset_modal" class="modal">
645
+ <div class="modal-box bg-space-900 border-2 border-neon-cyan/50">
646
+ <h3 class="font-bold text-lg text-neon-cyan flex items-center gap-2">
647
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
648
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
649
+ </svg>
650
+ <span x-text="$store.global.t('savePresetTitle') || 'Save Preset'">Save Preset</span>
651
+ </h3>
652
+ <p class="py-2 text-gray-400 text-sm" x-text="$store.global.t('savePresetDesc') || 'Save the current configuration as a reusable preset.'">
653
+ Save the current configuration as a reusable preset.
654
+ </p>
655
+ <div class="form-control mt-4">
656
+ <label class="label">
657
+ <span class="label-text text-gray-300" x-text="$store.global.t('presetName') || 'Preset Name'">Preset Name</span>
658
+ </label>
659
+ <input type="text" x-model="newPresetName"
660
+ class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
661
+ :placeholder="$store.global.t('presetNamePlaceholder') || 'e.g., My Work Setup'"
662
+ @keydown.enter="executeSavePreset(newPresetName)"
663
+ x-init="$watch('$el.closest(\'dialog\').open', open => { if (open) { $nextTick(() => $el.focus()) } })"
664
+ aria-label="Preset name">
665
+ </div>
666
+ <div class="modal-action">
667
+ <button class="btn btn-ghost text-gray-400" @click="newPresetName = ''; document.getElementById('save_preset_modal').close()"
668
+ x-text="$store.global.t('cancel')">Cancel</button>
669
+ <button class="btn bg-neon-cyan hover:bg-cyan-600 border-none text-black"
670
+ @click="executeSavePreset(newPresetName)"
671
+ :disabled="!newPresetName.trim() || savingPreset"
672
+ :class="{ 'loading': savingPreset }">
673
+ <span x-show="!savingPreset" x-text="$store.global.t('savePreset') || 'Save Preset'">Save Preset</span>
674
+ </button>
675
+ </div>
676
+ </div>
677
+ <form method="dialog" class="modal-backdrop">
678
+ <button @click="newPresetName = ''">close</button>
679
+ </form>
680
+ </dialog>
681
+ </div>
682
+
683
+ <!-- Tab 3: Models Configuration -->
684
+ <div x-show="activeTab === 'models'" x-data="window.Components.modelManager()"
685
+ class="space-y-6 max-w-3xl animate-fade-in">
686
+
687
+ <div class="flex items-center justify-between">
688
+ <div>
689
+ <div class="text-sm text-gray-400" x-text="$store.global.t('modelsDesc')">Configure model visibility, pinning, and request mapping.</div>
690
+ <div class="text-xs text-gray-600 mt-1" x-text="$store.global.t('modelMappingHint')">Model mapping: server-side redirection. Claude Code users: see 'Claude CLI' tab for client-side setup.</div>
691
+ </div>
692
+ <div class="flex items-center gap-2">
693
+ <span class="text-xs text-gray-500" x-text="$store.global.t('showHidden')">Show Hidden Models</span>
694
+ <label class="relative inline-flex items-center cursor-pointer">
695
+ <input type="checkbox" class="sr-only peer"
696
+ :checked="$store.settings.showHiddenModels === true"
697
+ @change="$store.settings.showHiddenModels = $event.target.checked; $store.settings.saveSettings(true)"
698
+ aria-label="Show hidden models toggle">
699
+ <div
700
+ class="w-8 h-[18px] bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-3.5 after:w-3.5 after:transition-all peer-checked:bg-neon-purple peer-checked:after:bg-white">
701
+ </div>
702
+ </label>
703
+ </div>
704
+ </div>
705
+
706
+ <!-- Models List -->
707
+ <div class="view-card !p-0">
708
+ <table class="standard-table">
709
+ <thead>
710
+ <tr>
711
+ <th class="pl-4 w-5/12" x-text="$store.global.t('modelId')">Model ID</th>
712
+ <th class="w-5/12" x-text="$store.global.t('modelMapping')">Mapping (Target Model ID)</th>
713
+ <th class="w-2/12 text-right pr-4" x-text="$store.global.t('actions')">Actions</th>
714
+ </tr>
715
+ </thead>
716
+ <tbody>
717
+ <template x-for="modelId in $store.data.models" :key="modelId">
718
+ <tr class="hover:bg-white/5 transition-colors group"
719
+ :class="isHidden ? 'opacity-50' : ''"
720
+ x-show="!isHidden || $store.settings.showHiddenModels"
721
+ x-data="{
722
+ newMapping: '',
723
+ get config() { return $store.data.modelConfig[modelId] || {} },
724
+ get isPinned() { return !!this.config.pinned },
725
+ get isHidden() {
726
+ if (this.config.hidden !== undefined) return this.config.hidden;
727
+ const family = $store.data.getModelFamily(modelId);
728
+ return (family === 'other' || family === 'unknown');
729
+ }
730
+ }" x-init="newMapping = config.mapping || ''">
731
+ <td class="pl-4 font-mono text-xs text-gray-300">
732
+ <div class="flex items-center gap-2">
733
+ <span class="w-1.5 h-1.5 rounded-full"
734
+ :class="$store.data.getModelFamily(modelId) === 'claude' ? 'bg-neon-purple shadow-[0_0_5px_rgba(168,85,247,0.5)]' : ($store.data.getModelFamily(modelId) === 'gemini' ? 'bg-neon-green shadow-[0_0_5px_rgba(34,197,94,0.5)]' : 'bg-gray-600')"></span>
735
+ <span x-text="modelId"></span>
736
+ </div>
737
+ </td>
738
+ <td>
739
+ <div x-show="!isEditing(modelId)"
740
+ class="flex items-center gap-2 group-hover:text-white transition-colors cursor-pointer py-2"
741
+ @click="startEditing(modelId); newMapping = config.mapping || ''; $nextTick(() => $refs['input-' + modelId]?.focus())">
742
+ <span x-text="config.mapping || $store.global.t('clickToSet')"
743
+ :class="{'text-gray-600 italic': !config.mapping, 'text-gray-300': config.mapping}"
744
+ class="text-xs font-mono"></span>
745
+ <svg class="w-3 h-3 text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
746
+ fill="none" viewBox="0 0 24 24" stroke="currentColor">
747
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
748
+ d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
749
+ </svg>
750
+ </div>
751
+ <div x-show="isEditing(modelId)" class="flex items-center gap-1">
752
+ <select x-model="newMapping"
753
+ :x-ref="'input-' + modelId"
754
+ class="select select-sm bg-space-800 border-space-border text-white focus:outline-none focus:border-neon-cyan flex-1 font-mono text-xs !h-8 min-h-0"
755
+ @keydown.enter="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
756
+ @keydown.escape="newMapping = config.mapping || ''; stopEditing()">
757
+ <option value="" x-text="$store.global.t('none')">None</option>
758
+ <template x-for="mId in $store.data.models" :key="mId">
759
+ <option :value="mId" x-text="mId" :selected="mId === newMapping"></option>
760
+ </template>
761
+ </select>
762
+ <button class="btn-action-success"
763
+ @click="await updateModelConfig(modelId, { mapping: newMapping }); stopEditing()"
764
+ :title="$store.global.t('saveChanges')">
765
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
766
+ viewBox="0 0 24 24" stroke="currentColor">
767
+ <path stroke-linecap="round" stroke-linejoin="round"
768
+ stroke-width="2" d="M5 13l4 4L19 7" />
769
+ </svg>
770
+ </button>
771
+ <button class="btn-action-neutral"
772
+ @click="newMapping = config.mapping || ''; stopEditing()"
773
+ :title="$store.global.t('cancel')">
774
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
775
+ viewBox="0 0 24 24" stroke="currentColor">
776
+ <path stroke-linecap="round" stroke-linejoin="round"
777
+ stroke-width="2" d="M6 18L18 6M6 6l12 12" />
778
+ </svg>
779
+ </button>
780
+ <button x-show="config.mapping"
781
+ class="btn-action-danger"
782
+ @click="await updateModelConfig(modelId, { mapping: '' }); stopEditing()"
783
+ :title="$store.global.t('delete')">
784
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none"
785
+ viewBox="0 0 24 24" stroke="currentColor">
786
+ <path stroke-linecap="round" stroke-linejoin="round"
787
+ stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
788
+ </svg>
789
+ </button>
790
+ </div>
791
+ </td>
792
+ <td class="text-right pr-4">
793
+ <div class="flex items-center justify-end gap-2">
794
+ <!-- Pin Toggle -->
795
+ <button class="btn btn-xs btn-circle transition-colors"
796
+ :class="isPinned ? 'bg-neon-purple/20 text-neon-purple border-neon-purple/50 hover:bg-neon-purple/30' : 'btn-ghost text-gray-600 hover:text-gray-300'"
797
+ @click="await updateModelConfig(modelId, { pinned: !isPinned })"
798
+ :title="$store.global.t('pinToTop')">
799
+ <svg x-show="isPinned" xmlns="http://www.w3.org/2000/svg"
800
+ class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
801
+ <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" />
802
+ </svg>
803
+ <svg x-show="!isPinned" xmlns="http://www.w3.org/2000/svg"
804
+ class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24"
805
+ stroke="currentColor">
806
+ <path stroke-linecap="round" stroke-linejoin="round"
807
+ stroke-width="2"
808
+ d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
809
+ </svg>
810
+ </button>
811
+
812
+ <!-- Hide Toggle -->
813
+ <button class="btn btn-xs btn-circle transition-colors"
814
+ :class="isHidden ? 'bg-red-500/20 text-red-400 border-red-500/50 hover:bg-red-500/30' : 'btn-ghost text-gray-400 hover:text-white'"
815
+ @click="await updateModelConfig(modelId, { hidden: !isHidden })"
816
+ :title="$store.global.t('toggleVisibility')">
817
+ <svg x-show="!isHidden" xmlns="http://www.w3.org/2000/svg"
818
+ class="h-4 w-4" fill="none" viewBox="0 0 24 24"
819
+ stroke="currentColor">
820
+ <path stroke-linecap="round" stroke-linejoin="round"
821
+ stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
822
+ <path stroke-linecap="round" stroke-linejoin="round"
823
+ stroke-width="2"
824
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
825
+ </svg>
826
+ <svg x-show="isHidden" xmlns="http://www.w3.org/2000/svg"
827
+ class="h-4 w-4" fill="none" viewBox="0 0 24 24"
828
+ stroke="currentColor">
829
+ <path stroke-linecap="round" stroke-linejoin="round"
830
+ stroke-width="2"
831
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
832
+ </svg>
833
+ </button>
834
+ </div>
835
+ </td>
836
+ </tr>
837
+ </template>
838
+ <tr x-show="!$store.data.models.length">
839
+ <td colspan="3" class="text-center py-8 text-gray-600 text-xs font-mono"
840
+ x-text="$store.global.t('noModels')">
841
+ NO MODELS DETECTED
842
+ </td>
843
+ </tr>
844
+ </tbody>
845
+ </table>
846
+ </div>
847
+ </div>
848
+
849
+ <!-- Tab 4: Server Configuration -->
850
+ <div x-show="activeTab === 'server'" x-data="window.Components.serverConfig()"
851
+ class="space-y-6 max-w-2xl animate-fade-in pb-10">
852
+ <!-- 🔐 Security Section -->
853
+ <div class="view-card border-neon-yellow/20 bg-neon-yellow/5">
854
+ <div class="flex items-center justify-between">
855
+ <div class="flex items-center gap-4">
856
+ <div
857
+ class="w-10 h-10 rounded-lg bg-neon-yellow/10 flex items-center justify-center text-neon-yellow">
858
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24"
859
+ stroke="currentColor">
860
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
861
+ d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
862
+ </svg>
863
+ </div>
864
+ <div>
865
+ <h4 class="text-sm font-bold text-white mb-0.5"
866
+ x-text="$store.global.t('changePassword')">WebUI Password</h4>
867
+ <p class="text-xs text-gray-500" x-text="$store.global.t('changePasswordDesc')">
868
+ Authentication for accessing this dashboard</p>
869
+ </div>
870
+ </div>
871
+ <button
872
+ class="btn btn-sm border-neon-yellow/30 bg-transparent text-neon-yellow hover:bg-neon-yellow hover:text-black transition-all gap-2"
873
+ @click="showPasswordDialog()">
874
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
875
+ stroke="currentColor">
876
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
877
+ d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
878
+ </svg>
879
+ <span x-text="$store.global.t('changePassword')">Change Password</span>
880
+ </button>
881
+ </div>
882
+ </div>
883
+
884
+ <!-- ⚡ Common Settings -->
885
+ <div class="space-y-3">
886
+ <div class="flex items-center gap-2 mb-1 px-1">
887
+ <span class="text-[10px] uppercase text-gray-500 font-bold tracking-widest"
888
+ x-text="$store.global.t('runtimeConfig')">Common Settings</span>
889
+ <div class="h-px flex-1 bg-space-border/30"></div>
890
+ </div>
891
+
892
+ <!-- Debug Mode -->
893
+ <div
894
+ class="form-control view-card border-space-border/50 hover:border-neon-purple/50">
895
+ <div class="flex items-center justify-between">
896
+ <div class="flex flex-col gap-1">
897
+ <span class="text-sm font-medium text-gray-200"
898
+ :class="serverConfig.debug ? 'text-neon-purple' : ''"
899
+ x-text="$store.global.t('debugMode')">Debug Mode</span>
900
+ <span class="text-[11px] text-gray-500" x-text="$store.global.t('debugDesc')">Detailed
901
+ logging in the Logs tab</span>
902
+ </div>
903
+ <label class="relative inline-flex items-center cursor-pointer">
904
+ <input type="checkbox" class="sr-only peer" :checked="serverConfig.debug === true"
905
+ @change="toggleDebug($el.checked)"
906
+ aria-label="Debug mode toggle">
907
+ <div
908
+ class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-purple peer-checked:after:bg-white">
909
+ </div>
910
+ </label>
911
+ </div>
912
+ </div>
913
+
914
+ <!-- Token Cache -->
915
+ <div
916
+ class="form-control view-card border-space-border/50 hover:border-neon-green/50">
917
+ <div class="flex items-center justify-between">
918
+ <div class="flex flex-col gap-1">
919
+ <span class="text-sm font-medium text-gray-200"
920
+ :class="serverConfig.persistTokenCache ? 'text-neon-green' : ''"
921
+ x-text="$store.global.t('persistentSessions')">Persist Token Cache</span>
922
+ <span class="text-[11px] text-gray-500"
923
+ x-text="$store.global.t('persistTokenDesc')">Save OAuth tokens to disk for faster
924
+ restarts</span>
925
+ </div>
926
+ <label class="relative inline-flex items-center cursor-pointer">
927
+ <input type="checkbox" class="sr-only peer"
928
+ :checked="serverConfig.persistTokenCache === true"
929
+ @change="toggleTokenCache($el.checked)"
930
+ aria-label="Persist token cache toggle">
931
+ <div
932
+ class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white">
933
+ </div>
934
+ </label>
935
+ </div>
936
+ </div>
937
+
938
+ <!-- Max Accounts -->
939
+ <div class="form-control view-card border-space-border/50 hover:border-neon-cyan/50">
940
+ <label class="label pt-0">
941
+ <span class="label-text text-gray-400 text-xs">Max Accounts</span>
942
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
943
+ x-text="serverConfig.maxAccounts || 10"></span>
944
+ </label>
945
+ <div class="flex gap-3 items-center">
946
+ <input type="range" min="1" max="100" class="custom-range custom-range-cyan flex-1"
947
+ :value="serverConfig.maxAccounts || 10"
948
+ :style="`background-size: ${((serverConfig.maxAccounts || 10) - 1) / 99 * 100}% 100%`"
949
+ @input="toggleMaxAccounts($event.target.value)"
950
+ aria-label="Max accounts slider">
951
+ <input type="number" min="1" max="100"
952
+ class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
953
+ :value="serverConfig.maxAccounts || 10"
954
+ @change="toggleMaxAccounts($event.target.value)"
955
+ aria-label="Max accounts value">
956
+ </div>
957
+ <span class="text-[11px] text-gray-500 mt-1">Maximum number of Google accounts allowed</span>
958
+ </div>
959
+ </div>
960
+
961
+ <!-- 🔀 Account Selection Strategy -->
962
+ <div class="space-y-3">
963
+ <div class="flex items-center gap-2 mb-1 px-1">
964
+ <span class="text-[10px] uppercase text-gray-500 font-bold tracking-widest"
965
+ x-text="$store.global.t('accountSelectionStrategy')">Account Selection Strategy</span>
966
+ <div class="h-px flex-1 bg-space-border/30"></div>
967
+ </div>
968
+
969
+ <div class="form-control view-card border-space-border/50 hover:border-neon-cyan/50">
970
+ <div class="flex items-center justify-between">
971
+ <div class="flex flex-col gap-1 flex-1">
972
+ <span class="text-sm font-medium text-gray-200"
973
+ x-text="$store.global.t('selectionStrategy')">Selection Strategy</span>
974
+ <span class="text-[11px] text-gray-500"
975
+ x-text="currentStrategyDescription()">How accounts are selected for requests</span>
976
+ </div>
977
+ <select
978
+ class="select bg-space-800 border-space-border text-gray-200 focus:border-neon-cyan focus:ring-neon-cyan/20 w-64"
979
+ :value="serverConfig.accountSelection?.strategy || 'hybrid'"
980
+ @change="toggleStrategy($el.value)"
981
+ aria-label="Account selection strategy">
982
+ <option value="hybrid" x-text="$store.global.t('strategyHybridLabel')">Hybrid (Smart Distribution)</option>
983
+ <option value="sticky" x-text="$store.global.t('strategyStickyLabel')">Sticky (Cache Optimized)</option>
984
+ <option value="round-robin" x-text="$store.global.t('strategyRoundRobinLabel')">Round Robin (Load Balanced)</option>
985
+ </select>
986
+ </div>
987
+ </div>
988
+ </div>
989
+
990
+ <!-- ▼ Advanced Tuning (Fixed Logic) -->
991
+ <div class="view-card !p-0 border-space-border/50">
992
+ <div class="flex items-center justify-between p-4 cursor-pointer hover:bg-white/5 transition-colors"
993
+ @click="advancedExpanded = !advancedExpanded">
994
+ <div class="flex items-center gap-3">
995
+ <div class="w-8 h-8 rounded bg-space-800 flex items-center justify-center text-neon-cyan">
996
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
997
+ stroke="currentColor">
998
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
999
+ d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
1000
+ </svg>
1001
+ </div>
1002
+ <div>
1003
+ <span class="text-sm font-semibold text-gray-200"
1004
+ x-text="$store.global.t('advancedSettings')">Advanced Settings</span>
1005
+ <span class="text-[10px] text-gray-600 block"
1006
+ x-text="$store.global.t('serverReadOnly')">Settings managed via config.json</span>
1007
+ </div>
1008
+ </div>
1009
+ <svg xmlns="http://www.w3.org/2000/svg"
1010
+ class="w-5 h-5 text-gray-600 transition-transform duration-300"
1011
+ :class="advancedExpanded ? 'rotate-180' : ''" fill="none" viewBox="0 0 24 24"
1012
+ stroke="currentColor">
1013
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1014
+ </svg>
1015
+ </div>
1016
+
1017
+ <div x-show="advancedExpanded" x-cloak class="p-5 pt-0 space-y-6">
1018
+ <div class="h-px bg-space-border/20 mb-4"></div>
1019
+
1020
+ <!-- Network Retry Settings -->
1021
+ <div class="space-y-4">
1022
+ <div class="flex items-center gap-2 mb-2">
1023
+ <span class="text-[10px] text-gray-500 font-bold uppercase tracking-widest"
1024
+ x-text="$store.global.t('networkRetry')">Network Retry Settings</span>
1025
+ </div>
1026
+
1027
+ <div class="form-control">
1028
+ <label class="label pt-0">
1029
+ <span class="label-text text-gray-400 text-xs"
1030
+ x-text="$store.global.t('maxRetries')">Max Retries</span>
1031
+ <span class="label-text-alt font-mono text-neon-purple text-xs font-semibold"
1032
+ x-text="serverConfig.maxRetries || 5"></span>
1033
+ </label>
1034
+ <div class="flex gap-3 items-center">
1035
+ <input type="range" min="1" max="20" class="custom-range custom-range-purple flex-1"
1036
+ :value="serverConfig.maxRetries || 5"
1037
+ :style="`background-size: ${((serverConfig.maxRetries || 5) - 1) / 19 * 100}% 100%`"
1038
+ @input="toggleMaxRetries($event.target.value)"
1039
+ aria-label="Max retries slider">
1040
+ <input type="number" min="1" max="20"
1041
+ class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
1042
+ :value="serverConfig.maxRetries || 5"
1043
+ @change="toggleMaxRetries($event.target.value)"
1044
+ aria-label="Max retries value">
1045
+ </div>
1046
+ </div>
1047
+
1048
+ <div class="grid grid-cols-2 gap-4">
1049
+ <div class="form-control">
1050
+ <label class="label pt-0">
1051
+ <span class="label-text text-gray-400 text-xs"
1052
+ x-text="$store.global.t('retryBaseDelay')">Base Delay</span>
1053
+ <span class="label-text-alt font-mono text-neon-green text-xs font-semibold"
1054
+ x-text="((serverConfig.retryBaseMs || 1000) < 10000 ? (serverConfig.retryBaseMs || 1000) + 'ms' : Math.round((serverConfig.retryBaseMs || 1000) / 1000) + 's')"></span>
1055
+ </label>
1056
+ <div class="flex gap-2 items-center">
1057
+ <input type="range" min="100" max="10000" step="100"
1058
+ class="custom-range custom-range-green flex-1"
1059
+ :value="serverConfig.retryBaseMs || 1000"
1060
+ :style="`background-size: ${((serverConfig.retryBaseMs || 1000) - 100) / 99}% 100%`"
1061
+ @input="toggleRetryBaseMs($event.target.value)"
1062
+ aria-label="Retry base delay slider">
1063
+ <input type="number" min="100" max="10000" step="100"
1064
+ class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
1065
+ :value="serverConfig.retryBaseMs || 1000"
1066
+ @change="toggleRetryBaseMs($event.target.value)"
1067
+ aria-label="Retry base delay value">
1068
+ </div>
1069
+ </div>
1070
+ <div class="form-control">
1071
+ <label class="label pt-0">
1072
+ <span class="label-text text-gray-400 text-xs"
1073
+ x-text="$store.global.t('retryMaxDelay')">Max Delay</span>
1074
+ <span class="label-text-alt font-mono text-neon-green text-xs font-semibold"
1075
+ x-text="Math.round((serverConfig.retryMaxMs || 30000) / 1000) + 's'"></span>
1076
+ </label>
1077
+ <div class="flex gap-2 items-center">
1078
+ <input type="range" min="1000" max="120000" step="1000"
1079
+ class="custom-range custom-range-green flex-1"
1080
+ :value="serverConfig.retryMaxMs || 30000"
1081
+ :style="`background-size: ${((serverConfig.retryMaxMs || 30000) - 1000) / 1190}% 100%`"
1082
+ @input="toggleRetryMaxMs($event.target.value)"
1083
+ aria-label="Retry max delay slider">
1084
+ <input type="number" min="1000" max="120000" step="1000"
1085
+ class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
1086
+ :value="serverConfig.retryMaxMs || 30000"
1087
+ @change="toggleRetryMaxMs($event.target.value)"
1088
+ aria-label="Retry max delay value">
1089
+ </div>
1090
+ </div>
1091
+ </div>
1092
+ </div>
1093
+
1094
+ <!-- Account Rate Limiting -->
1095
+ <div class="space-y-4 pt-2 border-t border-space-border/10">
1096
+ <div class="flex items-center gap-2 mb-2">
1097
+ <span class="text-[10px] text-gray-500 font-bold uppercase tracking-widest"
1098
+ x-text="$store.global.t('rateLimiting')">Rate Limiting & Timeouts</span>
1099
+ </div>
1100
+
1101
+ <div class="form-control">
1102
+ <label class="label pt-0">
1103
+ <span class="label-text text-gray-400 text-xs"
1104
+ x-text="$store.global.t('defaultCooldown')">Default Cooldown</span>
1105
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
1106
+ x-text="Math.round((serverConfig.defaultCooldownMs || 10000) / 1000) + 's'"></span>
1107
+ </label>
1108
+ <div class="flex gap-3 items-center">
1109
+ <input type="range" min="1000" max="300000" step="1000"
1110
+ class="custom-range custom-range-cyan flex-1"
1111
+ :value="serverConfig.defaultCooldownMs || 10000"
1112
+ :style="`background-size: ${((serverConfig.defaultCooldownMs || 10000) - 1000) / 2990}% 100%`"
1113
+ @input="toggleDefaultCooldownMs($event.target.value)"
1114
+ aria-label="Default cooldown slider">
1115
+ <input type="number" min="1000" max="300000" step="1000"
1116
+ class="input input-xs input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
1117
+ :value="serverConfig.defaultCooldownMs || 10000"
1118
+ @change="toggleDefaultCooldownMs($event.target.value)"
1119
+ aria-label="Default cooldown value">
1120
+ </div>
1121
+ <p class="text-[9px] text-gray-600 mt-1 leading-tight"
1122
+ x-text="$store.global.t('defaultCooldownDesc')">Fallback cooldown when API doesn't provide a reset time.</p>
1123
+ </div>
1124
+
1125
+ <div class="form-control">
1126
+ <label class="label pt-0">
1127
+ <span class="label-text text-gray-400 text-xs"
1128
+ x-text="$store.global.t('maxWaitThreshold')">Max Wait Before Error</span>
1129
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
1130
+ x-text="((serverConfig.maxWaitBeforeErrorMs || 120000) >= 60000 ? Math.round((serverConfig.maxWaitBeforeErrorMs || 120000) / 60000) + 'm' : Math.round((serverConfig.maxWaitBeforeErrorMs || 120000) / 1000) + 's')"></span>
1131
+ </label>
1132
+ <div class="flex gap-3 items-center">
1133
+ <input type="range" min="0" max="600000" step="10000"
1134
+ class="custom-range custom-range-cyan flex-1"
1135
+ :value="serverConfig.maxWaitBeforeErrorMs || 120000"
1136
+ :style="`background-size: ${(serverConfig.maxWaitBeforeErrorMs || 120000) / 6000}% 100%`"
1137
+ @input="toggleMaxWaitBeforeErrorMs($event.target.value)"
1138
+ aria-label="Max wait before error slider">
1139
+ <input type="number" min="0" max="600000" step="10000"
1140
+ class="input input-xs input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
1141
+ :value="serverConfig.maxWaitBeforeErrorMs || 120000"
1142
+ @change="toggleMaxWaitBeforeErrorMs($event.target.value)"
1143
+ aria-label="Max wait before error value">
1144
+ </div>
1145
+ <p class="text-[9px] text-gray-600 mt-1 leading-tight"
1146
+ x-text="$store.global.t('maxWaitDesc')">If all accounts are rate-limited longer than this, error immediately.</p>
1147
+ </div>
1148
+ </div>
1149
+
1150
+ <!-- Error Handling Tuning -->
1151
+ <div class="space-y-4 pt-2 border-t border-space-border/10">
1152
+ <div class="flex items-center gap-2 mb-2">
1153
+ <span class="text-[10px] text-gray-500 font-bold uppercase tracking-widest"
1154
+ x-text="$store.global.t('errorHandlingTuning')">Error Handling Tuning</span>
1155
+ </div>
1156
+
1157
+ <div class="form-control">
1158
+ <label class="label pt-0">
1159
+ <span class="label-text text-gray-400 text-xs"
1160
+ x-text="$store.global.t('rateLimitDedupWindow')">Rate Limit Dedup Window</span>
1161
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
1162
+ x-text="Math.round((serverConfig.rateLimitDedupWindowMs || 5000) / 1000) + 's'"></span>
1163
+ </label>
1164
+ <div class="flex gap-3 items-center">
1165
+ <input type="range" min="1000" max="30000" step="1000"
1166
+ class="custom-range custom-range-cyan flex-1"
1167
+ :value="serverConfig.rateLimitDedupWindowMs || 5000"
1168
+ :style="`background-size: ${((serverConfig.rateLimitDedupWindowMs || 5000) - 1000) / 290}% 100%`"
1169
+ @input="toggleRateLimitDedupWindowMs($event.target.value)"
1170
+ aria-label="Rate limit dedup window slider">
1171
+ <input type="number" min="1000" max="30000" step="1000"
1172
+ class="input input-xs input-bordered w-20 bg-space-800 border-space-border text-white font-mono text-center"
1173
+ :value="serverConfig.rateLimitDedupWindowMs || 5000"
1174
+ @change="toggleRateLimitDedupWindowMs($event.target.value)"
1175
+ aria-label="Rate limit dedup window value">
1176
+ </div>
1177
+ <p class="text-[9px] text-gray-600 mt-1 leading-tight"
1178
+ x-text="$store.global.t('rateLimitDedupWindowDesc')">Prevents concurrent retry storms.</p>
1179
+ </div>
1180
+
1181
+ <div class="form-control">
1182
+ <label class="label pt-0">
1183
+ <span class="label-text text-gray-400 text-xs"
1184
+ x-text="$store.global.t('maxConsecutiveFailures')">Max Consecutive Failures</span>
1185
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
1186
+ x-text="serverConfig.maxConsecutiveFailures || 3"></span>
1187
+ </label>
1188
+ <div class="flex gap-3 items-center">
1189
+ <input type="range" min="1" max="10" step="1"
1190
+ class="custom-range custom-range-cyan flex-1"
1191
+ :value="serverConfig.maxConsecutiveFailures || 3"
1192
+ :style="`background-size: ${((serverConfig.maxConsecutiveFailures || 3) - 1) / 0.09}% 100%`"
1193
+ @input="toggleMaxConsecutiveFailures($event.target.value)"
1194
+ aria-label="Max consecutive failures slider">
1195
+ <input type="number" min="1" max="10" step="1"
1196
+ class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
1197
+ :value="serverConfig.maxConsecutiveFailures || 3"
1198
+ @change="toggleMaxConsecutiveFailures($event.target.value)"
1199
+ aria-label="Max consecutive failures value">
1200
+ </div>
1201
+ <p class="text-[9px] text-gray-600 mt-1 leading-tight"
1202
+ x-text="$store.global.t('maxConsecutiveFailuresDesc')">Failures before extended cooldown.</p>
1203
+ </div>
1204
+
1205
+ <div class="form-control">
1206
+ <label class="label pt-0">
1207
+ <span class="label-text text-gray-400 text-xs"
1208
+ x-text="$store.global.t('extendedCooldown')">Extended Cooldown</span>
1209
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
1210
+ x-text="((serverConfig.extendedCooldownMs || 60000) >= 60000 ? Math.round((serverConfig.extendedCooldownMs || 60000) / 60000) + 'm' : Math.round((serverConfig.extendedCooldownMs || 60000) / 1000) + 's')"></span>
1211
+ </label>
1212
+ <div class="flex gap-3 items-center">
1213
+ <input type="range" min="10000" max="300000" step="10000"
1214
+ class="custom-range custom-range-cyan flex-1"
1215
+ :value="serverConfig.extendedCooldownMs || 60000"
1216
+ :style="`background-size: ${((serverConfig.extendedCooldownMs || 60000) - 10000) / 2900}% 100%`"
1217
+ @input="toggleExtendedCooldownMs($event.target.value)"
1218
+ aria-label="Extended cooldown slider">
1219
+ <input type="number" min="10000" max="300000" step="10000"
1220
+ class="input input-xs input-bordered w-24 bg-space-800 border-space-border text-white font-mono text-center"
1221
+ :value="serverConfig.extendedCooldownMs || 60000"
1222
+ @change="toggleExtendedCooldownMs($event.target.value)"
1223
+ aria-label="Extended cooldown value">
1224
+ </div>
1225
+ <p class="text-[9px] text-gray-600 mt-1 leading-tight"
1226
+ x-text="$store.global.t('extendedCooldownDesc')">Applied after max consecutive failures.</p>
1227
+ </div>
1228
+
1229
+ <div class="form-control">
1230
+ <label class="label pt-0">
1231
+ <span class="label-text text-gray-400 text-xs"
1232
+ x-text="$store.global.t('maxCapacityRetries')">Max Capacity Retries</span>
1233
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
1234
+ x-text="serverConfig.maxCapacityRetries || 5"></span>
1235
+ </label>
1236
+ <div class="flex gap-3 items-center">
1237
+ <input type="range" min="1" max="10" step="1"
1238
+ class="custom-range custom-range-cyan flex-1"
1239
+ :value="serverConfig.maxCapacityRetries || 5"
1240
+ :style="`background-size: ${((serverConfig.maxCapacityRetries || 5) - 1) / 0.09}% 100%`"
1241
+ @input="toggleMaxCapacityRetries($event.target.value)"
1242
+ aria-label="Max capacity retries slider">
1243
+ <input type="number" min="1" max="10" step="1"
1244
+ class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
1245
+ :value="serverConfig.maxCapacityRetries || 5"
1246
+ @change="toggleMaxCapacityRetries($event.target.value)"
1247
+ aria-label="Max capacity retries value">
1248
+ </div>
1249
+ <p class="text-[9px] text-gray-600 mt-1 leading-tight"
1250
+ x-text="$store.global.t('maxCapacityRetriesDesc')">Retries before switching accounts.</p>
1251
+ </div>
1252
+ </div>
1253
+ </div>
1254
+ </div>
1255
+
1256
+ <!-- ℹ️ Info Notice -->
1257
+ <div class="flex items-center gap-3 text-xs text-gray-600 italic pt-2">
1258
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24"
1259
+ stroke="currentColor">
1260
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1261
+ d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
1262
+ </svg>
1263
+ <span
1264
+ x-text="$store.global.t('serverRestartAlert', {path: '~/.config/antigravity-proxy/config.json'})">All
1265
+ changes are saved automatically. Some settings may require server restart.</span>
1266
+ </div>
1267
+
1268
+ <!-- Password Dialog -->
1269
+ <div x-show="passwordDialog.show"
1270
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]"
1271
+ @click.self="hidePasswordDialog()" x-cloak>
1272
+ <div class="bg-space-900 border border-space-border rounded-lg p-6 max-w-md w-full mx-4"
1273
+ @click.stop>
1274
+ <h3 class="text-lg font-bold text-white mb-4" x-text="$store.global.t('changePassword')">Change
1275
+ WebUI Password</h3>
1276
+
1277
+ <div class="space-y-4">
1278
+ <div class="form-control">
1279
+ <label class="label">
1280
+ <span class="label-text text-gray-300"
1281
+ x-text="$store.global.t('currentPassword')">Current Password</span>
1282
+ </label>
1283
+ <input type="password" x-model="passwordDialog.oldPassword"
1284
+ class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
1285
+ :placeholder="$store.global.t('passwordEmptyDesc')"
1286
+ aria-label="Current password">
1287
+ </div>
1288
+
1289
+ <div class="form-control">
1290
+ <label class="label">
1291
+ <span class="label-text text-gray-300" x-text="$store.global.t('newPassword')">New
1292
+ Password</span>
1293
+ </label>
1294
+ <input type="password" x-model="passwordDialog.newPassword"
1295
+ class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
1296
+ :placeholder="$store.global.t('passwordLengthDesc')"
1297
+ aria-label="New password">
1298
+ </div>
1299
+
1300
+ <div class="form-control">
1301
+ <label class="label">
1302
+ <span class="label-text text-gray-300"
1303
+ x-text="$store.global.t('confirmNewPassword')">Confirm New Password</span>
1304
+ </label>
1305
+ <input type="password" x-model="passwordDialog.confirmPassword"
1306
+ class="input input-sm input-bordered bg-space-800 border-space-border text-white w-full"
1307
+ :placeholder="$store.global.t('passwordConfirmDesc')"
1308
+ @keydown.enter="changePassword()"
1309
+ aria-label="Confirm new password">
1310
+ </div>
1311
+ </div>
1312
+
1313
+ <div class="flex justify-end gap-2 mt-6">
1314
+ <button class="btn btn-sm btn-ghost text-gray-400" @click="hidePasswordDialog()"
1315
+ x-text="$store.global.t('cancel')">Cancel</button>
1316
+ <button class="btn btn-sm bg-neon-purple hover:bg-purple-600 border-none text-white"
1317
+ @click="changePassword()" x-text="$store.global.t('changePassword')">
1318
+ Change Password
1319
+ </button>
1320
+ </div>
1321
+ </div>
1322
+ </div>
1323
+ </div>
1324
+
1325
+ </div>
1326
+ </div>
1327
+ </div>