103 auto& model_cache = context->model_cache();
106 auto provider_button = [&](
const char* label,
const char* value,
107 const ImVec4& color) {
108 bool active = config.ai_provider == value;
110 ImGui::PushStyleColor(ImGuiCol_Button, color);
111 ImGui::PushStyleColor(
112 ImGuiCol_ButtonHovered,
113 ImVec4(color.x * 1.15f, color.y * 1.15f, color.z * 1.15f, color.w));
115 if (ImGui::Button(label, ImVec2(90, 28))) {
116 config.ai_provider = value;
117 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
121 ImGui::PopStyleColor(2);
127 provider_button(
ICON_MD_CLOUD " Ollama",
"ollama", theme.provider_ollama);
130 theme.provider_openai);
135 ImGui::Text(
"Ollama Host:");
137 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
138 if (ImGui::InputTextWithHint(
"##ollama_host",
"http://localhost:11434",
139 config.ollama_host_buffer,
140 IM_ARRAYSIZE(config.ollama_host_buffer))) {
141 config.ollama_host = config.ollama_host_buffer;
144 ImGui::Text(
"Gemini Key:");
146 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
147 if (ImGui::InputTextWithHint(
"##gemini_key",
"API key...",
148 config.gemini_key_buffer,
149 IM_ARRAYSIZE(config.gemini_key_buffer),
150 ImGuiInputTextFlags_Password)) {
151 config.gemini_api_key = config.gemini_key_buffer;
155 const char* env_key = std::getenv(
"GEMINI_API_KEY");
157 std::snprintf(config.gemini_key_buffer,
sizeof(config.gemini_key_buffer),
159 config.gemini_api_key = env_key;
161 toast_manager->
Show(
"Loaded GEMINI_API_KEY from environment",
164 }
else if (toast_manager) {
169 ImGui::Text(
"OpenAI Key:");
171 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
172 if (ImGui::InputTextWithHint(
"##openai_key",
"API key...",
173 config.openai_key_buffer,
174 IM_ARRAYSIZE(config.openai_key_buffer),
175 ImGuiInputTextFlags_Password)) {
176 config.openai_api_key = config.openai_key_buffer;
180 const char* env_key = std::getenv(
"OPENAI_API_KEY");
182 std::snprintf(config.openai_key_buffer,
sizeof(config.openai_key_buffer),
184 config.openai_api_key = env_key;
186 toast_manager->
Show(
"Loaded OPENAI_API_KEY from environment",
189 }
else if (toast_manager) {
197 if (ImGui::InputTextWithHint(
"##ai_model",
"Model name...",
199 IM_ARRAYSIZE(config.model_buffer))) {
200 config.ai_model = config.model_buffer;
204 static bool filter_by_provider =
false;
205 ImGui::Checkbox(
"Filter by selected provider", &filter_by_provider);
210 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
211 ImGui::InputTextWithHint(
"##model_search",
"Search all models...",
212 model_cache.search_buffer,
213 IM_ARRAYSIZE(model_cache.search_buffer));
222 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
223 ImGui::BeginChild(
"UnifiedModelList", ImVec2(0, 140),
true);
224 std::string filter = absl::AsciiStrToLower(model_cache.search_buffer);
226 if (model_cache.available_models.empty() && model_cache.model_names.empty()) {
227 ImGui::TextDisabled(
"No cached models. Refresh to discover.");
229 auto get_provider_color = [&theme](
const std::string& provider) -> ImVec4 {
230 if (provider ==
"ollama")
231 return theme.provider_ollama;
232 if (provider ==
"gemini")
233 return theme.provider_gemini;
234 if (provider ==
"openai")
235 return theme.provider_openai;
236 return theme.provider_mock;
239 if (!model_cache.available_models.empty()) {
241 for (
const auto& info : model_cache.available_models) {
242 std::string lower_name = absl::AsciiStrToLower(info.name);
243 std::string lower_provider = absl::AsciiStrToLower(info.provider);
245 if (filter_by_provider && info.provider != config.ai_provider) {
249 if (!filter.empty()) {
250 bool match = lower_name.find(filter) != std::string::npos ||
251 lower_provider.find(filter) != std::string::npos;
252 if (!match && !info.parameter_size.empty()) {
253 match = absl::AsciiStrToLower(info.parameter_size).find(filter) !=
256 if (!match && !info.family.empty()) {
257 match = absl::AsciiStrToLower(info.family).find(filter) !=
264 ImGui::PushID(model_index++);
266 bool is_selected = config.ai_model == info.name;
268 ImVec4 provider_color = get_provider_color(info.provider);
269 ImGui::PushStyleColor(ImGuiCol_Button, provider_color);
270 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
271 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2));
272 ImGui::SmallButton(info.provider.c_str());
273 ImGui::PopStyleVar(2);
274 ImGui::PopStyleColor();
277 if (ImGui::Selectable(
278 info.name.c_str(), is_selected, ImGuiSelectableFlags_None,
279 ImVec2(ImGui::GetContentRegionAvail().x - 60, 0))) {
280 config.ai_model = info.name;
281 config.ai_provider = info.provider;
282 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
284 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
285 "%s", info.provider.c_str());
289 bool is_favorite = std::find(config.favorite_models.begin(),
290 config.favorite_models.end(),
291 info.name) != config.favorite_models.end();
292 ImGui::PushStyleColor(ImGuiCol_Text, is_favorite
293 ? theme.status_warning
294 : theme.text_secondary_color);
298 config.favorite_models.erase(
299 std::remove(config.favorite_models.begin(),
300 config.favorite_models.end(), info.name),
301 config.favorite_models.end());
302 config.model_chain.erase(
303 std::remove(config.model_chain.begin(),
304 config.model_chain.end(), info.name),
305 config.model_chain.end());
307 config.favorite_models.push_back(info.name);
310 ImGui::PopStyleColor();
311 if (ImGui::IsItemHovered()) {
312 ImGui::SetTooltip(is_favorite ?
"Remove from favorites"
319 preset.
name = info.name;
320 preset.
model = info.name;
321 preset.provider = info.provider;
322 preset.
host = (info.provider ==
"ollama") ? config.ollama_host :
"";
323 preset.
tags = {info.provider};
325 config.model_presets.push_back(std::move(preset));
330 if (ImGui::IsItemHovered()) {
331 ImGui::SetTooltip(
"Capture preset from this model");
334 std::string size_label = info.parameter_size.empty()
335 ? FormatByteSize(info.size_bytes)
336 : info.parameter_size;
337 ImGui::TextColored(theme.text_secondary_color,
" %s",
339 if (!info.quantization.empty()) {
341 ImGui::TextColored(theme.text_info,
" %s",
342 info.quantization.c_str());
344 if (!info.family.empty()) {
346 ImGui::TextColored(theme.text_secondary_gray,
" Family: %s",
347 info.family.c_str());
352 if (ImGui::IsItemHovered()) {
353 ImGui::SetTooltip(
"Running locally");
362 for (
const auto& model_name : model_cache.model_names) {
363 std::string lower = absl::AsciiStrToLower(model_name);
364 if (!filter.empty() && lower.find(filter) == std::string::npos) {
368 ImGui::PushID(model_index++);
370 bool is_selected = config.ai_model == model_name;
371 if (ImGui::Selectable(model_name.c_str(), is_selected)) {
372 config.ai_model = model_name;
373 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
379 std::find(config.favorite_models.begin(),
380 config.favorite_models.end(),
381 model_name) != config.favorite_models.end();
382 ImGui::PushStyleColor(ImGuiCol_Text, is_favorite
383 ? theme.status_warning
384 : theme.text_secondary_color);
388 config.favorite_models.erase(
389 std::remove(config.favorite_models.begin(),
390 config.favorite_models.end(), model_name),
391 config.favorite_models.end());
393 config.favorite_models.push_back(model_name);
396 ImGui::PopStyleColor();
403 ImGui::PopStyleColor();
405 if (model_cache.last_refresh != absl::InfinitePast()) {
407 absl::ToDoubleSeconds(absl::Now() - model_cache.last_refresh);
408 ImGui::TextDisabled(
"Last refresh %.0fs ago", seconds);
410 ImGui::TextDisabled(
"Models not refreshed yet");
413 if (config.ai_provider ==
"ollama") {
417 if (!config.favorite_models.empty()) {
419 ImGui::TextColored(theme.status_warning,
ICON_MD_STAR " Favorites");
420 for (
size_t i = 0; i < config.favorite_models.size(); ++i) {
421 auto& favorite = config.favorite_models[i];
422 ImGui::PushID(
static_cast<int>(i));
423 bool active = config.ai_model == favorite;
425 std::string provider_name;
426 for (
const auto& info : model_cache.available_models) {
427 if (info.name == favorite) {
428 provider_name = info.provider;
433 if (!provider_name.empty()) {
434 ImVec4 badge_color = theme.provider_mock;
435 if (provider_name ==
"ollama")
436 badge_color = theme.provider_ollama;
437 else if (provider_name ==
"gemini")
438 badge_color = theme.provider_gemini;
439 else if (provider_name ==
"openai")
440 badge_color = theme.provider_openai;
441 ImGui::PushStyleColor(ImGuiCol_Button, badge_color);
442 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
443 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3, 1));
444 ImGui::SmallButton(provider_name.c_str());
445 ImGui::PopStyleVar(2);
446 ImGui::PopStyleColor();
450 if (ImGui::Selectable(favorite.c_str(), active)) {
451 config.ai_model = favorite;
452 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
454 if (!provider_name.empty()) {
455 config.ai_provider = provider_name;
456 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
457 "%s", provider_name.c_str());
461 ImGui::PushStyleColor(ImGuiCol_Text, theme.status_error);
463 config.model_chain.erase(
464 std::remove(config.model_chain.begin(), config.model_chain.end(),
466 config.model_chain.end());
467 config.favorite_models.erase(config.favorite_models.begin() + i);
468 ImGui::PopStyleColor();
472 ImGui::PopStyleColor();