yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_configuration_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdio>
5#include <cstdlib>
6#include <string>
7#include <vector>
8
9#include "absl/strings/ascii.h"
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_join.h"
12#include "absl/time/clock.h"
15#include "app/gui/core/icons.h"
18#include "imgui/imgui.h"
19
20#if defined(__EMSCRIPTEN__)
21#include <emscripten/emscripten.h>
22#endif
23
24namespace yaze {
25namespace editor {
26
27namespace {
28
29std::string FormatByteSize(uint64_t bytes) {
30 if (bytes < 1024)
31 return absl::StrFormat("%d B", bytes);
32 if (bytes < 1024 * 1024)
33 return absl::StrFormat("%.1f KB", bytes / 1024.0);
34 if (bytes < 1024 * 1024 * 1024)
35 return absl::StrFormat("%.1f MB", bytes / (1024.0 * 1024.0));
36 return absl::StrFormat("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
37}
38
39std::string FormatRelativeTime(absl::Time timestamp) {
40 if (timestamp == absl::InfinitePast())
41 return "never";
42 auto delta = absl::Now() - timestamp;
43 if (delta < absl::Seconds(60))
44 return "just now";
45 if (delta < absl::Minutes(60))
46 return absl::StrFormat("%.0fm ago", absl::ToDoubleMinutes(delta));
47 if (delta < absl::Hours(24))
48 return absl::StrFormat("%.0fh ago", absl::ToDoubleHours(delta));
49 return absl::StrFormat("%.0fd ago", absl::ToDoubleHours(delta) / 24.0);
50}
51
52bool ContainsText(const std::string& haystack, const std::string& needle) {
53 return haystack.find(needle) != std::string::npos;
54}
55
56bool StartsWithText(const std::string& text, const std::string& prefix) {
57 return text.rfind(prefix, 0) == 0;
58}
59
60bool IsLocalEndpoint(const std::string& base_url) {
61 if (base_url.empty()) {
62 return false;
63 }
64 std::string lower = absl::AsciiStrToLower(base_url);
65 // Check for common local identifiers
66 return ContainsText(lower, "localhost") ||
67 ContainsText(lower, "127.0.0.1") ||
68 ContainsText(lower, "0.0.0.0") ||
69 ContainsText(lower, "::1") ||
70 // LAN IPs (rudimentary check)
71 ContainsText(lower, "192.168.") || StartsWithText(lower, "10.") ||
72 // LM Studio default port check just in case domain differs
73 ContainsText(lower, ":1234");
74}
75
76} // namespace
77
79 const Callbacks& callbacks,
80 ToastManager* toast_manager) {
81 const auto& theme = AgentUI::GetTheme();
82
83 gui::StyledChild config_child("AgentConfig", ImVec2(0, 0),
84 {.bg = theme.panel_bg_color}, true);
86 theme.command_text_color);
87
88 if (ImGui::CollapsingHeader(ICON_MD_SMART_TOY " Connection & Models",
89 ImGuiTreeNodeFlags_DefaultOpen)) {
90 RenderModelConfigControls(context, callbacks, toast_manager);
91 ImGui::Separator();
92 RenderModelDeck(context, callbacks, toast_manager);
93 }
94
95 if (ImGui::CollapsingHeader(ICON_MD_TUNE " Parameters",
96 ImGuiTreeNodeFlags_DefaultOpen)) {
98 }
99
100 if (ImGui::CollapsingHeader(ICON_MD_CONSTRUCTION " Tools & Editor Hooks",
101 ImGuiTreeNodeFlags_DefaultOpen)) {
102 RenderToolingControls(context->agent_config(), callbacks);
103 }
104
105 ImGui::Spacing();
106 // Note: persist_agent_config_with_history_ logic was local to AgentChatWidget.
107 // We might want to move it to AgentConfigState if it needs to be persisted.
108 // For now, we'll skip it or add it to AgentConfigState if needed.
109 // Assuming it's not critical for this refactor, or we can add it later.
110
111 if (ImGui::Button(ICON_MD_CLOUD_SYNC " Apply Provider Settings",
112 ImVec2(-1, 0))) {
113 if (callbacks.update_config) {
114 callbacks.update_config(context->agent_config());
115 }
116 }
117
118}
119
121 AgentUIContext* context, const Callbacks& callbacks,
122 ToastManager* toast_manager) {
123 const auto& theme = AgentUI::GetTheme();
124 ImGuiStyle& style = ImGui::GetStyle();
125 auto& config = context->agent_config();
126 auto& model_cache = context->model_cache();
127 static bool filter_by_provider = false;
128
129 if (model_cache.last_provider != config.ai_provider ||
130 model_cache.last_openai_base != config.openai_base_url ||
131 model_cache.last_ollama_host != config.ollama_host) {
132 model_cache.auto_refresh_requested = false;
133 model_cache.last_provider = config.ai_provider;
134 model_cache.last_openai_base = config.openai_base_url;
135 model_cache.last_ollama_host = config.ollama_host;
136 }
137
138 if (callbacks.refresh_models && !model_cache.loading &&
139 !model_cache.auto_refresh_requested) {
140 model_cache.auto_refresh_requested = true;
141 callbacks.refresh_models(false);
142 }
143 const float label_width = 120.0f;
144 const ImVec2 compact_padding(style.FramePadding.x,
145 std::max(2.0f, style.FramePadding.y * 0.6f));
146 const ImVec2 compact_spacing(style.ItemSpacing.x,
147 std::max(3.0f, style.ItemSpacing.y * 0.6f));
148 const float env_button_width =
149 ImGui::CalcTextSize(ICON_MD_SYNC " Env").x + compact_padding.x * 2.0f;
150
151 auto set_openai_base = [&](const std::string& base_url) {
152 std::snprintf(config.openai_base_url_buffer,
153 sizeof(config.openai_base_url_buffer), "%s",
154 base_url.c_str());
155 config.openai_base_url = config.openai_base_url_buffer;
156 };
157
158 // Provider selection buttons using theme colors
159 auto provider_button = [&](const char* label, const char* value,
160 const ImVec4& color) {
161 bool active = config.ai_provider == value;
162 ImGui::TableNextColumn();
163 std::optional<gui::StyleColorGuard> btn_guard;
164 if (active) {
165 btn_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
166 {ImGuiCol_Button, color},
167 {ImGuiCol_ButtonHovered,
168 ImVec4(color.x * 1.15f, color.y * 1.15f, color.z * 1.15f,
169 color.w)}});
170 }
171 if (ImGui::Button(label, ImVec2(-FLT_MIN, 28))) {
172 config.ai_provider = value;
173 std::snprintf(config.provider_buffer, sizeof(config.provider_buffer),
174 "%s", value);
175 }
176 };
177
178 {
179 gui::StyleVarGuard provider_var_guard(
180 {{ImGuiStyleVar_FramePadding, compact_padding},
181 {ImGuiStyleVar_ItemSpacing, compact_spacing}});
182 if (ImGui::BeginTable("AgentProviderConfigTable", 2,
183 ImGuiTableFlags_SizingFixedFit |
184 ImGuiTableFlags_BordersInnerV)) {
185 ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed,
186 label_width);
187 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
188
189 ImGui::TableNextRow();
190 ImGui::TableSetColumnIndex(0);
191 ImGui::TextDisabled("Provider");
192 ImGui::TableSetColumnIndex(1);
193 float provider_width = ImGui::GetContentRegionAvail().x;
194 int provider_columns = provider_width > 560.0f ? 3
195 : provider_width > 360.0f ? 2
196 : 1;
197 if (ImGui::BeginTable("AgentProviderButtons", provider_columns,
198 ImGuiTableFlags_SizingStretchSame)) {
199 provider_button(ICON_MD_SETTINGS " Mock", "mock", theme.provider_mock);
200 provider_button(ICON_MD_CLOUD " Ollama", "ollama", theme.provider_ollama);
201 provider_button(ICON_MD_SMART_TOY " Gemini", "gemini",
202 theme.provider_gemini);
203 provider_button(ICON_MD_PSYCHOLOGY " Anthropic", "anthropic",
204 theme.provider_openai);
205 provider_button(ICON_MD_AUTO_AWESOME " OpenAI", "openai",
206 theme.provider_openai);
207 ImGui::EndTable();
208 }
209
210 ImGui::TableNextRow();
211 ImGui::TableSetColumnIndex(0);
212 ImGui::TextDisabled("Ollama Host");
213 ImGui::TableSetColumnIndex(1);
214 ImGui::SetNextItemWidth(-1);
215 if (ImGui::InputTextWithHint("##ollama_host", "http://localhost:11434",
216 config.ollama_host_buffer,
217 IM_ARRAYSIZE(config.ollama_host_buffer))) {
218 config.ollama_host = config.ollama_host_buffer;
219 }
220
221 auto key_row = [&](const char* label, const char* hint, char* buffer,
222 size_t buffer_len, std::string* target,
223 const char* env_var, const char* input_id,
224 const char* button_id) {
225 ImGui::TableNextRow();
226 ImGui::TableSetColumnIndex(0);
227 ImGui::TextDisabled("%s", label);
228 ImGui::TableSetColumnIndex(1);
229 float input_width =
230 ImGui::GetContentRegionAvail().x - env_button_width -
231 style.ItemSpacing.x;
232 bool stack = input_width < 140.0f;
233 ImGui::SetNextItemWidth(stack ? -1 : input_width);
234 if (ImGui::InputTextWithHint(input_id, hint, buffer, buffer_len,
235 ImGuiInputTextFlags_Password)) {
236 if (target) {
237 *target = buffer;
238 }
239 }
240 if (!stack) {
241 ImGui::SameLine();
242 }
243 if (ImGui::SmallButton(button_id)) {
244 const char* env_key = std::getenv(env_var);
245 if (env_key) {
246 std::snprintf(buffer, buffer_len, "%s", env_key);
247 if (target) {
248 *target = env_key;
249 }
250 if (toast_manager) {
251 toast_manager->Show(
252 absl::StrFormat("Loaded %s from environment", env_var),
253 ToastType::kInfo, 2.0f);
254 }
255 } else if (toast_manager) {
256 toast_manager->Show(
257 absl::StrFormat("%s not set", env_var), ToastType::kWarning,
258 2.0f);
259 }
260 }
261 };
262
263 key_row("Gemini Key", "API key...", config.gemini_key_buffer,
264 IM_ARRAYSIZE(config.gemini_key_buffer), &config.gemini_api_key,
265 "GEMINI_API_KEY", "##gemini_key",
266 ICON_MD_SYNC " Env##gemini");
267 key_row("Anthropic Key", "API key...", config.anthropic_key_buffer,
268 IM_ARRAYSIZE(config.anthropic_key_buffer),
269 &config.anthropic_api_key, "ANTHROPIC_API_KEY", "##anthropic_key",
270 ICON_MD_SYNC " Env##anthropic");
271 key_row("OpenAI Key", "API key...", config.openai_key_buffer,
272 IM_ARRAYSIZE(config.openai_key_buffer), &config.openai_api_key,
273 "OPENAI_API_KEY", "##openai_key", ICON_MD_SYNC " Env##openai");
274
275 ImGui::TableNextRow();
276 ImGui::TableSetColumnIndex(0);
277 ImGui::TextDisabled("OpenAI Base");
278 ImGui::TableSetColumnIndex(1);
279 float openai_button_width =
280 ImGui::CalcTextSize("OpenAI").x + compact_padding.x * 2.0f;
281 float lm_button_width =
282 ImGui::CalcTextSize("LM Studio").x + compact_padding.x * 2.0f;
283 float reset_button_width =
284 ImGui::CalcTextSize("Reset").x + compact_padding.x * 2.0f;
285 float total_buttons =
286 openai_button_width + lm_button_width + reset_button_width +
287 style.ItemSpacing.x * 2.0f;
288 float base_available = ImGui::GetContentRegionAvail().x;
289 bool base_stack = base_available < total_buttons + 160.0f;
290 ImGui::SetNextItemWidth(
291 base_stack ? -1 : base_available - total_buttons - style.ItemSpacing.x);
292 if (ImGui::InputTextWithHint("##openai_base", "http://localhost:1234",
293 config.openai_base_url_buffer,
294 IM_ARRAYSIZE(config.openai_base_url_buffer))) {
295 config.openai_base_url = config.openai_base_url_buffer;
296 }
297 if (base_stack) {
298 ImGui::Spacing();
299 } else {
300 ImGui::SameLine();
301 }
302 if (ImGui::SmallButton("OpenAI")) {
303 set_openai_base("https://api.openai.com");
304 }
305 ImGui::SameLine();
306 if (ImGui::SmallButton("LM Studio")) {
307 set_openai_base("http://localhost:1234");
308 }
309 ImGui::SameLine();
310 if (ImGui::SmallButton("Reset")) {
311 set_openai_base("https://api.openai.com");
312 }
313
314 ImGui::EndTable();
315 }
316 } // provider_var_guard scope
317
318 if (IsLocalEndpoint(config.openai_base_url)) {
319 ImGui::TextColored(theme.status_success,
320 ICON_MD_COMPUTER " Local OpenAI-compatible server");
321 } else {
322 ImGui::TextColored(theme.text_secondary_color,
323 ICON_MD_PUBLIC " Remote OpenAI endpoint");
324 }
325
326 ImGui::Spacing();
327
328 {
329 gui::StyleVarGuard model_var_guard(
330 {{ImGuiStyleVar_FramePadding, compact_padding},
331 {ImGuiStyleVar_ItemSpacing, compact_spacing}});
332 if (ImGui::BeginTable("AgentModelControls", 2,
333 ImGuiTableFlags_SizingFixedFit |
334 ImGuiTableFlags_BordersInnerV)) {
335 ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed,
336 label_width);
337 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
338
339 ImGui::TableNextRow();
340 ImGui::TableSetColumnIndex(0);
341 ImGui::TextDisabled("Model");
342 ImGui::TableSetColumnIndex(1);
343 ImGui::SetNextItemWidth(-1);
344 if (ImGui::InputTextWithHint("##ai_model", "Model name...",
345 config.model_buffer,
346 IM_ARRAYSIZE(config.model_buffer))) {
347 config.ai_model = config.model_buffer;
348 }
349
350 ImGui::TableNextRow();
351 ImGui::TableSetColumnIndex(0);
352 ImGui::TextDisabled("Filter");
353 ImGui::TableSetColumnIndex(1);
354 ImGui::Checkbox("Provider", &filter_by_provider);
355 ImGui::SameLine();
357 ImGui::SameLine();
358
359 float refresh_width =
360 ImGui::CalcTextSize(ICON_MD_REFRESH).x + compact_padding.x * 2.0f;
361 float clear_width =
362 ImGui::CalcTextSize(ICON_MD_CLEAR).x + compact_padding.x * 2.0f;
363 float search_width = ImGui::GetContentRegionAvail().x - refresh_width -
364 style.ItemSpacing.x;
365 if (model_cache.search_buffer[0] != '\0') {
366 search_width -= clear_width + style.ItemSpacing.x;
367 }
368 ImGui::SetNextItemWidth(search_width);
369 ImGui::InputTextWithHint("##model_search", "Search models...",
370 model_cache.search_buffer,
371 IM_ARRAYSIZE(model_cache.search_buffer));
372 ImGui::SameLine();
373 if (model_cache.search_buffer[0] != '\0') {
374 if (ImGui::SmallButton(ICON_MD_CLEAR)) {
375 model_cache.search_buffer[0] = '\0';
376 }
377 if (ImGui::IsItemHovered()) {
378 ImGui::SetTooltip("Clear search");
379 }
380 ImGui::SameLine();
381 }
382 if (ImGui::SmallButton(model_cache.loading ? ICON_MD_SYNC
383 : ICON_MD_REFRESH)) {
384 if (callbacks.refresh_models) {
385 callbacks.refresh_models(true);
386 }
387 }
388
389 ImGui::EndTable();
390 }
391 } // model_var_guard scope
392 if (!model_cache.available_models.empty() ||
393 !model_cache.local_model_names.empty()) {
394 const int provider_count =
395 static_cast<int>(model_cache.available_models.size());
396 const int local_count =
397 static_cast<int>(model_cache.local_model_names.size());
398 if (provider_count > 0 && local_count > 0) {
399 ImGui::TextDisabled("Models: %d provider, %d local", provider_count,
400 local_count);
401 } else if (provider_count > 0) {
402 ImGui::TextDisabled("Models: %d provider", provider_count);
403 } else if (local_count > 0) {
404 ImGui::TextDisabled("Models: %d local files", local_count);
405 }
406 }
407
408 float list_height =
409 std::max(220.0f, ImGui::GetContentRegionAvail().y * 0.6f);
410 gui::StyleColorGuard model_list_bg(ImGuiCol_ChildBg, theme.panel_bg_darker);
411 ImGui::BeginChild("UnifiedModelList", ImVec2(0, list_height), true);
412 std::string filter = absl::AsciiStrToLower(model_cache.search_buffer);
413
414 struct ModelRow {
415 std::string name;
416 std::string provider;
417 std::string param_size;
418 std::string quantization;
419 std::string family;
420 uint64_t size_bytes = 0;
421 bool is_local = false;
422 bool is_file = false;
423 };
424
425 std::vector<ModelRow> rows;
426 if (!model_cache.available_models.empty()) {
427 rows.reserve(model_cache.available_models.size());
428 for (const auto& info : model_cache.available_models) {
429 ModelRow row;
430 row.name = info.name;
431 row.provider = info.provider;
432 row.param_size = info.parameter_size;
433 row.quantization = info.quantization;
434 row.family = info.family;
435 row.size_bytes = info.size_bytes;
436 row.is_local = info.is_local;
437 rows.push_back(std::move(row));
438 }
439 } else {
440 rows.reserve(model_cache.model_names.size());
441 for (const auto& model_name : model_cache.model_names) {
442 ModelRow row;
443 row.name = model_name;
444 row.provider = config.ai_provider;
445 rows.push_back(std::move(row));
446 }
447 }
448
449 if (!filter_by_provider && !model_cache.local_model_names.empty()) {
450 for (const auto& model_name : model_cache.local_model_names) {
451 ModelRow row;
452 row.name = model_name;
453 row.provider = "local";
454 row.is_local = true;
455 row.is_file = true;
456 rows.push_back(std::move(row));
457 }
458 }
459
460 auto get_provider_color = [&theme](const std::string& provider) -> ImVec4 {
461 if (provider == "ollama")
462 return theme.provider_ollama;
463 if (provider == "gemini")
464 return theme.provider_gemini;
465 if (provider == "anthropic")
466 return theme.provider_openai;
467 if (provider == "openai")
468 return theme.provider_openai;
469 return theme.provider_mock;
470 };
471
472 auto matches_filter = [&](const ModelRow& row) {
473 if (filter.empty()) {
474 return true;
475 }
476 std::string lower_name = absl::AsciiStrToLower(row.name);
477 std::string lower_provider = absl::AsciiStrToLower(row.provider);
478 if (ContainsText(lower_name, filter) ||
479 ContainsText(lower_provider, filter)) {
480 return true;
481 }
482 if (!row.param_size.empty() &&
483 ContainsText(absl::AsciiStrToLower(row.param_size), filter)) {
484 return true;
485 }
486 if (!row.family.empty() &&
487 ContainsText(absl::AsciiStrToLower(row.family), filter)) {
488 return true;
489 }
490 if (!row.quantization.empty() &&
491 ContainsText(absl::AsciiStrToLower(row.quantization), filter)) {
492 return true;
493 }
494 return false;
495 };
496
497 if (rows.empty()) {
498 ImGui::TextDisabled("No cached models. Refresh to discover.");
499 } else {
500 float list_width = ImGui::GetContentRegionAvail().x;
501 bool compact = list_width < 520.0f;
502 int column_count = compact ? 3 : 5;
503 ImGuiTableFlags table_flags =
504 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
505 ImGuiTableFlags_SizingStretchProp;
506 if (ImGui::BeginTable("ModelTable", column_count, table_flags)) {
507 if (compact) {
508 ImGui::TableSetupColumn("Provider", ImGuiTableColumnFlags_WidthFixed,
509 90.0f);
510 ImGui::TableSetupColumn("Model", ImGuiTableColumnFlags_WidthStretch);
511 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
512 70.0f);
513 } else {
514 ImGui::TableSetupColumn("Provider", ImGuiTableColumnFlags_WidthFixed,
515 90.0f);
516 ImGui::TableSetupColumn("Model", ImGuiTableColumnFlags_WidthStretch);
517 ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed,
518 80.0f);
519 ImGui::TableSetupColumn("Meta", ImGuiTableColumnFlags_WidthStretch);
520 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
521 80.0f);
522 }
523 ImGui::TableHeadersRow();
524
525 int row_id = 0;
526 for (const auto& row : rows) {
527 if (filter_by_provider) {
528 if (row.provider == "local") {
529 continue;
530 }
531 if (!row.provider.empty() && row.provider != config.ai_provider) {
532 continue;
533 }
534 }
535
536 if (!matches_filter(row)) {
537 continue;
538 }
539
540 ImGui::PushID(row_id++);
541 ImGui::TableNextRow();
542
543 ImGui::TableSetColumnIndex(0);
544 if (row.provider.empty()) {
545 ImGui::TextDisabled("-");
546 } else if (row.provider == "local") {
547 ImGui::TextDisabled(ICON_MD_FOLDER " local");
548 } else {
549 ImVec4 provider_color = get_provider_color(row.provider);
550 {
551 gui::StyleColorGuard badge_color(ImGuiCol_Button, provider_color);
552 gui::StyleVarGuard badge_var(
553 {{ImGuiStyleVar_FrameRounding, 6.0f},
554 {ImGuiStyleVar_FramePadding, ImVec2(4, 1)}});
555 ImGui::SmallButton(row.provider.c_str());
556 }
557 }
558
559 ImGui::TableSetColumnIndex(1);
560 bool is_selected = config.ai_model == row.name;
561 if (ImGui::Selectable(row.name.c_str(), is_selected)) {
562 config.ai_model = row.name;
563 std::snprintf(config.model_buffer, sizeof(config.model_buffer), "%s",
564 row.name.c_str());
565 if (!row.provider.empty() && row.provider != "local") {
566 config.ai_provider = row.provider;
567 std::snprintf(config.provider_buffer,
568 sizeof(config.provider_buffer), "%s",
569 row.provider.c_str());
570 }
571 }
572
573 if (row.is_file && ImGui::IsItemHovered()) {
574 ImGui::SetTooltip(
575 "Local file detected. Serve this model via LM Studio/Ollama to "
576 "use it.");
577 }
578
579 std::string size_label = row.param_size;
580 if (size_label.empty() && row.size_bytes > 0) {
581 size_label = FormatByteSize(row.size_bytes);
582 }
583
584 if (!compact) {
585 ImGui::TableSetColumnIndex(2);
586 ImGui::TextColored(theme.text_secondary_color, "%s",
587 size_label.c_str());
588
589 ImGui::TableSetColumnIndex(3);
590 if (!row.quantization.empty()) {
591 ImGui::TextColored(theme.text_info, "%s",
592 row.quantization.c_str());
593 if (!row.family.empty()) {
594 ImGui::SameLine();
595 }
596 }
597 if (!row.family.empty()) {
598 ImGui::TextColored(theme.text_secondary_gray, "%s",
599 row.family.c_str());
600 }
601 if (row.is_local && !row.is_file) {
602 ImGui::SameLine();
603 ImGui::TextColored(theme.status_success, ICON_MD_COMPUTER);
604 }
605 } else if (ImGui::IsItemHovered() && (!size_label.empty() ||
606 !row.quantization.empty() ||
607 !row.family.empty())) {
608 std::string meta;
609 if (!size_label.empty()) {
610 meta += size_label;
611 }
612 if (!row.quantization.empty()) {
613 if (!meta.empty()) {
614 meta += " • ";
615 }
616 meta += row.quantization;
617 }
618 if (!row.family.empty()) {
619 if (!meta.empty()) {
620 meta += " • ";
621 }
622 meta += row.family;
623 }
624 ImGui::SetTooltip("%s", meta.c_str());
625 }
626
627 int action_column = compact ? 2 : 4;
628 ImGui::TableSetColumnIndex(action_column);
629 gui::StyleVarGuard action_var(ImGuiStyleVar_FramePadding,
630 ImVec2(4, 1));
631 bool is_favorite =
632 std::find(config.favorite_models.begin(),
633 config.favorite_models.end(),
634 row.name) != config.favorite_models.end();
635 {
636 gui::StyleColorGuard star_color(
637 ImGuiCol_Text, is_favorite ? theme.status_warning
638 : theme.text_secondary_color);
639 if (ImGui::SmallButton(is_favorite ? ICON_MD_STAR
641 if (is_favorite) {
642 config.favorite_models.erase(
643 std::remove(config.favorite_models.begin(),
644 config.favorite_models.end(), row.name),
645 config.favorite_models.end());
646 config.model_chain.erase(
647 std::remove(config.model_chain.begin(),
648 config.model_chain.end(), row.name),
649 config.model_chain.end());
650 } else {
651 config.favorite_models.push_back(row.name);
652 }
653 }
654 }
655
656 if (ImGui::IsItemHovered()) {
657 ImGui::SetTooltip(is_favorite ? "Remove from favorites"
658 : "Favorite model");
659 }
660
661 if (!row.provider.empty() && row.provider != "local") {
662 ImGui::SameLine();
663 if (ImGui::SmallButton(ICON_MD_NOTE_ADD)) {
664 ModelPreset preset;
665 preset.name = row.name;
666 preset.model = row.name;
667 preset.provider = row.provider;
668 if (row.provider == "ollama") {
669 preset.host = config.ollama_host;
670 } else if (row.provider == "openai") {
671 preset.host = config.openai_base_url;
672 }
673 preset.tags = {row.provider};
674 preset.last_used = absl::Now();
675 config.model_presets.push_back(std::move(preset));
676 if (toast_manager) {
677 toast_manager->Show("Preset captured", ToastType::kSuccess,
678 2.0f);
679 }
680 }
681 if (ImGui::IsItemHovered()) {
682 ImGui::SetTooltip("Capture preset from this model");
683 }
684 }
685
686 ImGui::PopID();
687 }
688 ImGui::EndTable();
689 }
690 }
691 ImGui::EndChild();
692
693 if (model_cache.last_refresh != absl::InfinitePast()) {
694 double seconds =
695 absl::ToDoubleSeconds(absl::Now() - model_cache.last_refresh);
696 ImGui::TextDisabled("Last refresh %.0fs ago", seconds);
697 } else {
698 ImGui::TextDisabled("Models not refreshed yet");
699 }
700
701 if (config.ai_provider == "ollama") {
703 }
704
705 if (!config.favorite_models.empty()) {
706 ImGui::Separator();
707 ImGui::TextColored(theme.status_warning, ICON_MD_STAR " Favorites");
708 for (size_t i = 0; i < config.favorite_models.size(); ++i) {
709 auto& favorite = config.favorite_models[i];
710 ImGui::PushID(static_cast<int>(i));
711 bool active = config.ai_model == favorite;
712
713 std::string provider_name;
714 for (const auto& info : model_cache.available_models) {
715 if (info.name == favorite) {
716 provider_name = info.provider;
717 break;
718 }
719 }
720
721 if (!provider_name.empty()) {
722 ImVec4 badge_color = theme.provider_mock;
723 if (provider_name == "ollama")
724 badge_color = theme.provider_ollama;
725 else if (provider_name == "gemini")
726 badge_color = theme.provider_gemini;
727 else if (provider_name == "anthropic")
728 badge_color = theme.provider_openai;
729 else if (provider_name == "openai")
730 badge_color = theme.provider_openai;
731 {
732 gui::StyleColorGuard fav_badge_color(ImGuiCol_Button, badge_color);
733 gui::StyleVarGuard fav_badge_var(
734 {{ImGuiStyleVar_FrameRounding, 6.0f},
735 {ImGuiStyleVar_FramePadding, ImVec2(3, 1)}});
736 ImGui::SmallButton(provider_name.c_str());
737 }
738 ImGui::SameLine();
739 }
740
741 if (ImGui::Selectable(favorite.c_str(), active)) {
742 config.ai_model = favorite;
743 std::snprintf(config.model_buffer, sizeof(config.model_buffer), "%s",
744 favorite.c_str());
745 if (!provider_name.empty()) {
746 config.ai_provider = provider_name;
747 std::snprintf(config.provider_buffer, sizeof(config.provider_buffer),
748 "%s", provider_name.c_str());
749 }
750 }
751 ImGui::SameLine();
752 {
753 gui::StyleColorGuard close_color(ImGuiCol_Text, theme.status_error);
754 if (ImGui::SmallButton(ICON_MD_CLOSE)) {
755 config.model_chain.erase(
756 std::remove(config.model_chain.begin(), config.model_chain.end(),
757 favorite),
758 config.model_chain.end());
759 config.favorite_models.erase(config.favorite_models.begin() + i);
760 ImGui::PopID();
761 break;
762 }
763 }
764 ImGui::PopID();
765 }
766 }
767}
768
770 const Callbacks& callbacks,
771 ToastManager* toast_manager) {
772 const auto& theme = AgentUI::GetTheme();
773 ImGuiStyle& style = ImGui::GetStyle();
774 auto& config = context->agent_config();
775 auto& model_cache = context->model_cache();
776
777 ImGui::TextDisabled("Presets");
778 if (config.model_presets.empty()) {
779 ImGui::TextDisabled("Capture a preset to swap models quickly.");
780 }
781
782 float capture_width =
783 ImGui::CalcTextSize(ICON_MD_NOTE_ADD " Capture").x +
784 style.FramePadding.x * 2.0f;
785 float capture_input_width = ImGui::GetContentRegionAvail().x -
786 capture_width - style.ItemSpacing.x;
787 if (capture_input_width > 120.0f) {
788 ImGui::SetNextItemWidth(capture_input_width);
789 }
790 ImGui::InputTextWithHint("##new_preset_name", "Preset name...",
791 model_cache.new_preset_name,
792 IM_ARRAYSIZE(model_cache.new_preset_name));
793 if (capture_input_width > 120.0f) {
794 ImGui::SameLine();
795 }
796 if (ImGui::SmallButton(ICON_MD_NOTE_ADD " Capture")) {
797 ModelPreset preset;
798 preset.name = model_cache.new_preset_name[0]
799 ? std::string(model_cache.new_preset_name)
800 : config.ai_model;
801 preset.model = config.ai_model;
802 preset.provider = config.ai_provider;
803 if (config.ai_provider == "ollama") {
804 preset.host = config.ollama_host;
805 } else if (config.ai_provider == "openai") {
806 preset.host = config.openai_base_url;
807 }
808 preset.tags = {config.ai_provider};
809 preset.last_used = absl::Now();
810 config.model_presets.push_back(std::move(preset));
811 model_cache.new_preset_name[0] = '\0';
812 if (toast_manager) {
813 toast_manager->Show("Captured chat preset", ToastType::kSuccess, 2.0f);
814 }
815 }
816
817 float deck_height =
818 std::max(90.0f, ImGui::GetContentRegionAvail().y * 0.32f);
819 gui::StyledChild preset_child("PresetList", ImVec2(0, deck_height),
820 {.bg = theme.panel_bg_darker}, true);
821 if (config.model_presets.empty()) {
822 ImGui::TextDisabled("No presets yet");
823 } else {
824 ImGuiTableFlags table_flags =
825 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
826 ImGuiTableFlags_SizingStretchProp;
827 if (ImGui::BeginTable("PresetTable", 3, table_flags)) {
828 ImGui::TableSetupColumn("Preset", ImGuiTableColumnFlags_WidthStretch);
829 ImGui::TableSetupColumn("Host/Provider",
830 ImGuiTableColumnFlags_WidthStretch);
831 ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed,
832 90.0f);
833 ImGui::TableHeadersRow();
834
835 for (int i = 0; i < static_cast<int>(config.model_presets.size()); ++i) {
836 auto& preset = config.model_presets[i];
837 ImGui::PushID(i);
838 ImGui::TableNextRow();
839
840 ImGui::TableSetColumnIndex(0);
841 bool selected = model_cache.active_preset_index == i;
842 if (ImGui::Selectable(preset.name.c_str(), selected)) {
843 model_cache.active_preset_index = i;
844 if (callbacks.apply_preset) {
845 callbacks.apply_preset(preset);
846 }
847 }
848 if (ImGui::IsItemHovered()) {
849 std::string tooltip = absl::StrFormat("Model: %s", preset.model);
850 if (!preset.tags.empty()) {
851 tooltip += absl::StrFormat("\nTags: %s",
852 absl::StrJoin(preset.tags, ", "));
853 }
854 if (preset.last_used != absl::InfinitePast()) {
855 tooltip += absl::StrFormat("\nLast used %s",
856 FormatRelativeTime(preset.last_used));
857 }
858 ImGui::SetTooltip("%s", tooltip.c_str());
859 }
860
861 ImGui::TableSetColumnIndex(1);
862 if (!preset.host.empty()) {
863 ImGui::TextDisabled("%s", preset.host.c_str());
864 } else if (!preset.provider.empty()) {
865 ImGui::TextDisabled("%s", preset.provider.c_str());
866 } else {
867 ImGui::TextDisabled("-");
868 }
869
870 ImGui::TableSetColumnIndex(2);
871 {
872 gui::StyleVarGuard preset_action_var(ImGuiStyleVar_FramePadding,
873 ImVec2(4, 1));
874 if (ImGui::SmallButton(ICON_MD_PLAY_ARROW "##apply")) {
875 model_cache.active_preset_index = i;
876 if (callbacks.apply_preset) {
877 callbacks.apply_preset(preset);
878 }
879 }
880 ImGui::SameLine();
881 if (ImGui::SmallButton(preset.pinned ? ICON_MD_STAR
883 preset.pinned = !preset.pinned;
884 }
885 ImGui::SameLine();
886 if (ImGui::SmallButton(ICON_MD_DELETE)) {
887 config.model_presets.erase(config.model_presets.begin() + i);
888 if (model_cache.active_preset_index == i) {
889 model_cache.active_preset_index = -1;
890 }
891 ImGui::PopID();
892 break;
893 }
894 }
895 ImGui::PopID();
896 }
897 ImGui::EndTable();
898 }
899 }
900}
901
903 AgentConfigState& config) {
904 ImGui::SliderFloat("Temperature", &config.temperature, 0.0f, 1.5f);
905 ImGui::SliderFloat("Top P", &config.top_p, 0.0f, 1.0f);
906 ImGui::SliderInt("Max Output Tokens", &config.max_output_tokens, 256, 8192);
907 ImGui::SliderInt("Max Tool Iterations", &config.max_tool_iterations, 1, 10);
908 ImGui::SliderInt("Max Retry Attempts", &config.max_retry_attempts, 0, 5);
909 ImGui::Checkbox("Stream responses", &config.stream_responses);
910 ImGui::SameLine();
911 ImGui::Checkbox("Show reasoning", &config.show_reasoning);
912 ImGui::SameLine();
913 ImGui::Checkbox("Verbose logs", &config.verbose);
914}
915
917 AgentConfigState& config, const Callbacks& callbacks) {
918 struct ToolToggleEntry {
919 const char* label;
920 bool* flag;
921 const char* hint;
922 } entries[] = {
923 {"Resources", &config.tool_config.resources, "resource-list/search"},
924 {"Dungeon", &config.tool_config.dungeon, "Room + sprite inspection"},
925 {"Overworld", &config.tool_config.overworld, "Map + entrance analysis"},
926 {"Dialogue", &config.tool_config.dialogue, "Dialogue list/search"},
927 {"Messages", &config.tool_config.messages, "Message table + ROM text"},
928 {"GUI Automation", &config.tool_config.gui, "GUI automation tools"},
929 {"Music", &config.tool_config.music, "Music info & tracks"},
930 {"Sprite", &config.tool_config.sprite, "Sprite palette/properties"},
931 {"Emulator", &config.tool_config.emulator, "Emulator controls"},
932 {"Memory", &config.tool_config.memory_inspector, "RAM inspection & watch"}};
933
934 ImGui::TextDisabled(
935 "Expose tools in the agent sidebar and editor panels.");
936 int columns = ImGui::GetContentRegionAvail().x > 360.0f ? 2 : 1;
937 ImGuiTableFlags table_flags =
938 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_RowBg;
939 if (ImGui::BeginTable("AgentToolTable", columns, table_flags)) {
940 for (size_t i = 0; i < std::size(entries); ++i) {
941 ImGui::TableNextColumn();
942 if (ImGui::Checkbox(entries[i].label, entries[i].flag)) {
943 if (callbacks.apply_tool_preferences) {
944 callbacks.apply_tool_preferences();
945 }
946 }
947 if (ImGui::IsItemHovered() && entries[i].hint) {
948 ImGui::SetTooltip("%s", entries[i].hint);
949 }
950 }
951 ImGui::EndTable();
952 }
953}
954
956 AgentConfigState& config) {
957 ImGui::Spacing();
958 ImGui::TextDisabled("Chain Mode (Experimental)");
959
960 bool round_robin = config.chain_mode == ChainMode::kRoundRobin;
961 if (ImGui::Checkbox("Round Robin", &round_robin)) {
962 config.chain_mode =
964 }
965 if (ImGui::IsItemHovered()) {
966 ImGui::SetTooltip("Rotate through favorite models for each response");
967 }
968
969 ImGui::SameLine();
970 bool consensus = config.chain_mode == ChainMode::kConsensus;
971 if (ImGui::Checkbox("Consensus", &consensus)) {
972 config.chain_mode =
974 }
975 if (ImGui::IsItemHovered()) {
976 ImGui::SetTooltip("Ask multiple models and synthesize a response");
977 }
978}
979
980} // namespace editor
981} // namespace yaze
void RenderToolingControls(AgentConfigState &config, const Callbacks &callbacks)
void RenderParameterControls(AgentConfigState &config)
void RenderModelConfigControls(AgentUIContext *context, const Callbacks &callbacks, ToastManager *toast_manager)
void RenderModelDeck(AgentUIContext *context, const Callbacks &callbacks, ToastManager *toast_manager)
void RenderChainModeControls(AgentConfigState &config)
void Draw(AgentUIContext *context, const Callbacks &callbacks, ToastManager *toast_manager)
Unified context for agent UI components.
AgentConfigState & agent_config()
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
RAII guard for ImGui child windows with optional styling.
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_NOTE_ADD
Definition icons.h:1330
#define ICON_MD_CLOUD_SYNC
Definition icons.h:429
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CONSTRUCTION
Definition icons.h:458
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_AUTO_AWESOME
Definition icons.h:214
#define ICON_MD_PUBLIC
Definition icons.h:1524
#define ICON_MD_PSYCHOLOGY
Definition icons.h:1523
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_FOLDER
Definition icons.h:809
#define ICON_MD_STAR_BORDER
Definition icons.h:1849
#define ICON_MD_SYNC
Definition icons.h:1919
#define ICON_MD_CLOUD
Definition icons.h:423
#define ICON_MD_COMPUTER
Definition icons.h:452
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_SMART_TOY
Definition icons.h:1781
void HorizontalSpacing(float amount)
const AgentUITheme & GetTheme()
void RenderSectionHeader(const char *icon, const char *label, const ImVec4 &color)
std::function< void(const AgentConfigState &) update_config)
std::function< void(bool force)> refresh_models
std::function< void(const ModelPreset &) apply_preset)
Agent configuration state.
Model preset for quick switching.
std::vector< std::string > tags