98 auto& model_cache = context->model_cache();
101 auto provider_button = [&](
const char* label,
const char* value,
102 const ImVec4& color) {
103 bool active = config.ai_provider == value;
105 ImGui::PushStyleColor(ImGuiCol_Button, color);
106 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
107 ImVec4(color.x * 1.15f, color.y * 1.15f,
108 color.z * 1.15f, color.w));
110 if (ImGui::Button(label, ImVec2(90, 28))) {
111 config.ai_provider = value;
112 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
116 ImGui::PopStyleColor(2);
122 provider_button(
ICON_MD_CLOUD " Ollama",
"ollama", theme.provider_ollama);
129 ImGui::Text(
"Ollama Host:");
131 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
132 if (ImGui::InputTextWithHint(
"##ollama_host",
"http://localhost:11434",
133 config.ollama_host_buffer,
134 IM_ARRAYSIZE(config.ollama_host_buffer))) {
135 config.ollama_host = config.ollama_host_buffer;
138 ImGui::Text(
"Gemini Key:");
140 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
141 if (ImGui::InputTextWithHint(
"##gemini_key",
"API key...",
142 config.gemini_key_buffer,
143 IM_ARRAYSIZE(config.gemini_key_buffer),
144 ImGuiInputTextFlags_Password)) {
145 config.gemini_api_key = config.gemini_key_buffer;
149 const char* env_key = std::getenv(
"GEMINI_API_KEY");
151 std::snprintf(config.gemini_key_buffer,
sizeof(config.gemini_key_buffer),
153 config.gemini_api_key = env_key;
155 toast_manager->
Show(
"Loaded GEMINI_API_KEY from environment",
158 }
else if (toast_manager) {
163 ImGui::Text(
"OpenAI Key:");
165 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
166 if (ImGui::InputTextWithHint(
"##openai_key",
"API key...",
167 config.openai_key_buffer,
168 IM_ARRAYSIZE(config.openai_key_buffer),
169 ImGuiInputTextFlags_Password)) {
170 config.openai_api_key = config.openai_key_buffer;
174 const char* env_key = std::getenv(
"OPENAI_API_KEY");
176 std::snprintf(config.openai_key_buffer,
sizeof(config.openai_key_buffer),
178 config.openai_api_key = env_key;
180 toast_manager->
Show(
"Loaded OPENAI_API_KEY from environment",
183 }
else if (toast_manager) {
191 if (ImGui::InputTextWithHint(
"##ai_model",
"Model name...",
193 IM_ARRAYSIZE(config.model_buffer))) {
194 config.ai_model = config.model_buffer;
198 static bool filter_by_provider =
false;
199 ImGui::Checkbox(
"Filter by selected provider", &filter_by_provider);
204 ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
205 ImGui::InputTextWithHint(
"##model_search",
"Search all models...",
206 model_cache.search_buffer,
207 IM_ARRAYSIZE(model_cache.search_buffer));
216 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
217 ImGui::BeginChild(
"UnifiedModelList", ImVec2(0, 140),
true);
218 std::string filter = absl::AsciiStrToLower(model_cache.search_buffer);
220 if (model_cache.available_models.empty() && model_cache.model_names.empty()) {
221 ImGui::TextDisabled(
"No cached models. Refresh to discover.");
223 auto get_provider_color = [&theme](
const std::string& provider) -> ImVec4 {
224 if (provider ==
"ollama")
return theme.provider_ollama;
225 if (provider ==
"gemini")
return theme.provider_gemini;
226 if (provider ==
"openai")
return theme.provider_openai;
227 return theme.provider_mock;
230 if (!model_cache.available_models.empty()) {
232 for (
const auto& info : model_cache.available_models) {
233 std::string lower_name = absl::AsciiStrToLower(info.name);
234 std::string lower_provider = absl::AsciiStrToLower(info.provider);
236 if (filter_by_provider && info.provider != config.ai_provider) {
240 if (!filter.empty()) {
241 bool match = lower_name.find(filter) != std::string::npos ||
242 lower_provider.find(filter) != std::string::npos;
243 if (!match && !info.parameter_size.empty()) {
244 match = absl::AsciiStrToLower(info.parameter_size).find(filter) !=
247 if (!match && !info.family.empty()) {
248 match = absl::AsciiStrToLower(info.family).find(filter) !=
251 if (!match)
continue;
254 ImGui::PushID(model_index++);
256 bool is_selected = config.ai_model == info.name;
258 ImVec4 provider_color = get_provider_color(info.provider);
259 ImGui::PushStyleColor(ImGuiCol_Button, provider_color);
260 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
261 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2));
262 ImGui::SmallButton(info.provider.c_str());
263 ImGui::PopStyleVar(2);
264 ImGui::PopStyleColor();
267 if (ImGui::Selectable(info.name.c_str(), is_selected,
268 ImGuiSelectableFlags_None,
269 ImVec2(ImGui::GetContentRegionAvail().x - 60, 0))) {
270 config.ai_model = info.name;
271 config.ai_provider = info.provider;
272 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
274 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
275 "%s", info.provider.c_str());
280 std::find(config.favorite_models.begin(),
281 config.favorite_models.end(),
282 info.name) != config.favorite_models.end();
283 ImGui::PushStyleColor(ImGuiCol_Text,
284 is_favorite ? theme.status_warning
285 : theme.text_secondary_color);
289 config.favorite_models.erase(
290 std::remove(config.favorite_models.begin(),
291 config.favorite_models.end(), info.name),
292 config.favorite_models.end());
293 config.model_chain.erase(
294 std::remove(config.model_chain.begin(),
295 config.model_chain.end(), info.name),
296 config.model_chain.end());
298 config.favorite_models.push_back(info.name);
301 ImGui::PopStyleColor();
302 if (ImGui::IsItemHovered()) {
303 ImGui::SetTooltip(is_favorite ?
"Remove from favorites"
310 preset.
name = info.name;
311 preset.
model = info.name;
312 preset.provider = info.provider;
314 (info.provider ==
"ollama") ? config.ollama_host :
"";
315 preset.
tags = {info.provider};
317 config.model_presets.push_back(std::move(preset));
322 if (ImGui::IsItemHovered()) {
323 ImGui::SetTooltip(
"Capture preset from this model");
326 std::string size_label = info.parameter_size.empty()
327 ? FormatByteSize(info.size_bytes)
328 : info.parameter_size;
329 ImGui::TextColored(theme.text_secondary_color,
" %s",
331 if (!info.quantization.empty()) {
333 ImGui::TextColored(theme.text_info,
" %s", info.quantization.c_str());
335 if (!info.family.empty()) {
337 ImGui::TextColored(theme.text_secondary_gray,
" Family: %s",
338 info.family.c_str());
343 if (ImGui::IsItemHovered()) {
344 ImGui::SetTooltip(
"Running locally");
353 for (
const auto& model_name : model_cache.model_names) {
354 std::string lower = absl::AsciiStrToLower(model_name);
355 if (!filter.empty() && lower.find(filter) == std::string::npos) {
359 ImGui::PushID(model_index++);
361 bool is_selected = config.ai_model == model_name;
362 if (ImGui::Selectable(model_name.c_str(), is_selected)) {
363 config.ai_model = model_name;
364 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
370 std::find(config.favorite_models.begin(),
371 config.favorite_models.end(),
372 model_name) != config.favorite_models.end();
373 ImGui::PushStyleColor(ImGuiCol_Text,
374 is_favorite ? theme.status_warning
375 : theme.text_secondary_color);
379 config.favorite_models.erase(
380 std::remove(config.favorite_models.begin(),
381 config.favorite_models.end(), model_name),
382 config.favorite_models.end());
384 config.favorite_models.push_back(model_name);
387 ImGui::PopStyleColor();
394 ImGui::PopStyleColor();
396 if (model_cache.last_refresh != absl::InfinitePast()) {
397 double seconds = absl::ToDoubleSeconds(absl::Now() - model_cache.last_refresh);
398 ImGui::TextDisabled(
"Last refresh %.0fs ago", seconds);
400 ImGui::TextDisabled(
"Models not refreshed yet");
403 if (config.ai_provider ==
"ollama") {
407 if (!config.favorite_models.empty()) {
409 ImGui::TextColored(theme.status_warning,
ICON_MD_STAR " Favorites");
410 for (
size_t i = 0; i < config.favorite_models.size(); ++i) {
411 auto& favorite = config.favorite_models[i];
412 ImGui::PushID(
static_cast<int>(i));
413 bool active = config.ai_model == favorite;
415 std::string provider_name;
416 for (
const auto& info : model_cache.available_models) {
417 if (info.name == favorite) {
418 provider_name = info.provider;
423 if (!provider_name.empty()) {
424 ImVec4 badge_color = theme.provider_mock;
425 if (provider_name ==
"ollama") badge_color = theme.provider_ollama;
426 else if (provider_name ==
"gemini") badge_color = theme.provider_gemini;
427 else if (provider_name ==
"openai") badge_color = theme.provider_openai;
428 ImGui::PushStyleColor(ImGuiCol_Button, badge_color);
429 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
430 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3, 1));
431 ImGui::SmallButton(provider_name.c_str());
432 ImGui::PopStyleVar(2);
433 ImGui::PopStyleColor();
437 if (ImGui::Selectable(favorite.c_str(), active)) {
438 config.ai_model = favorite;
439 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
441 if (!provider_name.empty()) {
442 config.ai_provider = provider_name;
443 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
444 "%s", provider_name.c_str());
448 ImGui::PushStyleColor(ImGuiCol_Text, theme.status_error);
450 config.model_chain.erase(
451 std::remove(config.model_chain.begin(),
452 config.model_chain.end(), favorite),
453 config.model_chain.end());
454 config.favorite_models.erase(config.favorite_models.begin() + i);
455 ImGui::PopStyleColor();
459 ImGui::PopStyleColor();