AX Agent 프리셋 렌더와 주제 선택 흐름을 분리해 메인 창 책임을 줄인다
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow.TopicPresetPresentation을 추가해 프리셋 카드 생성, 커스텀 프리셋 메뉴, 주제 선택 적용 흐름을 별도 프레젠테이션 계층으로 이동한다

- ChatWindow.xaml.cs에서는 프리셋 UI 조립 코드를 제거하고 대화 orchestration 중심 구조를 더 강화한다

- claw-code parity plan과 개발 문서에 구조 개선 진행 상황을 반영해 큰 구조 개선 항목이 사실상 마감 단계임을 기록한다

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
This commit is contained in:
2026-04-06 10:15:22 +09:00
parent d0d66c1d52
commit 1b4566d192
4 changed files with 527 additions and 515 deletions

View File

@@ -0,0 +1,523 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
/// <summary>프리셋에서 대화 주제 버튼을 동적으로 생성합니다.</summary>
private void BuildTopicButtons()
{
TopicButtonPanel.Children.Clear();
TopicButtonPanel.Visibility = Visibility.Visible;
if (TopicPresetScrollViewer != null)
TopicPresetScrollViewer.Visibility = Visibility.Visible;
if (_activeTab == "Cowork" || _activeTab == "Code")
{
if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
? "코드 작업을 입력하세요"
: "작업 유형을 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
: "에이전트가 상세한 데이터를 작성합니다";
}
else
{
if (EmptyStateTitle != null) EmptyStateTitle.Text = "대화 주제를 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
}
if (_activeTab == "Code")
{
TopicButtonPanel.Visibility = Visibility.Collapsed;
if (TopicPresetScrollViewer != null)
TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
return;
}
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
var cardBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var cardHoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
var cardBorder = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
void AttachTopicCardHover(Border card, Brush normalBackground, Brush hoverBackground)
{
card.MouseEnter += (sender, _) =>
{
if (sender is Border hovered)
{
hovered.Background = hoverBackground;
hovered.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder;
}
};
card.MouseLeave += (sender, _) =>
{
if (sender is Border hovered)
{
hovered.Background = normalBackground;
hovered.BorderBrush = cardBorder;
}
};
}
foreach (var preset in presets)
{
var capturedPreset = preset;
var buttonColor = BrushFromHex(preset.Color);
var border = new Border
{
Background = cardBackground,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var contentGrid = new Grid();
var stack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var iconCircle = new Border
{
Width = 34,
Height = 34,
CornerRadius = new CornerRadius(17),
Background = new SolidColorBrush(((SolidColorBrush)buttonColor).Color) { Opacity = 0.15 },
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 9),
};
var iconBlock = new TextBlock
{
Text = preset.Symbol,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 15,
Foreground = buttonColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
iconCircle.Child = iconBlock;
stack.Children.Add(iconCircle);
stack.Children.Add(new TextBlock
{
Text = preset.Label,
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
MaxWidth = 112,
});
if (capturedPreset.IsCustom)
{
contentGrid.Children.Add(stack);
var badge = new Border
{
Width = 16,
Height = 16,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(2, 2, 0, 0),
};
badge.Child = new TextBlock
{
Text = "\uE710",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 8,
Foreground = buttonColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
contentGrid.Children.Add(badge);
}
else
{
contentGrid.Children.Add(stack);
}
border.Child = contentGrid;
AttachTopicCardHover(border, cardBackground, cardHoverBackground);
border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset);
if (capturedPreset.IsCustom)
{
border.MouseRightButtonUp += (sender, args) =>
{
args.Handled = true;
ShowCustomPresetContextMenu(sender as Border, capturedPreset);
};
}
TopicButtonPanel.Children.Add(border);
}
var etcColor = BrushFromHex("#6B7280");
var etcBorder = new Border
{
Background = cardBackground,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var etcGrid = new Grid();
var etcStack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var etcIconCircle = new Border
{
Width = 34,
Height = 34,
CornerRadius = new CornerRadius(17),
Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 9),
};
etcIconCircle.Child = new TextBlock
{
Text = "\uE70F",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 15,
Foreground = etcColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
etcStack.Children.Add(etcIconCircle);
etcStack.Children.Add(new TextBlock
{
Text = "기타",
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
});
etcGrid.Children.Add(etcStack);
etcBorder.Child = etcGrid;
AttachTopicCardHover(etcBorder, cardBackground, cardHoverBackground);
etcBorder.MouseLeftButtonDown += (_, _) =>
{
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
};
TopicButtonPanel.Children.Add(etcBorder);
var addBorder = new Border
{
Background = Brushes.Transparent,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var addGrid = new Grid();
var addStack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
addStack.Children.Add(new TextBlock
{
Text = "\uE710",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 18,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 8, 0, 8),
});
addStack.Children.Add(new TextBlock
{
Text = "프리셋 추가",
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
});
addGrid.Children.Add(addStack);
addBorder.Child = addGrid;
AttachTopicCardHover(addBorder, Brushes.Transparent, cardHoverBackground);
addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
TopicButtonPanel.Children.Add(addBorder);
UpdateTopicPresetScrollMode();
}
private void UpdateTopicPresetScrollMode()
{
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
return;
Dispatcher.BeginInvoke(new Action(() =>
{
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
return;
TopicPresetScrollViewer.UpdateLayout();
var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
? ScrollBarVisibility.Auto
: ScrollBarVisibility.Disabled;
TopicPresetScrollViewer.Padding = shouldScroll
? new Thickness(0, 2, 6, 0)
: new Thickness(0, 2, 0, 0);
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
private void ShowCustomPresetDialog(CustomPresetEntry? existing = null)
{
var dialog = new CustomPresetDialog(
existingName: existing?.Label ?? "",
existingDesc: existing?.Description ?? "",
existingPrompt: existing?.SystemPrompt ?? "",
existingColor: existing?.Color ?? "#6366F1",
existingSymbol: existing?.Symbol ?? "\uE713",
existingTab: existing?.Tab ?? _activeTab)
{
Owner = this,
};
if (dialog.ShowDialog() != true)
return;
if (existing != null)
{
existing.Label = dialog.PresetName;
existing.Description = dialog.PresetDescription;
existing.SystemPrompt = dialog.PresetSystemPrompt;
existing.Color = dialog.PresetColor;
existing.Symbol = dialog.PresetSymbol;
existing.Tab = dialog.PresetTab;
}
else
{
_settings.Settings.Llm.CustomPresets.Add(new CustomPresetEntry
{
Label = dialog.PresetName,
Description = dialog.PresetDescription,
SystemPrompt = dialog.PresetSystemPrompt,
Color = dialog.PresetColor,
Symbol = dialog.PresetSymbol,
Tab = dialog.PresetTab,
});
}
_settings.Save();
BuildTopicButtons();
}
private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset)
{
if (anchor == null || preset.CustomId == null)
return;
var popup = new Popup
{
PlacementTarget = anchor,
Placement = PlacementMode.Bottom,
StaysOpen = false,
AllowsTransparency = true,
};
var menuBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var menuBorder = new Border
{
Background = menuBackground,
CornerRadius = new CornerRadius(10),
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(4),
MinWidth = 120,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 12,
ShadowDepth = 2,
Opacity = 0.3,
Color = Colors.Black,
},
};
var stack = new StackPanel();
var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText);
editItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(item => item.Id == preset.CustomId);
if (entry != null)
ShowCustomPresetDialog(entry);
};
stack.Children.Add(editItem);
var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)));
deleteItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var result = CustomMessageBox.Show(
$"'{preset.Label}' 프리셋을 삭제하시겠습니까?",
"프리셋 삭제",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
_settings.Settings.Llm.CustomPresets.RemoveAll(item => item.Id == preset.CustomId);
_settings.Save();
BuildTopicButtons();
};
stack.Children.Add(deleteItem);
menuBorder.Child = stack;
popup.Child = menuBorder;
popup.IsOpen = true;
}
private Border CreateContextMenuItem(string icon, string label, Brush foreground)
{
var item = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(10, 6, 14, 6),
Cursor = Cursors.Hand,
};
var stack = new StackPanel { Orientation = Orientation.Horizontal };
stack.Children.Add(new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 13,
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
stack.Children.Add(new TextBlock
{
Text = label,
FontSize = 13,
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
});
item.Child = stack;
var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
item.MouseEnter += (sender, _) =>
{
if (sender is Border hovered)
hovered.Background = hoverBackground;
};
item.MouseLeave += (sender, _) =>
{
if (sender is Border hovered)
hovered.Background = Brushes.Transparent;
};
return item;
}
private Border CreateContextMenuItem(string icon, string label, Brush foreground, Brush secondaryForeground)
=> CreateContextMenuItem(icon, label, foreground);
private void SelectTopic(Services.TopicPreset preset)
{
bool hasConversation;
bool hasMessages;
lock (_convLock)
{
hasConversation = _currentConversation != null;
hasMessages = _currentConversation?.Messages.Count > 0;
}
var hasInput = !string.IsNullOrEmpty(InputBox.Text);
if (!hasConversation)
StartNewConversation();
lock (_convLock)
{
if (_currentConversation == null)
return;
var session = ChatSession;
if (session != null)
{
_currentConversation = session.UpdateConversationMetadata(_activeTab, conversation =>
{
conversation.SystemCommand = preset.SystemPrompt;
conversation.Category = preset.Category;
}, _storage);
}
else
{
_currentConversation.SystemCommand = preset.SystemPrompt;
_currentConversation.Category = preset.Category;
}
}
UpdateCategoryLabel();
SaveConversationSettings();
RefreshConversationList();
UpdateSelectedPresetGuide();
if (EmptyState != null)
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
if (!string.IsNullOrEmpty(preset.Placeholder))
{
_promptCardPlaceholder = preset.Placeholder;
if (!hasMessages && !hasInput)
ShowPlaceholder();
}
if (hasMessages || hasInput)
ShowToast($"프리셋 변경: {preset.Label}");
if (_activeTab == "Cowork")
BuildBottomBar();
}
}

View File

@@ -10110,521 +10110,6 @@ public partial class ChatWindow : Window
_toastHideTimer.Start();
}
// ─── 대화 주제 버튼 ──────────────────────────────────────────────────
/// <summary>프리셋에서 대화 주제 버튼을 동적으로 생성합니다.</summary>
private void BuildTopicButtons()
{
TopicButtonPanel.Children.Clear();
TopicButtonPanel.Visibility = Visibility.Visible;
if (TopicPresetScrollViewer != null)
TopicPresetScrollViewer.Visibility = Visibility.Visible;
// 탭별 EmptyState 텍스트
if (_activeTab == "Cowork" || _activeTab == "Code")
{
if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
? "코드 작업을 입력하세요"
: "작업 유형을 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
: "에이전트가 상세한 데이터를 작성합니다";
}
else
{
if (EmptyStateTitle != null) EmptyStateTitle.Text = "대화 주제를 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
}
if (_activeTab == "Code")
{
TopicButtonPanel.Visibility = Visibility.Collapsed;
if (TopicPresetScrollViewer != null)
TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
return;
}
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
var cardBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var cardHoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
var cardBorder = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
void AttachTopicCardHover(Border card, Brush normalBackground, Brush hoverBackground)
{
card.MouseEnter += (s, _) =>
{
if (s is Border b)
{
b.Background = hoverBackground;
b.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder;
}
};
card.MouseLeave += (s, _) =>
{
if (s is Border b)
{
b.Background = normalBackground;
b.BorderBrush = cardBorder;
}
};
}
foreach (var preset in presets)
{
var capturedPreset = preset;
var btnColor = BrushFromHex(preset.Color);
var border = new Border
{
Background = cardBackground,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var contentGrid = new Grid();
var stack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var iconCircle = new Border
{
Width = 34, Height = 34,
CornerRadius = new CornerRadius(17),
Background = new SolidColorBrush(((SolidColorBrush)btnColor).Color) { Opacity = 0.15 },
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 9),
};
var iconTb = new TextBlock
{
Text = preset.Symbol,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 15,
Foreground = btnColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
iconCircle.Child = iconTb;
stack.Children.Add(iconCircle);
stack.Children.Add(new TextBlock
{
Text = preset.Label,
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
MaxWidth = 112,
});
// 커스텀 프리셋: 좌측 상단 뱃지
if (capturedPreset.IsCustom)
{
contentGrid.Children.Add(stack);
var badge = new Border
{
Width = 16, Height = 16,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(2, 2, 0, 0),
};
badge.Child = new TextBlock
{
Text = "\uE710", // + 아이콘
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 8,
Foreground = btnColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
contentGrid.Children.Add(badge);
}
else
{
contentGrid.Children.Add(stack);
}
border.Child = contentGrid;
AttachTopicCardHover(border, cardBackground, cardHoverBackground);
// 클릭 → 해당 주제로 새 대화 시작
border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset);
// 커스텀 프리셋: 우클릭 메뉴 (편집/삭제)
if (capturedPreset.IsCustom)
{
border.MouseRightButtonUp += (s, e) =>
{
e.Handled = true;
ShowCustomPresetContextMenu(s as Border, capturedPreset);
};
}
TopicButtonPanel.Children.Add(border);
}
// "기타" 자유 입력 버튼 추가
{
var etcColor = BrushFromHex("#6B7280"); // 회색
var etcBorder = new Border
{
Background = cardBackground,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var etcGrid = new Grid();
var etcStack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var etcIconCircle = new Border
{
Width = 34, Height = 34,
CornerRadius = new CornerRadius(17),
Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 9),
};
etcIconCircle.Child = new TextBlock
{
Text = "\uE70F", // Edit 아이콘
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 15,
Foreground = etcColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
etcStack.Children.Add(etcIconCircle);
etcStack.Children.Add(new TextBlock
{
Text = "기타",
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
});
etcGrid.Children.Add(etcStack);
etcBorder.Child = etcGrid;
AttachTopicCardHover(etcBorder, cardBackground, cardHoverBackground);
etcBorder.MouseLeftButtonDown += (_, _) =>
{
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
};
TopicButtonPanel.Children.Add(etcBorder);
}
// ── "+" 커스텀 프리셋 추가 버튼 ──
{
var addColor = BrushFromHex("#6366F1");
var addBorder = new Border
{
Background = Brushes.Transparent,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var addGrid = new Grid();
var addStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
// + 아이콘
var plusIcon = new TextBlock
{
Text = "\uE710",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 18,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 8, 0, 8),
};
addStack.Children.Add(plusIcon);
addStack.Children.Add(new TextBlock
{
Text = "프리셋 추가",
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
});
addGrid.Children.Add(addStack);
addBorder.Child = addGrid;
AttachTopicCardHover(addBorder, Brushes.Transparent, cardHoverBackground);
addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
TopicButtonPanel.Children.Add(addBorder);
}
UpdateTopicPresetScrollMode();
}
private void UpdateTopicPresetScrollMode()
{
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
return;
Dispatcher.BeginInvoke(new Action(() =>
{
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
return;
TopicPresetScrollViewer.UpdateLayout();
var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
? ScrollBarVisibility.Auto
: ScrollBarVisibility.Disabled;
TopicPresetScrollViewer.Padding = shouldScroll
? new Thickness(0, 2, 6, 0)
: new Thickness(0, 2, 0, 0);
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
// ─── 커스텀 프리셋 관리 ─────────────────────────────────────────────
/// <summary>커스텀 프리셋 추가 다이얼로그를 표시합니다.</summary>
private void ShowCustomPresetDialog(Models.CustomPresetEntry? existing = null)
{
bool isEdit = existing != null;
var dlg = new CustomPresetDialog(
existingName: existing?.Label ?? "",
existingDesc: existing?.Description ?? "",
existingPrompt: existing?.SystemPrompt ?? "",
existingColor: existing?.Color ?? "#6366F1",
existingSymbol: existing?.Symbol ?? "\uE713",
existingTab: existing?.Tab ?? _activeTab)
{
Owner = this,
};
if (dlg.ShowDialog() == true)
{
if (isEdit)
{
existing!.Label = dlg.PresetName;
existing.Description = dlg.PresetDescription;
existing.SystemPrompt = dlg.PresetSystemPrompt;
existing.Color = dlg.PresetColor;
existing.Symbol = dlg.PresetSymbol;
existing.Tab = dlg.PresetTab;
}
else
{
_settings.Settings.Llm.CustomPresets.Add(new Models.CustomPresetEntry
{
Label = dlg.PresetName,
Description = dlg.PresetDescription,
SystemPrompt = dlg.PresetSystemPrompt,
Color = dlg.PresetColor,
Symbol = dlg.PresetSymbol,
Tab = dlg.PresetTab,
});
}
_settings.Save();
BuildTopicButtons();
}
}
/// <summary>커스텀 프리셋 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset)
{
if (anchor == null || preset.CustomId == null) return;
var popup = new System.Windows.Controls.Primitives.Popup
{
PlacementTarget = anchor,
Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom,
StaysOpen = false,
AllowsTransparency = true,
};
var menuBg = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var menuBorder = new Border
{
Background = menuBg,
CornerRadius = new CornerRadius(10),
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(4),
MinWidth = 120,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black,
},
};
var stack = new StackPanel();
// 편집 버튼
var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText, secondaryText);
editItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(c => c.Id == preset.CustomId);
if (entry != null) ShowCustomPresetDialog(entry);
};
stack.Children.Add(editItem);
// 삭제 버튼
var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), secondaryText);
deleteItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var result = CustomMessageBox.Show(
$"'{preset.Label}' 프리셋을 삭제하시겠습니까?",
"프리셋 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_settings.Settings.Llm.CustomPresets.RemoveAll(c => c.Id == preset.CustomId);
_settings.Save();
BuildTopicButtons();
}
};
stack.Children.Add(deleteItem);
menuBorder.Child = stack;
popup.Child = menuBorder;
popup.IsOpen = true;
}
/// <summary>컨텍스트 메뉴 항목을 생성합니다.</summary>
private Border CreateContextMenuItem(string icon, string label, Brush fg, Brush secondaryFg)
{
var item = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(10, 6, 14, 6),
Cursor = Cursors.Hand,
};
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 13, Foreground = fg,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
sp.Children.Add(new TextBlock
{
Text = label, FontSize = 13, Foreground = fg,
VerticalAlignment = VerticalAlignment.Center,
});
item.Child = sp;
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
return item;
}
/// <summary>대화 주제 선택 — 프리셋 시스템 프롬프트 + 카테고리 적용.</summary>
private void SelectTopic(Services.TopicPreset preset)
{
bool hasMessages;
bool hasConversation;
lock (_convLock)
{
hasConversation = _currentConversation != null;
hasMessages = _currentConversation?.Messages.Count > 0;
}
// 입력란에 텍스트가 있으면 기존 대화를 유지 (입력 내용 보존)
bool hasInput = !string.IsNullOrEmpty(InputBox.Text);
bool keepConversation = hasConversation;
if (!keepConversation)
{
// 현재 대화가 아예 없는 경우에만 새 대화 시작
StartNewConversation();
keepConversation = true;
}
// 프리셋 적용 (기존 대화에도 프리셋 변경 가능)
lock (_convLock)
{
if (_currentConversation != null)
{
var session = ChatSession;
if (session != null)
{
_currentConversation = session.UpdateConversationMetadata(_activeTab, c =>
{
c.SystemCommand = preset.SystemPrompt;
c.Category = preset.Category;
}, _storage);
}
else
{
_currentConversation.SystemCommand = preset.SystemPrompt;
_currentConversation.Category = preset.Category;
}
}
}
UpdateCategoryLabel();
SaveConversationSettings();
RefreshConversationList();
UpdateSelectedPresetGuide();
if (EmptyState != null)
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
if (!string.IsNullOrEmpty(preset.Placeholder))
{
_promptCardPlaceholder = preset.Placeholder;
if (!hasMessages && !hasInput) ShowPlaceholder();
}
if (hasMessages || hasInput)
ShowToast($"프리셋 변경: {preset.Label}");
// Cowork 탭: 하단 바 갱신
if (_activeTab == "Cowork")
BuildBottomBar();
}
/// <summary>선택된 디자인 무드 키 (HtmlSkill에서 사용).</summary>
private string _selectedMood = null!; // Loaded 이벤트에서 초기화
private string _selectedLanguage = "auto"; // Code 탭 개발 언어