- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함 - OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함 - AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함 - 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함 - README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
1039 lines
42 KiB
C#
1039 lines
42 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Threading;
|
|
using Microsoft.Win32;
|
|
using AxCopilot.Models;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Services.Agent;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ChatWindow
|
|
{
|
|
// ─── 프로젝트 문맥 파일 (AGENTS.md) ──────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 작업 폴더에 AGENTS.md가 있으면 내용을 읽어 시스템 프롬프트에 주입합니다.
|
|
/// 프로젝트 로컬 컨텍스트 규약 파일(AGENTS.md) 형식을 사용합니다.
|
|
/// </summary>
|
|
private static string LoadProjectContext(string workFolder)
|
|
{
|
|
if (string.IsNullOrEmpty(workFolder)) return "";
|
|
|
|
// AGENTS.md 탐색 (작업 폴더 → 상위 폴더 순, 레거시 AX.md 폴백)
|
|
var searchDir = workFolder;
|
|
for (int i = 0; i < 3; i++) // 최대 3단계 상위까지
|
|
{
|
|
if (string.IsNullOrEmpty(searchDir)) break;
|
|
var agentsPath = System.IO.Path.Combine(searchDir, "AGENTS.md");
|
|
var legacyPath = System.IO.Path.Combine(searchDir, "AX.md");
|
|
var filePath = System.IO.File.Exists(agentsPath) ? agentsPath : legacyPath;
|
|
if (System.IO.File.Exists(filePath))
|
|
{
|
|
try
|
|
{
|
|
var content = System.IO.File.ReadAllText(filePath);
|
|
if (content.Length > 8000) content = content[..8000] + "\n... (8000자 초과 생략)";
|
|
var sourceName = System.IO.Path.GetFileName(filePath);
|
|
return $"\n## Project Context (from {sourceName})\n{content}\n";
|
|
}
|
|
catch { }
|
|
}
|
|
searchDir = System.IO.Directory.GetParent(searchDir)?.FullName;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// ─── 부드러운 스크롤 애니메이션 (재사용 타이머) ──────────────────────
|
|
|
|
private DispatcherTimer? _smoothScrollTimer;
|
|
private double _smoothScrollStartOffset;
|
|
private double _smoothScrollDiff;
|
|
private DateTime _smoothScrollStartTime;
|
|
|
|
private void SmoothScrollTimer_Tick(object? sender, EventArgs e)
|
|
{
|
|
var elapsed = (DateTime.UtcNow - _smoothScrollStartTime).TotalMilliseconds;
|
|
var progress = Math.Min(elapsed / 200.0, 1.0);
|
|
var eased = 1.0 - Math.Pow(1.0 - progress, 3);
|
|
ScrollTranscriptToVerticalOffset(_smoothScrollStartOffset + _smoothScrollDiff * eased);
|
|
|
|
if (progress >= 1.0)
|
|
_smoothScrollTimer?.Stop();
|
|
}
|
|
|
|
// ─── 무지개 글로우 애니메이션 ─────────────────────────────────────────
|
|
|
|
private DispatcherTimer? _rainbowTimer;
|
|
private DateTime _rainbowStartTime;
|
|
|
|
private bool TryGetStreamingElapsed(out TimeSpan elapsed)
|
|
{
|
|
elapsed = TimeSpan.Zero;
|
|
if (_streamStartTime.Year < 2000)
|
|
return false;
|
|
|
|
var now = DateTime.UtcNow;
|
|
if (_streamStartTime > now.AddSeconds(1))
|
|
return false;
|
|
|
|
elapsed = now - _streamStartTime;
|
|
if (elapsed < TimeSpan.Zero || elapsed > TimeSpan.FromHours(6))
|
|
{
|
|
elapsed = TimeSpan.Zero;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private long GetStreamingElapsedMsOrZero()
|
|
=> TryGetStreamingElapsed(out var elapsed)
|
|
? Math.Max(0L, (long)elapsed.TotalMilliseconds)
|
|
: 0L;
|
|
|
|
/// <summary>입력창 테두리에 무지개 그라데이션 회전 애니메이션을 재생합니다 (3초).</summary>
|
|
private void PlayRainbowGlow()
|
|
{
|
|
if (!_settings.Settings.Llm.EnableChatRainbowGlow) return;
|
|
if (_rainbowTimer != null) return; // 이미 실행 중이면 opacity 리셋 없이 그냥 유지
|
|
|
|
_rainbowStartTime = DateTime.UtcNow;
|
|
InputGlowBorder.Visibility = Visibility.Visible;
|
|
InputGlowBorder.Effect = new System.Windows.Media.Effects.BlurEffect { Radius = 4 };
|
|
InputGlowBorder.BeginAnimation(UIElement.OpacityProperty,
|
|
new System.Windows.Media.Animation.DoubleAnimation(0, 0.92, TimeSpan.FromMilliseconds(180)));
|
|
|
|
_rainbowTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
|
|
_rainbowTimer.Tick += (_, _) =>
|
|
{
|
|
var elapsed = (DateTime.UtcNow - _rainbowStartTime).TotalMilliseconds;
|
|
var shift = (elapsed / 2000.0) % 1.0;
|
|
var brush = InputGlowBorder.BorderBrush as LinearGradientBrush;
|
|
if (brush == null) return;
|
|
|
|
var angle = shift * Math.PI * 2;
|
|
brush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(angle), 0.5 + 0.5 * Math.Sin(angle));
|
|
brush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(angle), 0.5 - 0.5 * Math.Sin(angle));
|
|
};
|
|
_rainbowTimer.Start();
|
|
}
|
|
|
|
/// <summary>레인보우 글로우 효과를 페이드아웃하며 중지합니다.</summary>
|
|
private void StopRainbowGlow()
|
|
{
|
|
_rainbowTimer?.Stop();
|
|
_rainbowTimer = null;
|
|
if (InputGlowBorder.Opacity > 0 || InputGlowBorder.Visibility == Visibility.Visible)
|
|
{
|
|
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(
|
|
InputGlowBorder.Opacity, 0, TimeSpan.FromMilliseconds(600));
|
|
fadeOut.Completed += (_, _) =>
|
|
{
|
|
InputGlowBorder.Opacity = 0;
|
|
InputGlowBorder.Visibility = Visibility.Collapsed;
|
|
};
|
|
InputGlowBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
|
}
|
|
else
|
|
{
|
|
InputGlowBorder.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
// ─── 토스트 알림 ──────────────────────────────────────────────────────
|
|
|
|
private DispatcherTimer? _toastHideTimer;
|
|
|
|
/// <summary>ToastBorder를 즉시 페이드아웃하고 숨깁니다.</summary>
|
|
private void HideToast()
|
|
{
|
|
_toastHideTimer?.Stop();
|
|
_toastHideTimer = null;
|
|
_tipDismissTimer?.Stop();
|
|
_tipDismissTimer = null;
|
|
if (ToastBorder?.Visibility != Visibility.Visible) return;
|
|
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(200));
|
|
fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed;
|
|
ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
|
}
|
|
|
|
private void ShowToast(string message, string icon = "\uE73E", int durationMs = 2000)
|
|
{
|
|
// 두 타이머 모두 중지 (ShowToast/ShowTip이 같은 ToastBorder를 공유)
|
|
_toastHideTimer?.Stop();
|
|
_tipDismissTimer?.Stop();
|
|
|
|
ToastText.Text = message;
|
|
ToastIcon.Text = icon;
|
|
ToastBorder.Visibility = Visibility.Visible;
|
|
|
|
// 페이드인
|
|
ToastBorder.BeginAnimation(UIElement.OpacityProperty,
|
|
new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
|
|
|
// 자동 숨기기 — 타이머 인스턴스를 로컬 변수로 캡처해 필드 재할당 간섭 방지
|
|
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(durationMs) };
|
|
_toastHideTimer = timer;
|
|
timer.Tick += (_, _) =>
|
|
{
|
|
if (_toastHideTimer != timer) return; // 다른 ShowToast가 교체한 경우 무시
|
|
timer.Stop();
|
|
_toastHideTimer = null;
|
|
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
|
|
fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed;
|
|
ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
|
};
|
|
timer.Start();
|
|
}
|
|
|
|
/// <summary>선택된 디자인 무드 키 (HtmlSkill에서 사용).</summary>
|
|
private string _selectedMood = null!; // Loaded 이벤트에서 초기화
|
|
private string _selectedLanguage = "auto"; // Code 탭 개발 언어
|
|
private string _folderDataUsage = null!; // Loaded 이벤트에서 초기화
|
|
|
|
/// <summary>하단 바를 구성합니다 (Cowork 작업 제어 중심).</summary>
|
|
private void BuildBottomBar()
|
|
{
|
|
MoodIconPanel.Children.Clear();
|
|
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
/// <summary>Code 탭 하단 바: 로컬 / 브랜치 / 워크트리 흐름 중심.</summary>
|
|
private void BuildCodeBottomBar()
|
|
{
|
|
MoodIconPanel.Children.Clear();
|
|
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
private Border CreateWorkspaceFolderBarButton()
|
|
{
|
|
var currentFolder = GetCurrentWorkFolder();
|
|
var label = string.IsNullOrWhiteSpace(currentFolder)
|
|
? "워크스페이스"
|
|
: TruncateForStatus(Path.GetFileName(currentFolder.TrimEnd('\\', '/')), 18);
|
|
var tooltip = string.IsNullOrWhiteSpace(currentFolder)
|
|
? "워크스페이스 선택"
|
|
: $"워크스페이스 선택\n현재: {currentFolder}";
|
|
return CreateFolderBarButton("\uE8B7", label, tooltip, "#4B5EFC");
|
|
}
|
|
|
|
private string GetWorktreeModeLabel()
|
|
{
|
|
var folder = GetCurrentWorkFolder();
|
|
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
|
return "로컬";
|
|
|
|
var root = WorktreeStateStore.ResolveRoot(folder);
|
|
var active = WorktreeStateStore.Load(root).Active;
|
|
return string.Equals(Path.GetFullPath(active), Path.GetFullPath(root), StringComparison.OrdinalIgnoreCase)
|
|
? "로컬"
|
|
: "워크트리";
|
|
}
|
|
|
|
private List<string> GetAvailableWorkspaceVariants(string root, string? active)
|
|
{
|
|
var variants = new List<string>();
|
|
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
|
|
return variants;
|
|
|
|
try
|
|
{
|
|
var parent = Directory.GetParent(root)?.FullName ?? root;
|
|
var repoName = Path.GetFileName(root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
variants.AddRange(Directory.GetDirectories(parent, $"{repoName}-wt-*"));
|
|
variants.AddRange(Directory.GetDirectories(parent, $"{repoName}-copy-*"));
|
|
}
|
|
catch
|
|
{
|
|
// ignore discovery failures
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(active) && Directory.Exists(active))
|
|
variants.Add(active);
|
|
|
|
return variants
|
|
.Where(path => !string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
|
|
.Where(path => !string.Equals(Path.GetFullPath(path), Path.GetFullPath(root), StringComparison.OrdinalIgnoreCase))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(path => string.Equals(Path.GetFullPath(path), Path.GetFullPath(active ?? ""), StringComparison.OrdinalIgnoreCase))
|
|
.ThenByDescending(path => Directory.GetLastWriteTime(path))
|
|
.Take(8)
|
|
.ToList();
|
|
}
|
|
|
|
private void SwitchToWorkspace(string targetPath, string rootPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(targetPath) || !Directory.Exists(targetPath))
|
|
return;
|
|
|
|
if (!string.IsNullOrWhiteSpace(rootPath))
|
|
{
|
|
var state = WorktreeStateStore.Load(rootPath);
|
|
state.Active = targetPath;
|
|
WorktreeStateStore.Save(rootPath, state);
|
|
}
|
|
|
|
SetWorkFolder(targetPath);
|
|
ShowToast(string.Equals(targetPath, rootPath, StringComparison.OrdinalIgnoreCase) ? "로컬 워크스페이스로 전환했습니다." : "워크트리로 전환했습니다.");
|
|
}
|
|
|
|
private async Task CreateCurrentBranchWorktreeAsync()
|
|
{
|
|
var currentFolder = GetCurrentWorkFolder();
|
|
if (string.IsNullOrWhiteSpace(currentFolder) || !Directory.Exists(currentFolder))
|
|
return;
|
|
|
|
var root = WorktreeStateStore.ResolveRoot(currentFolder);
|
|
var gitRoot = ResolveGitRoot(root);
|
|
if (!string.IsNullOrWhiteSpace(gitRoot))
|
|
{
|
|
await CreateGitWorktreeAsync(gitRoot);
|
|
return;
|
|
}
|
|
|
|
var copied = CreateWorkspaceCopy(root);
|
|
SwitchToWorkspace(copied, root);
|
|
}
|
|
|
|
private async Task CreateGitWorktreeAsync(string gitRoot)
|
|
{
|
|
var gitPath = FindGitExecutablePath();
|
|
if (string.IsNullOrWhiteSpace(gitPath))
|
|
return;
|
|
|
|
var branchResult = await RunGitAsync(gitPath, gitRoot, new[] { "rev-parse", "--abbrev-ref", "HEAD" }, CancellationToken.None);
|
|
var branchName = branchResult.ExitCode == 0 ? branchResult.StdOut.Trim() : "worktree";
|
|
if (string.IsNullOrWhiteSpace(branchName))
|
|
branchName = "worktree";
|
|
|
|
var safeBranch = string.Concat(branchName.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')).Trim('-');
|
|
if (string.IsNullOrWhiteSpace(safeBranch))
|
|
safeBranch = "worktree";
|
|
|
|
var parent = Directory.GetParent(gitRoot)?.FullName ?? gitRoot;
|
|
var repoName = Path.GetFileName(gitRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
var suffix = DateTime.Now.ToString("MMddHHmm");
|
|
var worktreePath = Path.Combine(parent, $"{repoName}-wt-{safeBranch}-{suffix}");
|
|
var worktreeBranch = $"ax/{safeBranch}-{suffix}";
|
|
|
|
var addResult = await RunGitAsync(gitPath, gitRoot, new[] { "worktree", "add", "-b", worktreeBranch, worktreePath, branchName }, CancellationToken.None);
|
|
if (addResult.ExitCode != 0)
|
|
{
|
|
CustomMessageBox.Show($"워크트리 생성에 실패했습니다.\n{addResult.StdErr.Trim()}", "워크트리", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
SwitchToWorkspace(worktreePath, gitRoot);
|
|
await RefreshGitBranchStatusAsync();
|
|
}
|
|
|
|
private string CreateWorkspaceCopy(string root)
|
|
{
|
|
var parent = Directory.GetParent(root)?.FullName ?? root;
|
|
var repoName = Path.GetFileName(root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
var copyPath = Path.Combine(parent, $"{repoName}-copy-{DateTime.Now:MMddHHmm}");
|
|
CopyDirectoryRecursive(root, copyPath, skipGitMetadata: true);
|
|
return copyPath;
|
|
}
|
|
|
|
private static void CopyDirectoryRecursive(string source, string destination, bool skipGitMetadata)
|
|
{
|
|
Directory.CreateDirectory(destination);
|
|
|
|
foreach (var file in Directory.GetFiles(source))
|
|
{
|
|
var name = Path.GetFileName(file);
|
|
if (skipGitMetadata && string.Equals(name, ".git", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
File.Copy(file, Path.Combine(destination, name), overwrite: true);
|
|
}
|
|
|
|
foreach (var directory in Directory.GetDirectories(source))
|
|
{
|
|
var name = Path.GetFileName(directory);
|
|
if (skipGitMetadata && string.Equals(name, ".git", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
CopyDirectoryRecursive(directory, Path.Combine(destination, name), skipGitMetadata);
|
|
}
|
|
}
|
|
|
|
/// <summary>하단 바에 실행 이력 상세도 선택 버튼을 추가합니다.</summary>
|
|
private void AppendLogLevelButton()
|
|
{
|
|
// 구분선
|
|
MoodIconPanel.Children.Add(new Border
|
|
{
|
|
Width = 1, Height = 18,
|
|
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
|
Margin = new Thickness(4, 0, 4, 0),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
|
|
var currentLevel = _settings.Settings.Llm.AgentLogLevel ?? "detailed";
|
|
var levelLabel = currentLevel switch
|
|
{
|
|
"debug" => "디버그",
|
|
"detailed" => "상세",
|
|
"hidden" => "숨김",
|
|
_ => "간략",
|
|
};
|
|
var logBtn = CreateFolderBarButton("\uE946", levelLabel, "실행 이력 상세도", "#059669");
|
|
logBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowLogLevelMenu(); };
|
|
try { RegisterName("BtnLogLevelMenu", logBtn); } catch { try { UnregisterName("BtnLogLevelMenu"); RegisterName("BtnLogLevelMenu", logBtn); } catch { } }
|
|
MoodIconPanel.Children.Add(logBtn);
|
|
}
|
|
|
|
/// <summary>실행 이력 상세도 팝업 메뉴를 표시합니다.</summary>
|
|
private void ShowLogLevelMenu()
|
|
{
|
|
FormatMenuItems.Children.Clear();
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
|
|
var levels = new (string Key, string Label, string Desc)[]
|
|
{
|
|
("hidden", "Hidden (숨김)", "실행 로그를 표시하지 않음"),
|
|
("simple", "Simple (간략)", "도구 결과만 한 줄로 표시"),
|
|
("detailed", "Detailed (상세)", "도구 호출/결과 + 접이식 상세"),
|
|
("debug", "Debug (디버그)", "모든 정보 + 파라미터 표시"),
|
|
};
|
|
|
|
var current = _settings.Settings.Llm.AgentLogLevel ?? "detailed";
|
|
|
|
foreach (var (key, label, desc) in levels)
|
|
{
|
|
var isActive = current == key;
|
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
sp.Children.Add(CreateCheckIcon(isActive, accentBrush));
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = label,
|
|
FontSize = 13,
|
|
Foreground = isActive ? accentBrush : primaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 8, 0),
|
|
});
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = desc,
|
|
FontSize = 10,
|
|
Foreground = secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
|
|
var item = new Border
|
|
{
|
|
Child = sp,
|
|
Padding = new Thickness(12, 8, 12, 8),
|
|
CornerRadius = new CornerRadius(6),
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
};
|
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
item.MouseEnter += (s, _) => ((Border)s!).Background = hoverBg;
|
|
item.MouseLeave += (s, _) => ((Border)s!).Background = Brushes.Transparent;
|
|
item.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
_settings.Settings.Llm.AgentLogLevel = key;
|
|
_settings.Save();
|
|
FormatMenuPopup.IsOpen = false;
|
|
if (_activeTab == "Cowork") BuildBottomBar();
|
|
else if (_activeTab == "Code") BuildCodeBottomBar();
|
|
};
|
|
FormatMenuItems.Children.Add(item);
|
|
}
|
|
|
|
try
|
|
{
|
|
var target = FindName("BtnLogLevelMenu") as UIElement;
|
|
if (target != null) FormatMenuPopup.PlacementTarget = target;
|
|
}
|
|
catch { }
|
|
FormatMenuPopup.IsOpen = true;
|
|
}
|
|
|
|
private void ShowLanguageMenu()
|
|
{
|
|
FormatMenuItems.Children.Clear();
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
|
|
var languages = new (string Key, string Label, string Icon)[]
|
|
{
|
|
("auto", "자동 감지", "🔧"),
|
|
("python", "Python", "🐍"),
|
|
("java", "Java", "☕"),
|
|
("csharp", "C# (.NET)", "🔷"),
|
|
("cpp", "C/C++", "⚙"),
|
|
("javascript", "JavaScript / Vue", "🌐"),
|
|
};
|
|
|
|
foreach (var (key, label, icon) in languages)
|
|
{
|
|
var isActive = _selectedLanguage == key;
|
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
sp.Children.Add(CreateCheckIcon(isActive, accentBrush));
|
|
sp.Children.Add(new TextBlock { Text = icon, FontSize = 13, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) });
|
|
sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = isActive ? accentBrush : primaryText, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal });
|
|
|
|
var itemBorder = new Border
|
|
{
|
|
Child = sp, Background = Brushes.Transparent,
|
|
CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand,
|
|
Padding = new Thickness(8, 7, 12, 7),
|
|
};
|
|
ApplyMenuItemHover(itemBorder);
|
|
|
|
var capturedKey = key;
|
|
itemBorder.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
FormatMenuPopup.IsOpen = false;
|
|
_selectedLanguage = capturedKey;
|
|
BuildCodeBottomBar();
|
|
};
|
|
FormatMenuItems.Children.Add(itemBorder);
|
|
}
|
|
|
|
if (FindName("BtnLangMenu") is UIElement langTarget)
|
|
FormatMenuPopup.PlacementTarget = langTarget;
|
|
FormatMenuPopup.IsOpen = true;
|
|
}
|
|
|
|
/// <summary>폴더바 내 드롭다운 버튼 (소극/적극 스타일과 동일)</summary>
|
|
private Border CreateFolderBarButton(string? mdlIcon, string label, string tooltip, string? iconColorHex = null)
|
|
{
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var borderColor = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
|
|
var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
|
var iconColor = iconColorHex != null ? BrushFromHex(iconColorHex) : secondaryText;
|
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
|
|
if (mdlIcon != null)
|
|
{
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = mdlIcon,
|
|
FontFamily = s_segoeIconFont,
|
|
FontSize = 12,
|
|
Foreground = iconColor,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 4, 0),
|
|
});
|
|
}
|
|
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = label,
|
|
FontSize = 12,
|
|
Foreground = secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
|
|
var chip = new Border
|
|
{
|
|
Child = sp,
|
|
Background = Brushes.Transparent,
|
|
BorderBrush = borderColor,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(999),
|
|
Padding = new Thickness(10, 5, 10, 5),
|
|
Margin = new Thickness(0, 0, 4, 0),
|
|
Cursor = Cursors.Hand,
|
|
ToolTip = tooltip,
|
|
};
|
|
chip.MouseEnter += (_, _) => chip.Background = hoverBackground;
|
|
chip.MouseLeave += (_, _) => chip.Background = Brushes.Transparent;
|
|
return chip;
|
|
}
|
|
|
|
|
|
private static string GetFormatLabel(string key) => key switch
|
|
{
|
|
"xlsx" => "Excel",
|
|
"html" => "HTML 보고서",
|
|
"docx" => "Word",
|
|
"md" => "Markdown",
|
|
"csv" => "CSV",
|
|
_ => "AI 자동",
|
|
};
|
|
|
|
/// <summary>현재 프리셋/카테고리에 맞는 에이전트 이름, 심볼, 색상을 반환합니다.</summary>
|
|
private (string Name, string Symbol, string Color) GetAgentIdentity()
|
|
{
|
|
string? category = null;
|
|
lock (_convLock)
|
|
{
|
|
category = _currentConversation?.Category;
|
|
}
|
|
|
|
return category switch
|
|
{
|
|
// Cowork 프리셋 카테고리
|
|
"보고서" => ("보고서 에이전트", "◆", "#3B82F6"),
|
|
"데이터" => ("데이터 분석 에이전트", "◆", "#10B981"),
|
|
"문서" => ("문서 작성 에이전트", "◆", "#6366F1"),
|
|
"논문" => ("논문 분석 에이전트", "◆", "#6366F1"),
|
|
"파일" => ("파일 관리 에이전트", "◆", "#8B5CF6"),
|
|
"자동화" => ("자동화 에이전트", "◆", "#EF4444"),
|
|
// Code 프리셋 카테고리
|
|
"코드개발" => ("코드 개발 에이전트", "◆", "#3B82F6"),
|
|
"리팩터링" => ("리팩터링 에이전트", "◆", "#6366F1"),
|
|
"코드리뷰" => ("코드 리뷰 에이전트", "◆", "#10B981"),
|
|
"보안점검" => ("보안 점검 에이전트", "◆", "#EF4444"),
|
|
"테스트" => ("테스트 에이전트", "◆", "#F59E0B"),
|
|
// Chat 카테고리
|
|
"연구개발" => ("연구개발 에이전트", "◆", "#0EA5E9"),
|
|
"시스템" => ("시스템 에이전트", "◆", "#64748B"),
|
|
"수율분석" => ("수율분석 에이전트", "◆", "#F59E0B"),
|
|
"제품분석" => ("제품분석 에이전트", "◆", "#EC4899"),
|
|
"경영" => ("경영 분석 에이전트", "◆", "#8B5CF6"),
|
|
"인사" => ("인사 관리 에이전트", "◆", "#14B8A6"),
|
|
"제조기술" => ("제조기술 에이전트", "◆", "#F97316"),
|
|
"재무" => ("재무 분석 에이전트", "◆", "#6366F1"),
|
|
_ when _activeTab == "Code" => ("코드 에이전트", "◆", "#3B82F6"),
|
|
_ when _activeTab == "Cowork" => ("코워크 에이전트", "◆", "#4B5EFC"),
|
|
_ => ("AX 에이전트", "◆", "#4B5EFC"),
|
|
};
|
|
}
|
|
|
|
/// <summary>포맷 선택 팝업 메뉴를 표시합니다.</summary>
|
|
private void ShowFormatMenu()
|
|
{
|
|
FormatMenuItems.Children.Clear();
|
|
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
var currentFormat = _settings.Settings.Llm.DefaultOutputFormat ?? "auto";
|
|
|
|
var formats = new (string Key, string Label, string Icon, string Color)[]
|
|
{
|
|
("auto", "AI 자동 선택", "\uE8BD", "#8B5CF6"),
|
|
("xlsx", "Excel", "\uE9F9", "#217346"),
|
|
("html", "HTML 보고서", "\uE12B", "#E44D26"),
|
|
("docx", "Word", "\uE8A5", "#2B579A"),
|
|
("md", "Markdown", "\uE943", "#6B7280"),
|
|
("csv", "CSV", "\uE9D9", "#10B981"),
|
|
};
|
|
|
|
foreach (var (key, label, icon, color) in formats)
|
|
{
|
|
var isActive = key == currentFormat;
|
|
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
|
|
// 커스텀 체크 아이콘
|
|
sp.Children.Add(CreateCheckIcon(isActive, accentBrush));
|
|
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = icon,
|
|
FontFamily = s_segoeIconFont,
|
|
FontSize = 13,
|
|
Foreground = BrushFromHex(color),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 8, 0),
|
|
});
|
|
|
|
sp.Children.Add(new TextBlock
|
|
{
|
|
Text = label, FontSize = 13,
|
|
Foreground = primaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
|
|
var itemBorder = new Border
|
|
{
|
|
Child = sp,
|
|
Background = Brushes.Transparent,
|
|
CornerRadius = new CornerRadius(8),
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(8, 7, 12, 7),
|
|
};
|
|
ApplyMenuItemHover(itemBorder);
|
|
|
|
var capturedKey = key;
|
|
itemBorder.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
FormatMenuPopup.IsOpen = false;
|
|
_settings.Settings.Llm.DefaultOutputFormat = capturedKey;
|
|
_settings.Save();
|
|
RefreshOverlaySettingsPanel();
|
|
BuildBottomBar();
|
|
};
|
|
|
|
FormatMenuItems.Children.Add(itemBorder);
|
|
}
|
|
|
|
// PlacementTarget을 동적 등록된 버튼으로 설정
|
|
if (FormatMenuPopup.PlacementTarget == null && FindName("BtnFormatMenu") is UIElement formatTarget)
|
|
FormatMenuPopup.PlacementTarget = formatTarget;
|
|
FormatMenuPopup.IsOpen = true;
|
|
}
|
|
|
|
private void BtnOverlayDefaultOutputFormat_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is UIElement element)
|
|
FormatMenuPopup.PlacementTarget = element;
|
|
ShowFormatMenu();
|
|
}
|
|
|
|
/// <summary>디자인 무드 선택 팝업 메뉴를 표시합니다.</summary>
|
|
private void ShowMoodMenu()
|
|
{
|
|
MoodMenuItems.Children.Clear();
|
|
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
|
|
// 2열 갤러리 그리드
|
|
var grid = new System.Windows.Controls.Primitives.UniformGrid { Columns = 2 };
|
|
|
|
foreach (var mood in TemplateService.AllMoods)
|
|
{
|
|
var isActive = _selectedMood == mood.Key;
|
|
var isCustom = _settings.Settings.Llm.CustomMoods.Any(cm => cm.Key == mood.Key);
|
|
var colors = TemplateService.GetMoodColors(mood.Key);
|
|
|
|
// 미니 프리뷰 카드
|
|
var previewCard = new Border
|
|
{
|
|
Width = 160, Height = 80,
|
|
CornerRadius = new CornerRadius(6),
|
|
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Background)),
|
|
BorderBrush = isActive ? accentBrush : new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Border)),
|
|
BorderThickness = new Thickness(isActive ? 2 : 1),
|
|
Padding = new Thickness(8, 6, 8, 6),
|
|
Margin = new Thickness(2),
|
|
};
|
|
|
|
var previewContent = new StackPanel();
|
|
// 헤딩 라인
|
|
previewContent.Children.Add(new Border
|
|
{
|
|
Width = 60, Height = 6, CornerRadius = new CornerRadius(2),
|
|
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.PrimaryText)),
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
Margin = new Thickness(0, 0, 0, 4),
|
|
});
|
|
// 악센트 라인
|
|
previewContent.Children.Add(new Border
|
|
{
|
|
Width = 40, Height = 3, CornerRadius = new CornerRadius(1),
|
|
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Accent)),
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
});
|
|
// 텍스트 라인들
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
previewContent.Children.Add(new Border
|
|
{
|
|
Width = 120 - i * 20, Height = 3, CornerRadius = new CornerRadius(1),
|
|
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.SecondaryText)) { Opacity = 0.5 },
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
Margin = new Thickness(0, 0, 0, 3),
|
|
});
|
|
}
|
|
// 미니 카드 영역
|
|
var cardRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 0) };
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
cardRow.Children.Add(new Border
|
|
{
|
|
Width = 28, Height = 14, CornerRadius = new CornerRadius(2),
|
|
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.CardBg)),
|
|
BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(colors.Border)),
|
|
BorderThickness = new Thickness(0.5),
|
|
Margin = new Thickness(0, 0, 4, 0),
|
|
});
|
|
}
|
|
previewContent.Children.Add(cardRow);
|
|
previewCard.Child = previewContent;
|
|
|
|
// 무드 라벨
|
|
var labelPanel = new StackPanel { Margin = new Thickness(4, 2, 4, 4) };
|
|
var labelRow = new StackPanel { Orientation = Orientation.Horizontal };
|
|
labelRow.Children.Add(new TextBlock
|
|
{
|
|
Text = mood.Icon, FontSize = 12,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 4, 0),
|
|
});
|
|
labelRow.Children.Add(new TextBlock
|
|
{
|
|
Text = mood.Label, FontSize = 11.5,
|
|
Foreground = primaryText,
|
|
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
if (isActive)
|
|
{
|
|
labelRow.Children.Add(new TextBlock
|
|
{
|
|
Text = " ✓", FontSize = 11,
|
|
Foreground = accentBrush,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
}
|
|
labelPanel.Children.Add(labelRow);
|
|
|
|
// 전체 카드 래퍼
|
|
var cardWrapper = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(8),
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(4),
|
|
Margin = new Thickness(2),
|
|
};
|
|
var wrapperContent = new StackPanel();
|
|
wrapperContent.Children.Add(previewCard);
|
|
wrapperContent.Children.Add(labelPanel);
|
|
cardWrapper.Child = wrapperContent;
|
|
|
|
// 호버
|
|
cardWrapper.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); };
|
|
cardWrapper.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
|
|
|
var capturedMood = mood;
|
|
cardWrapper.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
MoodMenuPopup.IsOpen = false;
|
|
_selectedMood = capturedMood.Key;
|
|
_settings.Settings.Llm.DefaultMood = capturedMood.Key;
|
|
_settings.Save();
|
|
SaveConversationSettings();
|
|
RefreshOverlaySettingsPanel();
|
|
BuildBottomBar();
|
|
};
|
|
|
|
// 커스텀 무드: 우클릭
|
|
if (isCustom)
|
|
{
|
|
cardWrapper.MouseRightButtonUp += (s, e) =>
|
|
{
|
|
e.Handled = true;
|
|
MoodMenuPopup.IsOpen = false;
|
|
ShowCustomMoodContextMenu(s as Border, capturedMood.Key);
|
|
};
|
|
}
|
|
|
|
grid.Children.Add(cardWrapper);
|
|
}
|
|
|
|
MoodMenuItems.Children.Add(grid);
|
|
|
|
// ── 구분선 + 추가 버튼 ──
|
|
MoodMenuItems.Children.Add(new System.Windows.Shapes.Rectangle
|
|
{
|
|
Height = 1,
|
|
Fill = borderBrush,
|
|
Margin = new Thickness(8, 4, 8, 4),
|
|
Opacity = 0.4,
|
|
});
|
|
|
|
var addSp = new StackPanel { Orientation = Orientation.Horizontal };
|
|
addSp.Children.Add(new TextBlock
|
|
{
|
|
Text = "\uE710",
|
|
FontFamily = s_segoeIconFont,
|
|
FontSize = 13,
|
|
Foreground = secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(4, 0, 8, 0),
|
|
});
|
|
addSp.Children.Add(new TextBlock
|
|
{
|
|
Text = "커스텀 무드 추가",
|
|
FontSize = 13,
|
|
Foreground = secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
var addBorder = new Border
|
|
{
|
|
Child = addSp,
|
|
Background = Brushes.Transparent,
|
|
CornerRadius = new CornerRadius(8),
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(8, 6, 12, 6),
|
|
};
|
|
ApplyMenuItemHover(addBorder);
|
|
addBorder.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
MoodMenuPopup.IsOpen = false;
|
|
ShowCustomMoodDialog();
|
|
};
|
|
MoodMenuItems.Children.Add(addBorder);
|
|
|
|
if (MoodMenuPopup.PlacementTarget == null && FindName("BtnMoodMenu") is UIElement moodTarget)
|
|
MoodMenuPopup.PlacementTarget = moodTarget;
|
|
MoodMenuPopup.IsOpen = true;
|
|
}
|
|
|
|
private void BtnOverlayDefaultMood_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is UIElement element)
|
|
MoodMenuPopup.PlacementTarget = element;
|
|
ShowMoodMenu();
|
|
}
|
|
|
|
/// <summary>커스텀 무드 추가/편집 다이얼로그를 표시합니다.</summary>
|
|
private void ShowCustomMoodDialog(Models.CustomMoodEntry? existing = null)
|
|
{
|
|
bool isEdit = existing != null;
|
|
var dlg = new CustomMoodDialog(
|
|
existingKey: existing?.Key ?? "",
|
|
existingLabel: existing?.Label ?? "",
|
|
existingIcon: existing?.Icon ?? "🎯",
|
|
existingDesc: existing?.Description ?? "",
|
|
existingCss: existing?.Css ?? "")
|
|
{
|
|
Owner = this,
|
|
};
|
|
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
if (isEdit)
|
|
{
|
|
existing!.Label = dlg.MoodLabel;
|
|
existing.Icon = dlg.MoodIcon;
|
|
existing.Description = dlg.MoodDescription;
|
|
existing.Css = dlg.MoodCss;
|
|
}
|
|
else
|
|
{
|
|
_settings.Settings.Llm.CustomMoods.Add(new Models.CustomMoodEntry
|
|
{
|
|
Key = dlg.MoodKey,
|
|
Label = dlg.MoodLabel,
|
|
Icon = dlg.MoodIcon,
|
|
Description = dlg.MoodDescription,
|
|
Css = dlg.MoodCss,
|
|
});
|
|
}
|
|
_settings.Save();
|
|
TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods);
|
|
BuildBottomBar();
|
|
}
|
|
}
|
|
|
|
/// <summary>커스텀 무드 우클릭 컨텍스트 메뉴.</summary>
|
|
private void ShowCustomMoodContextMenu(Border? anchor, string moodKey)
|
|
{
|
|
if (anchor == null) return;
|
|
|
|
var popup = new System.Windows.Controls.Primitives.Popup
|
|
{
|
|
PlacementTarget = anchor,
|
|
Placement = System.Windows.Controls.Primitives.PlacementMode.Right,
|
|
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.CustomMoods.FirstOrDefault(c => c.Key == moodKey);
|
|
if (entry != null) ShowCustomMoodDialog(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(
|
|
$"이 디자인 무드를 삭제하시겠습니까?",
|
|
"무드 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
if (result == MessageBoxResult.Yes)
|
|
{
|
|
_settings.Settings.Llm.CustomMoods.RemoveAll(c => c.Key == moodKey);
|
|
if (_selectedMood == moodKey) _selectedMood = "modern";
|
|
_settings.Save();
|
|
TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods);
|
|
BuildBottomBar();
|
|
}
|
|
};
|
|
stack.Children.Add(deleteItem);
|
|
|
|
menuBorder.Child = stack;
|
|
popup.Child = menuBorder;
|
|
popup.IsOpen = true;
|
|
}
|
|
|
|
|
|
private string? _promptCardPlaceholder;
|
|
|
|
private void ShowPlaceholder()
|
|
{
|
|
RefreshInputWatermarkText();
|
|
InputWatermark.Visibility = Visibility.Visible;
|
|
InputBox.Text = "";
|
|
InputBox.Focus();
|
|
}
|
|
|
|
private void UpdateWatermarkVisibility()
|
|
{
|
|
// 슬래시 칩이 활성화되어 있으면 워터마크 숨기기 (겹침 방지)
|
|
if (_slashPalette.ActiveCommand != null)
|
|
{
|
|
InputWatermark.Visibility = Visibility.Collapsed;
|
|
return;
|
|
}
|
|
|
|
RefreshInputWatermarkText();
|
|
|
|
if (string.IsNullOrEmpty(InputBox.Text))
|
|
InputWatermark.Visibility = Visibility.Visible;
|
|
else
|
|
InputWatermark.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
private void ClearPromptCardPlaceholder()
|
|
{
|
|
_promptCardPlaceholder = null;
|
|
RefreshInputWatermarkText();
|
|
UpdateWatermarkVisibility();
|
|
}
|
|
|
|
private void BtnSettings_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
OpenAgentSettingsWindow();
|
|
}
|
|
}
|