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