11208 lines
469 KiB
Plaintext
11208 lines
469 KiB
Plaintext
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 AxCopilot.Models;
|
||
using AxCopilot.Services;
|
||
using AxCopilot.Services.Agent;
|
||
|
||
namespace AxCopilot.Views;
|
||
|
||
/// <summary>AX Agent 李? Claude Desktop ?ㅽ??????ъ씠?쒕컮 + 移댄뀒怨좊━ 遺꾨쪟 + ??꾨씪??</summary>
|
||
public partial class ChatWindow : Window
|
||
{
|
||
private readonly SettingsService _settings;
|
||
private readonly ChatStorageService _storage;
|
||
private readonly LlmService _llm;
|
||
private readonly ToolRegistry _toolRegistry;
|
||
private readonly AgentLoopService _agentLoop;
|
||
private readonly ModelRouterService _router;
|
||
private readonly ChatSessionStateService _sessionState;
|
||
private readonly AppStateService _appState;
|
||
private readonly object _convLock = new();
|
||
private ChatConversation? _currentConversation
|
||
{
|
||
get => _sessionState.CurrentConversation;
|
||
set => _sessionState.CurrentConversation = value;
|
||
}
|
||
private CancellationTokenSource? _streamCts;
|
||
private bool _isStreaming
|
||
{
|
||
get => _sessionState.IsStreaming;
|
||
set => _sessionState.IsStreaming = value;
|
||
}
|
||
private bool _sidebarVisible = true;
|
||
private string _selectedCategory = ""; // "" = ?꾩껜
|
||
private DraftQueueItem? _activeQueuedDraftItem;
|
||
private Popup? _taskSummaryPopup;
|
||
private string _taskSummaryFilter = "all";
|
||
private UIElement? _taskSummaryTarget;
|
||
private bool _forceClose = false; // ??醫낅즺 ??吏꾩쭨 ?リ린 ?뚮옒洹?
|
||
// ?ㅽ듃由щ컢 UI ??而ㅼ꽌 源쒕묀??+ 濡쒕뵫 ?꾩씠肄? private readonly DispatcherTimer _cursorTimer;
|
||
private bool _cursorVisible = true;
|
||
private TextBlock? _activeStreamText;
|
||
private string _cachedStreamContent = ""; // sb.ToString() 罹먯떆 ??以묐났 ?몄텧 諛⑹?
|
||
private TextBlock? _activeAiIcon; // 濡쒕뵫 ?꾩뒪 以묒씤 AI ?꾩씠肄? private bool _aiIconPulseStopped; // ?꾩뒪 1?뚮쭔 以묒?
|
||
private WorkflowAnalyzerWindow? _analyzerWindow; // ?뚰겕?뚮줈??遺꾩꽍湲? private PlanViewerWindow? _planViewerWindow; // ?ㅽ뻾 怨꾪쉷 酉곗뼱
|
||
private bool _userScrolled; // ?ъ슜?먭? ?꾨줈 ?ㅽ겕濡ㅽ뻽?붿?
|
||
|
||
// 寃쎄낵 ?쒓컙 ?쒖떆
|
||
private readonly DispatcherTimer _elapsedTimer;
|
||
private DateTime _streamStartTime;
|
||
private TextBlock? _elapsedLabel;
|
||
|
||
// ??댄븨 ?④낵
|
||
private readonly DispatcherTimer _typingTimer;
|
||
private int _displayedLength; // ?꾩옱 ?붾㈃???쒖떆??湲????
|
||
private sealed class ConversationMeta
|
||
{
|
||
public string Id { get; init; } = "";
|
||
public string Title { get; init; } = "";
|
||
public string UpdatedAtText { get; init; } = "";
|
||
public bool Pinned { get; init; }
|
||
public string Category { get; init; } = ChatCategory.General;
|
||
public string Symbol { get; init; } = "\uE8BD";
|
||
public string ColorHex { get; init; } = "#6B7280";
|
||
public string Tab { get; init; } = "Chat";
|
||
public DateTime UpdatedAt { get; init; }
|
||
/// <summary>泥??ъ슜??硫붿떆吏 ?붿빟 (寃?됱슜, 理쒕? 100??.</summary>
|
||
public string Preview { get; init; } = "";
|
||
/// <summary>遺꾧린 ?먮낯 ???ID. null?대㈃ ?먮낯 ???</summary>
|
||
public string? ParentId { get; init; }
|
||
}
|
||
|
||
public ChatWindow(SettingsService settings)
|
||
{
|
||
InitializeComponent();
|
||
_settings = settings;
|
||
var app = System.Windows.Application.Current as App;
|
||
_sessionState = app?.ChatSessionState ?? new ChatSessionStateService();
|
||
_appState = app?.AppState ?? new AppStateService();
|
||
_sessionState.Load(_settings);
|
||
_appState.AttachChatSession(_sessionState);
|
||
_appState.LoadFromSettings(_settings);
|
||
_storage = new ChatStorageService();
|
||
_llm = new LlmService(settings);
|
||
_router = new ModelRouterService(settings);
|
||
_toolRegistry = ToolRegistry.CreateDefault();
|
||
_appState.RefreshAgentCatalog(_toolRegistry);
|
||
_agentLoop = new AgentLoopService(_llm, _toolRegistry, settings)
|
||
{
|
||
Dispatcher = action => System.Windows.Application.Current.Dispatcher.Invoke(action),
|
||
AskPermissionCallback = async (toolName, filePath) =>
|
||
{
|
||
var result = MessageBoxResult.None;
|
||
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
|
||
{
|
||
result = CustomMessageBox.Show(
|
||
$"?꾧뎄 '{toolName}'??媛) ?ㅼ쓬 ?뚯씪???묎렐?섎젮 ?⑸땲??\n\n{filePath}\n\n?덉슜?섏떆寃좎뒿?덇퉴?",
|
||
"AX Agent ??沅뚰븳 ?뺤씤",
|
||
MessageBoxButton.YesNo,
|
||
MessageBoxImage.Question);
|
||
});
|
||
return result == MessageBoxResult.Yes;
|
||
},
|
||
UserAskCallback = async (question, options, defaultValue) =>
|
||
{
|
||
string? response = null;
|
||
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
|
||
{
|
||
response = UserAskDialog.Show(question, options, defaultValue);
|
||
});
|
||
return response;
|
||
},
|
||
};
|
||
|
||
// ?ㅼ젙?먯꽌 珥덇린媛?濡쒕뱶 (Loaded ?꾩뿉??null 諛⑹?)
|
||
_selectedMood = settings.Settings.Llm.DefaultMood ?? "modern";
|
||
_folderDataUsage = settings.Settings.Llm.FolderDataUsage ?? "active";
|
||
|
||
_cursorTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(530) };
|
||
_cursorTimer.Tick += CursorTimer_Tick;
|
||
|
||
_elapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||
_elapsedTimer.Tick += ElapsedTimer_Tick;
|
||
|
||
_typingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(12) };
|
||
_typingTimer.Tick += TypingTimer_Tick;
|
||
SubAgentTool.StatusChanged += OnSubAgentStatusChanged;
|
||
_appState.StateChanged += OnAppStateChanged;
|
||
|
||
KeyDown += ChatWindow_KeyDown;
|
||
Loaded += (_, _) =>
|
||
{
|
||
// ?? 利됱떆 ?꾩슂??UI 珥덇린?붾쭔 ?숆린 ?ㅽ뻾 ??
|
||
SetupUserInfo();
|
||
ApplySessionTabSelection();
|
||
ApplyRuntimeStateToUi();
|
||
_selectedMood = _settings.Settings.Llm.DefaultMood ?? "modern";
|
||
_folderDataUsage = _settings.Settings.Llm.FolderDataUsage ?? "active";
|
||
UpdateAnalyzerButtonVisibility();
|
||
UpdateModelLabel();
|
||
InputBox.Focus();
|
||
MessageScroll.ScrollChanged += MessageScroll_ScrollChanged;
|
||
|
||
// ?? 臾닿굅???묒뾽? ?좏쑕 ?쒖젏??鍮꾨룞湲??ㅽ뻾 ??
|
||
Dispatcher.BeginInvoke(() =>
|
||
{
|
||
TemplateService.LoadCustomMoods(_settings.Settings.Llm.CustomMoods);
|
||
BuildTopicButtons();
|
||
RestoreLastConversations();
|
||
RefreshConversationList();
|
||
|
||
// ?곗씠???뺣━ (?붿뒪??I/O)
|
||
_ = Task.Run(() =>
|
||
{
|
||
var retention = _settings.Settings.Llm.RetentionDays;
|
||
if (retention > 0) _storage.PurgeExpired(retention);
|
||
_storage.PurgeForDiskSpace();
|
||
});
|
||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||
|
||
// ?낅젰 諛??ъ빱??湲濡쒖슦 ?④낵
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
InputBox.GotFocus += (_, _) => InputBorder.BorderBrush = accentBrush;
|
||
InputBox.LostFocus += (_, _) => InputBorder.BorderBrush = borderBrush;
|
||
|
||
// ?쒕옒洹????쒕∼ ?뚯씪 泥⑤? + AI ?≪뀡 ?앹뾽
|
||
InputBorder.AllowDrop = true;
|
||
InputBorder.DragOver += (_, de) =>
|
||
{
|
||
de.Effects = de.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
|
||
de.Handled = true;
|
||
};
|
||
InputBorder.Drop += (_, de) =>
|
||
{
|
||
if (de.Data.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0)
|
||
{
|
||
var enableAi = _settings.Settings.Llm.EnableDragDropAiActions;
|
||
if (enableAi && files.Length <= 5)
|
||
ShowDropActionMenu(files);
|
||
else
|
||
foreach (var f in files) AddAttachedFile(f);
|
||
}
|
||
};
|
||
|
||
// ?ㅽ궗 ?쒖뒪??珥덇린?? if (_settings.Settings.Llm.EnableSkillSystem)
|
||
{
|
||
SkillService.EnsureSkillFolder();
|
||
SkillService.LoadSkills(_settings.Settings.Llm.SkillsFolderPath);
|
||
_appState.RefreshSkillCatalog(SkillService.Skills, _settings.Settings.Llm.SkillsFolderPath, true);
|
||
}
|
||
else
|
||
{
|
||
_appState.RefreshSkillCatalog([], _settings.Settings.Llm.SkillsFolderPath, false);
|
||
}
|
||
|
||
_ = InitializeDynamicAgentSourcesAsync();
|
||
|
||
// ?щ옒???앹뾽 ?ㅻ퉬寃뚯씠??踰꾪듉
|
||
SlashNavUp.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
_slashPageOffset = Math.Max(0, _slashPageOffset - SlashPageSize);
|
||
RenderSlashPage();
|
||
};
|
||
SlashNavDown.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
_slashPageOffset = Math.Min(_slashAllMatches.Count - 1,
|
||
_slashPageOffset + SlashPageSize);
|
||
RenderSlashPage();
|
||
};
|
||
|
||
// ?щ옒??紐낅졊??移??リ린 (횞 踰꾪듉)
|
||
SlashChipClose.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
HideSlashChip(restoreText: true);
|
||
InputBox.Focus();
|
||
};
|
||
|
||
// InputBox?먯꽌 ?щ옒???앹뾽 ?대┛ ?곹깭濡?留덉슦???????앹뾽 ?ㅽ겕濡? InputBox.PreviewMouseWheel += (_, me) =>
|
||
{
|
||
if (!SlashPopup.IsOpen) return;
|
||
me.Handled = true;
|
||
SlashPopup_ScrollByDelta(me.Delta);
|
||
};
|
||
|
||
// ??UI 珥덇린 ?곹깭
|
||
UpdateFolderBar();
|
||
|
||
// ?몃쾭 ?좊땲硫붿씠?????낅┰ 怨듦컙???덈뒗 踰꾪듉?먮쭔 Scale ?곸슜
|
||
// (GhostBtn ?ㅽ???踰꾪듉? XAML?먯꽌 諛곌꼍??opacity ?몃쾭 泥섎━)
|
||
ApplyHoverBounceAnimation(BtnModelSelector);
|
||
ApplyHoverBounceAnimation(BtnTemplateSelector, -1.5);
|
||
ApplyHoverScaleAnimation(BtnSend, 1.12);
|
||
ApplyHoverScaleAnimation(BtnStop, 1.12);
|
||
};
|
||
Closed += (_, _) =>
|
||
{
|
||
SubAgentTool.StatusChanged -= OnSubAgentStatusChanged;
|
||
_appState.StateChanged -= OnAppStateChanged;
|
||
if (_taskSummaryPopup != null) _taskSummaryPopup.IsOpen = false;
|
||
_streamCts?.Cancel();
|
||
_cursorTimer.Stop();
|
||
_elapsedTimer.Stop();
|
||
_typingTimer.Stop();
|
||
_llm.Dispose();
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// X 踰꾪듉?쇰줈 ?レ쓣 ??李쎌쓣 ?④린湲곕쭔 ?⑸땲??(?ъ궗?⑹쑝濡??ㅼ쓬 踰?鍮좊Ⅴ寃??대┝).
|
||
/// ??醫낅즺 ?쒖뿉??ForceClose()瑜??ъ슜?⑸땲??
|
||
/// </summary>
|
||
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||
{
|
||
if (!_forceClose)
|
||
{
|
||
e.Cancel = true;
|
||
Hide();
|
||
return;
|
||
}
|
||
base.OnClosing(e);
|
||
}
|
||
|
||
/// <summary>??醫낅즺 ??李쎌쓣 ?ㅼ젣濡??レ뒿?덈떎.</summary>
|
||
public void ForceClose()
|
||
{
|
||
// ?꾩옱 ??????+ ??퀎 留덉?留????ID瑜??ㅼ젙???곸냽 ??? lock (_convLock)
|
||
{
|
||
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
||
{
|
||
_sessionState.RememberConversation(_activeTab, _currentConversation.Id);
|
||
try { _storage.Save(_currentConversation); } catch { }
|
||
}
|
||
}
|
||
SaveLastConversations();
|
||
|
||
_forceClose = true;
|
||
Close();
|
||
}
|
||
|
||
// ??? ?ъ슜???뺣낫 ????????????????????????????????????????????????????
|
||
|
||
private void SetupUserInfo()
|
||
{
|
||
var userName = Environment.UserName;
|
||
// AD\, AD/, AD: ?묐몢???쒓굅
|
||
var cleanName = userName;
|
||
foreach (var sep in new[] { '\\', '/', ':' })
|
||
{
|
||
var idx = cleanName.LastIndexOf(sep);
|
||
if (idx >= 0) cleanName = cleanName[(idx + 1)..];
|
||
}
|
||
|
||
var initial = cleanName.Length > 0 ? cleanName[..1].ToUpper() : "U";
|
||
var pcName = Environment.MachineName;
|
||
|
||
UserInitialSidebar.Text = initial;
|
||
UserInitialIconBar.Text = initial;
|
||
UserNameText.Text = cleanName;
|
||
UserPcText.Text = pcName;
|
||
BtnUserIconBar.ToolTip = $"{cleanName} ({pcName})";
|
||
}
|
||
|
||
// ??? ?ㅽ겕濡??숈옉 ??????????????????????????????????????????????????
|
||
|
||
private void MessageScroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
||
{
|
||
// ?ㅽ겕濡?媛???곸뿭???놁쑝硫?肄섑뀗痢좉? 吏㏃쓬) ??긽 諛붾떏
|
||
if (MessageScroll.ScrollableHeight <= 1)
|
||
{
|
||
_userScrolled = false;
|
||
return;
|
||
}
|
||
|
||
// 肄섑뀗痢??ш린 蹂寃?ExtentHeightChange > 0)???섑븳 ?ㅽ겕濡ㅼ? 臾댁떆 ???ъ슜??議곗옉留?媛먯?
|
||
if (Math.Abs(e.ExtentHeightChange) > 0.5)
|
||
return;
|
||
|
||
var atBottom = MessageScroll.VerticalOffset >= MessageScroll.ScrollableHeight - 40;
|
||
_userScrolled = !atBottom;
|
||
}
|
||
|
||
private void AutoScrollIfNeeded()
|
||
{
|
||
if (!_userScrolled)
|
||
SmoothScrollToEnd();
|
||
}
|
||
|
||
/// <summary>???묐떟 ?쒖옉 ??媛뺤젣濡??섎떒 ?ㅽ겕濡ㅽ빀?덈떎 (?ъ슜???ㅽ겕濡??곹깭 由ъ뀑).</summary>
|
||
private void ForceScrollToEnd()
|
||
{
|
||
_userScrolled = false;
|
||
Dispatcher.InvokeAsync(() => SmoothScrollToEnd(), DispatcherPriority.Background);
|
||
}
|
||
|
||
/// <summary>遺?쒕윭???먮룞 ?ㅽ겕濡????섎떒?쇰줈 遺?쒕읇寃??대룞?⑸땲??</summary>
|
||
private void SmoothScrollToEnd()
|
||
{
|
||
var targetOffset = MessageScroll.ScrollableHeight;
|
||
var currentOffset = MessageScroll.VerticalOffset;
|
||
var diff = targetOffset - currentOffset;
|
||
|
||
// 李⑥씠媛 ?묒쑝硫?利됱떆 ?대룞 (源쒕묀??諛⑹?)
|
||
if (diff <= 60)
|
||
{
|
||
MessageScroll.ScrollToEnd();
|
||
return;
|
||
}
|
||
|
||
// 遺?쒕읇寃??ㅽ겕濡?(DoubleAnimation)
|
||
var animation = new DoubleAnimation
|
||
{
|
||
From = currentOffset,
|
||
To = targetOffset,
|
||
Duration = TimeSpan.FromMilliseconds(200),
|
||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut },
|
||
};
|
||
animation.Completed += (_, _) => MessageScroll.ScrollToVerticalOffset(targetOffset);
|
||
|
||
// ScrollViewer??吏곸젒 ?좊땲硫붿씠?섏쓣 ?곸슜?????놁쑝誘濡???대㉧ 湲곕컲?쇰줈 蹂닿컙
|
||
var startTime = DateTime.UtcNow;
|
||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; // ~60fps
|
||
EventHandler tickHandler = null!;
|
||
tickHandler = (_, _) =>
|
||
{
|
||
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
||
var progress = Math.Min(elapsed / 200.0, 1.0);
|
||
var eased = 1.0 - Math.Pow(1.0 - progress, 3);
|
||
var offset = currentOffset + diff * eased;
|
||
MessageScroll.ScrollToVerticalOffset(offset);
|
||
|
||
if (progress >= 1.0)
|
||
{
|
||
timer.Stop();
|
||
timer.Tick -= tickHandler;
|
||
}
|
||
};
|
||
timer.Tick += tickHandler;
|
||
timer.Start();
|
||
}
|
||
|
||
// ??? ????쒕ぉ ?몃씪???몄쭛 ??????????????????????????????????????????
|
||
|
||
private void ChatTitle_MouseDown(object sender, MouseButtonEventArgs e)
|
||
{
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation == null) return;
|
||
}
|
||
ChatTitle.Visibility = Visibility.Collapsed;
|
||
ChatTitleEdit.Text = ChatTitle.Text;
|
||
ChatTitleEdit.Visibility = Visibility.Visible;
|
||
ChatTitleEdit.Focus();
|
||
ChatTitleEdit.SelectAll();
|
||
}
|
||
|
||
private void ChatTitleEdit_LostFocus(object sender, RoutedEventArgs e) => CommitTitleEdit();
|
||
private void ChatTitleEdit_KeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (e.Key == Key.Enter) { CommitTitleEdit(); e.Handled = true; }
|
||
if (e.Key == Key.Escape) { CancelTitleEdit(); e.Handled = true; }
|
||
}
|
||
|
||
private void CommitTitleEdit()
|
||
{
|
||
var newTitle = ChatTitleEdit.Text.Trim();
|
||
ChatTitleEdit.Visibility = Visibility.Collapsed;
|
||
ChatTitle.Visibility = Visibility.Visible;
|
||
|
||
if (string.IsNullOrEmpty(newTitle)) return;
|
||
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation == null) return;
|
||
_currentConversation.Title = newTitle;
|
||
}
|
||
|
||
ChatTitle.Text = newTitle;
|
||
try
|
||
{
|
||
ChatConversation conv;
|
||
lock (_convLock) conv = _currentConversation!;
|
||
_storage.Save(conv);
|
||
}
|
||
catch { }
|
||
RefreshConversationList();
|
||
}
|
||
|
||
private void CancelTitleEdit()
|
||
{
|
||
ChatTitleEdit.Visibility = Visibility.Collapsed;
|
||
ChatTitle.Visibility = Visibility.Visible;
|
||
}
|
||
|
||
// ??? 移댄뀒怨좊━ ?쒕∼?ㅼ슫 ??????????????????????????????????????????????
|
||
|
||
private void BtnCategoryDrop_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255));
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
|
||
var popup = new Popup
|
||
{
|
||
StaysOpen = false,
|
||
AllowsTransparency = true,
|
||
PopupAnimation = PopupAnimation.Fade,
|
||
PlacementTarget = BtnCategoryDrop,
|
||
Placement = PlacementMode.Bottom,
|
||
HorizontalOffset = 0,
|
||
VerticalOffset = 4,
|
||
};
|
||
|
||
var container = new Border
|
||
{
|
||
Background = bgBrush,
|
||
BorderBrush = borderBrush,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(6),
|
||
MinWidth = 180,
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black
|
||
},
|
||
};
|
||
|
||
var stack = new StackPanel();
|
||
|
||
Border CreateCatItem(string icon, string text, Brush iconColor, bool isSelected, Action onClick)
|
||
{
|
||
var item = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 7, 10, 7),
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var g = new Grid();
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) });
|
||
|
||
var iconTb = new TextBlock
|
||
{
|
||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(iconTb, 0);
|
||
g.Children.Add(iconTb);
|
||
|
||
var textTb = new TextBlock
|
||
{
|
||
Text = text, FontSize = 12.5, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(textTb, 1);
|
||
g.Children.Add(textTb);
|
||
|
||
if (isSelected)
|
||
{
|
||
var check = CreateSimpleCheck(accentBrush, 14);
|
||
Grid.SetColumn(check, 2);
|
||
g.Children.Add(check);
|
||
}
|
||
|
||
item.Child = g;
|
||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); };
|
||
return item;
|
||
}
|
||
|
||
Border CreateSep() => new()
|
||
{
|
||
Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4),
|
||
};
|
||
|
||
// ?꾩껜 蹂닿린
|
||
var allLabel = _activeTab switch
|
||
{
|
||
"Cowork" => "紐⑤뱺 ?묒뾽",
|
||
"Code" => "紐⑤뱺 ?묒뾽",
|
||
_ => "紐⑤뱺 二쇱젣",
|
||
};
|
||
stack.Children.Add(CreateCatItem("\uE8BD", allLabel, secondaryText,
|
||
string.IsNullOrEmpty(_selectedCategory),
|
||
() => { _selectedCategory = ""; UpdateCategoryLabel(); RefreshConversationList(); }));
|
||
|
||
stack.Children.Add(CreateSep());
|
||
|
||
if (_activeTab == "Cowork" || _activeTab == "Code")
|
||
{
|
||
// 肄붿썙??肄붾뱶: ?꾨━??移댄뀒怨좊━ 湲곕컲 ?꾪꽣
|
||
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
|
||
var seen = new HashSet<string>();
|
||
foreach (var p in presets)
|
||
{
|
||
if (p.IsCustom) continue; // 而ㅼ뒪?? 蹂꾨룄 洹몃9
|
||
if (!seen.Add(p.Category)) continue;
|
||
var capturedCat = p.Category;
|
||
stack.Children.Add(CreateCatItem(p.Symbol, p.Label, BrushFromHex(p.Color),
|
||
_selectedCategory == capturedCat,
|
||
() => { _selectedCategory = capturedCat; UpdateCategoryLabel(); RefreshConversationList(); }));
|
||
}
|
||
// 而ㅼ뒪? ?꾨━???듯빀 ?꾪꽣
|
||
if (presets.Any(p => p.IsCustom))
|
||
{
|
||
stack.Children.Add(CreateSep());
|
||
stack.Children.Add(CreateCatItem("\uE710", "而ㅼ뒪? ?꾨━??, secondaryText,
|
||
_selectedCategory == "__custom__",
|
||
() => { _selectedCategory = "__custom__"; UpdateCategoryLabel(); RefreshConversationList(); }));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Chat: 湲곗〈 ChatCategory 湲곕컲
|
||
foreach (var (key, label, symbol, color) in ChatCategory.All)
|
||
{
|
||
var capturedKey = key;
|
||
stack.Children.Add(CreateCatItem(symbol, label, BrushFromHex(color),
|
||
_selectedCategory == capturedKey,
|
||
() => { _selectedCategory = capturedKey; UpdateCategoryLabel(); RefreshConversationList(); }));
|
||
}
|
||
// 而ㅼ뒪? ?꾨━???듯빀 ?꾪꽣 (Chat)
|
||
var chatCustom = _settings.Settings.Llm.CustomPresets.Where(c => c.Tab == "Chat").ToList();
|
||
if (chatCustom.Count > 0)
|
||
{
|
||
stack.Children.Add(CreateSep());
|
||
stack.Children.Add(CreateCatItem("\uE710", "而ㅼ뒪? ?꾨━??, secondaryText,
|
||
_selectedCategory == "__custom__",
|
||
() => { _selectedCategory = "__custom__"; UpdateCategoryLabel(); RefreshConversationList(); }));
|
||
}
|
||
}
|
||
|
||
container.Child = stack;
|
||
popup.Child = container;
|
||
popup.IsOpen = true;
|
||
}
|
||
|
||
private void UpdateCategoryLabel()
|
||
{
|
||
if (string.IsNullOrEmpty(_selectedCategory))
|
||
{
|
||
CategoryLabel.Text = _activeTab switch { "Cowork" or "Code" => "紐⑤뱺 ?묒뾽", _ => "紐⑤뱺 二쇱젣" };
|
||
CategoryIcon.Text = "\uE8BD";
|
||
}
|
||
else if (_selectedCategory == "__custom__")
|
||
{
|
||
CategoryLabel.Text = "而ㅼ뒪? ?꾨━??;
|
||
CategoryIcon.Text = "\uE710";
|
||
}
|
||
else
|
||
{
|
||
// ChatCategory?먯꽌 李얘린
|
||
foreach (var (key, label, symbol, _) in ChatCategory.All)
|
||
{
|
||
if (key == _selectedCategory)
|
||
{
|
||
CategoryLabel.Text = label;
|
||
CategoryIcon.Text = symbol;
|
||
return;
|
||
}
|
||
}
|
||
// ?꾨━??移댄뀒怨좊━?먯꽌 李얘린 (Cowork/Code)
|
||
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
|
||
var match = presets.FirstOrDefault(p => p.Category == _selectedCategory);
|
||
if (match != null)
|
||
{
|
||
CategoryLabel.Text = match.Label;
|
||
CategoryIcon.Text = match.Symbol;
|
||
}
|
||
else
|
||
{
|
||
CategoryLabel.Text = _selectedCategory;
|
||
CategoryIcon.Text = "\uE8BD";
|
||
}
|
||
}
|
||
}
|
||
|
||
// ??? 李?而⑦듃濡???????????????????????????????????????????????????????
|
||
|
||
// WindowChrome??CaptionHeight媛 ?쒕옒洹몃? 泥섎━?섎?濡?蹂꾨룄 ?몃뱾??遺덊븘??
|
||
protected override void OnSourceInitialized(EventArgs e)
|
||
{
|
||
base.OnSourceInitialized(e);
|
||
var source = System.Windows.Interop.HwndSource.FromHwnd(
|
||
new System.Windows.Interop.WindowInteropHelper(this).Handle);
|
||
source?.AddHook(WndProc);
|
||
}
|
||
|
||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||
{
|
||
// WM_GETMINMAXINFO ??理쒕??????묒뾽 ?쒖떆以??곸뿭 ?뺣낫
|
||
if (msg == 0x0024)
|
||
{
|
||
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
|
||
var workArea = screen.WorkingArea;
|
||
var monitor = screen.Bounds;
|
||
|
||
var source = System.Windows.Interop.HwndSource.FromHwnd(hwnd);
|
||
var dpiScale = source?.CompositionTarget?.TransformToDevice.M11 ?? 1.0;
|
||
|
||
// MINMAXINFO: ptReserved(0,4) ptMaxSize(8,12) ptMaxPosition(16,20) ptMinTrackSize(24,28) ptMaxTrackSize(32,36)
|
||
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 8, workArea.Width); // ptMaxSize.cx
|
||
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 12, workArea.Height); // ptMaxSize.cy
|
||
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 16, workArea.Left - monitor.Left); // ptMaxPosition.x
|
||
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 20, workArea.Top - monitor.Top); // ptMaxPosition.y
|
||
handled = true;
|
||
}
|
||
return IntPtr.Zero;
|
||
}
|
||
|
||
private void BtnMinimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||
private void BtnMaximize_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
||
MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 蹂듭썝/理쒕????꾩씠肄? }
|
||
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
|
||
|
||
// ??? ???꾪솚 ??????????????????????????????????????????????????????????
|
||
|
||
private string _activeTab
|
||
{
|
||
get => _sessionState.ActiveTab;
|
||
set => _sessionState.ActiveTab = value;
|
||
}
|
||
|
||
private void SaveCurrentTabConversationId()
|
||
{
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
||
{
|
||
_sessionState.RememberConversation(_activeTab, _currentConversation.Id);
|
||
// ???꾪솚 ???꾩옱 ??붾? 利됱떆 ???(?ㅽ듃由щ컢 以묒씠?대룄 吏꾪뻾 以묒씤 ?댁슜 蹂댁〈)
|
||
try { _storage.Save(_currentConversation); } catch { }
|
||
}
|
||
}
|
||
// ??퀎 留덉?留????ID瑜??ㅼ젙???곸냽 ???(???ъ떆????蹂듭썝??
|
||
SaveLastConversations();
|
||
}
|
||
|
||
/// <summary>???꾪솚 ???ㅽ듃由щ컢 以묒씠硫?利됱떆 以묐떒?⑸땲??</summary>
|
||
private void StopStreamingIfActive()
|
||
{
|
||
if (!_isStreaming) return;
|
||
// ?ㅽ듃由щ컢 以묐떒
|
||
_streamCts?.Cancel();
|
||
_cursorTimer.Stop();
|
||
_elapsedTimer.Stop();
|
||
_typingTimer.Stop();
|
||
StopRainbowGlow();
|
||
HideStickyProgress();
|
||
_activeStreamText = null;
|
||
_elapsedLabel = null;
|
||
_cachedStreamContent = "";
|
||
_isStreaming = false;
|
||
BtnSend.IsEnabled = true;
|
||
BtnStop.Visibility = Visibility.Collapsed;
|
||
BtnPause.Visibility = Visibility.Collapsed;
|
||
PauseIcon.Text = "\uE769"; // 由ъ뀑
|
||
BtnSend.Visibility = Visibility.Visible;
|
||
_streamCts?.Dispose();
|
||
_streamCts = null;
|
||
_sessionState.ClearRuntimeActivities("?묒뾽 以묐떒");
|
||
_appState.ClearTasksByPrefix("agent:", "?묒뾽 以묐떒", "cancelled");
|
||
_appState.ClearTasksByPrefix("tool:", "?묒뾽 以묐떒", "cancelled");
|
||
SetStatusIdle();
|
||
}
|
||
|
||
private void TabChat_Checked(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_activeTab == "Chat") return;
|
||
StopStreamingIfActive();
|
||
SaveCurrentTabConversationId();
|
||
_activeTab = "Chat";
|
||
_selectedCategory = ""; UpdateCategoryLabel();
|
||
UpdateTabUI();
|
||
}
|
||
|
||
private void TabCowork_Checked(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_activeTab == "Cowork") return;
|
||
StopStreamingIfActive();
|
||
SaveCurrentTabConversationId();
|
||
_activeTab = "Cowork";
|
||
_selectedCategory = ""; UpdateCategoryLabel();
|
||
UpdateTabUI();
|
||
}
|
||
|
||
private void TabCode_Checked(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_activeTab == "Code") return;
|
||
StopStreamingIfActive();
|
||
SaveCurrentTabConversationId();
|
||
_activeTab = "Code";
|
||
_selectedCategory = ""; UpdateCategoryLabel();
|
||
UpdateTabUI();
|
||
}
|
||
|
||
private void ApplySessionTabSelection()
|
||
{
|
||
switch (_activeTab)
|
||
{
|
||
case "Cowork":
|
||
if (TabCowork != null) TabCowork.IsChecked = true;
|
||
break;
|
||
case "Code":
|
||
if (TabCode != null && TabCode.IsEnabled) TabCode.IsChecked = true;
|
||
else if (TabChat != null) TabChat.IsChecked = true;
|
||
break;
|
||
default:
|
||
if (TabChat != null) TabChat.IsChecked = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void ApplyRuntimeStateToUi()
|
||
{
|
||
if (StatusLabel != null)
|
||
StatusLabel.Text = string.IsNullOrWhiteSpace(_sessionState.StatusText) ? "대기 중" : _sessionState.StatusText;
|
||
|
||
if (_sessionState.IsStatusSpinning)
|
||
StartStatusAnimation();
|
||
else
|
||
StopStatusAnimation();
|
||
|
||
UpdateRuntimeActivityIndicators();
|
||
}
|
||
|
||
UpdateRuntimeActivityIndicators();
|
||
}
|
||
|
||
private void UpdateRuntimeActivityIndicators()
|
||
{
|
||
if (RuntimeActivityBadge != null && RuntimeActivityLabel != null)
|
||
{
|
||
var count = _appState.ActiveTasks.Count;
|
||
RuntimeActivityBadge.Visibility = count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||
RuntimeActivityLabel.Text = $"?ㅽ뻾 以?{count}";
|
||
|
||
var tooltipLines = _appState.ActiveTasks
|
||
.OrderBy(task => task.StartedAt)
|
||
.Select(task => string.IsNullOrWhiteSpace(task.Summary)
|
||
? task.Title
|
||
: $"{task.Title}: {task.Summary}")
|
||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||
.ToList();
|
||
RuntimeActivityBadge.ToolTip = tooltipLines.Count > 0 ? string.Join(Environment.NewLine, tooltipLines) : null;
|
||
}
|
||
|
||
if (LastCompletedLabel != null)
|
||
{
|
||
var recentTask = _appState.RecentTasks.FirstOrDefault();
|
||
var summary = recentTask?.Summary;
|
||
LastCompletedLabel.Text = string.IsNullOrWhiteSpace(summary)
|
||
? ""
|
||
: $"留덉?留?{GetTaskStatusLabel(recentTask?.Status)}: {TruncateForStatus(summary, 48)}";
|
||
LastCompletedLabel.Visibility = string.IsNullOrWhiteSpace(summary)
|
||
? Visibility.Collapsed
|
||
: Visibility.Visible;
|
||
LastCompletedLabel.ToolTip = string.IsNullOrWhiteSpace(summary)
|
||
? null
|
||
: $"{recentTask?.Title}{Environment.NewLine}{summary}";
|
||
}
|
||
}
|
||
|
||
private static string GetTaskStatusLabel(string? status)
|
||
=> status switch
|
||
{
|
||
"failed" => "?ㅽ뙣",
|
||
"cancelled" => "以묐떒",
|
||
_ => "?꾨즺",
|
||
};
|
||
|
||
private static string GetAgentRunStatusLabel(string? status)
|
||
=> status switch
|
||
{
|
||
"failed" => "?ㅽ뙣",
|
||
"completed" => "?꾨즺",
|
||
"paused" => "?쇱떆以묒?",
|
||
_ => "吏꾪뻾 以?,
|
||
};
|
||
|
||
private static string GetAgentTaskKey(AgentEvent evt)
|
||
=> string.IsNullOrWhiteSpace(evt.RunId) ? "agent:main" : $"agent:{evt.RunId}";
|
||
|
||
private static string GetToolTaskKey(AgentEvent evt)
|
||
{
|
||
var suffix = string.IsNullOrWhiteSpace(evt.ToolName) ? "unknown" : evt.ToolName;
|
||
return string.IsNullOrWhiteSpace(evt.RunId)
|
||
? $"tool:{suffix}"
|
||
: $"tool:{evt.RunId}:{suffix}";
|
||
}
|
||
|
||
private void RuntimeTaskSummary_Click(object sender, MouseButtonEventArgs e)
|
||
{
|
||
e.Handled = true;
|
||
_taskSummaryTarget = sender as UIElement ?? RuntimeActivityBadge;
|
||
ShowTaskSummaryPopup(_taskSummaryTarget);
|
||
}
|
||
|
||
private void ShowTaskSummaryPopup(UIElement? target)
|
||
{
|
||
if (target == null)
|
||
return;
|
||
|
||
_taskSummaryTarget = target;
|
||
|
||
if (_taskSummaryPopup != null)
|
||
_taskSummaryPopup.IsOpen = false;
|
||
|
||
var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, 0, 0, 0));
|
||
|
||
var panel = new StackPanel { Margin = new Thickness(2) };
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = "?묒뾽 ?곹깭",
|
||
FontSize = 12.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primaryText,
|
||
Margin = new Thickness(10, 8, 10, 2),
|
||
});
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = $"?쒖꽦 {_appState.ActiveTasks.Count}媛?쨌 理쒓렐 {_appState.RecentTasks.Count}媛?,
|
||
FontSize = 11,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(10, 0, 10, 8),
|
||
});
|
||
|
||
if (!string.IsNullOrWhiteSpace(_appState.AgentRun.RunId))
|
||
panel.Children.Add(BuildAgentRunSummaryCard(primaryText, secondaryText));
|
||
|
||
var filterRow = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(8, 0, 8, 8),
|
||
};
|
||
filterRow.Children.Add(CreateTaskFilterChip("?꾩껜", "all"));
|
||
filterRow.Children.Add(CreateTaskFilterChip("?먯씠?꾪듃", "agent"));
|
||
filterRow.Children.Add(CreateTaskFilterChip("?꾧뎄", "tool"));
|
||
filterRow.Children.Add(CreateTaskFilterChip("?쒕툕", "subagent"));
|
||
filterRow.Children.Add(CreateTaskFilterChip("?ㅽ뙣", "failed"));
|
||
panel.Children.Add(filterRow);
|
||
|
||
AddTaskSummarySection(panel, "?꾩옱 ?ㅽ뻾 以?, FilterTaskRuns(_appState.ActiveTasks).OrderBy(t => t.StartedAt).Take(8), hoverBg, primaryText, secondaryText);
|
||
AddTaskSummarySection(panel, "理쒓렐 ?묒뾽", FilterTaskRuns(_appState.RecentTasks).Take(8), hoverBg, primaryText, secondaryText);
|
||
|
||
var container = new Border
|
||
{
|
||
Background = bg,
|
||
BorderBrush = borderBrush,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(6),
|
||
Child = new ScrollViewer
|
||
{
|
||
Content = panel,
|
||
MaxHeight = 360,
|
||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||
},
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
BlurRadius = 20,
|
||
ShadowDepth = 4,
|
||
Opacity = 0.18,
|
||
},
|
||
};
|
||
|
||
_taskSummaryPopup = new Popup
|
||
{
|
||
PlacementTarget = target,
|
||
Placement = PlacementMode.Top,
|
||
AllowsTransparency = true,
|
||
StaysOpen = false,
|
||
PopupAnimation = PopupAnimation.Fade,
|
||
Child = container,
|
||
};
|
||
Dispatcher.BeginInvoke(() => _taskSummaryPopup.IsOpen = true, DispatcherPriority.Input);
|
||
}
|
||
|
||
private Border BuildAgentRunSummaryCard(Brush primaryText, Brush secondaryText)
|
||
{
|
||
var statusChip = CreateMetaChip(
|
||
GetAgentRunStatusLabel(_appState.AgentRun.Status),
|
||
_appState.AgentRun.Status switch
|
||
{
|
||
"failed" => "#FEE2E2",
|
||
"completed" => "#E0F2FE",
|
||
"paused" => "#FEF3C7",
|
||
_ => "#DCFCE7",
|
||
},
|
||
_appState.AgentRun.Status switch
|
||
{
|
||
"failed" => "#B91C1C",
|
||
"completed" => "#0369A1",
|
||
"paused" => "#B45309",
|
||
_ => "#166534",
|
||
});
|
||
|
||
var header = new DockPanel();
|
||
DockPanel.SetDock(statusChip, Dock.Right);
|
||
header.Children.Add(statusChip);
|
||
header.Children.Add(new TextBlock
|
||
{
|
||
Text = $"?먯씠?꾪듃 ?ㅽ뻾 run {ShortRunId(_appState.AgentRun.RunId)}",
|
||
FontSize = 12,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primaryText,
|
||
});
|
||
|
||
var body = new StackPanel();
|
||
body.Children.Add(header);
|
||
body.Children.Add(new TextBlock
|
||
{
|
||
Text = $"iteration {_appState.AgentRun.LastIteration} 쨌 {GetRelativeTime(_appState.AgentRun.UpdatedAt)}",
|
||
FontSize = 10.5,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(0, 4, 0, 2),
|
||
});
|
||
if (!string.IsNullOrWhiteSpace(_appState.AgentRun.Summary))
|
||
{
|
||
body.Children.Add(new TextBlock
|
||
{
|
||
Text = _appState.AgentRun.Summary,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
FontSize = 11.5,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
});
|
||
}
|
||
|
||
return new Border
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8FAFC")),
|
||
BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E2E8F0")),
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(10, 9, 10, 9),
|
||
Margin = new Thickness(10, 0, 10, 8),
|
||
Child = body,
|
||
};
|
||
}
|
||
|
||
private IEnumerable<AppStateService.TaskRunState> FilterTaskRuns(IEnumerable<AppStateService.TaskRunState> tasks)
|
||
{
|
||
return _taskSummaryFilter switch
|
||
{
|
||
"agent" => tasks.Where(t => string.Equals(t.Kind, "agent", StringComparison.OrdinalIgnoreCase)),
|
||
"tool" => tasks.Where(t => string.Equals(t.Kind, "tool", StringComparison.OrdinalIgnoreCase) || string.Equals(t.Kind, "skill", StringComparison.OrdinalIgnoreCase)),
|
||
"subagent" => tasks.Where(t => string.Equals(t.Kind, "subagent", StringComparison.OrdinalIgnoreCase)),
|
||
"failed" => tasks.Where(t => string.Equals(t.Status, "failed", StringComparison.OrdinalIgnoreCase)),
|
||
_ => tasks,
|
||
};
|
||
}
|
||
|
||
private Border CreateTaskFilterChip(string label, string filterKey)
|
||
{
|
||
var isSelected = string.Equals(_taskSummaryFilter, filterKey, StringComparison.OrdinalIgnoreCase);
|
||
var chip = CreateMetaChip(
|
||
label,
|
||
isSelected ? "#DBEAFE" : "#F3F4F6",
|
||
isSelected ? "#1D4ED8" : "#4B5563");
|
||
chip.Cursor = Cursors.Hand;
|
||
chip.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
_taskSummaryFilter = filterKey;
|
||
if (_taskSummaryTarget != null)
|
||
ShowTaskSummaryPopup(_taskSummaryTarget);
|
||
};
|
||
return chip;
|
||
}
|
||
|
||
private void AddTaskSummarySection(
|
||
Panel panel,
|
||
string title,
|
||
IEnumerable<AppStateService.TaskRunState> tasks,
|
||
Brush hoverBg,
|
||
Brush primaryText,
|
||
Brush secondaryText)
|
||
{
|
||
var taskList = tasks.ToList();
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = title,
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(10, 4, 10, 6),
|
||
});
|
||
|
||
if (taskList.Count == 0)
|
||
{
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = "?쒖떆???묒뾽???놁뒿?덈떎.",
|
||
FontSize = 11.5,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(10, 0, 10, 8),
|
||
});
|
||
return;
|
||
}
|
||
|
||
foreach (var task in taskList)
|
||
{
|
||
var item = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(4, 0, 4, 4),
|
||
ToolTip = string.IsNullOrWhiteSpace(task.FilePath)
|
||
? task.Summary
|
||
: $"{task.Summary}{Environment.NewLine}{task.FilePath}",
|
||
};
|
||
item.MouseEnter += (_, _) => item.Background = hoverBg;
|
||
item.MouseLeave += (_, _) => item.Background = Brushes.Transparent;
|
||
|
||
var stack = new StackPanel();
|
||
var header = new DockPanel();
|
||
header.Children.Add(new TextBlock
|
||
{
|
||
Text = task.Title,
|
||
FontSize = 12,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = primaryText,
|
||
});
|
||
var statusChip = CreateMetaChip(
|
||
GetTaskStatusLabel(task.Status),
|
||
task.Status switch
|
||
{
|
||
"failed" => "#FEE2E2",
|
||
"cancelled" => "#FEF3C7",
|
||
"running" => "#DCFCE7",
|
||
_ => "#E0F2FE",
|
||
},
|
||
task.Status switch
|
||
{
|
||
"failed" => "#B91C1C",
|
||
"cancelled" => "#B45309",
|
||
"running" => "#166534",
|
||
_ => "#0369A1",
|
||
});
|
||
DockPanel.SetDock(statusChip, Dock.Right);
|
||
header.Children.Add(statusChip);
|
||
stack.Children.Add(header);
|
||
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = TruncateForStatus(task.Summary, 96),
|
||
FontSize = 11.5,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
});
|
||
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = $"{task.Kind} 쨌 {task.UpdatedAt:HH:mm:ss}",
|
||
FontSize = 10.5,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
});
|
||
|
||
var runId = ExtractRunIdFromTaskKey(task.Id);
|
||
if (!string.IsNullOrWhiteSpace(runId))
|
||
{
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = $"run {runId}",
|
||
FontSize = 10,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
});
|
||
}
|
||
|
||
item.Child = stack;
|
||
panel.Children.Add(item);
|
||
}
|
||
}
|
||
|
||
private static string ExtractRunIdFromTaskKey(string? taskId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(taskId))
|
||
return "";
|
||
|
||
var parts = taskId.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||
if (parts.Length < 2)
|
||
return "";
|
||
|
||
if (string.Equals(parts[0], "agent", StringComparison.OrdinalIgnoreCase))
|
||
return parts[1].Length > 8 ? parts[1][..8] : parts[1];
|
||
|
||
if (string.Equals(parts[0], "tool", StringComparison.OrdinalIgnoreCase) && parts.Length >= 3)
|
||
return parts[1].Length > 8 ? parts[1][..8] : parts[1];
|
||
|
||
return "";
|
||
}
|
||
|
||
/// <summary>??퀎濡?留덉?留됱쑝濡??쒖꽦?붾맂 ???ID瑜?湲곗뼲.</summary>
|
||
private Dictionary<string, string?> _tabConversationId => _sessionState.TabConversationIds;
|
||
|
||
private void UpdateTabUI()
|
||
{
|
||
// ?대뜑 諛붾뒗 Cowork/Code ??뿉?쒕쭔 ?쒖떆
|
||
if (FolderBar != null)
|
||
FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
|
||
|
||
// ??퀎 ?낅젰 ?덈궡 臾멸뎄
|
||
if (InputWatermark != null)
|
||
{
|
||
InputWatermark.Text = _activeTab switch
|
||
{
|
||
"Cowork" => "?먯씠?꾪듃?먭쾶 ?묒뾽???붿껌?섏꽭??(?뚯씪 ?쎄린/?곌린, 臾몄꽌 ?앹꽦...)",
|
||
"Code" => "肄붾뱶 愿???묒뾽???붿껌?섏꽭??..",
|
||
_ => _promptCardPlaceholder,
|
||
};
|
||
}
|
||
|
||
// 沅뚰븳 湲곕낯媛??곸슜 (Cowork/Code ??? ?ㅼ젙??湲곕낯媛??ъ슜)
|
||
ApplyTabDefaultPermission();
|
||
|
||
// ?щ㎎/?붿옄???쒕∼?ㅼ슫? Cowork ??뿉?쒕쭔 ?쒖떆
|
||
if (_activeTab == "Cowork")
|
||
{
|
||
BuildBottomBar();
|
||
if (_settings.Settings.Llm.ShowFileBrowser && FileBrowserPanel != null)
|
||
{
|
||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||
BuildFileTree();
|
||
}
|
||
}
|
||
else if (_activeTab == "Code")
|
||
{
|
||
// Code ?? ?몄뼱 ?좏깮湲?+ ?뚯씪 ?먯깋湲? BuildCodeBottomBar();
|
||
if (_settings.Settings.Llm.ShowFileBrowser && FileBrowserPanel != null)
|
||
{
|
||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||
BuildFileTree();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
MoodIconPanel.Children.Clear();
|
||
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
|
||
if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
// ??퀎 ?꾨━??踰꾪듉 ?ш뎄?? BuildTopicButtons();
|
||
|
||
// ?꾩옱 ??붾? ?대떦 ????붾줈 ?꾪솚
|
||
SwitchToTabConversation();
|
||
|
||
// Cowork/Code ???꾪솚 ?????쒖떆
|
||
ShowRandomTip();
|
||
}
|
||
|
||
private void SwitchToTabConversation()
|
||
{
|
||
// ?댁쟾 ??쓽 ?????? lock (_convLock)
|
||
{
|
||
_sessionState.SaveCurrentConversation(_storage, _activeTab);
|
||
}
|
||
|
||
lock (_convLock)
|
||
_currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings);
|
||
|
||
RenderMessages();
|
||
_attachedFiles.Clear();
|
||
RefreshAttachedFilesUI();
|
||
UpdateChatTitle();
|
||
RefreshConversationList();
|
||
UpdateFolderBar();
|
||
}
|
||
|
||
// ??? ?묒뾽 ?대뜑 ?????????????????????????????????????????????????????????
|
||
|
||
private readonly List<string> _attachedFiles = new();
|
||
private readonly List<ImageAttachment> _pendingImages = new();
|
||
|
||
private void FolderPathLabel_Click(object sender, MouseButtonEventArgs e) => ShowFolderMenu();
|
||
|
||
private void ShowFolderMenu()
|
||
{
|
||
FolderMenuItems.Children.Clear();
|
||
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
// 理쒓렐 ?대뜑 紐⑸줉
|
||
var maxDisplay = Math.Clamp(_settings.Settings.Llm.MaxRecentFolders, 3, 30);
|
||
var recentFolders = _settings.Settings.Llm.RecentWorkFolders
|
||
.Where(p => IsPathAllowed(p) && System.IO.Directory.Exists(p))
|
||
.Take(maxDisplay)
|
||
.ToList();
|
||
|
||
if (recentFolders.Count > 0)
|
||
{
|
||
FolderMenuItems.Children.Add(new TextBlock
|
||
{
|
||
Text = "理쒓렐 ?대뜑",
|
||
FontSize = 12.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(10, 6, 10, 4),
|
||
});
|
||
|
||
var currentFolder = GetCurrentWorkFolder();
|
||
foreach (var folder in recentFolders)
|
||
{
|
||
var isActive = folder.Equals(currentFolder, StringComparison.OrdinalIgnoreCase);
|
||
var displayName = System.IO.Path.GetFileName(folder);
|
||
if (string.IsNullOrEmpty(displayName)) displayName = folder;
|
||
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
if (isActive)
|
||
{
|
||
var checkEl = CreateSimpleCheck(accentBrush, 14);
|
||
checkEl.Margin = new Thickness(0, 0, 8, 0);
|
||
sp.Children.Add(checkEl);
|
||
}
|
||
var nameBlock = new TextBlock
|
||
{
|
||
Text = displayName,
|
||
FontSize = 14,
|
||
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
|
||
Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
MaxWidth = 340,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
};
|
||
sp.Children.Add(nameBlock);
|
||
|
||
var itemBorder = new Border
|
||
{
|
||
Child = sp,
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(10, 7, 10, 7),
|
||
ToolTip = folder,
|
||
};
|
||
itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); };
|
||
itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
var capturedPath = folder;
|
||
itemBorder.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
FolderMenuPopup.IsOpen = false;
|
||
SetWorkFolder(capturedPath);
|
||
};
|
||
// ?고겢由???而⑦뀓?ㅽ듃 硫붾돱 (??젣, ?대뜑 ?닿린)
|
||
itemBorder.MouseRightButtonUp += (_, re) =>
|
||
{
|
||
re.Handled = true;
|
||
ShowRecentFolderContextMenu(capturedPath);
|
||
};
|
||
FolderMenuItems.Children.Add(itemBorder);
|
||
}
|
||
|
||
// 援щ텇?? FolderMenuItems.Children.Add(new Border
|
||
{
|
||
Height = 1,
|
||
Background = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||
Margin = new Thickness(8, 4, 8, 4),
|
||
Opacity = 0.5,
|
||
});
|
||
}
|
||
|
||
// ?대뜑 李얠븘蹂닿린 踰꾪듉
|
||
var browseSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
browseSp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uED25",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 14,
|
||
Foreground = accentBrush,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
browseSp.Children.Add(new TextBlock
|
||
{
|
||
Text = "?대뜑 李얠븘蹂닿린...",
|
||
FontSize = 14,
|
||
Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
var browseBorder = new Border
|
||
{
|
||
Child = browseSp,
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(10, 7, 10, 7),
|
||
};
|
||
browseBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); };
|
||
browseBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
browseBorder.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
FolderMenuPopup.IsOpen = false;
|
||
BrowseWorkFolder();
|
||
};
|
||
FolderMenuItems.Children.Add(browseBorder);
|
||
|
||
FolderMenuPopup.IsOpen = true;
|
||
}
|
||
|
||
private void BrowseWorkFolder()
|
||
{
|
||
var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||
{
|
||
Description = "?묒뾽 ?대뜑瑜??좏깮?섏꽭??,
|
||
ShowNewFolderButton = false,
|
||
UseDescriptionForTitle = true,
|
||
};
|
||
|
||
var currentFolder = GetCurrentWorkFolder();
|
||
if (!string.IsNullOrEmpty(currentFolder) && System.IO.Directory.Exists(currentFolder))
|
||
dlg.SelectedPath = currentFolder;
|
||
|
||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||
|
||
if (!IsPathAllowed(dlg.SelectedPath))
|
||
{
|
||
CustomMessageBox.Show("??寃쎈줈???묒뾽 ?대뜑濡??좏깮?????놁뒿?덈떎.", "寃쎈줈 ?쒗븳", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||
return;
|
||
}
|
||
|
||
SetWorkFolder(dlg.SelectedPath);
|
||
}
|
||
|
||
/// <summary>寃쎈줈 ?좏슚??寃????李⑤떒 ???寃쎈줈 ?꾪꽣留?</summary>
|
||
private static bool IsPathAllowed(string path)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path)) return false;
|
||
// C:\ 猷⑦듃 李⑤떒
|
||
var normalized = path.TrimEnd('\\', '/');
|
||
if (normalized.Equals("C:", StringComparison.OrdinalIgnoreCase)) return false;
|
||
// "Document" ?ы븿 寃쎈줈 李⑤떒 (??뚮Ц??臾댁떆)
|
||
if (path.IndexOf("Document", StringComparison.OrdinalIgnoreCase) >= 0) return false;
|
||
return true;
|
||
}
|
||
|
||
private void SetWorkFolder(string path)
|
||
{
|
||
// 猷⑦듃 ?쒕씪?대툕 ?꾩껜瑜??묒뾽怨듦컙?쇰줈 ?ㅼ젙?섎뒗 寃껋쓣 李⑤떒
|
||
// ?? "C:\", "D:\", "E:\" ?? var fullPath = System.IO.Path.GetFullPath(path);
|
||
var root = System.IO.Path.GetPathRoot(fullPath);
|
||
if (!string.IsNullOrEmpty(root) && fullPath.TrimEnd('\\', '/').Equals(root.TrimEnd('\\', '/'), StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
ShowToast($"?쒕씪?대툕 猷⑦듃({root})???묒뾽怨듦컙?쇰줈 ?ㅼ젙?????놁뒿?덈떎. ?섏쐞 ?대뜑瑜??좏깮?섏꽭??", "\uE783", 3000);
|
||
return;
|
||
}
|
||
|
||
FolderPathLabel.Text = path;
|
||
FolderPathLabel.ToolTip = path;
|
||
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null)
|
||
_currentConversation.WorkFolder = path;
|
||
}
|
||
|
||
// 理쒓렐 ?대뜑 紐⑸줉??異붽? (李⑤떒 寃쎈줈 ?쒖쇅)
|
||
var recent = _settings.Settings.Llm.RecentWorkFolders;
|
||
recent.RemoveAll(p => !IsPathAllowed(p));
|
||
recent.Remove(path);
|
||
recent.Insert(0, path);
|
||
var maxRecent = Math.Clamp(_settings.Settings.Llm.MaxRecentFolders, 3, 30);
|
||
if (recent.Count > maxRecent) recent.RemoveRange(maxRecent, recent.Count - maxRecent);
|
||
_settings.Settings.Llm.WorkFolder = path;
|
||
_settings.Save();
|
||
}
|
||
|
||
private string GetCurrentWorkFolder()
|
||
{
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.WorkFolder))
|
||
return _currentConversation.WorkFolder;
|
||
}
|
||
return _settings.Settings.Llm.WorkFolder;
|
||
}
|
||
|
||
/// <summary>?뚮쭏??留욌뒗 ContextMenu瑜??앹꽦?⑸땲??</summary>
|
||
private ContextMenu CreateThemedContextMenu()
|
||
{
|
||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||
var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
return new ContextMenu
|
||
{
|
||
Background = bg,
|
||
BorderBrush = border,
|
||
BorderThickness = new Thickness(1),
|
||
Padding = new Thickness(4),
|
||
};
|
||
}
|
||
|
||
/// <summary>理쒓렐 ?대뜑 ??ぉ ?고겢由?而⑦뀓?ㅽ듃 硫붾돱瑜??쒖떆?⑸땲??</summary>
|
||
private void ShowRecentFolderContextMenu(string folderPath)
|
||
{
|
||
var menu = CreateThemedContextMenu();
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
void AddItem(string icon, string label, Action action)
|
||
{
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = secondaryText,
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = label, FontSize = 12, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) };
|
||
mi.Click += (_, _) => action();
|
||
menu.Items.Add(mi);
|
||
}
|
||
|
||
AddItem("\uED25", "?대뜑 ?닿린", () =>
|
||
{
|
||
try
|
||
{
|
||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||
{
|
||
FileName = folderPath,
|
||
UseShellExecute = true,
|
||
});
|
||
}
|
||
catch { }
|
||
});
|
||
|
||
AddItem("\uE8C8", "寃쎈줈 蹂듭궗", () =>
|
||
{
|
||
try { Clipboard.SetText(folderPath); } catch { }
|
||
});
|
||
|
||
menu.Items.Add(new Separator());
|
||
|
||
AddItem("\uE74D", "紐⑸줉?먯꽌 ??젣", () =>
|
||
{
|
||
_settings.Settings.Llm.RecentWorkFolders.RemoveAll(
|
||
p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
|
||
_settings.Save();
|
||
// 硫붾돱 ?덈줈怨좎묠
|
||
if (FolderMenuPopup.IsOpen)
|
||
ShowFolderMenu();
|
||
});
|
||
|
||
menu.IsOpen = true;
|
||
}
|
||
|
||
private void BtnFolderClear_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
FolderPathLabel.Text = "?대뜑瑜??좏깮?섏꽭??;
|
||
FolderPathLabel.ToolTip = null;
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null)
|
||
_currentConversation.WorkFolder = "";
|
||
}
|
||
}
|
||
|
||
private void UpdateFolderBar()
|
||
{
|
||
if (FolderBar == null) return;
|
||
if (_activeTab == "Chat")
|
||
{
|
||
FolderBar.Visibility = Visibility.Collapsed;
|
||
return;
|
||
}
|
||
FolderBar.Visibility = Visibility.Visible;
|
||
var folder = GetCurrentWorkFolder();
|
||
if (!string.IsNullOrEmpty(folder))
|
||
{
|
||
FolderPathLabel.Text = folder;
|
||
FolderPathLabel.ToolTip = folder;
|
||
}
|
||
else
|
||
{
|
||
FolderPathLabel.Text = "?대뜑瑜??좏깮?섏꽭??;
|
||
FolderPathLabel.ToolTip = null;
|
||
}
|
||
// ??붾퀎 ?ㅼ젙 蹂듭썝 (?놁쑝硫??꾩뿭 湲곕낯媛?
|
||
LoadConversationSettings();
|
||
UpdatePermissionUI();
|
||
UpdateDataUsageUI();
|
||
}
|
||
|
||
/// <summary>?꾩옱 ??붿쓽 媛쒕퀎 ?ㅼ젙??濡쒕뱶?⑸땲?? null?대㈃ ?꾩뿭 湲곕낯媛??ъ슜.</summary>
|
||
private void LoadConversationSettings()
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
var llm = _settings.Settings.Llm;
|
||
|
||
if (conv != null && conv.Permission != null)
|
||
_settings.Settings.Llm.FilePermission = conv.Permission;
|
||
|
||
_folderDataUsage = conv?.DataUsage ?? llm.FolderDataUsage ?? "active";
|
||
_selectedMood = conv?.Mood ?? llm.DefaultMood ?? "modern";
|
||
ApplyConversationUiState(conv);
|
||
}
|
||
|
||
/// <summary>?꾩옱 ?섎떒 諛??ㅼ젙????붿뿉 ??ν빀?덈떎.</summary>
|
||
private void SaveConversationSettings()
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv == null) return;
|
||
|
||
conv.Permission = _settings.Settings.Llm.FilePermission;
|
||
conv.DataUsage = _folderDataUsage;
|
||
conv.Mood = _selectedMood;
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"???????ㅽ뙣: {ex.Message}"); }
|
||
}
|
||
|
||
private void SaveConversationUiState()
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv == null) return;
|
||
|
||
conv.ShowExecutionHistory = !Equals(BtnToggleExecutionLog?.Tag, "hidden");
|
||
var queueItems = GetDraftQueueState().Select(CloneDraftQueueItem).ToList();
|
||
conv.DraftQueueItems = queueItems;
|
||
conv.DraftQueue = queueItems.Select(item => item.Text).ToList();
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"???UI ?곹깭 ????ㅽ뙣: {ex.Message}"); }
|
||
}
|
||
|
||
private void ApplyConversationUiState(ChatConversation? conv)
|
||
{
|
||
var queue = GetConversationDraftQueueItems(conv);
|
||
DraftQueuePanel.Tag = queue;
|
||
|
||
var showExecutionHistory = conv?.ShowExecutionHistory ?? true;
|
||
if (BtnToggleExecutionLog != null)
|
||
BtnToggleExecutionLog.Tag = showExecutionHistory ? "visible" : "hidden";
|
||
|
||
if (ExecutionLogLabel != null)
|
||
ExecutionLogLabel.Text = showExecutionHistory ? "?ㅽ뻾 濡쒓렇 ?④?" : "?ㅽ뻾 濡쒓렇";
|
||
|
||
if (ExecutionLogIcon != null)
|
||
ExecutionLogIcon.Text = showExecutionHistory ? "\uE8F8" : "\uE946";
|
||
|
||
RefreshDraftComposerUi();
|
||
}
|
||
|
||
private bool IsExecutionHistoryVisible()
|
||
=> !Equals(BtnToggleExecutionLog?.Tag, "hidden");
|
||
|
||
private AgentEvent ToAgentEvent(ChatExecutionEvent evt)
|
||
{
|
||
if (!Enum.TryParse<AgentEventType>(evt.Type, true, out var parsedType))
|
||
parsedType = AgentEventType.Thinking;
|
||
|
||
return new AgentEvent
|
||
{
|
||
Timestamp = evt.Timestamp,
|
||
RunId = evt.RunId ?? "",
|
||
Type = parsedType,
|
||
ToolName = evt.ToolName ?? "",
|
||
Summary = evt.Summary ?? "",
|
||
FilePath = evt.FilePath,
|
||
Success = evt.Success,
|
||
StepCurrent = evt.StepCurrent,
|
||
StepTotal = evt.StepTotal,
|
||
Steps = evt.Steps,
|
||
ElapsedMs = evt.ElapsedMs,
|
||
InputTokens = evt.InputTokens,
|
||
OutputTokens = evt.OutputTokens,
|
||
ToolInput = evt.ToolInput,
|
||
Iteration = evt.Iteration,
|
||
};
|
||
}
|
||
|
||
private ChatExecutionEvent ToChatExecutionEvent(AgentEvent evt)
|
||
=> new()
|
||
{
|
||
Timestamp = evt.Timestamp,
|
||
RunId = evt.RunId,
|
||
Type = evt.Type.ToString(),
|
||
ToolName = evt.ToolName,
|
||
Summary = evt.Summary,
|
||
FilePath = evt.FilePath,
|
||
Success = evt.Success,
|
||
StepCurrent = evt.StepCurrent,
|
||
StepTotal = evt.StepTotal,
|
||
Steps = evt.Steps?.ToList(),
|
||
ElapsedMs = evt.ElapsedMs,
|
||
InputTokens = evt.InputTokens,
|
||
OutputTokens = evt.OutputTokens,
|
||
ToolInput = evt.ToolInput,
|
||
Iteration = evt.Iteration,
|
||
};
|
||
|
||
private sealed class ConversationTimelineItem
|
||
{
|
||
public DateTime Timestamp { get; init; }
|
||
public ChatMessage? Message { get; init; }
|
||
public ChatExecutionEvent? ExecutionEvent { get; init; }
|
||
}
|
||
|
||
// ??? 沅뚰븳 硫붾돱 ?????????????????????????????????????????????????????????
|
||
|
||
private void BtnPermission_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (PermissionPopup == null) return;
|
||
PermissionItems.Children.Clear();
|
||
|
||
var levels = new (string Level, string Sym, string Desc, string Color)[] {
|
||
("Ask", "\uE8D7", "留ㅻ쾲 ?뺤씤 ???뚯씪 ?묎렐 ???ъ슜?먯뿉寃?臾살뒿?덈떎", "#4B5EFC"),
|
||
("Auto", "\uE73E", "?먮룞 ?덉슜 ???뚯씪???먮룞?쇰줈 ?쎄퀬 ?곷땲??, "#DD6B20"),
|
||
("Deny", "\uE711", "?묎렐 李⑤떒 ???뚯씪 ?묎렐???덉슜?섏? ?딆뒿?덈떎", "#C50F1F"),
|
||
};
|
||
var current = _settings.Settings.Llm.FilePermission;
|
||
foreach (var (level, sym, desc, color) in levels)
|
||
{
|
||
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
|
||
|
||
// ?쇱슫??肄붾꼫 ?쒗뵆由?(湲곕낯 Button ?щ\ ?쒓굅)
|
||
var template = new ControlTemplate(typeof(Button));
|
||
var bdFactory = new FrameworkElementFactory(typeof(Border));
|
||
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
|
||
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
|
||
bdFactory.Name = "Bd";
|
||
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
|
||
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
|
||
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||
bdFactory.AppendChild(cpFactory);
|
||
template.VisualTree = bdFactory;
|
||
// ?몃쾭 ?④낵
|
||
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
|
||
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
|
||
template.Triggers.Add(hoverTrigger);
|
||
|
||
var btn = new Button
|
||
{
|
||
Template = template,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand,
|
||
HorizontalContentAlignment = HorizontalAlignment.Left,
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
};
|
||
ApplyHoverScaleAnimation(btn, 1.02);
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
// 而ㅼ뒪? 泥댄겕 ?꾩씠肄? sp.Children.Add(CreateCheckIcon(isActive));
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = sym, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14,
|
||
Foreground = BrushFromHex(color),
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||
});
|
||
var textStack = new StackPanel();
|
||
textStack.Children.Add(new TextBlock
|
||
{
|
||
Text = level, FontSize = 13, FontWeight = FontWeights.Bold,
|
||
Foreground = BrushFromHex(color),
|
||
});
|
||
textStack.Children.Add(new TextBlock
|
||
{
|
||
Text = desc, FontSize = 11,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
MaxWidth = 220,
|
||
});
|
||
sp.Children.Add(textStack);
|
||
btn.Content = sp;
|
||
|
||
var capturedLevel = level;
|
||
btn.Click += (_, _) =>
|
||
{
|
||
_settings.Settings.Llm.FilePermission = capturedLevel;
|
||
UpdatePermissionUI();
|
||
SaveConversationSettings();
|
||
PermissionPopup.IsOpen = false;
|
||
};
|
||
PermissionItems.Children.Add(btn);
|
||
}
|
||
PermissionPopup.IsOpen = true;
|
||
}
|
||
|
||
private bool _autoWarningDismissed; // Auto 寃쎄퀬 諛곕꼫 ?ъ슜?먭? ?レ븯?붿?
|
||
|
||
private void BtnAutoWarningClose_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
_autoWarningDismissed = true;
|
||
if (AutoPermissionWarning != null)
|
||
AutoPermissionWarning.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
private void UpdatePermissionUI()
|
||
{
|
||
if (PermissionLabel == null || PermissionIcon == null) return;
|
||
var perm = _settings.Settings.Llm.FilePermission;
|
||
PermissionLabel.Text = perm;
|
||
PermissionIcon.Text = perm switch
|
||
{
|
||
"Auto" => "\uE73E",
|
||
"Deny" => "\uE711",
|
||
_ => "\uE8D7",
|
||
};
|
||
|
||
// Auto 紐⑤뱶????寃쎄퀬 ?됱긽 + 諛곕꼫 ?쒖떆
|
||
if (perm == "Auto")
|
||
{
|
||
var warnColor = new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20));
|
||
PermissionLabel.Foreground = warnColor;
|
||
PermissionIcon.Foreground = warnColor;
|
||
// Auto ?꾪솚 ??????붿뿉?쒕쭔 1???꾩닔 ?쒖떆 (湲곗〈 ??붿뿉???대? Auto??쇰㈃ ?④?)
|
||
ChatConversation? convForWarn;
|
||
lock (_convLock) convForWarn = _currentConversation;
|
||
var isExisting = convForWarn != null && convForWarn.Messages.Count > 0 && convForWarn.Permission == "Auto";
|
||
if (AutoPermissionWarning != null && !_autoWarningDismissed && !isExisting)
|
||
AutoPermissionWarning.Visibility = Visibility.Visible;
|
||
}
|
||
else
|
||
{
|
||
_autoWarningDismissed = false; // Auto媛 ?꾨땶 紐⑤뱶濡??꾪솚?섎㈃ 由ъ뀑
|
||
var defaultFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var iconFg = perm == "Deny" ? new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F))
|
||
: new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); // Ask = ?뚮??? PermissionLabel.Foreground = defaultFg;
|
||
PermissionIcon.Foreground = iconFg;
|
||
if (AutoPermissionWarning != null)
|
||
AutoPermissionWarning.Visibility = Visibility.Collapsed;
|
||
}
|
||
}
|
||
|
||
// ???? ?곗씠???쒖슜 ?섏? 硫붾돱 ????
|
||
|
||
private void BtnDataUsage_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
if (DataUsagePopup == null) return;
|
||
DataUsageItems.Children.Clear();
|
||
|
||
var options = new (string Key, string Sym, string Label, string Desc, string Color)[]
|
||
{
|
||
("active", "\uE9F5", "?곴레 ?쒖슜", "?대뜑 ??臾몄꽌瑜??먮룞 ?먯깋?섏뿬 蹂닿퀬???묒꽦???곴레 ?쒖슜?⑸땲??, "#107C10"),
|
||
("passive", "\uE8FD", "?뚭레 ?쒖슜", "?ъ슜?먭? ?붿껌???뚮쭔 ?대뜑 ?곗씠?곕? 李몄“?⑸땲??, "#D97706"),
|
||
("none", "\uE8D8", "?쒖슜?섏? ?딆쓬", "?대뜑 ??臾몄꽌瑜??쎄굅??李몄“?섏? ?딆뒿?덈떎", "#9CA3AF"),
|
||
};
|
||
|
||
foreach (var (key, sym, label, desc, color) in options)
|
||
{
|
||
var isActive = key.Equals(_folderDataUsage, StringComparison.OrdinalIgnoreCase);
|
||
|
||
var template = new ControlTemplate(typeof(Button));
|
||
var bdFactory = new FrameworkElementFactory(typeof(Border));
|
||
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
|
||
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
|
||
bdFactory.Name = "Bd";
|
||
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
|
||
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
|
||
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||
bdFactory.AppendChild(cpFactory);
|
||
template.VisualTree = bdFactory;
|
||
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
|
||
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
|
||
template.Triggers.Add(hoverTrigger);
|
||
|
||
var btn = new Button
|
||
{
|
||
Template = template,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand,
|
||
HorizontalContentAlignment = HorizontalAlignment.Left,
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
};
|
||
ApplyHoverScaleAnimation(btn, 1.02);
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
// 而ㅼ뒪? 泥댄겕 ?꾩씠肄? sp.Children.Add(CreateCheckIcon(isActive));
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = sym, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14,
|
||
Foreground = BrushFromHex(color),
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||
});
|
||
var textStack = new StackPanel();
|
||
textStack.Children.Add(new TextBlock
|
||
{
|
||
Text = label, FontSize = 13, FontWeight = FontWeights.Bold,
|
||
Foreground = BrushFromHex(color),
|
||
});
|
||
textStack.Children.Add(new TextBlock
|
||
{
|
||
Text = desc, FontSize = 11,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
MaxWidth = 240,
|
||
});
|
||
sp.Children.Add(textStack);
|
||
btn.Content = sp;
|
||
|
||
var capturedKey = key;
|
||
btn.Click += (_, _) =>
|
||
{
|
||
_folderDataUsage = capturedKey;
|
||
UpdateDataUsageUI();
|
||
SaveConversationSettings();
|
||
DataUsagePopup.IsOpen = false;
|
||
};
|
||
DataUsageItems.Children.Add(btn);
|
||
}
|
||
DataUsagePopup.IsOpen = true;
|
||
}
|
||
|
||
private void UpdateDataUsageUI()
|
||
{
|
||
if (DataUsageLabel == null || DataUsageIcon == null) return;
|
||
var (label, icon, color) = _folderDataUsage switch
|
||
{
|
||
"passive" => ("?뚭레", "\uE8FD", "#D97706"),
|
||
"none" => ("誘몄궗??, "\uE8D8", "#9CA3AF"),
|
||
_ => ("?곴레", "\uE9F5", "#107C10"),
|
||
};
|
||
DataUsageLabel.Text = label;
|
||
DataUsageIcon.Text = icon;
|
||
DataUsageIcon.Foreground = BrushFromHex(color);
|
||
}
|
||
|
||
/// <summary>Cowork/Code ??吏꾩엯 ???ㅼ젙??湲곕낯 沅뚰븳???곸슜.</summary>
|
||
private void ApplyTabDefaultPermission()
|
||
{
|
||
if (_activeTab == "Chat")
|
||
{
|
||
// Chat ?? 寃쎄퀬 諛곕꼫 ?④린怨?湲곕낯 Ask 紐⑤뱶濡?蹂듭썝
|
||
_settings.Settings.Llm.FilePermission = "Ask";
|
||
UpdatePermissionUI();
|
||
return;
|
||
}
|
||
var defaultPerm = _settings.Settings.Llm.DefaultAgentPermission;
|
||
if (!string.IsNullOrEmpty(defaultPerm))
|
||
{
|
||
_settings.Settings.Llm.FilePermission = defaultPerm;
|
||
UpdatePermissionUI();
|
||
}
|
||
}
|
||
|
||
// ??? ?뚯씪 泥⑤? ?????????????????????????????????????????????????????????
|
||
|
||
private void BtnAttach_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||
{
|
||
Multiselect = true,
|
||
Title = "泥⑤????뚯씪???좏깮?섏꽭??,
|
||
Filter = "紐⑤뱺 ?뚯씪 (*.*)|*.*|?띿뒪??(*.txt;*.md;*.csv)|*.txt;*.md;*.csv|肄붾뱶 (*.cs;*.py;*.js;*.ts)|*.cs;*.py;*.js;*.ts",
|
||
};
|
||
|
||
// ?묒뾽 ?대뜑媛 ?덉쑝硫?珥덇린 寃쎈줈 ?ㅼ젙
|
||
var workFolder = GetCurrentWorkFolder();
|
||
if (!string.IsNullOrEmpty(workFolder) && System.IO.Directory.Exists(workFolder))
|
||
dlg.InitialDirectory = workFolder;
|
||
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
foreach (var file in dlg.FileNames)
|
||
AddAttachedFile(file);
|
||
}
|
||
|
||
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"
|
||
};
|
||
|
||
private void AddAttachedFile(string filePath)
|
||
{
|
||
if (_attachedFiles.Contains(filePath)) return;
|
||
|
||
// ?뚯씪 ?ш린 ?쒗븳 (10MB)
|
||
try
|
||
{
|
||
var fi = new System.IO.FileInfo(filePath);
|
||
if (fi.Length > 10 * 1024 * 1024)
|
||
{
|
||
CustomMessageBox.Show($"?뚯씪???덈Т ?쎈땲??(10MB 珥덇낵):\n{fi.Name}", "泥⑤? ?쒗븳", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||
return;
|
||
}
|
||
|
||
// ?대?吏 ?뚯씪 ??Vision API??base64 蹂?? var ext = fi.Extension.ToLowerInvariant();
|
||
if (ImageExtensions.Contains(ext) && _settings.Settings.Llm.EnableImageInput)
|
||
{
|
||
var maxKb = _settings.Settings.Llm.MaxImageSizeKb;
|
||
if (maxKb <= 0) maxKb = 5120;
|
||
if (fi.Length > maxKb * 1024)
|
||
{
|
||
CustomMessageBox.Show($"?대?吏媛 ?덈Т ?쎈땲??({fi.Length / 1024}KB, 理쒕? {maxKb}KB).",
|
||
"?대?吏 ?ш린 珥덇낵", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||
return;
|
||
}
|
||
|
||
var bytes = System.IO.File.ReadAllBytes(filePath);
|
||
var mimeType = ext switch
|
||
{
|
||
".jpg" or ".jpeg" => "image/jpeg",
|
||
".gif" => "image/gif",
|
||
".bmp" => "image/bmp",
|
||
".webp" => "image/webp",
|
||
_ => "image/png",
|
||
};
|
||
var attachment = new ImageAttachment
|
||
{
|
||
Base64 = Convert.ToBase64String(bytes),
|
||
MimeType = mimeType,
|
||
FileName = fi.Name,
|
||
};
|
||
|
||
// 以묐났 ?뺤씤
|
||
if (_pendingImages.Any(i => i.FileName == attachment.FileName)) return;
|
||
|
||
_pendingImages.Add(attachment);
|
||
AddImagePreview(attachment);
|
||
return;
|
||
}
|
||
}
|
||
catch { return; }
|
||
|
||
_attachedFiles.Add(filePath);
|
||
RefreshAttachedFilesUI();
|
||
}
|
||
|
||
private void RemoveAttachedFile(string filePath)
|
||
{
|
||
_attachedFiles.Remove(filePath);
|
||
RefreshAttachedFilesUI();
|
||
}
|
||
|
||
private void RefreshAttachedFilesUI()
|
||
{
|
||
AttachedFilesPanel.Items.Clear();
|
||
if (_attachedFiles.Count == 0)
|
||
{
|
||
AttachedFilesPanel.Visibility = Visibility.Collapsed;
|
||
return;
|
||
}
|
||
|
||
AttachedFilesPanel.Visibility = Visibility.Visible;
|
||
var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hintBg = TryFindResource("HintBackground") as Brush ?? Brushes.LightGray;
|
||
|
||
foreach (var file in _attachedFiles.ToList())
|
||
{
|
||
var fileName = System.IO.Path.GetFileName(file);
|
||
var capturedFile = file;
|
||
|
||
var chip = new Border
|
||
{
|
||
Background = hintBg,
|
||
CornerRadius = new CornerRadius(6),
|
||
Padding = new Thickness(8, 4, 4, 4),
|
||
Margin = new Thickness(0, 0, 4, 4),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE8A5", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10,
|
||
Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = fileName, FontSize = 11, Foreground = secondaryBrush,
|
||
VerticalAlignment = VerticalAlignment.Center, MaxWidth = 150, TextTrimming = TextTrimming.CharacterEllipsis,
|
||
ToolTip = file,
|
||
});
|
||
var removeBtn = new Button
|
||
{
|
||
Content = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 8, Foreground = secondaryBrush },
|
||
Background = Brushes.Transparent, BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0),
|
||
};
|
||
removeBtn.Click += (_, _) => RemoveAttachedFile(capturedFile);
|
||
sp.Children.Add(removeBtn);
|
||
chip.Child = sp;
|
||
AttachedFilesPanel.Items.Add(chip);
|
||
}
|
||
}
|
||
|
||
/// <summary>泥⑤? ?뚯씪 ?댁슜???쒖뒪??硫붿떆吏濡?蹂?섑빀?덈떎.</summary>
|
||
private string BuildFileContextPrompt()
|
||
{
|
||
if (_attachedFiles.Count == 0) return "";
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine("\n[泥⑤? ?뚯씪 而⑦뀓?ㅽ듃]");
|
||
|
||
foreach (var file in _attachedFiles)
|
||
{
|
||
try
|
||
{
|
||
var ext = System.IO.Path.GetExtension(file).ToLowerInvariant();
|
||
var isBinary = ext is ".exe" or ".dll" or ".zip" or ".7z" or ".rar" or ".tar" or ".gz"
|
||
or ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" or ".webp" or ".svg"
|
||
or ".pdf" or ".docx" or ".xlsx" or ".pptx" or ".doc" or ".xls" or ".ppt"
|
||
or ".mp3" or ".mp4" or ".avi" or ".mov" or ".mkv" or ".wav" or ".flac"
|
||
or ".psd" or ".ai" or ".sketch" or ".fig"
|
||
or ".msi" or ".iso" or ".img" or ".bin" or ".dat" or ".db" or ".sqlite";
|
||
if (isBinary)
|
||
{
|
||
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (諛붿씠?덈━ ?뚯씪, ?댁슜 ?앸왂) ---");
|
||
continue;
|
||
}
|
||
|
||
var content = System.IO.File.ReadAllText(file);
|
||
// 理쒕? 8000?먮줈 ?쒗븳
|
||
if (content.Length > 8000)
|
||
content = content[..8000] + "\n... (?댄븯 ?앸왂)";
|
||
|
||
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} ---");
|
||
sb.AppendLine(content);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (?쎄린 ?ㅽ뙣: {ex.Message}) ---");
|
||
}
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
private void ResizeGrip_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
|
||
{
|
||
var newW = Width + e.HorizontalChange;
|
||
var newH = Height + e.VerticalChange;
|
||
if (newW >= MinWidth) Width = newW;
|
||
if (newH >= MinHeight) Height = newH;
|
||
}
|
||
|
||
// ??? ?ъ씠?쒕컮 ?좉? ???????????????????????????????????????????????????
|
||
|
||
private void BtnToggleSidebar_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
_sidebarVisible = !_sidebarVisible;
|
||
if (_sidebarVisible)
|
||
{
|
||
// ?ъ씠?쒕컮 ?닿린, ?꾩씠肄?諛??④린湲? IconBarColumn.Width = new GridLength(0);
|
||
IconBarPanel.Visibility = Visibility.Collapsed;
|
||
SidebarPanel.Visibility = Visibility.Visible;
|
||
ToggleSidebarIcon.Text = "\uE76B";
|
||
AnimateSidebar(0, 270, () => SidebarColumn.MinWidth = 200);
|
||
}
|
||
else
|
||
{
|
||
// ?ъ씠?쒕컮 ?リ린, ?꾩씠肄?諛??쒖떆
|
||
SidebarColumn.MinWidth = 0;
|
||
ToggleSidebarIcon.Text = "\uE76C";
|
||
AnimateSidebar(270, 0, () =>
|
||
{
|
||
SidebarPanel.Visibility = Visibility.Collapsed;
|
||
IconBarColumn.Width = new GridLength(52);
|
||
IconBarPanel.Visibility = Visibility.Visible;
|
||
});
|
||
}
|
||
}
|
||
|
||
private void AnimateSidebar(double from, double to, Action? onComplete = null)
|
||
{
|
||
var duration = 200.0;
|
||
var start = DateTime.UtcNow;
|
||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) };
|
||
EventHandler tickHandler = null!;
|
||
tickHandler = (_, _) =>
|
||
{
|
||
var elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
|
||
var t = Math.Min(elapsed / duration, 1.0);
|
||
t = 1 - (1 - t) * (1 - t);
|
||
SidebarColumn.Width = new GridLength(from + (to - from) * t);
|
||
if (elapsed >= duration)
|
||
{
|
||
timer.Stop();
|
||
timer.Tick -= tickHandler;
|
||
SidebarColumn.Width = new GridLength(to);
|
||
onComplete?.Invoke();
|
||
}
|
||
};
|
||
timer.Tick += tickHandler;
|
||
timer.Start();
|
||
}
|
||
|
||
// ??? ???紐⑸줉 ????????????????????????????????????????????????????????
|
||
|
||
public void RefreshConversationList()
|
||
{
|
||
var metas = _storage.LoadAllMeta();
|
||
// ?꾨━??移댄뀒怨좊━ ???꾩씠肄??됱긽 留ㅽ븨 (ChatCategory???녿뒗 肄붿썙??肄붾뱶 移댄뀒怨좊━ 吏??
|
||
var allPresets = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets)
|
||
.Concat(Services.PresetService.GetByTabWithCustom("Code", _settings.Settings.Llm.CustomPresets))
|
||
.Concat(Services.PresetService.GetByTabWithCustom("Chat", _settings.Settings.Llm.CustomPresets));
|
||
var presetMap = new Dictionary<string, (string Symbol, string Color)>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var p in allPresets)
|
||
presetMap.TryAdd(p.Category, (p.Symbol, p.Color));
|
||
|
||
var items = metas.Select(c =>
|
||
{
|
||
var symbol = ChatCategory.GetSymbol(c.Category);
|
||
var color = ChatCategory.GetColor(c.Category);
|
||
// ChatCategory 湲곕낯媛믪씠硫??꾨━?뗭뿉??寃?? if (symbol == "\uE8BD" && color == "#6B7280" && c.Category != ChatCategory.General)
|
||
{
|
||
if (presetMap.TryGetValue(c.Category, out var pm))
|
||
{
|
||
symbol = pm.Symbol;
|
||
color = pm.Color;
|
||
}
|
||
}
|
||
return new ConversationMeta
|
||
{
|
||
Id = c.Id,
|
||
Title = c.Title,
|
||
Pinned = c.Pinned,
|
||
Category = c.Category,
|
||
Symbol = symbol,
|
||
ColorHex = color,
|
||
Tab = string.IsNullOrEmpty(c.Tab) ? "Chat" : c.Tab,
|
||
UpdatedAtText = FormatDate(c.UpdatedAt),
|
||
UpdatedAt = c.UpdatedAt,
|
||
Preview = c.Preview ?? "",
|
||
ParentId = c.ParentId,
|
||
};
|
||
}).ToList();
|
||
|
||
// ???꾪꽣 ???꾩옱 ?쒖꽦 ??쓽 ??붾쭔 ?쒖떆
|
||
items = items.Where(i => i.Tab == _activeTab).ToList();
|
||
|
||
// 移댄뀒怨좊━ ?꾪꽣 ?곸슜
|
||
if (_selectedCategory == "__custom__")
|
||
{
|
||
// 而ㅼ뒪? ?꾨━?뗭쑝濡?留뚮뱺 ??붾쭔 ?쒖떆
|
||
var customCats = _settings.Settings.Llm.CustomPresets
|
||
.Select(c => $"custom_{c.Id}").ToHashSet();
|
||
items = items.Where(i => customCats.Contains(i.Category)).ToList();
|
||
}
|
||
else if (!string.IsNullOrEmpty(_selectedCategory))
|
||
items = items.Where(i => i.Category == _selectedCategory).ToList();
|
||
|
||
// 寃???꾪꽣 (?쒕ぉ + ?댁슜 誘몃━蹂닿린)
|
||
var search = SearchBox?.Text?.Trim() ?? "";
|
||
if (!string.IsNullOrEmpty(search))
|
||
items = items.Where(i =>
|
||
i.Title.Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||
i.Preview.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||
).ToList();
|
||
|
||
RenderConversationList(items);
|
||
}
|
||
|
||
private const int ConversationPageSize = 50;
|
||
private List<ConversationMeta>? _pendingConversations;
|
||
|
||
private void RenderConversationList(List<ConversationMeta> items)
|
||
{
|
||
ConversationPanel.Children.Clear();
|
||
_pendingConversations = null;
|
||
|
||
if (items.Count == 0)
|
||
{
|
||
var empty = new TextBlock
|
||
{
|
||
Text = "??붽? ?놁뒿?덈떎",
|
||
FontSize = 12,
|
||
Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
Margin = new Thickness(0, 20, 0, 0)
|
||
};
|
||
ConversationPanel.Children.Add(empty);
|
||
return;
|
||
}
|
||
|
||
// ?ㅻ뒛 / ?댁쟾 洹몃9 遺꾨━
|
||
var today = DateTime.Today;
|
||
var todayItems = items.Where(i => i.UpdatedAt.Date == today).ToList();
|
||
var olderItems = items.Where(i => i.UpdatedAt.Date < today).ToList();
|
||
|
||
var allOrdered = new List<(string Group, ConversationMeta Item)>();
|
||
foreach (var item in todayItems) allOrdered.Add(("?ㅻ뒛", item));
|
||
foreach (var item in olderItems) allOrdered.Add(("?댁쟾", item));
|
||
|
||
// 泥??섏씠吏留??뚮뜑留? var firstPage = allOrdered.Take(ConversationPageSize).ToList();
|
||
string? lastGroup = null;
|
||
foreach (var (group, item) in firstPage)
|
||
{
|
||
if (group != lastGroup) { AddGroupHeader(group); lastGroup = group; }
|
||
AddConversationItem(item);
|
||
}
|
||
|
||
// ?섎㉧吏媛 ?덉쑝硫?"??蹂닿린" 踰꾪듉
|
||
if (allOrdered.Count > ConversationPageSize)
|
||
{
|
||
_pendingConversations = items;
|
||
AddLoadMoreButton(allOrdered.Count - ConversationPageSize);
|
||
}
|
||
}
|
||
|
||
private void AddLoadMoreButton(int remaining)
|
||
{
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
var btn = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(8, 10, 8, 10),
|
||
Margin = new Thickness(6, 4, 6, 4),
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
};
|
||
var sp = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = $"??蹂닿린 ({remaining}媛??⑥쓬)",
|
||
FontSize = 12,
|
||
Foreground = accentBrush,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
});
|
||
btn.Child = sp;
|
||
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); };
|
||
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
btn.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
// ?꾩껜 紐⑸줉 ?뚮뜑留? if (_pendingConversations != null)
|
||
{
|
||
var all = _pendingConversations;
|
||
_pendingConversations = null;
|
||
ConversationPanel.Children.Clear();
|
||
|
||
var today = DateTime.Today;
|
||
var todayItems = all.Where(i => i.UpdatedAt.Date == today).ToList();
|
||
var olderItems = all.Where(i => i.UpdatedAt.Date < today).ToList();
|
||
|
||
if (todayItems.Count > 0)
|
||
{
|
||
AddGroupHeader("?ㅻ뒛");
|
||
foreach (var item in todayItems) AddConversationItem(item);
|
||
}
|
||
if (olderItems.Count > 0)
|
||
{
|
||
AddGroupHeader("?댁쟾");
|
||
foreach (var item in olderItems) AddConversationItem(item);
|
||
}
|
||
}
|
||
};
|
||
ConversationPanel.Children.Add(btn);
|
||
}
|
||
|
||
private void AddGroupHeader(string text)
|
||
{
|
||
var header = new TextBlock
|
||
{
|
||
Text = text,
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
|
||
Margin = new Thickness(8, 12, 0, 4)
|
||
};
|
||
ConversationPanel.Children.Add(header);
|
||
}
|
||
|
||
private void AddConversationItem(ConversationMeta item)
|
||
{
|
||
var isSelected = false;
|
||
lock (_convLock)
|
||
isSelected = _currentConversation?.Id == item.Id;
|
||
|
||
var isBranch = !string.IsNullOrEmpty(item.ParentId);
|
||
var border = new Border
|
||
{
|
||
Background = isSelected
|
||
? new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC))
|
||
: Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = isBranch ? new Thickness(16, 1, 0, 1) : new Thickness(0, 1, 0, 1),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
|
||
var grid = new Grid();
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
|
||
// 移댄뀒怨좊━ ?꾩씠肄?(怨좎젙 ??? ?꾩씠肄? 洹???移댄뀒怨좊━ ?됱긽)
|
||
Brush iconBrush;
|
||
if (item.Pinned)
|
||
iconBrush = Brushes.Orange;
|
||
else
|
||
{
|
||
try { iconBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(item.ColorHex)); }
|
||
catch { iconBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; }
|
||
}
|
||
var iconText = item.Pinned ? "\uE718" : !string.IsNullOrEmpty(item.ParentId) ? "\uE8A5" : item.Symbol;
|
||
if (!string.IsNullOrEmpty(item.ParentId)) iconBrush = new SolidColorBrush(Color.FromRgb(0x8B, 0x5C, 0xF6)); // 遺꾧린: 蹂대씪?? var icon = new TextBlock
|
||
{
|
||
Text = iconText,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 13,
|
||
Foreground = iconBrush,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
Grid.SetColumn(icon, 0);
|
||
grid.Children.Add(icon);
|
||
|
||
// ?쒕ぉ + ?좎쭨 (?좏깮 ???쎄컙 諛앷쾶)
|
||
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var dateColor = TryFindResource("HintText") as Brush ?? Brushes.DarkGray;
|
||
|
||
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||
var title = new TextBlock
|
||
{
|
||
Text = item.Title,
|
||
FontSize = 12.5,
|
||
Foreground = titleColor,
|
||
TextTrimming = TextTrimming.CharacterEllipsis
|
||
};
|
||
var date = new TextBlock
|
||
{
|
||
Text = item.UpdatedAtText,
|
||
FontSize = 10,
|
||
Foreground = dateColor,
|
||
Margin = new Thickness(0, 2, 0, 0)
|
||
};
|
||
stack.Children.Add(title);
|
||
stack.Children.Add(date);
|
||
Grid.SetColumn(stack, 1);
|
||
grid.Children.Add(stack);
|
||
|
||
// 移댄뀒怨좊━ 蹂寃?踰꾪듉 (?몃쾭 ???쒖떆)
|
||
var catBtn = new Button
|
||
{
|
||
Content = new TextBlock
|
||
{
|
||
Text = "\uE70F", // Edit
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 10,
|
||
Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray)
|
||
},
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Visibility = Visibility.Collapsed,
|
||
Padding = new Thickness(4),
|
||
ToolTip = _activeTab == "Cowork" ? "?묒뾽 ?좏삎" : "???二쇱젣 蹂寃?
|
||
};
|
||
var capturedId = item.Id;
|
||
catBtn.Click += (_, _) => ShowConversationMenu(capturedId);
|
||
Grid.SetColumn(catBtn, 2);
|
||
grid.Children.Add(catBtn);
|
||
|
||
// ?좏깮 ??醫뚯륫 ?≪꽱??諛? if (isSelected)
|
||
{
|
||
border.BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
border.BorderThickness = new Thickness(2, 0, 0, 0);
|
||
}
|
||
|
||
border.Child = grid;
|
||
|
||
// ?몃쾭 ?대깽????諛곌꼍 + 誘몄꽭 ?뺣?
|
||
border.RenderTransformOrigin = new Point(0.5, 0.5);
|
||
border.RenderTransform = new ScaleTransform(1, 1);
|
||
var selectedBg = new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC));
|
||
border.MouseEnter += (_, _) =>
|
||
{
|
||
if (!isSelected)
|
||
border.Background = new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF));
|
||
catBtn.Visibility = Visibility.Visible;
|
||
var st = border.RenderTransform as ScaleTransform;
|
||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||
};
|
||
border.MouseLeave += (_, _) =>
|
||
{
|
||
if (!isSelected)
|
||
border.Background = Brushes.Transparent;
|
||
catBtn.Visibility = Visibility.Collapsed;
|
||
var st = border.RenderTransform as ScaleTransform;
|
||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||
};
|
||
|
||
// ?대┃ ???대? ?좏깮????붾㈃ ?쒕ぉ ?몄쭛, ?꾨땲硫?????꾪솚
|
||
border.MouseLeftButtonDown += (_, _) =>
|
||
{
|
||
try
|
||
{
|
||
if (isSelected)
|
||
{
|
||
// ?대? ?좏깮????????쒕ぉ ?몄쭛 紐⑤뱶
|
||
EnterTitleEditMode(title, item.Id, titleColor);
|
||
return;
|
||
}
|
||
// ?ㅽ듃由щ컢 以묒씠硫?痍⑥냼
|
||
if (_isStreaming)
|
||
{
|
||
_streamCts?.Cancel();
|
||
_cursorTimer.Stop();
|
||
_typingTimer.Stop();
|
||
_elapsedTimer.Stop();
|
||
_activeStreamText = null;
|
||
_elapsedLabel = null;
|
||
_isStreaming = false;
|
||
}
|
||
var conv = _storage.Load(item.Id);
|
||
if (conv != null)
|
||
{
|
||
// Tab 蹂댁젙
|
||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||
lock (_convLock) _currentConversation = conv;
|
||
_sessionState.RememberConversation(_activeTab, conv.Id);
|
||
UpdateChatTitle();
|
||
RenderMessages();
|
||
RefreshConversationList();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogService.Error($"????꾪솚 ?ㅻ쪟: {ex.Message}");
|
||
}
|
||
};
|
||
|
||
// ?고겢由??????愿由?硫붾돱 諛붾줈 ?쒖떆
|
||
border.MouseRightButtonUp += (_, me) =>
|
||
{
|
||
me.Handled = true;
|
||
// ?좏깮?섏? ?딆? ??붾? ?고겢由?븯硫?癒쇱? ?좏깮
|
||
if (!isSelected)
|
||
{
|
||
var conv = _storage.Load(item.Id);
|
||
if (conv != null)
|
||
{
|
||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||
lock (_convLock) _currentConversation = conv;
|
||
_sessionState.RememberConversation(_activeTab, conv.Id);
|
||
UpdateChatTitle();
|
||
RenderMessages();
|
||
}
|
||
}
|
||
// Dispatcher濡?吏???몄텧 ??留덉슦???대깽???꾨즺 ??Popup ?닿린
|
||
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(item.Id)), DispatcherPriority.Input);
|
||
};
|
||
|
||
ConversationPanel.Children.Add(border);
|
||
}
|
||
|
||
// ??? ????쒕ぉ ?몃씪???몄쭛 ????????????????????????????????????????????
|
||
|
||
private void EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor)
|
||
{
|
||
try
|
||
{
|
||
// titleTb媛 ?대? 遺紐⑥뿉??遺꾨━??寃쎌슦(?몄쭛 以? 臾댁떆
|
||
var parent = titleTb.Parent as StackPanel;
|
||
if (parent == null) return;
|
||
|
||
var idx = parent.Children.IndexOf(titleTb);
|
||
if (idx < 0) return;
|
||
|
||
var editBox = new TextBox
|
||
{
|
||
Text = titleTb.Text,
|
||
FontSize = 12.5,
|
||
Foreground = titleColor,
|
||
Background = Brushes.Transparent,
|
||
BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||
CaretBrush = titleColor,
|
||
Padding = new Thickness(0),
|
||
Margin = new Thickness(0),
|
||
};
|
||
|
||
// ?덉쟾?섍쾶 ?먯떇 援먯껜: 癒쇱? ?쒓굅 ???쎌엯
|
||
parent.Children.RemoveAt(idx);
|
||
parent.Children.Insert(idx, editBox);
|
||
|
||
var committed = false;
|
||
void CommitEdit()
|
||
{
|
||
if (committed) return;
|
||
committed = true;
|
||
|
||
var newTitle = editBox.Text.Trim();
|
||
if (string.IsNullOrEmpty(newTitle)) newTitle = titleTb.Text;
|
||
|
||
titleTb.Text = newTitle;
|
||
// editBox媛 ?꾩쭅 parent???덈뒗吏 ?뺤씤 ??援먯껜
|
||
try
|
||
{
|
||
var currentIdx = parent.Children.IndexOf(editBox);
|
||
if (currentIdx >= 0)
|
||
{
|
||
parent.Children.RemoveAt(currentIdx);
|
||
parent.Children.Insert(currentIdx, titleTb);
|
||
}
|
||
}
|
||
catch { /* 遺紐④? ?대? ?댁젣??寃쎌슦 臾댁떆 */ }
|
||
|
||
var conv = _storage.Load(conversationId);
|
||
if (conv != null)
|
||
{
|
||
conv.Title = newTitle;
|
||
_storage.Save(conv);
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation?.Id == conversationId)
|
||
_currentConversation.Title = newTitle;
|
||
}
|
||
UpdateChatTitle();
|
||
}
|
||
}
|
||
|
||
void CancelEdit()
|
||
{
|
||
if (committed) return;
|
||
committed = true;
|
||
try
|
||
{
|
||
var currentIdx = parent.Children.IndexOf(editBox);
|
||
if (currentIdx >= 0)
|
||
{
|
||
parent.Children.RemoveAt(currentIdx);
|
||
parent.Children.Insert(currentIdx, titleTb);
|
||
}
|
||
}
|
||
catch { /* 遺紐④? ?대? ?댁젣??寃쎌슦 臾댁떆 */ }
|
||
}
|
||
|
||
editBox.KeyDown += (_, ke) =>
|
||
{
|
||
if (ke.Key == Key.Enter) { ke.Handled = true; CommitEdit(); }
|
||
if (ke.Key == Key.Escape) { ke.Handled = true; CancelEdit(); }
|
||
};
|
||
editBox.LostFocus += (_, _) => CommitEdit();
|
||
|
||
editBox.Focus();
|
||
editBox.SelectAll();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogService.Error($"?쒕ぉ ?몄쭛 ?ㅻ쪟: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ??? 移댄뀒怨좊━ 蹂寃??앹뾽 ??????????????????????????????????????????????
|
||
|
||
private void ShowConversationMenu(string conversationId)
|
||
{
|
||
var conv = _storage.Load(conversationId);
|
||
var isPinned = conv?.Pinned ?? false;
|
||
|
||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255));
|
||
|
||
var popup = new Popup
|
||
{
|
||
StaysOpen = false,
|
||
AllowsTransparency = true,
|
||
PopupAnimation = PopupAnimation.Fade,
|
||
Placement = PlacementMode.MousePoint,
|
||
};
|
||
|
||
var container = new Border
|
||
{
|
||
Background = bgBrush,
|
||
BorderBrush = borderBrush,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(6),
|
||
MinWidth = 200,
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black
|
||
},
|
||
};
|
||
|
||
var stack = new StackPanel();
|
||
|
||
// 硫붾돱 ??ぉ ?ы띁
|
||
Border CreateMenuItem(string icon, string text, Brush iconColor, Action onClick)
|
||
{
|
||
var item = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 7, 10, 7),
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var g = new Grid();
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
|
||
var iconTb = new TextBlock
|
||
{
|
||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(iconTb, 0);
|
||
g.Children.Add(iconTb);
|
||
|
||
var textTb = new TextBlock
|
||
{
|
||
Text = text, FontSize = 12.5, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(textTb, 1);
|
||
g.Children.Add(textTb);
|
||
|
||
item.Child = g;
|
||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); };
|
||
return item;
|
||
}
|
||
|
||
Border CreateSeparator() => new()
|
||
{
|
||
Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4),
|
||
};
|
||
|
||
// 怨좎젙/?댁젣
|
||
stack.Children.Add(CreateMenuItem(
|
||
isPinned ? "\uE77A" : "\uE718",
|
||
isPinned ? "怨좎젙 ?댁젣" : "?곷떒 怨좎젙",
|
||
TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
() =>
|
||
{
|
||
var c = _storage.Load(conversationId);
|
||
if (c != null)
|
||
{
|
||
c.Pinned = !c.Pinned;
|
||
_storage.Save(c);
|
||
lock (_convLock) { if (_currentConversation?.Id == conversationId) _currentConversation.Pinned = c.Pinned; }
|
||
RefreshConversationList();
|
||
}
|
||
}));
|
||
|
||
// ?대쫫 蹂寃? stack.Children.Add(CreateMenuItem("\uE8AC", "?대쫫 蹂寃?, secondaryText, () =>
|
||
{
|
||
// ???紐⑸줉?먯꽌 ?대떦 ??ぉ 李얠븘???몄쭛 紐⑤뱶 吏꾩엯
|
||
foreach (UIElement child in ConversationPanel.Children)
|
||
{
|
||
if (child is Border b && b.Child is Grid g)
|
||
{
|
||
foreach (UIElement gc in g.Children)
|
||
{
|
||
if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb)
|
||
{
|
||
// title怨?留ㅼ묶
|
||
if (conv != null && tb.Text == conv.Title)
|
||
{
|
||
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
EnterTitleEditMode(tb, conversationId, titleColor);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}));
|
||
|
||
// Cowork/Code ?? ?묒뾽 ?좏삎 ?쎄린 ?꾩슜 ?쒖떆
|
||
if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null)
|
||
{
|
||
var catKey = conv.Category ?? ChatCategory.General;
|
||
// ChatCategory ?먮뒗 ?꾨━?뗭뿉???꾩씠肄??쇰꺼 寃?? string catSymbol = "\uE8BD", catLabel = catKey, catColor = "#6B7280";
|
||
var chatCat = ChatCategory.All.FirstOrDefault(c => c.Key == catKey);
|
||
if (chatCat != default && chatCat.Key != ChatCategory.General)
|
||
{
|
||
catSymbol = chatCat.Symbol; catLabel = chatCat.Label; catColor = chatCat.Color;
|
||
}
|
||
else
|
||
{
|
||
var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
|
||
.FirstOrDefault(p => p.Category == catKey);
|
||
if (preset != null)
|
||
{
|
||
catSymbol = preset.Symbol; catLabel = preset.Label; catColor = preset.Color;
|
||
}
|
||
}
|
||
|
||
stack.Children.Add(CreateSeparator());
|
||
var infoSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 4, 10, 4) };
|
||
try
|
||
{
|
||
var catBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(catColor));
|
||
infoSp.Children.Add(new TextBlock
|
||
{
|
||
Text = catSymbol, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = catBrush,
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||
});
|
||
infoSp.Children.Add(new TextBlock
|
||
{
|
||
Text = catLabel, FontSize = 12, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
}
|
||
catch
|
||
{
|
||
infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText });
|
||
}
|
||
stack.Children.Add(infoSp);
|
||
}
|
||
|
||
// Chat ??쭔 遺꾨쪟 蹂寃??쒖떆 (Cowork/Code ??? 遺꾨쪟 遺덊븘??
|
||
var showCategorySection = _activeTab == "Chat";
|
||
|
||
if (showCategorySection)
|
||
{
|
||
stack.Children.Add(CreateSeparator());
|
||
|
||
// 遺꾨쪟 ?ㅻ뜑
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = "遺꾨쪟 蹂寃?,
|
||
FontSize = 10.5,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(10, 4, 0, 4),
|
||
FontWeight = FontWeights.SemiBold,
|
||
});
|
||
|
||
var currentCategory = conv?.Category ?? ChatCategory.General;
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
|
||
foreach (var (key, label, symbol, color) in ChatCategory.All)
|
||
{
|
||
var capturedKey = key;
|
||
var isCurrentCat = capturedKey == currentCategory;
|
||
|
||
// 移댄뀒怨좊━ ??ぉ (泥댄겕 ?쒖떆 ?ы븿)
|
||
var catItem = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 7, 10, 7),
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var catGrid = new Grid();
|
||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) });
|
||
|
||
var catIcon = new TextBlock
|
||
{
|
||
Text = symbol, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(catIcon, 0);
|
||
catGrid.Children.Add(catIcon);
|
||
|
||
var catText = new TextBlock
|
||
{
|
||
Text = label, FontSize = 12.5, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
FontWeight = isCurrentCat ? FontWeights.Bold : FontWeights.Normal,
|
||
};
|
||
Grid.SetColumn(catText, 1);
|
||
catGrid.Children.Add(catText);
|
||
|
||
if (isCurrentCat)
|
||
{
|
||
var check = CreateSimpleCheck(accentBrush, 14);
|
||
Grid.SetColumn(check, 2);
|
||
catGrid.Children.Add(check);
|
||
}
|
||
|
||
catItem.Child = catGrid;
|
||
catItem.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||
catItem.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
catItem.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
popup.IsOpen = false;
|
||
var c = _storage.Load(conversationId);
|
||
if (c != null)
|
||
{
|
||
c.Category = capturedKey;
|
||
var preset = Services.PresetService.GetByCategory(capturedKey);
|
||
if (preset != null)
|
||
c.SystemCommand = preset.SystemPrompt;
|
||
_storage.Save(c);
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation?.Id == conversationId)
|
||
{
|
||
_currentConversation.Category = capturedKey;
|
||
if (preset != null)
|
||
_currentConversation.SystemCommand = preset.SystemPrompt;
|
||
}
|
||
}
|
||
// ?꾩옱 ??붿쓽 移댄뀒怨좊━媛 蹂寃쎈릺硫??낅젰 ?덈궡 臾멸뎄??媛깆떊
|
||
bool isCurrent;
|
||
lock (_convLock) { isCurrent = _currentConversation?.Id == conversationId; }
|
||
if (isCurrent && preset != null && !string.IsNullOrEmpty(preset.Placeholder))
|
||
{
|
||
_promptCardPlaceholder = preset.Placeholder;
|
||
UpdateWatermarkVisibility();
|
||
if (string.IsNullOrEmpty(InputBox.Text))
|
||
{
|
||
InputWatermark.Text = preset.Placeholder;
|
||
InputWatermark.Visibility = Visibility.Visible;
|
||
}
|
||
}
|
||
else if (isCurrent)
|
||
{
|
||
ClearPromptCardPlaceholder();
|
||
}
|
||
RefreshConversationList();
|
||
}
|
||
};
|
||
stack.Children.Add(catItem);
|
||
}
|
||
} // end showCategorySection
|
||
|
||
stack.Children.Add(CreateSeparator());
|
||
|
||
// ??젣
|
||
stack.Children.Add(CreateMenuItem("\uE74D", "???????젣", Brushes.IndianRed, () =>
|
||
{
|
||
var result = CustomMessageBox.Show("????붾? ??젣?섏떆寃좎뒿?덇퉴?", "?????젣",
|
||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||
if (result != MessageBoxResult.Yes) return;
|
||
_storage.Delete(conversationId);
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation?.Id == conversationId)
|
||
{
|
||
_currentConversation = null;
|
||
MessagePanel.Children.Clear();
|
||
EmptyState.Visibility = Visibility.Visible;
|
||
UpdateChatTitle();
|
||
}
|
||
}
|
||
RefreshConversationList();
|
||
}));
|
||
|
||
container.Child = stack;
|
||
popup.Child = container;
|
||
popup.IsOpen = true;
|
||
}
|
||
|
||
// ??? 寃??????????????????????????????????????????????????????????????
|
||
|
||
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||
{
|
||
RefreshConversationList();
|
||
}
|
||
|
||
private static string FormatDate(DateTime dt)
|
||
{
|
||
var diff = DateTime.Now - dt;
|
||
if (diff.TotalMinutes < 1) return "諛⑷툑 ??;
|
||
if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes}遺???;
|
||
if (diff.TotalDays < 1) return $"{(int)diff.TotalHours}?쒓컙 ??;
|
||
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}????;
|
||
return dt.ToString("MM/dd");
|
||
}
|
||
|
||
private void UpdateChatTitle()
|
||
{
|
||
lock (_convLock)
|
||
{
|
||
ChatTitle.Text = _currentConversation?.Title ?? "";
|
||
}
|
||
}
|
||
|
||
// ??? 硫붿떆吏 ?뚮뜑留????????????????????????????????????????????????????
|
||
|
||
private void RenderMessages()
|
||
{
|
||
MessagePanel.Children.Clear();
|
||
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
|
||
if (conv == null)
|
||
{
|
||
EmptyState.Visibility = Visibility.Visible;
|
||
return;
|
||
}
|
||
|
||
var items = new List<ConversationTimelineItem>();
|
||
items.AddRange(conv.Messages
|
||
.Where(msg => msg.Role != "system")
|
||
.Select(msg => new ConversationTimelineItem
|
||
{
|
||
Timestamp = msg.Timestamp,
|
||
Message = msg,
|
||
}));
|
||
|
||
if (IsExecutionHistoryVisible())
|
||
{
|
||
items.AddRange(conv.ExecutionEvents.Select(evt => new ConversationTimelineItem
|
||
{
|
||
Timestamp = evt.Timestamp,
|
||
ExecutionEvent = evt,
|
||
}));
|
||
}
|
||
|
||
if (items.Count == 0)
|
||
{
|
||
EmptyState.Visibility = Visibility.Visible;
|
||
return;
|
||
}
|
||
|
||
EmptyState.Visibility = Visibility.Collapsed;
|
||
|
||
foreach (var item in items.OrderBy(i => i.Timestamp))
|
||
{
|
||
if (item.Message != null)
|
||
{
|
||
AddMessageBubble(item.Message.Role, item.Message.Content, animate: false, message: item.Message);
|
||
continue;
|
||
}
|
||
|
||
if (item.ExecutionEvent != null)
|
||
AddAgentEventBanner(ToAgentEvent(item.ExecutionEvent), animate: false);
|
||
}
|
||
|
||
_ = Dispatcher.InvokeAsync(() => MessageScroll.ScrollToEnd(), DispatcherPriority.Background);
|
||
}
|
||
|
||
private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null)
|
||
{
|
||
var isUser = role == "user";
|
||
|
||
if (isUser)
|
||
{
|
||
// ?ъ슜?? ?곗륫 ?뺣젹, ?낆꽱??諛곌꼍 + ?몄쭛 踰꾪듉
|
||
var wrapper = new StackPanel
|
||
{
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
MaxWidth = 540,
|
||
Margin = new Thickness(120, 6, 40, 6),
|
||
};
|
||
|
||
var bubble = new Border
|
||
{
|
||
Background = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
CornerRadius = new CornerRadius(16, 4, 16, 16),
|
||
Padding = new Thickness(16, 10, 16, 10),
|
||
Child = new TextBlock
|
||
{
|
||
Text = content,
|
||
FontSize = 13.5,
|
||
Foreground = Brushes.White,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
LineHeight = 21,
|
||
}
|
||
};
|
||
wrapper.Children.Add(bubble);
|
||
|
||
// ?≪뀡 踰꾪듉 諛?(蹂듭궗 + ?몄쭛, hover ???쒖떆)
|
||
var userActionBar = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
Opacity = 0,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
};
|
||
var capturedUserContent = content;
|
||
var userBtnColor = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
userActionBar.Children.Add(CreateActionButton("\uE8C8", "蹂듭궗", userBtnColor, () =>
|
||
{
|
||
try { Clipboard.SetText(capturedUserContent); } catch { }
|
||
}));
|
||
userActionBar.Children.Add(CreateActionButton("\uE70F", "?몄쭛", userBtnColor,
|
||
() => EnterEditMode(wrapper, capturedUserContent)));
|
||
|
||
// ??꾩뒪?ы봽 + ?≪뀡 諛? var userBottomBar = new Grid { Margin = new Thickness(0, 2, 0, 0) };
|
||
var timestamp = message?.Timestamp ?? DateTime.Now;
|
||
userBottomBar.Children.Add(new TextBlock
|
||
{
|
||
Text = timestamp.ToString("HH:mm"),
|
||
FontSize = 10, Opacity = 0.5,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 4, 0),
|
||
});
|
||
userBottomBar.Children.Add(userActionBar);
|
||
wrapper.Children.Add(userBottomBar);
|
||
wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1;
|
||
wrapper.MouseLeave += (_, _) => userActionBar.Opacity = 0;
|
||
|
||
// ?고겢由???硫붿떆吏 而⑦뀓?ㅽ듃 硫붾돱
|
||
var userContent = content;
|
||
wrapper.MouseRightButtonUp += (_, re) =>
|
||
{
|
||
re.Handled = true;
|
||
ShowMessageContextMenu(userContent, "user");
|
||
};
|
||
|
||
if (animate) ApplyMessageEntryAnimation(wrapper);
|
||
MessagePanel.Children.Add(wrapper);
|
||
}
|
||
else
|
||
{
|
||
// ?댁떆?ㅽ꽩?? 醫뚯륫 ?뺣젹, ?ㅽ겕 諛곌꼍
|
||
var container = new StackPanel
|
||
{
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
MaxWidth = GetMessageMaxWidth(),
|
||
Margin = new Thickness(40, 8, 80, 8)
|
||
};
|
||
if (animate) ApplyMessageEntryAnimation(container);
|
||
|
||
// AI ?먯씠?꾪듃 ?대쫫 + ?꾩씠肄? var (agentName, agentSymbol, agentColor) = GetAgentIdentity();
|
||
var agentBrush = BrushFromHex(agentColor);
|
||
var headerSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 4) };
|
||
|
||
// ?ㅼ씠?꾨が???щ낵 ?꾩씠肄?(?뚯쟾 ?좊땲硫붿씠??
|
||
var iconBlock = new TextBlock
|
||
{
|
||
Text = "??,
|
||
FontSize = 13,
|
||
Foreground = agentBrush,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||
RenderTransform = new RotateTransform(0),
|
||
};
|
||
if (animate)
|
||
{
|
||
var spin = new System.Windows.Media.Animation.DoubleAnimation
|
||
{
|
||
From = 0, To = 360,
|
||
Duration = TimeSpan.FromSeconds(1.2),
|
||
EasingFunction = new System.Windows.Media.Animation.CubicEase { EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut },
|
||
};
|
||
((RotateTransform)iconBlock.RenderTransform).BeginAnimation(RotateTransform.AngleProperty, spin);
|
||
}
|
||
headerSp.Children.Add(iconBlock);
|
||
|
||
headerSp.Children.Add(new TextBlock
|
||
{
|
||
Text = agentName,
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = agentBrush,
|
||
Margin = new Thickness(6, 0, 0, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
container.Children.Add(headerSp);
|
||
|
||
// 留덊겕?ㅼ슫 ?뚮뜑留?(?뚯씪 寃쎈줈 媛뺤“ ?ㅼ젙 ?곕룞)
|
||
var app = System.Windows.Application.Current as App;
|
||
MarkdownRenderer.EnableFilePathHighlight =
|
||
app?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||
var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
|
||
var mdPanel = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush);
|
||
mdPanel.Margin = new Thickness(0, 0, 0, 4);
|
||
container.Children.Add(mdPanel);
|
||
|
||
// ?≪뀡 踰꾪듉 諛?(蹂듭궗 / 醫뗭븘??/ ?レ뼱??
|
||
var actionBar = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
Margin = new Thickness(0, 6, 0, 0)
|
||
};
|
||
|
||
var btnColor = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray);
|
||
var btnHoverColor = new SolidColorBrush(Color.FromRgb(0x8B, 0x90, 0xB0));
|
||
var capturedContent = content;
|
||
|
||
actionBar.Children.Add(CreateActionButton("\uE8C8", "蹂듭궗", btnColor, () =>
|
||
{
|
||
try { Clipboard.SetText(capturedContent); } catch { }
|
||
}));
|
||
actionBar.Children.Add(CreateActionButton("\uE72C", "?ㅼ떆 ?앹꽦", btnColor, () => _ = RegenerateLastAsync()));
|
||
actionBar.Children.Add(CreateActionButton("\uE70F", "?섏젙 ???ъ떆??, btnColor, () => ShowRetryWithFeedbackInput()));
|
||
AddLinkedFeedbackButtons(actionBar, btnColor, message);
|
||
|
||
// ??꾩뒪?ы봽
|
||
var aiTimestamp = message?.Timestamp ?? DateTime.Now;
|
||
actionBar.Children.Add(new TextBlock
|
||
{
|
||
Text = aiTimestamp.ToString("HH:mm"),
|
||
FontSize = 10, Opacity = 0.5,
|
||
Foreground = btnColor,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(8, 0, 0, 0),
|
||
});
|
||
|
||
container.Children.Add(actionBar);
|
||
|
||
// ?고겢由???硫붿떆吏 而⑦뀓?ㅽ듃 硫붾돱
|
||
var aiContent = content;
|
||
container.MouseRightButtonUp += (_, re) =>
|
||
{
|
||
re.Handled = true;
|
||
ShowMessageContextMenu(aiContent, "assistant");
|
||
};
|
||
|
||
MessagePanel.Children.Add(container);
|
||
}
|
||
}
|
||
|
||
// ??? 而ㅼ뒪? 泥댄겕 ?꾩씠肄?(紐⑤뱺 ?앹뾽 硫붾돱 怨듯넻) ?????????????????????????
|
||
|
||
/// <summary>而ㅼ뒪? 泥댄겕/誘몄꽑???꾩씠肄섏쓣 ?앹꽦?⑸땲?? Path ?꾪삎 湲곕컲, ?좏깮 ???ㅼ???諛붿슫???좊땲硫붿씠??</summary>
|
||
/// <summary>?꾩옱 ?뚮쭏??泥댄겕 ?ㅽ??쇱쓣 諛섑솚?⑸땲??</summary>
|
||
private string GetCheckStyle()
|
||
{
|
||
var theme = (_settings.Settings.Launcher.Theme ?? "system").ToLowerInvariant();
|
||
return theme switch
|
||
{
|
||
"dark" or "system" => "circle", // ??+ 泥댄겕留덊겕, 諛붿슫?? "oled" => "glow", // ?ㅼ삩 湲濡쒖슦 ?? ?섏씠?쒖씤
|
||
"light" => "roundrect", // ?κ렐 ?ш컖?? ?щ씪?대뱶?? "nord" => "diamond", // ?ㅼ씠?꾨が??留덈쫫紐?, ?ㅻТ???ㅼ??? "catppuccin" => "pill", // ??紐⑥뼇, ?ㅽ봽留?諛붿슫?? "monokai" => "square", // ?뺤궗媛곹삎, ???? "sepia" => "stamp", // ?꾩옣 ?ㅽ????? ?뚯쟾 ?깆옣
|
||
"alfred" => "minimal", // 誘몃땲硫 ?? ?곗븘???섏씠?? "alfredlight" => "minimal", // 誘몃땲硫 ?? ?곗븘???섏씠?? _ => "circle",
|
||
};
|
||
}
|
||
|
||
private FrameworkElement CreateCheckIcon(bool isChecked, Brush? accentBrush = null)
|
||
{
|
||
var accent = accentBrush ?? TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
|
||
// ?ы뵆 V 泥댄겕 ???좏깮 ??而щ윭 V, 誘몄꽑????鍮?怨듦컙
|
||
if (isChecked)
|
||
{
|
||
return CreateSimpleCheck(accent, 14);
|
||
}
|
||
|
||
// 誘몄꽑?? ?숈씪 ?ш린 鍮?怨듦컙 (?뺣젹 ?좎?)
|
||
return new System.Windows.Shapes.Rectangle
|
||
{
|
||
Width = 14, Height = 14,
|
||
Fill = Brushes.Transparent,
|
||
Margin = new Thickness(0, 0, 10, 0),
|
||
};
|
||
}
|
||
|
||
/// <summary>ScaleTransform 諛붿슫???ㅼ????좊땲硫붿씠???ы띁.</summary>
|
||
private static void AnimateScale(FrameworkElement el, double from, double to, int ms, IEasingFunction ease)
|
||
{
|
||
if (el.RenderTransform is TransformGroup tg)
|
||
{
|
||
var st = tg.Children.OfType<ScaleTransform>().FirstOrDefault();
|
||
if (st != null)
|
||
{
|
||
var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease };
|
||
st.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
|
||
st.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
|
||
return;
|
||
}
|
||
}
|
||
if (el.RenderTransform is ScaleTransform scale)
|
||
{
|
||
var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease };
|
||
scale.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
|
||
scale.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
|
||
}
|
||
}
|
||
|
||
/// <summary>留덉슦???ㅻ쾭 ???댁쭩 ?뺣? + 蹂듦??섎뒗 ?몃쾭 ?좊땲硫붿씠?섏쓣 ?곸슜?⑸땲??</summary>
|
||
/// <summary>
|
||
/// 留덉슦???ㅻ쾭 ???댁쭩 ?뺣??섎뒗 ?몃쾭 ?좊땲硫붿씠??
|
||
/// 二쇱쓽: ?몄젒 ?붿냼(??踰꾪듉, 媛濡??섏뿴 硫붾돱 ???먮뒗 ?ъ슜 湲덉? ???뺣? ???댁썐 ?붿냼瑜?媛由쎈땲??
|
||
/// ?낅┰??怨듦컙???덈뒗 踰꾪듉?먮쭔 ?곸슜?섏꽭??
|
||
/// </summary>
|
||
private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
|
||
{
|
||
// Loaded ?대깽?몄뿉???ㅽ뻾?댁빞 XAML Style??遊됱씤??Transform???덉쟾?섍쾶 援먯껜 媛?? void EnsureTransform()
|
||
{
|
||
element.RenderTransformOrigin = new Point(0.5, 0.5);
|
||
// 遊됱씤(frozen)??Transform?대㈃ ?덈줈 ?앹꽦?섏뿬 援먯껜
|
||
if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
|
||
element.RenderTransform = new ScaleTransform(1, 1);
|
||
}
|
||
|
||
element.Loaded += (_, _) => EnsureTransform();
|
||
|
||
element.MouseEnter += (_, _) =>
|
||
{
|
||
EnsureTransform();
|
||
var st = (ScaleTransform)element.RenderTransform;
|
||
var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
|
||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
||
st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
|
||
st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
|
||
};
|
||
element.MouseLeave += (_, _) =>
|
||
{
|
||
EnsureTransform();
|
||
var st = (ScaleTransform)element.RenderTransform;
|
||
var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
|
||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
||
st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
|
||
st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
|
||
};
|
||
}
|
||
|
||
/// <summary>留덉슦???ㅻ쾭 ???띿뒪?멸? ?댁쭩 ??댁삤瑜대뒗 諛붿슫???좊땲硫붿씠?섏쓣 ?곸슜?⑸땲??</summary>
|
||
/// <summary>
|
||
/// 留덉슦???ㅻ쾭 ???띿뒪?멸? ?댁쭩 ??댁삤瑜대뒗 諛붿슫???좊땲硫붿씠??
|
||
/// Scale怨??щ━ ?ш린媛 蹂?섏? ?딆븘 ?몄젒 ?붿냼瑜?媛由ъ? ?딆뒿?덈떎.
|
||
/// </summary>
|
||
private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
|
||
{
|
||
void EnsureTransform()
|
||
{
|
||
if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
|
||
element.RenderTransform = new TranslateTransform(0, 0);
|
||
}
|
||
|
||
element.Loaded += (_, _) => EnsureTransform();
|
||
|
||
element.MouseEnter += (_, _) =>
|
||
{
|
||
EnsureTransform();
|
||
var tt = (TranslateTransform)element.RenderTransform;
|
||
tt.BeginAnimation(TranslateTransform.YProperty,
|
||
new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
|
||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
|
||
};
|
||
element.MouseLeave += (_, _) =>
|
||
{
|
||
EnsureTransform();
|
||
var tt = (TranslateTransform)element.RenderTransform;
|
||
tt.BeginAnimation(TranslateTransform.YProperty,
|
||
new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
|
||
{ EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 10 } });
|
||
};
|
||
}
|
||
|
||
/// <summary>?ы뵆??V 泥댄겕 ?꾩씠肄섏쓣 ?앹꽦?⑸땲??(?붿옄???듭씪??.</summary>
|
||
private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
|
||
{
|
||
return new System.Windows.Shapes.Path
|
||
{
|
||
Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
|
||
Stroke = color,
|
||
StrokeThickness = 2,
|
||
StrokeStartLineCap = PenLineCap.Round,
|
||
StrokeEndLineCap = PenLineCap.Round,
|
||
StrokeLineJoin = PenLineJoin.Round,
|
||
Width = size,
|
||
Height = size,
|
||
Margin = new Thickness(0, 0, 10, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
}
|
||
|
||
/// <summary>?앹뾽 硫붾돱 ??ぉ???몃쾭 諛곌꼍??+ 誘몄꽭 ?뺣? ?④낵瑜??곸슜?⑸땲??</summary>
|
||
private static void ApplyMenuItemHover(Border item)
|
||
{
|
||
var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
|
||
if (originalBg.CanFreeze) originalBg.Freeze();
|
||
item.RenderTransformOrigin = new Point(0.5, 0.5);
|
||
item.RenderTransform = new ScaleTransform(1, 1);
|
||
item.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b)
|
||
{
|
||
// ?먮옒 諛곌꼍???щ챸?대㈃ 諛섑닾紐??곗깋, ?꾨땲硫?諛앷린 蹂寃? if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
|
||
b.Opacity = 0.85;
|
||
else
|
||
b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||
}
|
||
var st = item.RenderTransform as ScaleTransform;
|
||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||
};
|
||
item.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b)
|
||
{
|
||
b.Opacity = 1.0;
|
||
b.Background = originalBg;
|
||
}
|
||
var st = item.RenderTransform as ScaleTransform;
|
||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||
};
|
||
}
|
||
|
||
private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
|
||
{
|
||
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var icon = new TextBlock
|
||
{
|
||
Text = symbol,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12,
|
||
Foreground = foreground,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
var btn = new Button
|
||
{
|
||
Content = icon,
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(6, 4, 6, 4),
|
||
Margin = new Thickness(0, 0, 4, 0),
|
||
ToolTip = tooltip
|
||
};
|
||
btn.MouseEnter += (_, _) => icon.Foreground = hoverBrush;
|
||
btn.MouseLeave += (_, _) => icon.Foreground = foreground;
|
||
btn.Click += (_, _) => onClick();
|
||
ApplyHoverScaleAnimation(btn, 1.15);
|
||
return btn;
|
||
}
|
||
|
||
/// <summary>醫뗭븘???レ뼱???좉? ?쇰뱶諛?踰꾪듉 (?곹깭 ?곴뎄 ???</summary>
|
||
private Button CreateFeedbackButton(string outline, string filled, string tooltip,
|
||
Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "",
|
||
Action? resetSibling = null, Action<Action>? registerReset = null)
|
||
{
|
||
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var isActive = message?.Feedback == feedbackType;
|
||
var icon = new TextBlock
|
||
{
|
||
Text = isActive ? filled : outline,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12,
|
||
Foreground = isActive ? activeColor : normalColor,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||
RenderTransform = new ScaleTransform(1, 1)
|
||
};
|
||
var btn = new Button
|
||
{
|
||
Content = icon,
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(6, 4, 6, 4),
|
||
Margin = new Thickness(0, 0, 4, 0),
|
||
ToolTip = tooltip
|
||
};
|
||
// ?곷? 踰꾪듉??由ъ뀑?????덈룄濡??깅줉
|
||
registerReset?.Invoke(() =>
|
||
{
|
||
isActive = false;
|
||
icon.Text = outline;
|
||
icon.Foreground = normalColor;
|
||
});
|
||
btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; };
|
||
btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; };
|
||
btn.Click += (_, _) =>
|
||
{
|
||
isActive = !isActive;
|
||
icon.Text = isActive ? filled : outline;
|
||
icon.Foreground = isActive ? activeColor : normalColor;
|
||
|
||
// ?곹샇 諛고?: ?쒖꽦????諛섎?履?由ъ뀑
|
||
if (isActive) resetSibling?.Invoke();
|
||
|
||
// ?쇰뱶諛??곹깭 ??? if (message != null)
|
||
{
|
||
message.Feedback = isActive ? feedbackType : null;
|
||
try
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv != null) _storage.Save(conv);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
// 諛붿슫???좊땲硫붿씠?? var scale = (ScaleTransform)icon.RenderTransform;
|
||
var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250))
|
||
{ EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 5 } };
|
||
scale.BeginAnimation(ScaleTransform.ScaleXProperty, bounce);
|
||
scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce);
|
||
};
|
||
return btn;
|
||
}
|
||
|
||
/// <summary>醫뗭븘???レ뼱??踰꾪듉???곹샇 諛고?濡??곌껐?섏뿬 異붽?</summary>
|
||
private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message)
|
||
{
|
||
// resetSibling???섏쨷???ㅼ젙?섎?濡?Action ?섑띁濡?媛꾩젒 李몄“
|
||
Action? resetLikeAction = null;
|
||
Action? resetDislikeAction = null;
|
||
|
||
var likeBtn = CreateFeedbackButton("\uE8E1", "\uEB51", "醫뗭븘??, btnColor,
|
||
new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), message, "like",
|
||
resetSibling: () => resetDislikeAction?.Invoke(),
|
||
registerReset: reset => resetLikeAction = reset);
|
||
var dislikeBtn = CreateFeedbackButton("\uE8E0", "\uEB50", "?レ뼱??, btnColor,
|
||
new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), message, "dislike",
|
||
resetSibling: () => resetLikeAction?.Invoke(),
|
||
registerReset: reset => resetDislikeAction = reset);
|
||
|
||
actionBar.Children.Add(likeBtn);
|
||
actionBar.Children.Add(dislikeBtn);
|
||
}
|
||
|
||
// ??? 硫붿떆吏 ?깆옣 ?좊땲硫붿씠????????????????????????????????????????????
|
||
|
||
private static void ApplyMessageEntryAnimation(FrameworkElement element)
|
||
{
|
||
element.Opacity = 0;
|
||
element.RenderTransform = new TranslateTransform(0, 16);
|
||
element.BeginAnimation(UIElement.OpacityProperty,
|
||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350))
|
||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
|
||
((TranslateTransform)element.RenderTransform).BeginAnimation(
|
||
TranslateTransform.YProperty,
|
||
new DoubleAnimation(16, 0, TimeSpan.FromMilliseconds(400))
|
||
{ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
|
||
}
|
||
|
||
// ??? 硫붿떆吏 ?몄쭛 ??????????????????????????????????????????????????????
|
||
|
||
private bool _isEditing; // ?몄쭛 紐⑤뱶 以묐났 諛⑹?
|
||
|
||
private void EnterEditMode(StackPanel wrapper, string originalText)
|
||
{
|
||
if (_isStreaming || _isEditing) return;
|
||
_isEditing = true;
|
||
|
||
// wrapper ?꾩튂(?몃뜳?? 湲곗뼲
|
||
var idx = MessagePanel.Children.IndexOf(wrapper);
|
||
if (idx < 0) { _isEditing = false; return; }
|
||
|
||
// ?몄쭛 UI ?앹꽦
|
||
var editPanel = new StackPanel
|
||
{
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
MaxWidth = 540,
|
||
Margin = wrapper.Margin,
|
||
};
|
||
|
||
var editBox = new TextBox
|
||
{
|
||
Text = originalText,
|
||
FontSize = 13.5,
|
||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.DarkGray,
|
||
CaretBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
BorderThickness = new Thickness(1.5),
|
||
Padding = new Thickness(14, 10, 14, 10),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
AcceptsReturn = false,
|
||
MaxHeight = 200,
|
||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||
};
|
||
// ?κ렐 紐⑥꽌由? var editBorder = new Border
|
||
{
|
||
CornerRadius = new CornerRadius(14),
|
||
Child = editBox,
|
||
ClipToBounds = true,
|
||
};
|
||
editPanel.Children.Add(editBorder);
|
||
|
||
// 踰꾪듉 諛? var btnBar = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
Margin = new Thickness(0, 6, 0, 0),
|
||
};
|
||
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||
var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
// 痍⑥냼 踰꾪듉
|
||
var cancelBtn = new Button
|
||
{
|
||
Content = new TextBlock { Text = "痍⑥냼", FontSize = 12, Foreground = secondaryBrush },
|
||
Background = Brushes.Transparent,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(12, 5, 12, 5),
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
};
|
||
cancelBtn.Click += (_, _) =>
|
||
{
|
||
_isEditing = false;
|
||
if (idx >= 0 && idx < MessagePanel.Children.Count)
|
||
MessagePanel.Children[idx] = wrapper; // ?먮옒 踰꾨툝 蹂듭썝
|
||
};
|
||
btnBar.Children.Add(cancelBtn);
|
||
|
||
// ?꾩넚 踰꾪듉
|
||
var sendBtn = new Button
|
||
{
|
||
Cursor = Cursors.Hand,
|
||
Padding = new Thickness(0),
|
||
};
|
||
sendBtn.Template = (ControlTemplate)System.Windows.Markup.XamlReader.Parse(
|
||
"<ControlTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' TargetType='Button'>" +
|
||
"<Border Background='" + (accentBrush is SolidColorBrush sb ? sb.Color.ToString() : "#4B5EFC") + "' CornerRadius='8' Padding='12,5,12,5'>" +
|
||
"<TextBlock Text='?꾩넚' FontSize='12' Foreground='White' HorizontalAlignment='Center'/>" +
|
||
"</Border></ControlTemplate>");
|
||
sendBtn.Click += (_, _) =>
|
||
{
|
||
var newText = editBox.Text.Trim();
|
||
if (!string.IsNullOrEmpty(newText))
|
||
_ = SubmitEditAsync(idx, newText);
|
||
};
|
||
btnBar.Children.Add(sendBtn);
|
||
|
||
editPanel.Children.Add(btnBar);
|
||
|
||
// 湲곗〈 wrapper ??editPanel 援먯껜
|
||
MessagePanel.Children[idx] = editPanel;
|
||
|
||
// Enter ?ㅻ줈???꾩넚
|
||
editBox.KeyDown += (_, ke) =>
|
||
{
|
||
if (ke.Key == Key.Enter && Keyboard.Modifiers == ModifierKeys.None)
|
||
{
|
||
ke.Handled = true;
|
||
var newText = editBox.Text.Trim();
|
||
if (!string.IsNullOrEmpty(newText))
|
||
_ = SubmitEditAsync(idx, newText);
|
||
}
|
||
if (ke.Key == Key.Escape)
|
||
{
|
||
ke.Handled = true;
|
||
_isEditing = false;
|
||
if (idx >= 0 && idx < MessagePanel.Children.Count)
|
||
MessagePanel.Children[idx] = wrapper;
|
||
}
|
||
};
|
||
|
||
editBox.Focus();
|
||
editBox.SelectAll();
|
||
}
|
||
|
||
private async Task SubmitEditAsync(int bubbleIndex, string newText)
|
||
{
|
||
_isEditing = false;
|
||
if (_isStreaming) return;
|
||
|
||
ChatConversation conv;
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation == null) return;
|
||
conv = _currentConversation;
|
||
}
|
||
|
||
// bubbleIndex???대떦?섎뒗 user 硫붿떆吏 李얘린
|
||
// UI??children 以?user 硫붿떆吏媛 ?꾨땶 寃?system)? ?ㅽ궢?? // ?곗씠??紐⑤뜽?먯꽌 ?대떦 ?꾩튂??user 硫붿떆吏 李얘린
|
||
int userMsgIdx = -1;
|
||
int uiIdx = 0;
|
||
lock (_convLock)
|
||
{
|
||
for (int i = 0; i < conv.Messages.Count; i++)
|
||
{
|
||
if (conv.Messages[i].Role == "system") continue;
|
||
if (uiIdx == bubbleIndex)
|
||
{
|
||
userMsgIdx = i;
|
||
break;
|
||
}
|
||
uiIdx++;
|
||
}
|
||
}
|
||
|
||
if (userMsgIdx < 0) return;
|
||
|
||
// ?곗씠??紐⑤뜽?먯꽌 ?몄쭛??硫붿떆吏 ?댄썑 紐⑤몢 ?쒓굅
|
||
lock (_convLock)
|
||
{
|
||
conv.Messages[userMsgIdx].Content = newText;
|
||
while (conv.Messages.Count > userMsgIdx + 1)
|
||
conv.Messages.RemoveAt(conv.Messages.Count - 1);
|
||
}
|
||
|
||
// UI?먯꽌 ?몄쭛??踰꾨툝 ?댄썑 紐⑤몢 ?쒓굅
|
||
while (MessagePanel.Children.Count > bubbleIndex + 1)
|
||
MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1);
|
||
|
||
// ?몄쭛??硫붿떆吏瑜???踰꾨툝濡?援먯껜
|
||
MessagePanel.Children.RemoveAt(bubbleIndex);
|
||
AddMessageBubble("user", newText, animate: false);
|
||
|
||
// 留덉?留??꾩튂???쎌엯?섎룄濡?議곗젙 (AddMessageBubble? ?앹뿉 異붽???
|
||
// bubbleIndex媛 ?앹씠 ?꾨땲硫??대룞 ????寃쎌슦 ?댄썑媛 ???쒓굅?섏뿀?쇰?濡??앹뿉 異붽???
|
||
// AI ?ъ쓳?? await SendRegenerateAsync(conv);
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"???????ㅽ뙣: {ex.Message}"); }
|
||
RefreshConversationList();
|
||
}
|
||
|
||
// ??? ?ㅽ듃由щ컢 而ㅼ꽌 源쒕묀??+ AI ?꾩씠肄??꾩뒪 ????????????????????????????
|
||
|
||
private void StopAiIconPulse()
|
||
{
|
||
if (_aiIconPulseStopped || _activeAiIcon == null) return;
|
||
_activeAiIcon.BeginAnimation(UIElement.OpacityProperty, null);
|
||
_activeAiIcon.Opacity = 1.0;
|
||
_activeAiIcon = null;
|
||
_aiIconPulseStopped = true;
|
||
}
|
||
|
||
private void CursorTimer_Tick(object? sender, EventArgs e)
|
||
{
|
||
_cursorVisible = !_cursorVisible;
|
||
// 而ㅼ꽌 ?곹깭留??좉? ???ㅼ젣 ?띿뒪??媛깆떊? _typingTimer媛 ?대떦
|
||
if (_activeStreamText != null && _displayedLength > 0)
|
||
{
|
||
var displayed = _cachedStreamContent.Length > 0
|
||
? _cachedStreamContent[..Math.Min(_displayedLength, _cachedStreamContent.Length)]
|
||
: "";
|
||
_activeStreamText.Text = displayed + (_cursorVisible ? "\u258c" : " ");
|
||
}
|
||
}
|
||
|
||
private void ElapsedTimer_Tick(object? sender, EventArgs e)
|
||
{
|
||
var elapsed = DateTime.UtcNow - _streamStartTime;
|
||
var sec = (int)elapsed.TotalSeconds;
|
||
if (_elapsedLabel != null)
|
||
_elapsedLabel.Text = $"{sec}s";
|
||
|
||
// ?섎떒 ?곹깭諛??쒓컙 媛깆떊
|
||
if (StatusElapsed != null)
|
||
StatusElapsed.Text = $"{sec}珥?;
|
||
}
|
||
|
||
private void TypingTimer_Tick(object? sender, EventArgs e)
|
||
{
|
||
if (_activeStreamText == null || string.IsNullOrEmpty(_cachedStreamContent)) return;
|
||
|
||
var targetLen = _cachedStreamContent.Length;
|
||
if (_displayedLength >= targetLen) return;
|
||
|
||
// 踰꾪띁???볦씤 誘명몴??湲???섏뿉 ?곕씪 ?띾룄 ?곸쓳
|
||
var pending = targetLen - _displayedLength;
|
||
int step;
|
||
if (pending > 200) step = Math.Min(pending / 5, 40); // ???踰꾪띁: 鍮좊Ⅴ寃??곕씪?↔린
|
||
else if (pending > 50) step = Math.Min(pending / 4, 15); // 以묎컙 踰꾪띁: ?곷떦??媛?? else step = Math.Min(3, pending); // ?뚮웾: ?먯뿰?ㅻ윭??1~3??
|
||
_displayedLength += step;
|
||
|
||
var displayed = _cachedStreamContent[.._displayedLength];
|
||
_activeStreamText.Text = displayed + (_cursorVisible ? "\u258c" : " ");
|
||
|
||
// ?ㅽ듃由щ컢 以묒뿉??利됱떆 ?ㅽ겕濡?(遺?쒕윭???좊땲硫붿씠?섏? 吏???좊컻)
|
||
if (!_userScrolled)
|
||
MessageScroll.ScrollToVerticalOffset(MessageScroll.ScrollableHeight);
|
||
}
|
||
|
||
// ??? ?꾩넚 ??????????????????????????????????????????????????????????????
|
||
|
||
public void SendInitialMessage(string message)
|
||
{
|
||
StartNewConversation();
|
||
InputBox.Text = message;
|
||
_ = SendMessageAsync();
|
||
}
|
||
|
||
private void StartNewConversation()
|
||
{
|
||
// ?꾩옱 ??붽? ?덉쑝硫????????????쒖옉
|
||
lock (_convLock)
|
||
{
|
||
_sessionState.SaveCurrentConversation(_storage, _activeTab);
|
||
_currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings);
|
||
_currentConversation.Messages.Clear();
|
||
_currentConversation.ExecutionEvents.Clear();
|
||
_currentConversation.DraftQueue.Clear();
|
||
_currentConversation.DraftQueueItems.Clear();
|
||
_currentConversation.ShowExecutionHistory = true;
|
||
_currentConversation.Id = Guid.NewGuid().ToString("N");
|
||
_currentConversation.Title = "?????;
|
||
_currentConversation.CreatedAt = DateTime.Now;
|
||
_currentConversation.UpdatedAt = DateTime.Now;
|
||
_currentConversation.Preview = "";
|
||
_currentConversation.ParentId = null;
|
||
_currentConversation.BranchLabel = null;
|
||
_currentConversation.BranchAtIndex = null;
|
||
}
|
||
// ??湲곗뼲 珥덇린??(????붿씠誘濡?
|
||
_sessionState.RememberConversation(_activeTab, null);
|
||
MessagePanel.Children.Clear();
|
||
EmptyState.Visibility = Visibility.Visible;
|
||
_attachedFiles.Clear();
|
||
RefreshAttachedFilesUI();
|
||
UpdateChatTitle();
|
||
RefreshConversationList();
|
||
UpdateFolderBar();
|
||
if (_activeTab == "Cowork") BuildBottomBar();
|
||
}
|
||
|
||
/// <summary>?ㅼ젙????λ맂 ??퀎 留덉?留????ID瑜?蹂듭썝?섍퀬, ?꾩옱 ??쓽 ??붾? 濡쒕뱶?⑸땲??</summary>
|
||
private void RestoreLastConversations()
|
||
{
|
||
_sessionState.Load(_settings);
|
||
_currentConversation = null;
|
||
|
||
// ?꾩옱 ?쒖꽦 ??쓽 ??붾? 利됱떆 濡쒕뱶
|
||
var currentTabId = _sessionState.GetConversationId(_activeTab);
|
||
if (!string.IsNullOrEmpty(currentTabId))
|
||
{
|
||
var conv = _storage.Load(currentTabId);
|
||
if (conv != null)
|
||
{
|
||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||
lock (_convLock) _currentConversation = conv;
|
||
LoadConversationSettings();
|
||
RenderMessages();
|
||
UpdateChatTitle();
|
||
UpdateFolderBar();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
lock (_convLock)
|
||
_currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings);
|
||
}
|
||
}
|
||
|
||
/// <summary>?꾩옱 _tabConversationId瑜??ㅼ젙????ν빀?덈떎.</summary>
|
||
private void SaveLastConversations()
|
||
{
|
||
try { _sessionState.Save(_settings); } catch { }
|
||
}
|
||
|
||
private async void BtnSend_Click(object sender, RoutedEventArgs e) => await SendMessageAsync();
|
||
|
||
private async void InputBox_PreviewKeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
// Ctrl+V: ?대┰蹂대뱶 ?대?吏 遺숈뿬?j린
|
||
if (e.Key == Key.V && Keyboard.Modifiers.HasFlag(ModifierKeys.Control))
|
||
{
|
||
if (TryPasteClipboardImage())
|
||
{
|
||
e.Handled = true;
|
||
return;
|
||
}
|
||
// ?대?吏媛 ?꾨땲硫?湲곕낯 ?띿뒪??遺숈뿬?j린濡??꾩엫
|
||
}
|
||
|
||
if (e.Key == Key.Enter)
|
||
{
|
||
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
|
||
{
|
||
// Shift+Enter ??以꾨컮轅?(AcceptsReturn=true?대?濡?湲곕낯 ?숈옉?쇰줈 ?꾩엫)
|
||
return;
|
||
}
|
||
|
||
// ?щ옒???앹뾽???대젮 ?덉쑝硫??좏깮????ぉ ?ㅽ뻾
|
||
if (SlashPopup.IsOpen && _slashSelectedIndex >= 0)
|
||
{
|
||
e.Handled = true;
|
||
ExecuteSlashSelectedItem();
|
||
return;
|
||
}
|
||
|
||
// /help 吏곸젒 ?낅젰 ???꾩?留?李??쒖떆
|
||
if (InputBox.Text.Trim().Equals("/help", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
e.Handled = true;
|
||
InputBox.Text = "";
|
||
SlashPopup.IsOpen = false;
|
||
ShowSlashHelpWindow();
|
||
return;
|
||
}
|
||
|
||
// Enter留???硫붿떆吏 ?꾩넚
|
||
e.Handled = true;
|
||
await SendMessageAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>?대┰蹂대뱶???대?吏媛 ?덉쑝硫?遺숈뿬?j린. ?깃났 ??true.</summary>
|
||
private bool TryPasteClipboardImage()
|
||
{
|
||
if (!_settings.Settings.Llm.EnableImageInput) return false;
|
||
if (!Clipboard.ContainsImage()) return false;
|
||
|
||
try
|
||
{
|
||
var img = Clipboard.GetImage();
|
||
if (img == null) return false;
|
||
|
||
// base64 ?몄퐫?? var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(img));
|
||
using var ms = new System.IO.MemoryStream();
|
||
encoder.Save(ms);
|
||
var bytes = ms.ToArray();
|
||
|
||
// ?ш린 ?쒗븳 ?뺤씤
|
||
var maxKb = _settings.Settings.Llm.MaxImageSizeKb;
|
||
if (maxKb <= 0) maxKb = 5120;
|
||
if (bytes.Length > maxKb * 1024)
|
||
{
|
||
CustomMessageBox.Show($"?대?吏媛 ?덈Т ?쎈땲??({bytes.Length / 1024}KB, 理쒕? {maxKb}KB).",
|
||
"?대?吏 ?ш린 珥덇낵", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||
return true; // 泥섎━??(?먮윭?댁?留??대?吏???
|
||
}
|
||
|
||
var base64 = Convert.ToBase64String(bytes);
|
||
var attachment = new ImageAttachment
|
||
{
|
||
Base64 = base64,
|
||
MimeType = "image/png",
|
||
FileName = $"clipboard_{DateTime.Now:HHmmss}.png",
|
||
};
|
||
|
||
_pendingImages.Add(attachment);
|
||
AddImagePreview(attachment, img);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Services.LogService.Debug($"?대┰蹂대뱶 ?대?吏 遺숈뿬?j린 ?ㅽ뙣: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>?대?吏 誘몃━蹂닿린 UI 異붽?.</summary>
|
||
private void AddImagePreview(ImageAttachment attachment, System.Windows.Media.Imaging.BitmapSource? thumbnail = null)
|
||
{
|
||
var secondaryBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hintBg = TryFindResource("HintBackground") as Brush ?? Brushes.LightGray;
|
||
|
||
var chip = new Border
|
||
{
|
||
Background = hintBg,
|
||
CornerRadius = new CornerRadius(6),
|
||
Padding = new Thickness(4),
|
||
Margin = new Thickness(0, 0, 4, 4),
|
||
};
|
||
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
|
||
// ?몃꽕???대?吏
|
||
if (thumbnail != null)
|
||
{
|
||
sp.Children.Add(new System.Windows.Controls.Image
|
||
{
|
||
Source = thumbnail,
|
||
MaxHeight = 48, MaxWidth = 64,
|
||
Stretch = Stretch.Uniform,
|
||
Margin = new Thickness(0, 0, 4, 0),
|
||
});
|
||
}
|
||
else
|
||
{
|
||
// base64?먯꽌 ?몃꽕???앹꽦
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE8B9", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 16,
|
||
Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(2, 0, 4, 0),
|
||
});
|
||
}
|
||
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = attachment.FileName, FontSize = 10, Foreground = secondaryBrush,
|
||
VerticalAlignment = VerticalAlignment.Center, MaxWidth = 100,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
});
|
||
|
||
var capturedAttachment = attachment;
|
||
var capturedChip = chip;
|
||
var removeBtn = new Button
|
||
{
|
||
Content = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 8, Foreground = secondaryBrush },
|
||
Background = Brushes.Transparent, BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0),
|
||
};
|
||
removeBtn.Click += (_, _) =>
|
||
{
|
||
_pendingImages.Remove(capturedAttachment);
|
||
AttachedFilesPanel.Items.Remove(capturedChip);
|
||
if (_pendingImages.Count == 0 && _attachedFiles.Count == 0)
|
||
AttachedFilesPanel.Visibility = Visibility.Collapsed;
|
||
};
|
||
sp.Children.Add(removeBtn);
|
||
chip.Child = sp;
|
||
|
||
AttachedFilesPanel.Items.Add(chip);
|
||
AttachedFilesPanel.Visibility = Visibility.Visible;
|
||
}
|
||
|
||
// ??? ?щ옒??紐낅졊??????????????????????????????????????????????????????
|
||
|
||
// ?? ?щ옒??紐낅졊???앹뾽 ?섏씠吏???
|
||
private int SlashPageSize => Math.Clamp(_settings.Settings.Llm.SlashPopupPageSize, 3, 20);
|
||
private List<(string Cmd, string Label, bool IsSkill)> _slashAllMatches = [];
|
||
private int _slashPageOffset = 0;
|
||
private int _slashSelectedIndex = -1; // ?앹뾽 ???ㅻ낫???좏깮 ?몃뜳??(?섏씠吏 ???곷?)
|
||
|
||
// ?? ?щ옒??紐낅졊??移???
|
||
private string? _activeSlashCmd = null;
|
||
|
||
// ?? ?щ옒??紐낅졊??(??퀎 遺꾨쪟) ??
|
||
|
||
/// <summary>怨듯넻 ?щ옒??紐낅졊????紐⑤뱺 ??뿉???ъ슜 媛??</summary>
|
||
private static readonly Dictionary<string, (string Label, string SystemPrompt, string Tab)> SlashCommands = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
// 怨듯넻
|
||
["/summary"] = ("Summary", "?ъ슜?먭? ?쒓났???댁슜???듭떖 ?ъ씤???꾩<濡?媛꾧껐?섍쾶 ?붿빟??二쇱꽭?? 遺덈┸ ?ъ씤???뺤떇???ъ슜?섏꽭??", "all"),
|
||
["/translate"] = ("Translate", "?ъ슜?먭? ?쒓났???띿뒪?몃? ?곸뼱濡?踰덉뿭??二쇱꽭?? ?먮Ц???ㅺ낵 ?섏븰?ㅻ? ?좎??섏꽭??", "all"),
|
||
["/explain"] = ("Explain", "?ъ슜?먭? ?쒓났???댁슜???쎄퀬 ?먯꽭?섍쾶 ?ㅻ챸??二쇱꽭?? ?꾩슂?섎㈃ ?덉떆瑜??ы븿?섏꽭??", "all"),
|
||
["/fix"] = ("Fix", "?ъ슜?먭? ?쒓났???띿뒪?몄쓽 留욎땄踰? 臾몃쾿, ?먯뿰?ㅻ윭???쒗쁽??援먯젙??二쇱꽭?? ?섏젙 ?ы빆??紐낇솗???쒖떆?섏꽭??", "all"),
|
||
|
||
// Cowork/Code ?꾩슜
|
||
["/review"] = ("Code Review", "?묒뾽 ?대뜑??git diff瑜?遺꾩꽍?섏뿬 肄붾뱶 由щ럭瑜??섑뻾??二쇱꽭?? code_review ?꾧뎄瑜??ъ슜?섏꽭??", "dev"),
|
||
["/pr"] = ("PR Summary", "?묒뾽 ?대뜑??蹂寃쎌궗??쓣 PR ?ㅻ챸 ?뺤떇?쇰줈 ?붿빟??二쇱꽭?? code_review(action: pr_summary) ?꾧뎄瑜??ъ슜?섏꽭??", "dev"),
|
||
["/test"] = ("Test", "?묒뾽 ?대뜑??肄붾뱶??????⑥쐞 ?뚯뒪?몃? ?앹꽦??二쇱꽭?? test_loop ?꾧뎄瑜??ъ슜?섏꽭??", "dev"),
|
||
["/structure"] = ("Structure", "?묒뾽 ?대뜑???꾨줈?앺듃 援ъ“瑜?遺꾩꽍?섍퀬 ?ㅻ챸??二쇱꽭?? folder_map ?꾧뎄瑜??ъ슜?섏꽭??", "dev"),
|
||
["/build"] = ("Build", "?묒뾽 ?대뜑???꾨줈?앺듃瑜?鍮뚮뱶??二쇱꽭?? build_run ?꾧뎄瑜??ъ슜?섏꽭??", "dev"),
|
||
["/search"] = ("Search", "?묒뾽 ?대뜑?먯꽌 愿??肄붾뱶瑜?寃?됲빐 二쇱꽭?? search_codebase ?꾧뎄瑜??ъ슜?섏꽭??", "dev"),
|
||
|
||
// ?뱀닔
|
||
["/help"] = ("Help", "__HELP__", "all"),
|
||
};
|
||
|
||
private void InputBox_TextChanged(object sender, TextChangedEventArgs e)
|
||
{
|
||
UpdateWatermarkVisibility();
|
||
var text = InputBox.Text;
|
||
|
||
// 移⑹씠 ?쒖꽦?붾맂 ?곹깭?먯꽌 ?ъ슜?먭? /瑜???댄븨?섎㈃ 移??댁젣
|
||
if (_activeSlashCmd != null && text.StartsWith("/"))
|
||
HideSlashChip(restoreText: false);
|
||
|
||
if (text.StartsWith("/") && !text.Contains(' '))
|
||
{
|
||
// ??퀎 ?꾪꽣留? Chat ??"all"留? Cowork/Code ??"all" + "dev"
|
||
bool isDev = _activeTab is "Cowork" or "Code";
|
||
|
||
// ?댁옣 ?щ옒??紐낅졊??留ㅼ묶 (???꾪꽣)
|
||
var matches = SlashCommands
|
||
.Where(kv => kv.Key.StartsWith(text, StringComparison.OrdinalIgnoreCase))
|
||
.Where(kv => kv.Value.Tab == "all" || (isDev && kv.Value.Tab == "dev"))
|
||
.Select(kv => (Cmd: kv.Key, Label: kv.Value.Label, IsSkill: false))
|
||
.ToList();
|
||
|
||
// ?ㅽ궗 ?щ옒??紐낅졊??留ㅼ묶 (??퀎 ?꾪꽣)
|
||
if (_settings.Settings.Llm.EnableSkillSystem)
|
||
{
|
||
var skillMatches = SkillService.MatchSlashCommand(text)
|
||
.Where(s => s.IsVisibleInTab(_activeTab))
|
||
.Select(s => (Cmd: "/" + s.Name,
|
||
Label: s.IsAvailable ? s.Label : $"{s.Label} {s.UnavailableHint}",
|
||
IsSkill: true, Available: s.IsAvailable));
|
||
foreach (var sm in skillMatches)
|
||
matches.Add((sm.Cmd, sm.Label, sm.IsSkill));
|
||
}
|
||
|
||
if (matches.Count > 0)
|
||
{
|
||
// 利먭꺼李얘린瑜??곷떒??怨좎젙 ?뺣젹
|
||
var favorites = _settings.Settings.Llm.FavoriteSlashCommands;
|
||
if (favorites.Count > 0)
|
||
{
|
||
matches = matches
|
||
.OrderByDescending(m => favorites.Contains(m.Cmd, StringComparer.OrdinalIgnoreCase))
|
||
.ToList();
|
||
}
|
||
|
||
_slashAllMatches = matches;
|
||
_slashPageOffset = 0;
|
||
_slashSelectedIndex = -1;
|
||
RenderSlashPage();
|
||
SlashPopup.IsOpen = true;
|
||
return;
|
||
}
|
||
}
|
||
SlashPopup.IsOpen = false;
|
||
}
|
||
|
||
/// <summary>?꾩옱 ?섏씠吏???щ옒??紐낅졊????ぉ???뚮뜑留곹빀?덈떎.</summary>
|
||
private void RenderSlashPage()
|
||
{
|
||
SlashItems.Items.Clear();
|
||
var total = _slashAllMatches.Count;
|
||
var start = _slashPageOffset;
|
||
var end = Math.Min(start + SlashPageSize, total);
|
||
|
||
// ???붿궡?? if (start > 0)
|
||
{
|
||
SlashNavUp.Visibility = Visibility.Visible;
|
||
SlashNavUpText.Text = $"?? ?꾨줈 {start}媛?;
|
||
}
|
||
else
|
||
SlashNavUp.Visibility = Visibility.Collapsed;
|
||
|
||
// ?꾩씠???뚮뜑留? for (int i = start; i < end; i++)
|
||
{
|
||
var (cmd, label, isSkill) = _slashAllMatches[i];
|
||
var capturedCmd = cmd;
|
||
var skillDef = isSkill ? SkillService.Find(cmd.TrimStart('/')) : null;
|
||
var skillAvailable = skillDef?.IsAvailable ?? true;
|
||
|
||
var isFav = _settings.Settings.Llm.FavoriteSlashCommands
|
||
.Contains(cmd, StringComparer.OrdinalIgnoreCase);
|
||
|
||
var pageLocalIndex = i - start;
|
||
var isSelected = pageLocalIndex == _slashSelectedIndex;
|
||
var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
|
||
|
||
var item = new Border
|
||
{
|
||
Background = isSelected ? hoverBrushItem : Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(6),
|
||
Padding = new Thickness(10, 6, 10, 6),
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
Cursor = skillAvailable ? Cursors.Hand : Cursors.Arrow,
|
||
Opacity = skillAvailable ? 1.0 : 0.5,
|
||
};
|
||
var itemGrid = new Grid();
|
||
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
|
||
var stack = new StackPanel();
|
||
var headerRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||
if (isSkill)
|
||
{
|
||
headerRow.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE768 ",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 11,
|
||
Foreground = skillAvailable
|
||
? TryFindResource("AccentColor") as Brush ?? Brushes.Blue
|
||
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
}
|
||
if (isFav)
|
||
{
|
||
headerRow.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE735 ",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 10,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B)),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 2, 0),
|
||
});
|
||
}
|
||
headerRow.Children.Add(new TextBlock
|
||
{
|
||
Text = cmd,
|
||
FontSize = 13,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = skillAvailable
|
||
? TryFindResource("AccentColor") as Brush ?? Brushes.Blue
|
||
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
headerRow.Children.Add(new TextBlock
|
||
{
|
||
Text = $" ??{label}",
|
||
FontSize = 12,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
stack.Children.Add(headerRow);
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = isSkill ? (skillDef?.Description ?? label) : "?댁옣 紐낅졊?대? 鍮좊Ⅴ寃??곸슜?????덉뒿?덈떎.",
|
||
FontSize = 11.5,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
});
|
||
if (isSkill && skillDef != null)
|
||
{
|
||
var metaRow = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(0, 6, 0, 0),
|
||
};
|
||
var isSystemSkill = skillDef.FilePath.StartsWith(
|
||
System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "skills"),
|
||
StringComparison.OrdinalIgnoreCase);
|
||
metaRow.Children.Add(CreateMetaChip(
|
||
isSystemSkill ? "?쒖뒪?? : "?ъ슜??,
|
||
isSystemSkill ? "#EEF2FF" : "#ECFDF5",
|
||
isSystemSkill ? "#4338CA" : "#047857"));
|
||
metaRow.Children.Add(CreateMetaChip(
|
||
skillDef.IsStandardFormat ? "SKILL.md" : "legacy",
|
||
"#F3F4F6",
|
||
"#4B5563"));
|
||
if (!string.IsNullOrWhiteSpace(skillDef.Requires))
|
||
{
|
||
metaRow.Children.Add(CreateMetaChip(
|
||
skillDef.IsAvailable ? $"?섏〈??{skillDef.Requires}" : $"{skillDef.Requires} ?꾩슂",
|
||
skillDef.IsAvailable ? "#E0F2FE" : "#FEF3C7",
|
||
skillDef.IsAvailable ? "#0369A1" : "#B45309"));
|
||
}
|
||
stack.Children.Add(metaRow);
|
||
}
|
||
Grid.SetColumn(stack, 0);
|
||
itemGrid.Children.Add(stack);
|
||
|
||
// 利먭꺼李얘린 ?좉? 蹂??꾩씠肄? var favCapturedCmd = cmd;
|
||
var favBtn = new Border
|
||
{
|
||
Width = 24, Height = 24,
|
||
CornerRadius = new CornerRadius(4),
|
||
Background = Brushes.Transparent,
|
||
Cursor = Cursors.Hand,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Child = new TextBlock
|
||
{
|
||
Text = isFav ? "\uE735" : "\uE734",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 11,
|
||
Foreground = isFav
|
||
? new SolidColorBrush(Color.FromRgb(0xF5, 0x9E, 0x0B))
|
||
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Opacity = isFav ? 1.0 : 0.4,
|
||
},
|
||
};
|
||
favBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
|
||
favBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||
favBtn.MouseLeftButtonDown += (_, me) =>
|
||
{
|
||
me.Handled = true; // ?꾩씠???대┃ ?대깽??諛⑹?
|
||
ToggleSlashFavorite(favCapturedCmd);
|
||
};
|
||
Grid.SetColumn(favBtn, 1);
|
||
itemGrid.Children.Add(favBtn);
|
||
|
||
item.Child = itemGrid;
|
||
|
||
if (skillAvailable)
|
||
{
|
||
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
|
||
item.MouseEnter += (_, _) => item.Background = hoverBrush;
|
||
item.MouseLeave += (_, _) => item.Background = Brushes.Transparent;
|
||
item.MouseLeftButtonDown += (_, _) =>
|
||
{
|
||
SlashPopup.IsOpen = false;
|
||
if (capturedCmd.Equals("/help", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
InputBox.Text = "";
|
||
ShowSlashHelpWindow();
|
||
return;
|
||
}
|
||
// 移??쒖떆: 紐낅졊?대? 移⑹쑝濡? InputBox??鍮??띿뒪?몃줈
|
||
ShowSlashChip(capturedCmd);
|
||
InputBox.Focus();
|
||
};
|
||
}
|
||
SlashItems.Items.Add(item);
|
||
}
|
||
|
||
// ?꾨옒 ?붿궡?? if (end < total)
|
||
{
|
||
SlashNavDown.Visibility = Visibility.Visible;
|
||
SlashNavDownText.Text = $"?? ?꾨옒濡?{total - end}媛?;
|
||
}
|
||
else
|
||
SlashNavDown.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
/// <summary>?щ옒???앹뾽 留덉슦?????ㅽ겕濡?泥섎━.</summary>
|
||
private void SlashPopup_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
||
{
|
||
e.Handled = true;
|
||
SlashPopup_ScrollByDelta(e.Delta);
|
||
}
|
||
|
||
/// <summary>?щ옒???앹뾽??Delta 諛⑺뼢?쇰줈 ?ㅽ겕濡ㅽ빀?덈떎.</summary>
|
||
private void SlashPopup_ScrollByDelta(int delta)
|
||
{
|
||
if (_slashAllMatches.Count == 0) return;
|
||
var pageItemCount = Math.Min(SlashPageSize, _slashAllMatches.Count - _slashPageOffset);
|
||
|
||
if (delta > 0) // ?꾨줈 ?ㅽ겕濡?(Up ??
|
||
{
|
||
if (_slashSelectedIndex > 0)
|
||
_slashSelectedIndex--;
|
||
else if (_slashSelectedIndex == 0 && _slashPageOffset > 0)
|
||
{
|
||
_slashPageOffset = Math.Max(0, _slashPageOffset - 1);
|
||
_slashSelectedIndex = 0;
|
||
}
|
||
}
|
||
else // ?꾨옒濡??ㅽ겕濡?(Down ??
|
||
{
|
||
if (_slashSelectedIndex < 0)
|
||
{
|
||
// 珥덇린 ?곹깭: 泥?踰덉㎏ ??ぉ ?좏깮
|
||
_slashSelectedIndex = 0;
|
||
}
|
||
else if (_slashSelectedIndex < pageItemCount - 1)
|
||
_slashSelectedIndex++;
|
||
else if (_slashPageOffset + SlashPageSize < _slashAllMatches.Count)
|
||
{
|
||
_slashPageOffset++;
|
||
_slashSelectedIndex = Math.Min(SlashPageSize - 1,
|
||
_slashAllMatches.Count - _slashPageOffset - 1);
|
||
}
|
||
}
|
||
RenderSlashPage();
|
||
}
|
||
|
||
/// <summary>?ㅻ낫?쒕줈 ?좏깮???щ옒???꾩씠?쒖쓣 ?ㅽ뻾?⑸땲??</summary>
|
||
private void ExecuteSlashSelectedItem()
|
||
{
|
||
var absoluteIdx = _slashPageOffset + _slashSelectedIndex;
|
||
if (absoluteIdx < 0 || absoluteIdx >= _slashAllMatches.Count) return;
|
||
|
||
var (cmd, _, isSkill) = _slashAllMatches[absoluteIdx];
|
||
var skillDef = isSkill ? SkillService.Find(cmd.TrimStart('/')) : null;
|
||
var skillAvailable = skillDef?.IsAvailable ?? true;
|
||
if (!skillAvailable) return;
|
||
|
||
SlashPopup.IsOpen = false;
|
||
_slashSelectedIndex = -1;
|
||
|
||
if (cmd.Equals("/help", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
InputBox.Text = "";
|
||
ShowSlashHelpWindow();
|
||
return;
|
||
}
|
||
ShowSlashChip(cmd);
|
||
InputBox.Focus();
|
||
}
|
||
|
||
/// <summary>?щ옒??紐낅졊??利먭꺼李얘린瑜??좉??섍퀬 ?ㅼ젙????ν빀?덈떎.</summary>
|
||
private void ToggleSlashFavorite(string cmd)
|
||
{
|
||
var favs = _settings.Settings.Llm.FavoriteSlashCommands;
|
||
var existing = favs.FirstOrDefault(f => f.Equals(cmd, StringComparison.OrdinalIgnoreCase));
|
||
if (existing != null)
|
||
favs.Remove(existing);
|
||
else
|
||
favs.Add(cmd);
|
||
|
||
_settings.Save();
|
||
|
||
// ?앹뾽 ?덈줈怨좎묠: TextChanged ?대깽?몃? ?몃━嫄고븯???앹뾽 ?щ젋?붾쭅
|
||
var currentText = InputBox.Text;
|
||
InputBox.TextChanged -= InputBox_TextChanged;
|
||
InputBox.Text = "";
|
||
InputBox.TextChanged += InputBox_TextChanged;
|
||
InputBox.Text = currentText;
|
||
}
|
||
|
||
/// <summary>?щ옒??紐낅졊??移⑹쓣 ?쒖떆?섍퀬 InputBox瑜?鍮꾩썎?덈떎.</summary>
|
||
private void ShowSlashChip(string cmd)
|
||
{
|
||
_activeSlashCmd = cmd;
|
||
SlashChipText.Text = cmd;
|
||
SlashCommandChip.Visibility = Visibility.Visible;
|
||
|
||
// 移??덈퉬 痢≪젙 ??InputBox ?쇱そ ?щ갚 議곗젙
|
||
SlashCommandChip.UpdateLayout();
|
||
var chipRight = SlashCommandChip.Margin.Left + SlashCommandChip.ActualWidth + 6;
|
||
InputBox.Padding = new Thickness(chipRight, 10, 14, 10);
|
||
InputBox.Text = "";
|
||
}
|
||
|
||
/// <summary>?щ옒??紐낅졊??移⑹쓣 ?④퉩?덈떎.</summary>
|
||
/// <param name="restoreText">true?대㈃ InputBox??紐낅졊???띿뒪?몃? 蹂듭썝?⑸땲??</param>
|
||
private void HideSlashChip(bool restoreText = false)
|
||
{
|
||
if (_activeSlashCmd == null) return;
|
||
var prev = _activeSlashCmd;
|
||
_activeSlashCmd = null;
|
||
SlashCommandChip.Visibility = Visibility.Collapsed;
|
||
InputBox.Padding = new Thickness(14, 10, 14, 10);
|
||
if (restoreText)
|
||
{
|
||
InputBox.Text = prev + " ";
|
||
InputBox.CaretIndex = InputBox.Text.Length;
|
||
}
|
||
}
|
||
|
||
/// <summary>?щ옒??紐낅졊?대? 媛먯??섏뿬 ?쒖뒪???꾨\?꾪듃? ?ъ슜???띿뒪?몃? 遺꾨━?⑸땲??</summary>
|
||
private static (string? slashSystem, string userText) ParseSlashCommand(string input)
|
||
{
|
||
// ?댁옣 紐낅졊???곗꽑
|
||
foreach (var (cmd, (_, prompt, _)) in SlashCommands)
|
||
{
|
||
if (input.StartsWith(cmd, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// __HELP__???뱀닔 泥섎━ (ParseSlashCommand?먯꽌??臾댁떆)
|
||
if (prompt == "__HELP__") return (null, input);
|
||
var rest = input[cmd.Length..].Trim();
|
||
return (prompt, string.IsNullOrEmpty(rest) ? cmd : rest);
|
||
}
|
||
}
|
||
|
||
// ?ㅽ궗 紐낅졊??留ㅼ묶
|
||
foreach (var skill in SkillService.Skills)
|
||
{
|
||
var slashCmd = "/" + skill.Name;
|
||
if (input.StartsWith(slashCmd, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var rest = input[slashCmd.Length..].Trim();
|
||
return (skill.SystemPrompt, string.IsNullOrEmpty(rest) ? skill.Label : rest);
|
||
}
|
||
}
|
||
|
||
return (null, input);
|
||
}
|
||
|
||
// ??? ?쒕옒洹????쒕∼ AI ?≪뀡 ?앹뾽 ?????????????????????????????????????
|
||
|
||
private static readonly Dictionary<string, List<(string Label, string Icon, string Prompt)>> DropActions = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
["code"] =
|
||
[
|
||
("肄붾뱶 由щ럭", "\uE943", "泥⑤???肄붾뱶瑜?由щ럭??二쇱꽭?? 踰꾧렇, ?깅뒫 ?댁뒋, 蹂댁븞 痍⑥빟?? 媛쒖꽑?먯쓣 李얠븘 援ъ껜?곸쑝濡??쒖븞?섏꽭??"),
|
||
("肄붾뱶 ?ㅻ챸", "\uE946", "泥⑤???肄붾뱶瑜??곸꽭???ㅻ챸??二쇱꽭?? 二쇱슂 ?⑥닔, ?곗씠???먮쫫, ?ㅺ퀎 ?⑦꽩???ы븿?섏꽭??"),
|
||
("由ы뙥?좊쭅 ?쒖븞", "\uE70F", "泥⑤???肄붾뱶??由ы뙥?좊쭅 諛⑹븞???쒖븞??二쇱꽭?? 媛?낆꽦, ?좎?蹂댁닔?? ?깅뒫??怨좊젮?섏꽭??"),
|
||
("?뚯뒪???앹꽦", "\uE9D5", "泥⑤???肄붾뱶??????⑥쐞 ?뚯뒪??肄붾뱶瑜??앹꽦??二쇱꽭??"),
|
||
],
|
||
["document"] =
|
||
[
|
||
("?붿빟", "\uE8AB", "泥⑤???臾몄꽌瑜??듭떖 ?ъ씤???꾩<濡?媛꾧껐?섍쾶 ?붿빟??二쇱꽭??"),
|
||
("遺꾩꽍", "\uE9D9", "泥⑤???臾몄꽌???댁슜??遺꾩꽍?섍퀬 二쇱슂 ?몄궗?댄듃瑜??꾩텧??二쇱꽭??"),
|
||
("踰덉뿭", "\uE8C1", "泥⑤???臾몄꽌瑜??곸뼱濡?踰덉뿭??二쇱꽭?? ?먮Ц???ㅺ낵 ?섏븰?ㅻ? ?좎??섏꽭??"),
|
||
],
|
||
["data"] =
|
||
[
|
||
("?곗씠??遺꾩꽍", "\uE9D9", "泥⑤????곗씠?곕? 遺꾩꽍??二쇱꽭?? ?듦퀎, 異붿꽭, ?댁긽移섎? 李얠븘 蹂닿퀬?섏꽭??"),
|
||
("?쒓컖???쒖븞", "\uE9D9", "泥⑤????곗씠?곕? ?쒓컖?뷀븷 理쒖쟻??李⑦듃 ?좏삎???쒖븞?섍퀬 chart_create濡??앹꽦??二쇱꽭??"),
|
||
("?щ㎎ 蹂??, "\uE8AB", "泥⑤????곗씠?곕? ?ㅻⅨ ?뺤떇?쇰줈 蹂?섑빐 二쇱꽭?? (CSV?봋SON?봂xcel ??"),
|
||
],
|
||
["image"] =
|
||
[
|
||
("?대?吏 ?ㅻ챸", "\uE946", "泥⑤????대?吏瑜??먯꽭???ㅻ챸??二쇱꽭?? ?댁슜, ?덉씠?꾩썐, ?띿뒪?몃? 遺꾩꽍?섏꽭??"),
|
||
("UI 由щ럭", "\uE70F", "泥⑤???UI ?ㅽ겕由곗꺑??由щ럭??二쇱꽭?? UX 媛쒖꽑?? ?묎렐?? ?붿옄???쇨??깆쓣 ?됯??섏꽭??"),
|
||
],
|
||
};
|
||
|
||
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{ ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
|
||
private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{ ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
|
||
// ImageExtensions???대?吏 泥⑤? ?곸뿭(line ~1323)?먯꽌 ?뺤쓽?????ъ궗??
|
||
private Popup? _dropActionPopup;
|
||
|
||
private void ShowDropActionMenu(string[] files)
|
||
{
|
||
// ?뚯씪 ?좏삎 ?먮퀎
|
||
var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
|
||
string category;
|
||
if (CodeExtensions.Contains(ext)) category = "code";
|
||
else if (DataExtensions.Contains(ext)) category = "data";
|
||
else if (ImageExtensions.Contains(ext)) category = "image";
|
||
else category = "document";
|
||
|
||
var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
|
||
|
||
// ?앹뾽 ?앹꽦
|
||
_dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
|
||
|
||
var panel = new StackPanel();
|
||
// ?ㅻ뜑
|
||
var header = new TextBlock
|
||
{
|
||
Text = $"?뱨 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" ??{files.Length - 1}媛? : "")}",
|
||
FontSize = 11,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
Margin = new Thickness(12, 8, 12, 6),
|
||
};
|
||
panel.Children.Add(header);
|
||
|
||
// ?≪뀡 ??ぉ
|
||
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(24, 255, 255, 255));
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||
var textBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
|
||
foreach (var (label, icon, prompt) in actions)
|
||
{
|
||
var capturedPrompt = prompt;
|
||
var row = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(6),
|
||
Padding = new Thickness(12, 7, 12, 7),
|
||
Margin = new Thickness(4, 1, 4, 1),
|
||
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 = accentBrush,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||
Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
row.Child = stack;
|
||
|
||
row.MouseEnter += (_, _) => row.Background = hoverBrush;
|
||
row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
|
||
row.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
|
||
foreach (var f in files) AddAttachedFile(f);
|
||
InputBox.Text = capturedPrompt;
|
||
InputBox.CaretIndex = InputBox.Text.Length;
|
||
InputBox.Focus();
|
||
if (_settings.Settings.Llm.DragDropAutoSend)
|
||
_ = SendMessageAsync();
|
||
};
|
||
panel.Children.Add(row);
|
||
}
|
||
|
||
// "泥⑤?留? ??ぉ
|
||
var attachOnly = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(6),
|
||
Padding = new Thickness(12, 7, 12, 7),
|
||
Margin = new Thickness(4, 1, 4, 1),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
|
||
attachStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE723",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 13, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
attachStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "泥⑤?留?, FontSize = 13,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
attachOnly.Child = attachStack;
|
||
attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
|
||
attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
|
||
attachOnly.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
|
||
foreach (var f in files) AddAttachedFile(f);
|
||
InputBox.Focus();
|
||
};
|
||
panel.Children.Add(attachOnly);
|
||
|
||
var container = new Border
|
||
{
|
||
Background = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(26, 27, 46)),
|
||
CornerRadius = new CornerRadius(12),
|
||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||
BorderThickness = new Thickness(1),
|
||
Padding = new Thickness(4, 4, 4, 6),
|
||
Child = panel,
|
||
MinWidth = 200,
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
|
||
},
|
||
};
|
||
|
||
_dropActionPopup = new Popup
|
||
{
|
||
PlacementTarget = InputBorder,
|
||
Placement = PlacementMode.Top,
|
||
StaysOpen = false,
|
||
AllowsTransparency = true,
|
||
PopupAnimation = PopupAnimation.Fade,
|
||
Child = container,
|
||
};
|
||
_dropActionPopup.IsOpen = true;
|
||
}
|
||
|
||
// ??? /help ?꾩?留?李??????????????????????????????????????????????????
|
||
|
||
private void ShowSlashHelpWindow()
|
||
{
|
||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(26, 27, 46));
|
||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var fg2 = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255));
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(40, 255, 255, 255));
|
||
|
||
var win = new Window
|
||
{
|
||
Title = "AX Agent ???щ옒??紐낅졊???꾩?留?,
|
||
Width = 560, Height = 640, MinWidth = 440, MinHeight = 500,
|
||
WindowStyle = WindowStyle.None, AllowsTransparency = true,
|
||
Background = Brushes.Transparent, ResizeMode = ResizeMode.CanResize,
|
||
WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this,
|
||
Icon = Icon,
|
||
};
|
||
|
||
var mainBorder = new Border
|
||
{
|
||
Background = bg, CornerRadius = new CornerRadius(16),
|
||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||
BorderThickness = new Thickness(1), Margin = new Thickness(10),
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect { Color = Colors.Black, BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3 },
|
||
};
|
||
|
||
var rootGrid = new Grid();
|
||
rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(52) });
|
||
rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||
|
||
// ?ㅻ뜑
|
||
var headerBorder = new Border
|
||
{
|
||
CornerRadius = new CornerRadius(16, 16, 0, 0),
|
||
Background = new LinearGradientBrush(
|
||
Color.FromRgb(26, 27, 46), Color.FromRgb(59, 78, 204),
|
||
new Point(0, 0), new Point(1, 1)),
|
||
Padding = new Thickness(20, 0, 20, 0),
|
||
};
|
||
var headerGrid = new Grid();
|
||
var headerStack = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||
headerStack.Children.Add(new TextBlock { Text = "\uE946", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 16, Foreground = Brushes.LightCyan, Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center });
|
||
headerStack.Children.Add(new TextBlock { Text = "?щ옒??紐낅졊??(/ Commands)", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center });
|
||
headerGrid.Children.Add(headerStack);
|
||
|
||
var closeBtn = new Border { Width = 30, Height = 30, CornerRadius = new CornerRadius(8), Background = Brushes.Transparent, Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center };
|
||
closeBtn.Child = new TextBlock { Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = new SolidColorBrush(Color.FromArgb(136, 170, 255, 204)), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
|
||
closeBtn.MouseEnter += (_, _) => closeBtn.Background = new SolidColorBrush(Color.FromArgb(34, 255, 255, 255));
|
||
closeBtn.MouseLeave += (_, _) => closeBtn.Background = Brushes.Transparent;
|
||
closeBtn.MouseLeftButtonDown += (_, me) => { me.Handled = true; win.Close(); };
|
||
headerGrid.Children.Add(closeBtn);
|
||
headerBorder.Child = headerGrid;
|
||
Grid.SetRow(headerBorder, 0);
|
||
rootGrid.Children.Add(headerBorder);
|
||
|
||
// 肄섑뀗痢? var scroll = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Padding = new Thickness(20, 14, 20, 20) };
|
||
var contentPanel = new StackPanel();
|
||
|
||
// ?ㅻ챸
|
||
contentPanel.Children.Add(new TextBlock { Text = "?낅젰李쎌뿉 /瑜??낅젰?섎㈃ ?ъ슜?????덈뒗 紐낅졊?닿? ?쒖떆?⑸땲??\n紐낅졊?대? ?좏깮?????댁슜???낅젰?섎㈃ ?대떦 湲곕뒫???곸슜?⑸땲??", FontSize = 12, Foreground = fg2, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 16), LineHeight = 20 });
|
||
|
||
// 怨듯넻 紐낅졊???뱀뀡
|
||
AddHelpSection(contentPanel, "?뱦 怨듯넻 紐낅졊??, "紐⑤뱺 ??Chat, Cowork, Code)?먯꽌 ?ъ슜 媛??, fg, fg2, accent, itemBg, hoverBg,
|
||
("/summary", "?띿뒪??臾몄꽌瑜??듭떖 ?ъ씤??以묒떖?쇰줈 ?붿빟?⑸땲??"),
|
||
("/translate", "?띿뒪?몃? ?곸뼱濡?踰덉뿭?⑸땲?? ?먮Ц???ㅼ쓣 ?좎??⑸땲??"),
|
||
("/explain", "?댁슜???쎄퀬 ?먯꽭?섍쾶 ?ㅻ챸?⑸땲?? ?덉떆瑜??ы븿?⑸땲??"),
|
||
("/fix", "留욎땄踰? 臾몃쾿, ?먯뿰?ㅻ윭???쒗쁽??援먯젙?⑸땲??"));
|
||
|
||
// 媛쒕컻 紐낅졊???뱀뀡
|
||
AddHelpSection(contentPanel, "?썱截?媛쒕컻 紐낅졊??, "Cowork, Code ??뿉?쒕쭔 ?ъ슜 媛??, fg, fg2, accent, itemBg, hoverBg,
|
||
("/review", "Git diff瑜?遺꾩꽍?섏뿬 踰꾧렇, ?깅뒫, 蹂댁븞 ?댁뒋瑜?李얠뒿?덈떎."),
|
||
("/pr", "蹂寃쎌궗??쓣 PR ?ㅻ챸 ?뺤떇(Summary, Changes, Test Plan)?쇰줈 ?붿빟?⑸땲??"),
|
||
("/test", "肄붾뱶??????⑥쐞 ?뚯뒪?몃? ?먮룞 ?앹꽦?⑸땲??"),
|
||
("/structure", "?꾨줈?앺듃???대뜑/?뚯씪 援ъ“瑜?遺꾩꽍?섍퀬 ?ㅻ챸?⑸땲??"),
|
||
("/build", "?꾨줈?앺듃瑜?鍮뚮뱶?⑸땲?? ?ㅻ쪟 諛쒖깮 ??遺꾩꽍?⑸땲??"),
|
||
("/search", "?먯뿰?대줈 肄붾뱶踰좎씠?ㅻ? ?쒕㎤??寃?됲빀?덈떎."));
|
||
|
||
// ?ㅽ궗 紐낅졊???뱀뀡
|
||
var skills = SkillService.Skills;
|
||
if (skills.Count > 0)
|
||
{
|
||
var skillItems = skills.Select(s => ($"/{s.Name}", s.Description)).ToArray();
|
||
AddHelpSection(contentPanel, "???ㅽ궗 紐낅졊??, $"{skills.Count}媛?濡쒕뱶????%APPDATA%\\AxCopilot\\skills\\?먯꽌 異붽? 媛??, fg, fg2, accent, itemBg, hoverBg, skillItems);
|
||
}
|
||
|
||
// ?ъ슜 ?? contentPanel.Children.Add(new Border { Height = 1, Background = new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)), Margin = new Thickness(0, 12, 0, 12) });
|
||
var tipPanel = new StackPanel();
|
||
tipPanel.Children.Add(new TextBlock { Text = "?뮕 ?ъ슜 ??, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = fg, Margin = new Thickness(0, 0, 0, 8) });
|
||
var tips = new[]
|
||
{
|
||
"/ ?낅젰 ???꾩옱 ??뿉 留욌뒗 紐낅졊?대쭔 ?먮룞?꾩꽦?⑸땲??",
|
||
"?뚯씪???쒕옒洹명븯硫??좏삎蹂?AI ?≪뀡 ?앹뾽???섑??⑸땲??",
|
||
"?ㅽ궗 ?뚯씪(*.skill.md)??異붽??섎㈃ ?섎쭔???뚰겕?뚮줈?곕? 留뚮뱾 ???덉뒿?덈떎.",
|
||
"Cowork/Code ??뿉???먯씠?꾪듃媛 ?꾧뎄瑜??쒖슜?섏뿬 ??媛뺣젰???묒뾽???섑뻾?⑸땲??",
|
||
};
|
||
foreach (var tip in tips)
|
||
{
|
||
tipPanel.Children.Add(new TextBlock { Text = $"??{tip}", FontSize = 12, Foreground = fg2, Margin = new Thickness(8, 2, 0, 2), TextWrapping = TextWrapping.Wrap, LineHeight = 18 });
|
||
}
|
||
contentPanel.Children.Add(tipPanel);
|
||
|
||
scroll.Content = contentPanel;
|
||
Grid.SetRow(scroll, 1);
|
||
rootGrid.Children.Add(scroll);
|
||
|
||
mainBorder.Child = rootGrid;
|
||
win.Content = mainBorder;
|
||
// ?ㅻ뜑 ?곸뿭?먯꽌留??쒕옒洹??대룞 (?リ린 踰꾪듉 ?대┃ 諛⑺빐 諛⑹?)
|
||
headerBorder.MouseLeftButtonDown += (_, me) => { try { win.DragMove(); } catch { } };
|
||
win.ShowDialog();
|
||
}
|
||
|
||
private static void AddHelpSection(StackPanel parent, string title, string subtitle,
|
||
Brush fg, Brush fg2, Brush accent, Brush itemBg, Brush hoverBg,
|
||
params (string Cmd, string Desc)[] items)
|
||
{
|
||
parent.Children.Add(new TextBlock { Text = title, FontSize = 14, FontWeight = FontWeights.SemiBold, Foreground = fg, Margin = new Thickness(0, 8, 0, 2) });
|
||
parent.Children.Add(new TextBlock { Text = subtitle, FontSize = 11, Foreground = fg2, Margin = new Thickness(0, 0, 0, 8) });
|
||
|
||
foreach (var (cmd, desc) in items)
|
||
{
|
||
var row = new Border { Background = itemBg, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 8, 12, 8), Margin = new Thickness(0, 3, 0, 3) };
|
||
var grid = new Grid();
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) });
|
||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
|
||
var cmdText = new TextBlock { Text = cmd, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = accent, VerticalAlignment = VerticalAlignment.Center, FontFamily = new FontFamily("Consolas") };
|
||
Grid.SetColumn(cmdText, 0);
|
||
grid.Children.Add(cmdText);
|
||
|
||
var descText = new TextBlock { Text = desc, FontSize = 12, Foreground = fg2, VerticalAlignment = VerticalAlignment.Center, TextWrapping = TextWrapping.Wrap };
|
||
Grid.SetColumn(descText, 1);
|
||
grid.Children.Add(descText);
|
||
|
||
row.Child = grid;
|
||
row.MouseEnter += (_, _) => row.Background = hoverBg;
|
||
row.MouseLeave += (_, _) => row.Background = itemBg;
|
||
parent.Children.Add(row);
|
||
}
|
||
}
|
||
|
||
private async Task SendMessageAsync()
|
||
{
|
||
var rawText = InputBox.Text.Trim();
|
||
|
||
// ?щ옒??移⑹씠 ?쒖꽦?붾맂 寃쎌슦 紐낅졊???욎뿉 遺숈엫
|
||
var text = _activeSlashCmd != null
|
||
? (_activeSlashCmd + " " + rawText).Trim()
|
||
: rawText;
|
||
HideSlashChip(restoreText: false);
|
||
|
||
if (string.IsNullOrEmpty(text) || _isStreaming) return;
|
||
|
||
// placeholder ?뺣━
|
||
ClearPromptCardPlaceholder();
|
||
|
||
// ?щ옒??紐낅졊??泥섎━
|
||
var (slashSystem, displayText) = ParseSlashCommand(text);
|
||
|
||
// ???꾪솚 ?쒖뿉???щ컮瑜???뿉 ??ν븯湲??꾪빐 ?쒖옉 ?쒖젏????쓣 罹≪쿂
|
||
var originTab = _activeTab;
|
||
|
||
ChatConversation conv;
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation == null)
|
||
_currentConversation = _sessionState.LoadOrCreateConversation(_activeTab, _storage, _settings);
|
||
conv = _currentConversation;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(conv.Tab))
|
||
conv.Tab = _activeTab;
|
||
|
||
var userMsg = new ChatMessage { Role = "user", Content = text };
|
||
lock (_convLock) conv.Messages.Add(userMsg);
|
||
|
||
if (conv.Messages.Count(m => m.Role == "user") == 1)
|
||
conv.Title = text.Length > 30 ? text[..30] + "?? : text;
|
||
|
||
UpdateChatTitle();
|
||
AddMessageBubble("user", text);
|
||
InputBox.Text = "";
|
||
EmptyState.Visibility = Visibility.Collapsed;
|
||
|
||
// ????듦퀎 湲곕줉
|
||
Services.UsageStatisticsService.RecordChat(_activeTab);
|
||
|
||
ForceScrollToEnd(); // ?ъ슜??硫붿떆吏 ?꾩넚 ??媛뺤젣 ?섎떒 ?대룞
|
||
PlayRainbowGlow(); // 臾댁?媛?湲濡쒖슦 ?좊땲硫붿씠??
|
||
_isStreaming = true;
|
||
BtnSend.IsEnabled = false;
|
||
BtnSend.Visibility = Visibility.Collapsed;
|
||
BtnStop.Visibility = Visibility.Visible;
|
||
if (_activeTab == "Cowork" || _activeTab == "Code")
|
||
BtnPause.Visibility = Visibility.Visible;
|
||
_streamCts = new CancellationTokenSource();
|
||
|
||
var assistantMsg = new ChatMessage { Role = "assistant", Content = "" };
|
||
lock (_convLock) conv.Messages.Add(assistantMsg);
|
||
|
||
// ?댁떆?ㅽ꽩???ㅽ듃由щ컢 而⑦뀒?대꼫
|
||
var streamContainer = CreateStreamingContainer(out var streamText);
|
||
MessagePanel.Children.Add(streamContainer);
|
||
ForceScrollToEnd(); // ?묐떟 ?쒖옉 ??媛뺤젣 ?섎떒 ?대룞
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
_activeStreamText = streamText;
|
||
_cachedStreamContent = "";
|
||
_displayedLength = 0;
|
||
_cursorVisible = true;
|
||
_aiIconPulseStopped = false;
|
||
_cursorTimer.Start();
|
||
_typingTimer.Start();
|
||
_streamStartTime = DateTime.UtcNow;
|
||
_elapsedTimer.Start();
|
||
SetStatus("?묐떟 ?앹꽦 以?..", spinning: true);
|
||
|
||
// ?? ?먮룞 紐⑤뜽 ?쇱슦??(try ?몃? ?좎뼵 ??finally?먯꽌 ?뺣━) ??
|
||
ModelRouteResult? routeResult = null;
|
||
var finishedSuccessfully = false;
|
||
|
||
try
|
||
{
|
||
List<ChatMessage> sendMessages;
|
||
lock (_convLock) sendMessages = conv.Messages.SkipLast(1).ToList();
|
||
|
||
// ?쒖뒪??紐낅졊?닿? ?덉쑝硫??쎌엯
|
||
if (!string.IsNullOrEmpty(conv.SystemCommand))
|
||
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = conv.SystemCommand });
|
||
|
||
// ?щ옒??紐낅졊???쒖뒪???꾨\?꾪듃 ?쎌엯
|
||
if (!string.IsNullOrEmpty(slashSystem))
|
||
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = slashSystem });
|
||
|
||
// 泥⑤? ?뚯씪 而⑦뀓?ㅽ듃 ?쎌엯
|
||
if (_attachedFiles.Count > 0)
|
||
{
|
||
var fileContext = BuildFileContextPrompt();
|
||
if (!string.IsNullOrEmpty(fileContext))
|
||
{
|
||
var lastUserIdx = sendMessages.FindLastIndex(m => m.Role == "user");
|
||
if (lastUserIdx >= 0)
|
||
sendMessages[lastUserIdx] = new ChatMessage { Role = "user", Content = sendMessages[lastUserIdx].Content + fileContext };
|
||
}
|
||
// 泥⑤? ?뚯씪 紐⑸줉 湲곕줉 ????긽 ?뺣━ (?뚯씪 ?쎄린 ?ㅽ뙣?대룄)
|
||
userMsg.AttachedFiles = _attachedFiles.ToList();
|
||
_attachedFiles.Clear();
|
||
RefreshAttachedFilesUI();
|
||
}
|
||
|
||
// ?? ?대?吏 泥⑤? ??
|
||
if (_pendingImages.Count > 0)
|
||
{
|
||
userMsg.Images = _pendingImages.ToList();
|
||
// 留덉?留??ъ슜??硫붿떆吏???대?吏 ?곗씠???곌껐
|
||
var lastUserIdx = sendMessages.FindLastIndex(m => m.Role == "user");
|
||
if (lastUserIdx >= 0)
|
||
sendMessages[lastUserIdx] = new ChatMessage
|
||
{
|
||
Role = "user",
|
||
Content = sendMessages[lastUserIdx].Content,
|
||
Images = _pendingImages.ToList(),
|
||
};
|
||
_pendingImages.Clear();
|
||
AttachedFilesPanel.Items.Clear();
|
||
if (_attachedFiles.Count == 0) AttachedFilesPanel.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
// ?? ?먮룞 紐⑤뜽 ?쇱슦????
|
||
if (_settings.Settings.Llm.EnableAutoRouter)
|
||
{
|
||
routeResult = _router.Route(text);
|
||
if (routeResult != null)
|
||
{
|
||
_llm.PushRouteOverride(routeResult.Service, routeResult.Model);
|
||
SetStatus($"?쇱슦?? {routeResult.DetectedIntent} ??{routeResult.DisplayName}", spinning: true);
|
||
}
|
||
}
|
||
|
||
if (_activeTab == "Cowork")
|
||
{
|
||
// ?뚰겕?뚮줈??遺꾩꽍湲??먮룞 ?닿린
|
||
OpenWorkflowAnalyzerIfEnabled();
|
||
|
||
// ?꾩쟻 ?좏겙 珥덇린?? _agentCumulativeInputTokens = 0;
|
||
_agentCumulativeOutputTokens = 0;
|
||
|
||
// 肄붿썙???? ?먯씠?꾪듃 猷⑦봽 ?ъ슜
|
||
_agentLoop.EventOccurred += OnAgentEvent;
|
||
// ?ъ슜???섏궗寃곗젙 肄쒕갚 ??PlanViewerWindow濡?怨꾪쉷 ?쒖떆
|
||
_agentLoop.UserDecisionCallback = CreatePlanDecisionCallback();
|
||
try
|
||
{
|
||
// 肄붿썙???쒖뒪???꾨\?꾪듃 ?쎌엯
|
||
var coworkSystem = BuildCoworkSystemPrompt();
|
||
if (!string.IsNullOrEmpty(coworkSystem))
|
||
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = coworkSystem });
|
||
|
||
_agentLoop.ActiveTab = _activeTab;
|
||
var response = await _agentLoop.RunAsync(sendMessages, _streamCts!.Token);
|
||
sb.Append(response);
|
||
assistantMsg.Content = response;
|
||
StopAiIconPulse();
|
||
_cachedStreamContent = response;
|
||
|
||
// ?꾨즺 ?뚮┝
|
||
if (_settings.Settings.Llm.NotifyOnComplete)
|
||
Services.NotificationService.Notify("AX Cowork Agent", "肄붿썙???묒뾽???꾨즺?섏뿀?듬땲??");
|
||
}
|
||
finally
|
||
{
|
||
_agentLoop.EventOccurred -= OnAgentEvent;
|
||
_agentLoop.UserDecisionCallback = null;
|
||
}
|
||
}
|
||
else if (_activeTab == "Code")
|
||
{
|
||
// ?뚰겕?뚮줈??遺꾩꽍湲??먮룞 ?닿린
|
||
OpenWorkflowAnalyzerIfEnabled();
|
||
|
||
// ?꾩쟻 ?좏겙 珥덇린?? _agentCumulativeInputTokens = 0;
|
||
_agentCumulativeOutputTokens = 0;
|
||
|
||
// Code ?? ?먯씠?꾪듃 猷⑦봽 ?ъ슜 (Cowork怨??숈씪 ?⑦꽩)
|
||
_agentLoop.EventOccurred += OnAgentEvent;
|
||
_agentLoop.UserDecisionCallback = CreatePlanDecisionCallback();
|
||
try
|
||
{
|
||
var codeSystem = BuildCodeSystemPrompt();
|
||
if (!string.IsNullOrEmpty(codeSystem))
|
||
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = codeSystem });
|
||
|
||
_agentLoop.ActiveTab = "Code";
|
||
var response = await _agentLoop.RunAsync(sendMessages, _streamCts!.Token);
|
||
sb.Append(response);
|
||
assistantMsg.Content = response;
|
||
StopAiIconPulse();
|
||
_cachedStreamContent = response;
|
||
|
||
// ?꾨즺 ?뚮┝
|
||
if (_settings.Settings.Llm.NotifyOnComplete)
|
||
Services.NotificationService.Notify("AX Code Agent", "肄붾뱶 ?묒뾽???꾨즺?섏뿀?듬땲??");
|
||
}
|
||
finally
|
||
{
|
||
_agentLoop.EventOccurred -= OnAgentEvent;
|
||
_agentLoop.UserDecisionCallback = null;
|
||
}
|
||
}
|
||
else if (_settings.Settings.Llm.Streaming)
|
||
{
|
||
await foreach (var chunk in _llm.StreamAsync(sendMessages, _streamCts.Token))
|
||
{
|
||
sb.Append(chunk);
|
||
StopAiIconPulse();
|
||
_cachedStreamContent = sb.ToString();
|
||
// UI ?ㅻ젅?쒖뿉 ?쒖뼱 ?묐낫 ??DispatcherTimer媛 ?붾㈃ 媛깆떊?????덈룄濡? await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
|
||
}
|
||
_cachedStreamContent = sb.ToString();
|
||
assistantMsg.Content = _cachedStreamContent;
|
||
|
||
// ??댄븨 ?좊땲硫붿씠?섏씠 ?⑥? 踰꾪띁瑜??뚯쭊???뚭퉴吏 ?湲?(理쒕? 600ms)
|
||
var drainStart = DateTime.UtcNow;
|
||
while (_displayedLength < _cachedStreamContent.Length
|
||
&& (DateTime.UtcNow - drainStart).TotalMilliseconds < 600)
|
||
{
|
||
await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var response = await _llm.SendAsync(sendMessages, _streamCts.Token);
|
||
sb.Append(response);
|
||
assistantMsg.Content = response;
|
||
}
|
||
|
||
finishedSuccessfully = true;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
if (sb.Length == 0) sb.Append("(痍⑥냼??");
|
||
assistantMsg.Content = sb.ToString();
|
||
_activeQueuedDraftItem = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var errMsg = $"???ㅻ쪟: {ex.Message}";
|
||
sb.Clear(); sb.Append(errMsg);
|
||
assistantMsg.Content = errMsg;
|
||
RestoreFailedQueuedDraft(text, ex);
|
||
AddRetryButton();
|
||
}
|
||
finally
|
||
{
|
||
// ?먮룞 ?쇱슦???ㅻ쾭?쇱씠???댁젣
|
||
if (routeResult != null)
|
||
{
|
||
_llm.ClearRouteOverride();
|
||
UpdateModelLabel();
|
||
}
|
||
|
||
_cursorTimer.Stop();
|
||
_elapsedTimer.Stop();
|
||
_typingTimer.Stop();
|
||
HideStickyProgress(); // ?먯씠?꾪듃 ?꾨줈洹몃젅??諛?+ ??대㉧ ?뺣━
|
||
StopRainbowGlow(); // ?덉씤蹂댁슦 湲濡쒖슦 醫낅즺
|
||
_activeStreamText = null;
|
||
_elapsedLabel = null;
|
||
_cachedStreamContent = "";
|
||
_isStreaming = false;
|
||
BtnSend.IsEnabled = true;
|
||
BtnStop.Visibility = Visibility.Collapsed;
|
||
BtnSend.Visibility = Visibility.Visible;
|
||
_streamCts?.Dispose();
|
||
_streamCts = null;
|
||
SetStatusIdle();
|
||
if (finishedSuccessfully)
|
||
_activeQueuedDraftItem = null;
|
||
}
|
||
|
||
// ?ㅽ듃由щ컢 plaintext ??留덊겕?ㅼ슫 ?뚮뜑留곸쑝濡?援먯껜
|
||
FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg);
|
||
AutoScrollIfNeeded();
|
||
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"???????ㅽ뙣: {ex.Message}"); }
|
||
_sessionState.RememberConversation(originTab, conv.Id);
|
||
RefreshConversationList();
|
||
|
||
if (finishedSuccessfully)
|
||
await StartNextQueuedDraftIfAnyAsync();
|
||
}
|
||
|
||
// ??? 肄붿썙???먯씠?꾪듃 吏??????????????????????????????????????????????
|
||
|
||
private string BuildCoworkSystemPrompt()
|
||
{
|
||
var workFolder = GetCurrentWorkFolder();
|
||
var llm = _settings.Settings.Llm;
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine("You are AX Copilot Agent. You can read, write, and edit files using the provided tools.");
|
||
sb.AppendLine($"Today's date: {DateTime.Now:yyyy??M??d?? ({DateTime.Now:yyyy-MM-dd}, {DateTime.Now:dddd}).");
|
||
sb.AppendLine("Available skills: excel_create (.xlsx), docx_create (.docx), csv_create (.csv), markdown_create (.md), html_create (.html), script_create (.bat/.ps1), document_review (?덉쭏 寃利?, format_convert (?щ㎎ 蹂??.");
|
||
sb.AppendLine("Always explain your plan step by step BEFORE executing tools. After creating files, summarize what was created.");
|
||
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above. Never use placeholder or fictional dates.");
|
||
sb.AppendLine("IMPORTANT: When asked to create a document with multiple sections (reports, proposals, analyses, etc.), you MUST:");
|
||
sb.AppendLine(" 1. First, plan the document: decide the exact sections (headings), their order, and key points for each section based on the topic.");
|
||
sb.AppendLine(" 2. Call document_plan with sections_hint = your planned section titles (comma-separated). Example: sections_hint=\"?뚯궗 媛쒖슂, ?ъ뾽 ?꾪솴, ?щТ 遺꾩꽍, SWOT, ?꾨왂 ?쒖뼵, 寃곕줎\"");
|
||
sb.AppendLine(" This ensures the document structure matches YOUR plan, not a generic template.");
|
||
sb.AppendLine(" 3. Then immediately call html_create (or docx_create/file_write) using the scaffold from document_plan.");
|
||
sb.AppendLine(" 4. Write actual detailed content for EVERY section ??no skipping, no placeholders, no minimal content.");
|
||
sb.AppendLine(" 5. Do NOT call html_create directly without document_plan for multi-section documents.");
|
||
|
||
// 臾몄꽌 ?덉쭏 寃利?猷⑦봽
|
||
sb.AppendLine("\n## Document Quality Review");
|
||
sb.AppendLine("After creating any document (html_create, docx_create, excel_create, etc.), you MUST perform a self-review:");
|
||
sb.AppendLine("1. Use file_read to read the generated file and verify the content is complete");
|
||
sb.AppendLine("2. Check for logical errors: incorrect dates, inconsistent data, missing sections, broken formatting");
|
||
sb.AppendLine("3. Verify all requested topics/sections from the user's original request are covered");
|
||
sb.AppendLine("4. If issues found, fix them using file_write or file_edit, then re-verify");
|
||
sb.AppendLine("5. Report the review result to the user: what was checked and whether corrections were made");
|
||
|
||
// 臾몄꽌 ?щ㎎ 蹂??吏?? sb.AppendLine("\n## Format Conversion");
|
||
sb.AppendLine("When the user requests format conversion (e.g., HTML?뭌ord, Excel?묬SV, Markdown?묱TML):");
|
||
sb.AppendLine("1. Use file_read or document_read to read the source file content");
|
||
sb.AppendLine("2. Create a new file in the target format using the appropriate skill (docx_create, html_create, etc.)");
|
||
sb.AppendLine("3. Preserve the content structure, formatting, and data as closely as possible");
|
||
|
||
// ?ъ슜??吏??異쒕젰 ?щ㎎
|
||
var fmt = llm.DefaultOutputFormat;
|
||
if (!string.IsNullOrEmpty(fmt) && fmt != "auto")
|
||
{
|
||
var fmtMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
["xlsx"] = "Excel (.xlsx) using excel_create",
|
||
["docx"] = "Word (.docx) using docx_create",
|
||
["html"] = "HTML (.html) using html_create",
|
||
["md"] = "Markdown (.md) using markdown_create",
|
||
["csv"] = "CSV (.csv) using csv_create",
|
||
};
|
||
if (fmtMap.TryGetValue(fmt, out var fmtDesc))
|
||
sb.AppendLine($"IMPORTANT: User prefers output format: {fmtDesc}. Use this format unless the user specifies otherwise.");
|
||
}
|
||
|
||
// ?붿옄??臾대뱶 ??HTML 臾몄꽌 ?앹꽦 ??mood ?뚮씪誘명꽣濡??꾨떖?섎룄濡??덈궡
|
||
if (!string.IsNullOrEmpty(_selectedMood) && _selectedMood != "modern")
|
||
sb.AppendLine($"When creating HTML documents with html_create, use mood=\"{_selectedMood}\" for the design template.");
|
||
else
|
||
sb.AppendLine("When creating HTML documents with html_create, you can set 'mood' parameter: modern, professional, creative, minimal, elegant, dark, colorful, corporate, magazine, dashboard.");
|
||
|
||
if (!string.IsNullOrEmpty(workFolder))
|
||
sb.AppendLine($"Current work folder: {workFolder}");
|
||
sb.AppendLine($"File permission mode: {llm.FilePermission}");
|
||
|
||
// ?대뜑 ?곗씠???쒖슜 吏移? switch (_folderDataUsage)
|
||
{
|
||
case "active":
|
||
sb.AppendLine("IMPORTANT: Folder Data Usage = ACTIVE. You have 'document_read' and 'folder_map' tools available.");
|
||
sb.AppendLine("Before creating reports, use folder_map to scan the work folder structure. " +
|
||
"Then EVALUATE whether each document is RELEVANT to the user's current request topic. " +
|
||
"Only use document_read on files that are clearly related to the conversation subject. " +
|
||
"Do NOT read or reference files that are unrelated to the user's request, even if they exist in the folder. " +
|
||
"In your planning step, list which files you plan to read and explain WHY they are relevant.");
|
||
break;
|
||
case "passive":
|
||
sb.AppendLine("Folder Data Usage = PASSIVE. You have 'document_read' and 'folder_map' tools. " +
|
||
"Only read folder documents when the user explicitly asks you to reference or use them.");
|
||
break;
|
||
default: // "none"
|
||
sb.AppendLine("Folder Data Usage = NONE. Do NOT read or reference documents in the work folder unless the user explicitly provides a file path.");
|
||
break;
|
||
}
|
||
|
||
// ?꾨━???쒖뒪???꾨\?꾪듃媛 ?덉쑝硫?異붽?
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null && !string.IsNullOrEmpty(_currentConversation.SystemCommand))
|
||
sb.AppendLine("\n" + _currentConversation.SystemCommand);
|
||
}
|
||
|
||
// ?꾨줈?앺듃 臾몃㎘ ?뚯씪 (AX.md) 二쇱엯
|
||
sb.Append(LoadProjectContext(workFolder));
|
||
|
||
// ?꾨줈?앺듃 洹쒖튃 (.ax/rules/) ?먮룞 二쇱엯
|
||
sb.Append(BuildProjectRulesSection(workFolder));
|
||
|
||
// ?먯씠?꾪듃 硫붾え由?二쇱엯
|
||
sb.Append(BuildMemorySection(workFolder));
|
||
|
||
// ?쇰뱶諛??숈뒿 而⑦뀓?ㅽ듃 二쇱엯
|
||
sb.Append(BuildFeedbackContext());
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
private string BuildCodeSystemPrompt()
|
||
{
|
||
var workFolder = GetCurrentWorkFolder();
|
||
var llm = _settings.Settings.Llm;
|
||
var code = llm.Code;
|
||
var sb = new System.Text.StringBuilder();
|
||
|
||
sb.AppendLine("You are AX Copilot Code Agent ??a senior software engineer for enterprise development.");
|
||
sb.AppendLine($"Today's date: {DateTime.Now:yyyy??M??d?? ({DateTime.Now:yyyy-MM-dd}).");
|
||
sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), folder_map, process, dev_env_detect, build_run, git_tool.");
|
||
sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above.");
|
||
|
||
sb.AppendLine("\n## Core Workflow (MANDATORY ??follow this order)");
|
||
sb.AppendLine("1. ORIENT: Run folder_map (depth=2) to understand project structure. Check .gitignore, README, config files.");
|
||
sb.AppendLine("2. BASELINE: If tests exist, run build_run action='test' FIRST to establish baseline. Record pass/fail count.");
|
||
sb.AppendLine("3. ANALYZE: Use grep (with context_lines=2) + file_read to deeply understand the code you'll modify.");
|
||
sb.AppendLine(" - Always check callers/references: grep for function/class names to find all usage points.");
|
||
sb.AppendLine(" - Read test files related to the code you're changing to understand expected behavior.");
|
||
sb.AppendLine("4. PLAN: Present your analysis + impact assessment. List ALL files that will be modified.");
|
||
sb.AppendLine(" - Explain WHY each change is needed and what could break.");
|
||
sb.AppendLine(" - Wait for user approval before proceeding.");
|
||
sb.AppendLine("5. IMPLEMENT: Apply changes using file_edit (preferred ??shows diff). Use file_write only for new files.");
|
||
sb.AppendLine(" - Make the MINIMUM changes needed. Don't refactor unrelated code.");
|
||
sb.AppendLine(" - Prefer file_edit with replace_all=false for precision edits.");
|
||
sb.AppendLine("6. VERIFY: Run build_run action='build' then action='test'. Compare results with baseline.");
|
||
sb.AppendLine(" - If tests fail that passed before, fix immediately.");
|
||
sb.AppendLine(" - If build fails, analyze error output and correct.");
|
||
sb.AppendLine("7. GIT: Use git_tool to check status, create diff, and optionally commit.");
|
||
sb.AppendLine("8. REPORT: Summarize changes, test results, and any remaining concerns.");
|
||
|
||
sb.AppendLine("\n## Development Environment");
|
||
sb.AppendLine("Use dev_env_detect to check installed IDEs, runtimes, and build tools before running commands.");
|
||
sb.AppendLine("IMPORTANT: Do NOT attempt to install compilers, IDEs, or build tools. Only use what is already installed.");
|
||
|
||
// ?⑦궎吏 ??μ냼 ?뺣낫
|
||
sb.AppendLine("\n## Package Repositories");
|
||
if (!string.IsNullOrEmpty(code.NexusBaseUrl))
|
||
sb.AppendLine($"Enterprise Nexus: {code.NexusBaseUrl}");
|
||
sb.AppendLine($"NuGet (.NET): {code.NugetSource}");
|
||
sb.AppendLine($"PyPI/Conda (Python): {code.PypiSource}");
|
||
sb.AppendLine($"Maven (Java): {code.MavenSource}");
|
||
sb.AppendLine($"npm (JavaScript): {code.NpmSource}");
|
||
sb.AppendLine("When adding dependencies, use these repository URLs.");
|
||
|
||
// IDE ?뺣낫
|
||
if (!string.IsNullOrEmpty(code.PreferredIdePath))
|
||
sb.AppendLine($"\nPreferred IDE: {code.PreferredIdePath}");
|
||
|
||
// ?ъ슜???좏깮 媛쒕컻 ?몄뼱
|
||
if (_selectedLanguage != "auto")
|
||
{
|
||
var langName = _selectedLanguage switch { "python" => "Python", "java" => "Java", "csharp" => "C# (.NET)", "cpp" => "C/C++", "javascript" => "JavaScript/TypeScript", _ => _selectedLanguage };
|
||
sb.AppendLine($"\nIMPORTANT: User selected language: {langName}. Prioritize this language for code analysis and generation.");
|
||
}
|
||
|
||
// ?몄뼱蹂?媛?대뱶?쇱씤
|
||
sb.AppendLine("\n## Language Guidelines");
|
||
sb.AppendLine("- C# (.NET): Use dotnet CLI. NuGet for packages. Follow Microsoft naming conventions.");
|
||
sb.AppendLine("- Python: Use conda/pip. Follow PEP8. Use type hints. Virtual env preferred.");
|
||
sb.AppendLine("- Java: Use Maven/Gradle. Follow Google Java Style Guide.");
|
||
sb.AppendLine("- C++: Use CMake for build. Follow C++ Core Guidelines.");
|
||
sb.AppendLine("- JavaScript/TypeScript: Use npm/yarn. Follow ESLint rules. Vue3 uses Composition API.");
|
||
|
||
// 肄붾뱶 ?덉쭏 + ?덉쟾 ?섏튃
|
||
sb.AppendLine("\n## Code Quality & Safety");
|
||
sb.AppendLine("- NEVER delete or overwrite files without user confirmation.");
|
||
sb.AppendLine("- ALWAYS read a file before editing it. Don't guess contents.");
|
||
sb.AppendLine("- Prefer file_edit over file_write for existing files (shows diff).");
|
||
sb.AppendLine("- Use grep to find ALL references before renaming/removing anything.");
|
||
sb.AppendLine("- If unsure about a change's impact, ask the user first.");
|
||
sb.AppendLine("- For large refactors, do them incrementally with build verification between steps.");
|
||
sb.AppendLine("- Use git_tool action='diff' to review your changes before committing.");
|
||
|
||
sb.AppendLine("\n## Lint & Format");
|
||
sb.AppendLine("After code changes, check for available linters:");
|
||
sb.AppendLine("- Python: ruff, black, flake8, pylint");
|
||
sb.AppendLine("- JavaScript: eslint, prettier");
|
||
sb.AppendLine("- C#: dotnet format");
|
||
sb.AppendLine("- C++: clang-format");
|
||
sb.AppendLine("Run the appropriate linter via process tool if detected by dev_env_detect.");
|
||
|
||
if (!string.IsNullOrEmpty(workFolder))
|
||
sb.AppendLine($"\nCurrent work folder: {workFolder}");
|
||
sb.AppendLine($"File permission mode: {llm.FilePermission}");
|
||
|
||
// ?대뜑 ?곗씠???쒖슜
|
||
sb.AppendLine("\nFolder Data Usage = ACTIVE. Use folder_map and file_read to understand the codebase.");
|
||
sb.AppendLine("Analyze project structure before making changes. Read relevant files to understand context.");
|
||
|
||
// ?꾨━???쒖뒪???꾨\?꾪듃
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation?.SystemCommand is { Length: > 0 } sysCmd)
|
||
sb.AppendLine("\n" + sysCmd);
|
||
}
|
||
|
||
// ?꾨줈?앺듃 臾몃㎘ ?뚯씪 (AX.md) 二쇱엯
|
||
sb.Append(LoadProjectContext(workFolder));
|
||
|
||
// ?꾨줈?앺듃 洹쒖튃 (.ax/rules/) ?먮룞 二쇱엯
|
||
sb.Append(BuildProjectRulesSection(workFolder));
|
||
|
||
// ?먯씠?꾪듃 硫붾え由?二쇱엯
|
||
sb.Append(BuildMemorySection(workFolder));
|
||
|
||
// ?쇰뱶諛??숈뒿 而⑦뀓?ㅽ듃 二쇱엯
|
||
sb.Append(BuildFeedbackContext());
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>?꾨줈?앺듃 洹쒖튃 (.ax/rules/)???쒖뒪???꾨\?꾪듃 ?뱀뀡?쇰줈 ?щ㎎?⑸땲??</summary>
|
||
private string BuildProjectRulesSection(string? workFolder)
|
||
{
|
||
if (string.IsNullOrEmpty(workFolder)) return "";
|
||
if (!_settings.Settings.Llm.EnableProjectRules) return "";
|
||
|
||
try
|
||
{
|
||
var rules = Services.Agent.ProjectRulesService.LoadRules(workFolder);
|
||
if (rules.Count == 0) return "";
|
||
|
||
// 而⑦뀓?ㅽ듃蹂??꾪꽣留? Cowork=document, Code=always (湲곕낯)
|
||
var when = _activeTab == "Code" ? "always" : "always";
|
||
var filtered = Services.Agent.ProjectRulesService.FilterRules(rules, when);
|
||
return Services.Agent.ProjectRulesService.FormatForSystemPrompt(filtered);
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/// <summary>?먯씠?꾪듃 硫붾え由щ? ?쒖뒪???꾨\?꾪듃 ?뱀뀡?쇰줈 ?щ㎎?⑸땲??</summary>
|
||
private string BuildMemorySection(string? workFolder)
|
||
{
|
||
if (!_settings.Settings.Llm.EnableAgentMemory) return "";
|
||
|
||
var app = System.Windows.Application.Current as App;
|
||
var memService = app?.MemoryService;
|
||
if (memService == null || memService.Count == 0) return "";
|
||
|
||
// 硫붾え由щ? 濡쒕뱶 (?묒뾽 ?대뜑 蹂寃????щ줈??
|
||
memService.Load(workFolder ?? "");
|
||
|
||
var all = memService.All;
|
||
if (all.Count == 0) return "";
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine("\n## ?꾨줈?앺듃 硫붾え由?(?댁쟾 ??붿뿉???숈뒿???댁슜)");
|
||
sb.AppendLine("?꾨옒???댁쟾 ??붿뿉???숈뒿??洹쒖튃怨??좏샇?꾩엯?덈떎. ?묒뾽 ??李멸퀬?섏꽭??");
|
||
sb.AppendLine("?덈줈??洹쒖튃?대굹 ?좏샇?꾨? 諛쒓껄?섎㈃ memory ?꾧뎄??save ?≪뀡?쇰줈 ??ν븯?몄슂.");
|
||
sb.AppendLine("?ъ슜?먭? ?댁쟾 ?숈뒿 ?댁슜怨??ㅻⅨ 吏?쒕? ?섎㈃ memory ?꾧뎄??delete ???덈줈 save ?섏꽭??\n");
|
||
|
||
foreach (var group in all.GroupBy(e => e.Type))
|
||
{
|
||
var label = group.Key switch
|
||
{
|
||
"rule" => "?꾨줈?앺듃 洹쒖튃",
|
||
"preference" => "?ъ슜???좏샇",
|
||
"fact" => "?꾨줈?앺듃 ?ъ떎",
|
||
"correction" => "?댁쟾 援먯젙",
|
||
_ => group.Key,
|
||
};
|
||
sb.AppendLine($"[{label}]");
|
||
foreach (var e in group.OrderByDescending(e => e.UseCount).Take(15))
|
||
sb.AppendLine($"- {e.Content}");
|
||
sb.AppendLine();
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>?뚰겕?뚮줈???쒓컖???ㅼ젙??耳쒖졇?덉쑝硫?遺꾩꽍湲?李쎌쓣 ?닿퀬 ?대깽?몃? 援щ룆?⑸땲??</summary>
|
||
private void OpenWorkflowAnalyzerIfEnabled()
|
||
{
|
||
var llm = _settings.Settings.Llm;
|
||
if (!llm.DevMode || !llm.WorkflowVisualizer) return;
|
||
|
||
if (_analyzerWindow == null)
|
||
{
|
||
// ?덈줈 ?앹꽦
|
||
_analyzerWindow = new WorkflowAnalyzerWindow();
|
||
_analyzerWindow.Closed += (_, _) => _analyzerWindow = null;
|
||
// ?뚮쭏 由ъ냼???꾨떖
|
||
foreach (var dict in System.Windows.Application.Current.Resources.MergedDictionaries)
|
||
_analyzerWindow.Resources.MergedDictionaries.Add(dict);
|
||
_analyzerWindow.Show();
|
||
}
|
||
else if (!_analyzerWindow.IsVisible)
|
||
{
|
||
// Hide()濡??④꺼吏?李???湲곗〈 ?댁슜 ?좎???梨??ㅼ떆 ?쒖떆
|
||
_analyzerWindow.Show();
|
||
_analyzerWindow.Activate();
|
||
}
|
||
else
|
||
{
|
||
// ?대? 蹂댁씠???곹깭 ?????먯씠?꾪듃 ?ㅽ뻾???꾪빐 珥덇린?????쒖꽦?? _analyzerWindow.Reset();
|
||
_analyzerWindow.Activate();
|
||
}
|
||
|
||
// ??꾨씪????쑝濡??꾪솚 (???ㅽ뻾 ?쒖옉)
|
||
_analyzerWindow.SwitchToTimelineTab();
|
||
|
||
// ?대깽??援щ룆 (以묐났 諛⑹?)
|
||
_agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent;
|
||
_agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent;
|
||
}
|
||
|
||
/// <summary>?뚰겕?뚮줈??遺꾩꽍湲?踰꾪듉???쒖떆 ?곹깭瑜?媛깆떊?⑸땲??</summary>
|
||
private void UpdateAnalyzerButtonVisibility()
|
||
{
|
||
var llm = _settings.Settings.Llm;
|
||
BtnShowAnalyzer.Visibility = (llm.DevMode && llm.WorkflowVisualizer)
|
||
? Visibility.Visible : Visibility.Collapsed;
|
||
}
|
||
|
||
/// <summary>?뚰겕?뚮줈??遺꾩꽍湲?李쎌쓣 ?섎룞?쇰줈 ?닿굅???ъ빱?ㅽ빀?덈떎 (?섎떒 諛?踰꾪듉).</summary>
|
||
private void BtnShowAnalyzer_Click(object sender, MouseButtonEventArgs e)
|
||
{
|
||
if (_analyzerWindow == null)
|
||
{
|
||
_analyzerWindow = new WorkflowAnalyzerWindow();
|
||
_analyzerWindow.Closed += (_, _) => _analyzerWindow = null;
|
||
foreach (var dict in System.Windows.Application.Current.Resources.MergedDictionaries)
|
||
_analyzerWindow.Resources.MergedDictionaries.Add(dict);
|
||
// ?먯씠?꾪듃 ?대깽??援щ룆
|
||
_agentLoop.EventOccurred -= _analyzerWindow.OnAgentEvent;
|
||
_agentLoop.EventOccurred += _analyzerWindow.OnAgentEvent;
|
||
_analyzerWindow.Show();
|
||
}
|
||
else if (!_analyzerWindow.IsVisible)
|
||
{
|
||
_analyzerWindow.Show();
|
||
_analyzerWindow.Activate();
|
||
}
|
||
else
|
||
{
|
||
_analyzerWindow.Activate();
|
||
}
|
||
}
|
||
|
||
/// <summary>?먯씠?꾪듃 猷⑦봽 ?숈븞 ?꾩쟻 ?좏겙 (?섎떒 諛??쒖떆??</summary>
|
||
private int _agentCumulativeInputTokens;
|
||
private int _agentCumulativeOutputTokens;
|
||
|
||
private static readonly HashSet<string> WriteToolNames = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
"file_write", "file_edit", "html_create", "xlsx_create",
|
||
"docx_create", "csv_create", "md_create", "script_create",
|
||
"diff_preview", "open_external",
|
||
};
|
||
|
||
private void OnAgentEvent(AgentEvent evt)
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock)
|
||
{
|
||
conv = _currentConversation;
|
||
conv?.ExecutionEvents.Add(ToChatExecutionEvent(evt));
|
||
}
|
||
if (conv != null)
|
||
{
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"?ㅽ뻾 ?대깽??????ㅽ뙣: {ex.Message}"); }
|
||
}
|
||
|
||
UpdateRuntimeActivityState(evt);
|
||
|
||
// ?먯씠?꾪듃 ?대깽?몃? 梨꾪똿 UI???쒖떆 (?꾧뎄 ?몄텧/寃곌낵 諛곕꼫)
|
||
if (IsExecutionHistoryVisible())
|
||
{
|
||
AddAgentEventBanner(evt);
|
||
AutoScrollIfNeeded();
|
||
}
|
||
|
||
// ?섎떒 ?곹깭諛??낅뜲?댄듃
|
||
UpdateStatusBar(evt);
|
||
|
||
// ?섎떒 諛??좏겙 ?꾩쟻 ?낅뜲?댄듃 (?먯씠?꾪듃 猷⑦봽 ?꾩껜 ?⑷퀎)
|
||
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
|
||
{
|
||
_agentCumulativeInputTokens += evt.InputTokens;
|
||
_agentCumulativeOutputTokens += evt.OutputTokens;
|
||
UpdateStatusTokens(_agentCumulativeInputTokens, _agentCumulativeOutputTokens);
|
||
}
|
||
|
||
// ?ㅽ떚??吏꾪뻾瑜?諛??낅뜲?댄듃
|
||
UpdateAgentProgressBar(evt);
|
||
|
||
// 怨꾪쉷 酉곗뼱 ?④퀎 媛깆떊
|
||
if (evt.StepCurrent > 0 && evt.StepTotal > 0)
|
||
UpdatePlanViewerStep(evt);
|
||
if (evt.Type == AgentEventType.Complete)
|
||
CompletePlanViewer();
|
||
|
||
// ?뚯씪 ?먯깋湲??먮룞 ?덈줈怨좎묠
|
||
if (evt.Success && !string.IsNullOrEmpty(evt.FilePath))
|
||
RefreshFileTreeIfVisible();
|
||
|
||
// suggest_actions ?꾧뎄 寃곌낵 ???꾩냽 ?묒뾽 移??쒖떆
|
||
if (evt.Type == AgentEventType.ToolResult && evt.ToolName == "suggest_actions" && evt.Success)
|
||
RenderSuggestActionChips(evt.Summary);
|
||
|
||
// ?뚯씪 ?앹꽦/?섏젙 寃곌낵媛 ?덉쑝硫?誘몃━蹂닿린 ?먮룞 ?쒖떆 ?먮뒗 媛깆떊
|
||
if (evt.Success && !string.IsNullOrEmpty(evt.FilePath) &&
|
||
(evt.Type == AgentEventType.ToolResult || evt.Type == AgentEventType.Complete) &&
|
||
WriteToolNames.Contains(evt.ToolName))
|
||
{
|
||
var autoPreview = _settings.Settings.Llm.AutoPreview;
|
||
if (autoPreview == "auto")
|
||
{
|
||
// 蹂꾨룄 李?誘몃━蹂닿린: ?대? ?대┛ ?뚯씪?대㈃ ?덈줈怨좎묠, ?꾨땲硫?????異붽?
|
||
if (PreviewWindow.IsOpen)
|
||
PreviewWindow.RefreshIfOpen(evt.FilePath);
|
||
else
|
||
TryShowPreview(evt.FilePath);
|
||
|
||
// ???뚯씪?대㈃ ??긽 ?쒖떆
|
||
if (!PreviewWindow.IsOpen)
|
||
TryShowPreview(evt.FilePath);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateRuntimeActivityState(AgentEvent evt)
|
||
private void UpdateRuntimeActivityState(AgentEvent evt)
|
||
{
|
||
switch (evt.Type)
|
||
{
|
||
case AgentEventType.ToolCall:
|
||
_appState.UpsertTask(
|
||
GetToolTaskKey(evt),
|
||
"tool",
|
||
evt.ToolName,
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? $"{evt.ToolName} 실행 중" : evt.Summary,
|
||
"running",
|
||
evt.FilePath);
|
||
_sessionState.UpsertRuntimeActivity(
|
||
$"tool:{evt.ToolName}",
|
||
"tool",
|
||
evt.ToolName,
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? $"{evt.ToolName} 실행 중" : evt.Summary);
|
||
break;
|
||
case AgentEventType.ToolResult:
|
||
_appState.CompleteTask(
|
||
GetToolTaskKey(evt),
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary,
|
||
evt.Success ? "completed" : "failed");
|
||
_sessionState.RemoveRuntimeActivity(
|
||
$"tool:{evt.ToolName}",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary);
|
||
break;
|
||
case AgentEventType.Error:
|
||
if (!string.IsNullOrWhiteSpace(evt.ToolName))
|
||
{
|
||
_appState.CompleteTask(
|
||
GetToolTaskKey(evt),
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary,
|
||
"failed");
|
||
_sessionState.RemoveRuntimeActivity(
|
||
$"tool:{evt.ToolName}",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? evt.ToolName : evt.Summary);
|
||
}
|
||
else
|
||
{
|
||
_appState.CompleteTask(GetAgentTaskKey(evt), evt.Summary, "failed");
|
||
_appState.CompleteAgentRun(
|
||
evt.RunId,
|
||
"failed",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 실행 실패" : evt.Summary,
|
||
evt.Iteration);
|
||
}
|
||
break;
|
||
case AgentEventType.Thinking:
|
||
_appState.UpsertTask(GetAgentTaskKey(evt), "agent", "main", evt.Summary, "running");
|
||
_appState.UpsertAgentRun(
|
||
evt.RunId,
|
||
"running",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "응답을 준비하는 중" : evt.Summary,
|
||
evt.Iteration);
|
||
_sessionState.UpsertRuntimeActivity("agent:thinking", "agent", "thinking", evt.Summary);
|
||
break;
|
||
case AgentEventType.Planning:
|
||
_appState.UpsertTask(GetAgentTaskKey(evt), "agent", "main", evt.Summary, "running");
|
||
_appState.UpsertAgentRun(
|
||
evt.RunId,
|
||
"running",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "작업 계획을 세우는 중" : evt.Summary,
|
||
evt.Iteration);
|
||
_sessionState.UpsertRuntimeActivity("agent:planning", "agent", "planning", evt.Summary);
|
||
break;
|
||
case AgentEventType.SkillCall:
|
||
_appState.UpsertTask(
|
||
GetToolTaskKey(evt),
|
||
"skill",
|
||
evt.ToolName,
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? $"{evt.ToolName} 실행 중" : evt.Summary,
|
||
"running",
|
||
evt.FilePath);
|
||
break;
|
||
case AgentEventType.Complete:
|
||
_appState.CompleteTask(GetAgentTaskKey(evt), string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary);
|
||
_appState.CompleteAgentRun(
|
||
evt.RunId,
|
||
"completed",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary,
|
||
evt.Iteration);
|
||
_appState.ClearTasksByPrefix(
|
||
string.IsNullOrWhiteSpace(evt.RunId) ? "tool:" : $"tool:{evt.RunId}:",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary);
|
||
_sessionState.ClearRuntimeActivities(string.IsNullOrWhiteSpace(evt.Summary) ? "작업 완료" : evt.Summary);
|
||
break;
|
||
case AgentEventType.Paused:
|
||
_appState.UpsertAgentRun(
|
||
evt.RunId,
|
||
"paused",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 일시중지" : evt.Summary,
|
||
evt.Iteration);
|
||
break;
|
||
case AgentEventType.Resumed:
|
||
_appState.UpsertAgentRun(
|
||
evt.RunId,
|
||
"running",
|
||
string.IsNullOrWhiteSpace(evt.Summary) ? "에이전트 재개" : evt.Summary,
|
||
evt.Iteration);
|
||
break;
|
||
}
|
||
|
||
UpdateRuntimeActivityIndicators();
|
||
}
|
||
private void OnSubAgentStatusChanged(SubAgentStatusEvent evt)
|
||
{
|
||
Dispatcher.Invoke(() =>
|
||
{
|
||
var key = $"subagent:{evt.Id}";
|
||
switch (evt.Status)
|
||
{
|
||
case SubAgentRunStatus.Started:
|
||
_appState.UpsertTask(key, "subagent", evt.Id, evt.Summary, "running");
|
||
_sessionState.UpsertRuntimeActivity(key, "subagent", evt.Id, evt.Summary);
|
||
break;
|
||
case SubAgentRunStatus.Completed:
|
||
_appState.CompleteTask(key, evt.Summary, "completed");
|
||
_sessionState.RemoveRuntimeActivity(key, evt.Summary);
|
||
break;
|
||
case SubAgentRunStatus.Failed:
|
||
_appState.CompleteTask(key, evt.Summary, "failed");
|
||
_sessionState.RemoveRuntimeActivity(key, evt.Summary);
|
||
break;
|
||
}
|
||
|
||
UpdateRuntimeActivityIndicators();
|
||
});
|
||
}
|
||
|
||
// ??? Task Decomposition UI ????????????????????????????????????????????
|
||
|
||
private Border? _planningCard;
|
||
private StackPanel? _planStepsPanel;
|
||
private ProgressBar? _planProgressBar;
|
||
private TextBlock? _planProgressText;
|
||
|
||
/// <summary>?묒뾽 怨꾪쉷 移대뱶瑜??앹꽦?⑸땲??(?④퀎 紐⑸줉 + 吏꾪뻾瑜?諛?.</summary>
|
||
private void AddPlanningCard(AgentEvent evt)
|
||
{
|
||
var steps = evt.Steps!;
|
||
|
||
var card = new Border
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F4FF")),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(14, 10, 14, 10),
|
||
Margin = new Thickness(40, 4, 80, 4),
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
MaxWidth = 560,
|
||
};
|
||
|
||
var sp = new StackPanel();
|
||
|
||
// ?ㅻ뜑
|
||
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||
header.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE9D5", // plan icon
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 13,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
});
|
||
header.Children.Add(new TextBlock
|
||
{
|
||
Text = $"?묒뾽 怨꾪쉷 ??{steps.Count}?④퀎",
|
||
FontSize = 12.5, FontWeight = FontWeights.SemiBold,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#3730A3")),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
sp.Children.Add(header);
|
||
|
||
// 吏꾪뻾瑜?諛? var progressGrid = new Grid { Margin = new Thickness(0, 0, 0, 8) };
|
||
progressGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
progressGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
|
||
_planProgressBar = new ProgressBar
|
||
{
|
||
Minimum = 0,
|
||
Maximum = steps.Count,
|
||
Value = 0,
|
||
Height = 4,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")),
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D0D5FF")),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
// Remove the default border on ProgressBar
|
||
_planProgressBar.BorderThickness = new Thickness(0);
|
||
Grid.SetColumn(_planProgressBar, 0);
|
||
progressGrid.Children.Add(_planProgressBar);
|
||
|
||
_planProgressText = new TextBlock
|
||
{
|
||
Text = "0%",
|
||
FontSize = 10.5, FontWeight = FontWeights.SemiBold,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")),
|
||
Margin = new Thickness(8, 0, 0, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(_planProgressText, 1);
|
||
progressGrid.Children.Add(_planProgressText);
|
||
sp.Children.Add(progressGrid);
|
||
|
||
// ?④퀎 紐⑸줉
|
||
_planStepsPanel = new StackPanel();
|
||
for (int i = 0; i < steps.Count; i++)
|
||
{
|
||
var stepRow = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
Tag = i, // ?몃뜳????? };
|
||
|
||
stepRow.Children.Add(new TextBlock
|
||
{
|
||
Text = "??, // 鍮???(誘몄셿猷?
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
Tag = "status",
|
||
});
|
||
stepRow.Children.Add(new TextBlock
|
||
{
|
||
Text = $"{i + 1}. {steps[i]}",
|
||
FontSize = 11.5,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
MaxWidth = 480,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
|
||
_planStepsPanel.Children.Add(stepRow);
|
||
}
|
||
sp.Children.Add(_planStepsPanel);
|
||
|
||
card.Child = sp;
|
||
_planningCard = card;
|
||
|
||
// ?섏씠?쒖씤
|
||
card.Opacity = 0;
|
||
card.BeginAnimation(UIElement.OpacityProperty,
|
||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300)));
|
||
|
||
MessagePanel.Children.Add(card);
|
||
}
|
||
|
||
/// <summary>怨꾪쉷 移대뱶 ?꾨옒???뱀씤/?섏젙/痍⑥냼 ?섏궗寃곗젙 踰꾪듉??異붽??⑸땲??</summary>
|
||
private void AddDecisionButtons(TaskCompletionSource<string?> tcs, List<string> options)
|
||
{
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
var accentColor = ((SolidColorBrush)accentBrush).Color;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
var container = new Border
|
||
{
|
||
Margin = new Thickness(40, 2, 80, 6),
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
MaxWidth = 560,
|
||
};
|
||
|
||
var outerStack = new StackPanel();
|
||
|
||
// 踰꾪듉 ?? var btnRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 0) };
|
||
|
||
// ?뱀씤 踰꾪듉 (媛뺤“)
|
||
var approveBtn = new Border
|
||
{
|
||
Background = accentBrush,
|
||
CornerRadius = new CornerRadius(16),
|
||
Padding = new Thickness(16, 7, 16, 7),
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var approveSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
approveSp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11,
|
||
Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0),
|
||
});
|
||
approveSp.Children.Add(new TextBlock { Text = "?뱀씤", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White });
|
||
approveBtn.Child = approveSp;
|
||
ApplyMenuItemHover(approveBtn);
|
||
approveBtn.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
CollapseDecisionButtons(outerStack, "???뱀씤??, accentBrush);
|
||
tcs.TrySetResult(null); // null = ?뱀씤
|
||
};
|
||
btnRow.Children.Add(approveBtn);
|
||
|
||
// ?섏젙 ?붿껌 踰꾪듉
|
||
var editBtn = new Border
|
||
{
|
||
Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B)),
|
||
CornerRadius = new CornerRadius(16),
|
||
Padding = new Thickness(14, 7, 14, 7),
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
Cursor = Cursors.Hand,
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
|
||
BorderThickness = new Thickness(1),
|
||
};
|
||
var editSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
editSp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE70F", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11,
|
||
Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0),
|
||
});
|
||
editSp.Children.Add(new TextBlock { Text = "?섏젙 ?붿껌", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = accentBrush });
|
||
editBtn.Child = editSp;
|
||
ApplyMenuItemHover(editBtn);
|
||
|
||
// ?섏젙 ?붿껌???띿뒪???낅젰 ?⑤꼸 (珥덇린 ?④?)
|
||
var editInputPanel = new Border
|
||
{
|
||
Visibility = Visibility.Collapsed,
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8F9FC")),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(0, 8, 0, 0),
|
||
};
|
||
var editInputStack = new StackPanel();
|
||
editInputStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "?섏젙 ?ы빆???낅젰?섏꽭??",
|
||
FontSize = 11.5, Foreground = secondaryText,
|
||
Margin = new Thickness(0, 0, 0, 6),
|
||
});
|
||
var editTextBox = new TextBox
|
||
{
|
||
MinHeight = 36,
|
||
MaxHeight = 100,
|
||
AcceptsReturn = true,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
FontSize = 12.5,
|
||
Background = Brushes.White,
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)),
|
||
BorderThickness = new Thickness(1),
|
||
Padding = new Thickness(8, 6, 8, 6),
|
||
};
|
||
editInputStack.Children.Add(editTextBox);
|
||
|
||
var submitEditBtn = new Border
|
||
{
|
||
Background = accentBrush,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(12, 5, 12, 5),
|
||
Margin = new Thickness(0, 6, 0, 0),
|
||
Cursor = Cursors.Hand,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
};
|
||
submitEditBtn.Child = new TextBlock { Text = "?꾩넚", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White };
|
||
ApplyHoverScaleAnimation(submitEditBtn, 1.05);
|
||
submitEditBtn.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
var feedback = editTextBox.Text.Trim();
|
||
if (string.IsNullOrEmpty(feedback)) return;
|
||
CollapseDecisionButtons(outerStack, "???섏젙 ?붿껌??, accentBrush);
|
||
tcs.TrySetResult(feedback);
|
||
};
|
||
editInputStack.Children.Add(submitEditBtn);
|
||
editInputPanel.Child = editInputStack;
|
||
|
||
editBtn.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
editInputPanel.Visibility = editInputPanel.Visibility == Visibility.Visible
|
||
? Visibility.Collapsed : Visibility.Visible;
|
||
if (editInputPanel.Visibility == Visibility.Visible)
|
||
editTextBox.Focus();
|
||
};
|
||
btnRow.Children.Add(editBtn);
|
||
|
||
// 痍⑥냼 踰꾪듉
|
||
var cancelBtn = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(16),
|
||
Padding = new Thickness(14, 7, 14, 7),
|
||
Cursor = Cursors.Hand,
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)),
|
||
BorderThickness = new Thickness(1),
|
||
};
|
||
var cancelSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
cancelSp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0),
|
||
});
|
||
cancelSp.Children.Add(new TextBlock
|
||
{
|
||
Text = "痍⑥냼", FontSize = 12.5, FontWeight = FontWeights.SemiBold,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
|
||
});
|
||
cancelBtn.Child = cancelSp;
|
||
ApplyMenuItemHover(cancelBtn);
|
||
cancelBtn.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
CollapseDecisionButtons(outerStack, "??痍⑥냼??,
|
||
new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)));
|
||
tcs.TrySetResult("痍⑥냼");
|
||
};
|
||
btnRow.Children.Add(cancelBtn);
|
||
|
||
outerStack.Children.Add(btnRow);
|
||
outerStack.Children.Add(editInputPanel);
|
||
container.Child = outerStack;
|
||
|
||
// ?щ씪?대뱶 + ?섏씠???깆옣 ?좊땲硫붿씠?? ApplyMessageEntryAnimation(container);
|
||
MessagePanel.Children.Add(container);
|
||
ForceScrollToEnd(); // ?섏궗寃곗젙 踰꾪듉 ?쒖떆 ??媛뺤젣 ?섎떒 ?대룞
|
||
|
||
// PlanViewerWindow ???몃??먯꽌 TCS媛 ?꾨즺?섎㈃ ?몃씪??踰꾪듉???먮룞 ?묎린
|
||
var capturedOuterStack = outerStack;
|
||
var capturedAccent = accentBrush;
|
||
_ = tcs.Task.ContinueWith(t =>
|
||
{
|
||
Dispatcher.BeginInvoke(() =>
|
||
{
|
||
// ?대? ?묓??덉쑝硫??ㅽ궢 (?몃씪??踰꾪듉?쇰줈 吏곸젒 ?대┃??寃쎌슦)
|
||
if (capturedOuterStack.Children.Count <= 1) return;
|
||
|
||
var label = t.Result == null ? "???뱀씤??
|
||
: t.Result == "痍⑥냼" ? "??痍⑥냼??
|
||
: "???섏젙 ?붿껌??;
|
||
var fg = t.Result == "痍⑥냼"
|
||
? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26))
|
||
: capturedAccent;
|
||
CollapseDecisionButtons(capturedOuterStack, label, fg);
|
||
});
|
||
}, TaskScheduler.Default);
|
||
}
|
||
|
||
/// <summary>?섏궗寃곗젙 踰꾪듉???④린怨?寃곌낵 ?쇰꺼濡?援먯껜?⑸땲??</summary>
|
||
private void CollapseDecisionButtons(StackPanel outerStack, string resultText, Brush fg)
|
||
{
|
||
outerStack.Children.Clear();
|
||
var resultLabel = new TextBlock
|
||
{
|
||
Text = resultText,
|
||
FontSize = 12,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = fg,
|
||
Opacity = 0.8,
|
||
Margin = new Thickness(0, 2, 0, 2),
|
||
};
|
||
outerStack.Children.Add(resultLabel);
|
||
}
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// ?ㅽ뻾 怨꾪쉷 酉곗뼱 (PlanViewerWindow) ?곕룞
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
/// <summary>PlanViewerWindow瑜??ъ슜?섎뒗 UserDecisionCallback???앹꽦?⑸땲??</summary>
|
||
private Func<string, List<string>, Task<string?>> CreatePlanDecisionCallback()
|
||
{
|
||
return async (planSummary, options) =>
|
||
{
|
||
var tcs = new TaskCompletionSource<string?>();
|
||
var steps = Services.Agent.TaskDecomposer.ExtractSteps(planSummary);
|
||
|
||
await Dispatcher.InvokeAsync(() =>
|
||
{
|
||
// PlanViewerWindow ?앹꽦 ?먮뒗 ?ъ궗?? if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow))
|
||
{
|
||
_planViewerWindow = new PlanViewerWindow();
|
||
_planViewerWindow.Closing += (_, e) =>
|
||
{
|
||
e.Cancel = true;
|
||
_planViewerWindow.Hide();
|
||
};
|
||
}
|
||
|
||
// 怨꾪쉷 ?쒖떆 + ?뱀씤 ?湲? _planViewerWindow.ShowPlanAsync(planSummary, steps, tcs);
|
||
|
||
// 梨꾪똿 李쎌뿉 媛꾨왂 諛곕꼫 異붽? + ?몃씪???뱀씤 踰꾪듉???쒖떆
|
||
AddDecisionButtons(tcs, options);
|
||
|
||
// ?섎떒 諛?怨꾪쉷 踰꾪듉 ?쒖떆
|
||
ShowPlanButton(true);
|
||
});
|
||
|
||
// 5遺???꾩븘?? var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||
if (completed != tcs.Task)
|
||
{
|
||
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
||
return "痍⑥냼";
|
||
}
|
||
|
||
var result = await tcs.Task;
|
||
|
||
// ?뱀씤??寃쎌슦 ???ㅽ뻾 紐⑤뱶濡??꾪솚
|
||
if (result == null) // null = ?뱀씤
|
||
{
|
||
await Dispatcher.InvokeAsync(() =>
|
||
{
|
||
_planViewerWindow?.SwitchToExecutionMode();
|
||
_planViewerWindow?.Hide(); // ?④린怨??섎떒 踰꾪듉?쇰줈 ?ㅼ떆 ?닿린
|
||
});
|
||
}
|
||
else
|
||
{
|
||
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
||
}
|
||
|
||
return result;
|
||
};
|
||
}
|
||
|
||
/// <summary>?섎떒 諛붿뿉 怨꾪쉷 蹂닿린 踰꾪듉???쒖떆/?④??⑸땲??</summary>
|
||
private void ShowPlanButton(bool show)
|
||
{
|
||
if (!show)
|
||
{
|
||
// 怨꾪쉷 踰꾪듉 ?쒓굅
|
||
for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--)
|
||
{
|
||
if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn")
|
||
{
|
||
// ?욎쓽 援щ텇?좊룄 ?쒓굅
|
||
if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep")
|
||
MoodIconPanel.Children.RemoveAt(i - 1);
|
||
if (i < MoodIconPanel.Children.Count)
|
||
MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1));
|
||
break;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ?대? ?덉쑝硫?臾댁떆
|
||
foreach (var child in MoodIconPanel.Children)
|
||
{
|
||
if (child is Border b && b.Tag?.ToString() == "PlanBtn") return;
|
||
}
|
||
|
||
// 援щ텇?? var separator = new Border
|
||
{
|
||
Width = 1, Height = 18,
|
||
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
||
Margin = new Thickness(4, 0, 4, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Tag = "PlanSep",
|
||
};
|
||
MoodIconPanel.Children.Add(separator);
|
||
|
||
// 怨꾪쉷 踰꾪듉
|
||
var planBtn = CreateFolderBarButton("\uE9D2", "怨꾪쉷", "?ㅽ뻾 怨꾪쉷 蹂닿린", "#10B981");
|
||
planBtn.Tag = "PlanBtn";
|
||
planBtn.MouseLeftButtonUp += (_, e) =>
|
||
{
|
||
e.Handled = true;
|
||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||
{
|
||
_planViewerWindow.Show();
|
||
_planViewerWindow.Activate();
|
||
}
|
||
};
|
||
MoodIconPanel.Children.Add(planBtn);
|
||
}
|
||
|
||
/// <summary>怨꾪쉷 酉곗뼱?먯꽌 ?꾩옱 ?ㅽ뻾 ?④퀎瑜?媛깆떊?⑸땲??</summary>
|
||
private void UpdatePlanViewerStep(AgentEvent evt)
|
||
{
|
||
if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow)) return;
|
||
if (evt.StepCurrent > 0)
|
||
_planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1); // 0-based
|
||
}
|
||
|
||
/// <summary>怨꾪쉷 ?ㅽ뻾 ?꾨즺瑜?酉곗뼱???뚮┰?덈떎.</summary>
|
||
private void CompletePlanViewer()
|
||
{
|
||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||
_planViewerWindow.MarkComplete();
|
||
ShowPlanButton(false);
|
||
}
|
||
|
||
private static bool IsWindowAlive(Window? w)
|
||
{
|
||
if (w == null) return false;
|
||
try { var _ = w.IsVisible; return true; }
|
||
catch { return false; }
|
||
}
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// ?꾩냽 ?묒뾽 ?쒖븞 移?(suggest_actions)
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
/// <summary>suggest_actions ?꾧뎄 寃곌낵瑜??대┃ 媛?ν븳 移⑹쑝濡??뚮뜑留곹빀?덈떎.</summary>
|
||
private void RenderSuggestActionChips(string jsonSummary)
|
||
{
|
||
// JSON?먯꽌 ?≪뀡 紐⑸줉 ?뚯떛 ?쒕룄
|
||
List<(string label, string command)> actions = new();
|
||
try
|
||
{
|
||
// summary ?뺤떇: "label: command" 以꾨컮轅?援щ텇 ?먮뒗 JSON
|
||
if (jsonSummary.Contains("\"label\""))
|
||
{
|
||
using var doc = System.Text.Json.JsonDocument.Parse(jsonSummary);
|
||
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||
{
|
||
foreach (var item in doc.RootElement.EnumerateArray())
|
||
{
|
||
var label = item.TryGetProperty("label", out var l) ? l.GetString() ?? "" : "";
|
||
var cmd = item.TryGetProperty("command", out var c) ? c.GetString() ?? label : label;
|
||
if (!string.IsNullOrEmpty(label)) actions.Add((label, cmd));
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 以꾨컮轅??뺤떇: "1. label ??command"
|
||
foreach (var line in jsonSummary.Split('\n'))
|
||
{
|
||
var trimmed = line.Trim().TrimStart('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ' ');
|
||
if (string.IsNullOrEmpty(trimmed)) continue;
|
||
var parts = trimmed.Split('??, ':', '??);
|
||
if (parts.Length >= 2)
|
||
actions.Add((parts[0].Trim(), parts[1].Trim()));
|
||
else if (!string.IsNullOrEmpty(trimmed))
|
||
actions.Add((trimmed, trimmed));
|
||
}
|
||
}
|
||
}
|
||
catch { return; }
|
||
|
||
if (actions.Count == 0) return;
|
||
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush
|
||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||
|
||
var container = new Border
|
||
{
|
||
Margin = new Thickness(40, 4, 40, 8),
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
};
|
||
|
||
var headerStack = new StackPanel { Margin = new Thickness(0, 0, 0, 6) };
|
||
headerStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "?뮕 ?ㅼ쓬 ?묒뾽 ?쒖븞:",
|
||
FontSize = 12,
|
||
Foreground = secondaryText,
|
||
});
|
||
|
||
var chipPanel = new WrapPanel { Margin = new Thickness(0, 2, 0, 0) };
|
||
|
||
foreach (var (label, command) in actions.Take(5))
|
||
{
|
||
var capturedCmd = command;
|
||
var chip = new Border
|
||
{
|
||
CornerRadius = new CornerRadius(16),
|
||
Padding = new Thickness(14, 7, 14, 7),
|
||
Margin = new Thickness(0, 0, 8, 6),
|
||
Cursor = Cursors.Hand,
|
||
Background = new SolidColorBrush(Color.FromArgb(0x15,
|
||
((SolidColorBrush)accentBrush).Color.R,
|
||
((SolidColorBrush)accentBrush).Color.G,
|
||
((SolidColorBrush)accentBrush).Color.B)),
|
||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40,
|
||
((SolidColorBrush)accentBrush).Color.R,
|
||
((SolidColorBrush)accentBrush).Color.G,
|
||
((SolidColorBrush)accentBrush).Color.B)),
|
||
BorderThickness = new Thickness(1),
|
||
};
|
||
chip.Child = new TextBlock
|
||
{
|
||
Text = label,
|
||
FontSize = 12.5,
|
||
Foreground = accentBrush,
|
||
FontWeight = FontWeights.SemiBold,
|
||
};
|
||
chip.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
|
||
chip.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||
chip.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
// 移??⑤꼸 ?쒓굅 ???대떦 紐낅졊 ?ㅽ뻾
|
||
MessagePanel.Children.Remove(container);
|
||
if (capturedCmd.StartsWith("/"))
|
||
{
|
||
InputBox.Text = capturedCmd + " ";
|
||
InputBox.CaretIndex = InputBox.Text.Length;
|
||
InputBox.Focus();
|
||
}
|
||
else
|
||
{
|
||
InputBox.Text = capturedCmd;
|
||
_ = SendMessageAsync();
|
||
}
|
||
};
|
||
chipPanel.Children.Add(chip);
|
||
}
|
||
|
||
var outerStack = new StackPanel();
|
||
outerStack.Children.Add(headerStack);
|
||
outerStack.Children.Add(chipPanel);
|
||
container.Child = outerStack;
|
||
|
||
ApplyMessageEntryAnimation(container);
|
||
MessagePanel.Children.Add(container);
|
||
ForceScrollToEnd();
|
||
}
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// ?쇰뱶諛??숈뒿 諛섏쁺 (J)
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
/// <summary>理쒓렐 ??붿쓽 ?쇰뱶諛?醫뗭븘???レ뼱????遺꾩꽍?섏뿬 ?좏샇???붿빟??諛섑솚?⑸땲??</summary>
|
||
private string BuildFeedbackContext()
|
||
{
|
||
try
|
||
{
|
||
var recentConversations = _storage.LoadAllMeta()
|
||
.OrderByDescending(m => m.UpdatedAt)
|
||
.Take(20)
|
||
.ToList();
|
||
|
||
var likedPatterns = new List<string>();
|
||
var dislikedPatterns = new List<string>();
|
||
|
||
foreach (var meta in recentConversations)
|
||
{
|
||
var conv = _storage.Load(meta.Id);
|
||
if (conv == null) continue;
|
||
|
||
foreach (var msg in conv.Messages.Where(m => m.Role == "assistant" && m.Feedback != null))
|
||
{
|
||
// 泥?50?먮줈 ?⑦꽩 ?뚯븙
|
||
var preview = msg.Content?.Length > 80 ? msg.Content[..80] : msg.Content ?? "";
|
||
if (msg.Feedback == "like")
|
||
likedPatterns.Add(preview);
|
||
else if (msg.Feedback == "dislike")
|
||
dislikedPatterns.Add(preview);
|
||
}
|
||
}
|
||
|
||
if (likedPatterns.Count == 0 && dislikedPatterns.Count == 0)
|
||
return "";
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine("\n[?ъ슜???좏샇??李멸퀬]");
|
||
if (likedPatterns.Count > 0)
|
||
{
|
||
sb.AppendLine($"?ъ슜?먭? 醫뗭븘???묐떟 ?ㅽ???({likedPatterns.Count}嫄?:");
|
||
foreach (var p in likedPatterns.Take(5))
|
||
sb.AppendLine($" - \"{p}...\"");
|
||
}
|
||
if (dislikedPatterns.Count > 0)
|
||
{
|
||
sb.AppendLine($"?ъ슜?먭? ?レ뼱???묐떟 ?ㅽ???({dislikedPatterns.Count}嫄?:");
|
||
foreach (var p in dislikedPatterns.Take(5))
|
||
sb.AppendLine($" - \"{p}...\"");
|
||
}
|
||
sb.AppendLine("???좏샇?꾨? 李멸퀬?섏뿬 ?묐떟 ?ㅽ??쇱쓣 議곗젙?섏꽭??");
|
||
return sb.ToString();
|
||
}
|
||
catch { return ""; }
|
||
}
|
||
|
||
/// <summary>吏꾪뻾瑜?諛붿? ?④퀎 ?곹깭瑜??낅뜲?댄듃?⑸땲??</summary>
|
||
private void UpdateProgressBar(AgentEvent evt)
|
||
{
|
||
if (_planProgressBar == null || _planStepsPanel == null || _planProgressText == null)
|
||
return;
|
||
|
||
var stepIdx = evt.StepCurrent - 1; // 0-based
|
||
var total = evt.StepTotal;
|
||
|
||
// 吏꾪뻾瑜?諛??낅뜲?댄듃
|
||
_planProgressBar.Value = evt.StepCurrent;
|
||
var pct = (int)((double)evt.StepCurrent / total * 100);
|
||
_planProgressText.Text = $"{pct}%";
|
||
|
||
// ?댁쟾 ?④퀎 ?꾨즺 ?쒖떆 + ?꾩옱 ?④퀎 媛뺤“
|
||
for (int i = 0; i < _planStepsPanel.Children.Count; i++)
|
||
{
|
||
if (_planStepsPanel.Children[i] is StackPanel row && row.Children.Count >= 2)
|
||
{
|
||
var statusTb = row.Children[0] as TextBlock;
|
||
var textTb = row.Children[1] as TextBlock;
|
||
if (statusTb == null || textTb == null) continue;
|
||
|
||
if (i < stepIdx)
|
||
{
|
||
// ?꾨즺
|
||
statusTb.Text = "??;
|
||
statusTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#16A34A"));
|
||
textTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280"));
|
||
}
|
||
else if (i == stepIdx)
|
||
{
|
||
// ?꾩옱 吏꾪뻾 以? statusTb.Text = "??;
|
||
statusTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC"));
|
||
textTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1E293B"));
|
||
textTb.FontWeight = FontWeights.SemiBold;
|
||
}
|
||
else
|
||
{
|
||
// ?湲? statusTb.Text = "??;
|
||
statusTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF"));
|
||
textTb.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563"));
|
||
textTb.FontWeight = FontWeights.Normal;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Diff ?띿뒪?몃? ?됱긽 ?섏씠?쇱씠?낅맂 StackPanel濡??뚮뜑留곹빀?덈떎.</summary>
|
||
private static UIElement BuildDiffView(string text)
|
||
{
|
||
var panel = new StackPanel
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FAFAFA")),
|
||
MaxWidth = 520,
|
||
};
|
||
|
||
var diffStarted = false;
|
||
foreach (var rawLine in text.Split('\n'))
|
||
{
|
||
var line = rawLine.TrimEnd('\r');
|
||
|
||
// diff ?ㅻ뜑 ?꾩쓽 ?쇰컲 ?띿뒪?? if (!diffStarted && !line.StartsWith("--- "))
|
||
{
|
||
panel.Children.Add(new TextBlock
|
||
{
|
||
Text = line,
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")),
|
||
FontFamily = new FontFamily("Consolas"),
|
||
Margin = new Thickness(0, 0, 0, 1),
|
||
});
|
||
continue;
|
||
}
|
||
diffStarted = true;
|
||
|
||
string bgHex, fgHex;
|
||
if (line.StartsWith("---") || line.StartsWith("+++"))
|
||
{
|
||
bgHex = "#F3F4F6"; fgHex = "#374151";
|
||
}
|
||
else if (line.StartsWith("@@"))
|
||
{
|
||
bgHex = "#EFF6FF"; fgHex = "#3B82F6";
|
||
}
|
||
else if (line.StartsWith("+"))
|
||
{
|
||
bgHex = "#ECFDF5"; fgHex = "#059669";
|
||
}
|
||
else if (line.StartsWith("-"))
|
||
{
|
||
bgHex = "#FEF2F2"; fgHex = "#DC2626";
|
||
}
|
||
else
|
||
{
|
||
bgHex = "Transparent"; fgHex = "#6B7280";
|
||
}
|
||
|
||
var tb = new TextBlock
|
||
{
|
||
Text = line,
|
||
FontSize = 10.5,
|
||
FontFamily = new FontFamily("Consolas"),
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex)),
|
||
Padding = new Thickness(4, 1, 4, 1),
|
||
};
|
||
if (bgHex != "Transparent")
|
||
tb.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgHex));
|
||
|
||
panel.Children.Add(tb);
|
||
}
|
||
|
||
return panel;
|
||
}
|
||
|
||
private void AddAgentEventBanner(AgentEvent evt, bool animate = true)
|
||
{
|
||
var logLevel = _settings.Settings.Llm.AgentLogLevel;
|
||
|
||
// Planning ?대깽?몃뒗 ?④퀎 紐⑸줉 移대뱶濡?蹂꾨룄 ?뚮뜑留? if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 })
|
||
{
|
||
AddPlanningCard(evt);
|
||
return;
|
||
}
|
||
|
||
// StepStart ?대깽?몃뒗 吏꾪뻾瑜?諛??낅뜲?댄듃
|
||
if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0)
|
||
{
|
||
UpdateProgressBar(evt);
|
||
return;
|
||
}
|
||
|
||
// simple 紐⑤뱶: ToolCall? 嫄대꼫? (ToolResult留???以꾨줈 ?쒖떆)
|
||
if (logLevel == "simple" && evt.Type == AgentEventType.ToolCall)
|
||
return;
|
||
|
||
// ?꾩껜 ?듦퀎 ?대깽?몃뒗 蹂꾨룄 ?됱긽 (蹂대씪??怨꾩뿴)
|
||
var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats";
|
||
|
||
var (icon, label, bgHex, fgHex) = isTotalStats
|
||
? ("\uE9D2", "Total Stats", "#F3EEFF", "#7C3AED")
|
||
: evt.Type switch
|
||
{
|
||
AgentEventType.Thinking => ("\uE8BD", "Thinking", "#F0F0FF", "#6B7BC4"),
|
||
AgentEventType.ToolCall => ("\uE8A7", evt.ToolName, "#EEF6FF", "#3B82F6"),
|
||
AgentEventType.ToolResult => ("\uE73E", evt.ToolName, "#EEF9EE", "#16A34A"),
|
||
AgentEventType.SkillCall => ("\uE8A5", evt.ToolName, "#FFF7ED", "#EA580C"),
|
||
AgentEventType.Error => ("\uE783", "Error", "#FEF2F2", "#DC2626"),
|
||
AgentEventType.Complete => ("\uE930", "Complete", "#F0FFF4", "#15803D"),
|
||
AgentEventType.StepDone => ("\uE73E", "Step Done", "#EEF9EE", "#16A34A"),
|
||
AgentEventType.Paused => ("\uE769", "Paused", "#FFFBEB", "#D97706"),
|
||
AgentEventType.Resumed => ("\uE768", "Resumed", "#ECFDF5", "#059669"),
|
||
_ => ("\uE946", "Agent", "#F5F5F5", "#6B7280"),
|
||
};
|
||
|
||
var banner = new Border
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgHex)),
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(12, 8, 12, 8),
|
||
Margin = new Thickness(40, 2, 40, 2),
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
};
|
||
|
||
var sp = new StackPanel();
|
||
|
||
// ?ㅻ뜑: Grid濡?醫뚯륫(?꾩씠肄??쇰꺼) / ?곗륫(??대컢+?좏겙) 遺꾨━ 怨좎젙
|
||
var headerGrid = new Grid();
|
||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
|
||
// 醫뚯륫: ?꾩씠肄?+ ?쇰꺼
|
||
var headerLeft = new StackPanel { Orientation = Orientation.Horizontal };
|
||
headerLeft.Children.Add(new TextBlock
|
||
{
|
||
Text = icon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex)),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
});
|
||
headerLeft.Children.Add(new TextBlock
|
||
{
|
||
Text = label,
|
||
FontSize = 11.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex)),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
Grid.SetColumn(headerLeft, 0);
|
||
|
||
// ?곗륫: ?뚯슂 ?쒓컙 + ?좏겙 諛곗? (??긽 ?곗륫 ?앹뿉 怨좎젙)
|
||
var headerRight = new StackPanel { Orientation = Orientation.Horizontal };
|
||
if (logLevel != "simple" && evt.ElapsedMs > 0)
|
||
{
|
||
headerRight.Children.Add(new TextBlock
|
||
{
|
||
Text = evt.ElapsedMs < 1000 ? $"{evt.ElapsedMs}ms" : $"{evt.ElapsedMs / 1000.0:F1}s",
|
||
FontSize = 10,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(8, 0, 0, 0),
|
||
});
|
||
}
|
||
if (logLevel != "simple" && (evt.InputTokens > 0 || evt.OutputTokens > 0))
|
||
{
|
||
var tokenText = evt.InputTokens > 0 && evt.OutputTokens > 0
|
||
? $"{evt.InputTokens}??evt.OutputTokens}t"
|
||
: evt.InputTokens > 0 ? $"??evt.InputTokens}t" : $"??evt.OutputTokens}t";
|
||
headerRight.Children.Add(new Border
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F0F5")),
|
||
CornerRadius = new CornerRadius(4),
|
||
Padding = new Thickness(5, 1, 5, 1),
|
||
Margin = new Thickness(6, 0, 0, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Child = new TextBlock
|
||
{
|
||
Text = tokenText,
|
||
FontSize = 9.5,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#8B8FA3")),
|
||
FontFamily = new FontFamily("Consolas"),
|
||
},
|
||
});
|
||
}
|
||
Grid.SetColumn(headerRight, 1);
|
||
|
||
headerGrid.Children.Add(headerLeft);
|
||
headerGrid.Children.Add(headerRight);
|
||
|
||
// header 蹂?섎? headerLeft濡??ㅼ젙 (?댄썑 expandIcon 異붽? ???ъ슜)
|
||
var header = headerLeft;
|
||
|
||
sp.Children.Add(headerGrid);
|
||
|
||
// simple 紐⑤뱶: ?붿빟 ??以꾨쭔 ?쒖떆 (?묎린 ?놁쓬)
|
||
if (logLevel == "simple")
|
||
{
|
||
if (!string.IsNullOrEmpty(evt.Summary))
|
||
{
|
||
var shortSummary = evt.Summary.Length > 100
|
||
? evt.Summary[..100] + "??
|
||
: evt.Summary;
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = shortSummary,
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")),
|
||
TextWrapping = TextWrapping.NoWrap,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
});
|
||
}
|
||
}
|
||
// detailed/debug 紐⑤뱶: 湲곗〈 ?묒씠???쒖떆
|
||
else if (!string.IsNullOrEmpty(evt.Summary))
|
||
{
|
||
var summaryText = evt.Summary;
|
||
var isExpandable = (evt.Type == AgentEventType.ToolCall || evt.Type == AgentEventType.ToolResult)
|
||
&& summaryText.Length > 60;
|
||
|
||
if (isExpandable)
|
||
{
|
||
// 泥?以꾨쭔 ?쒖떆?섍퀬 ?대┃?섎㈃ ?꾩껜 ?댁슜 ?쇱묠
|
||
var shortText = summaryText.Length > 80 ? summaryText[..80] + "..." : summaryText;
|
||
var summaryTb = new TextBlock
|
||
{
|
||
Text = shortText,
|
||
FontSize = 11.5,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
Margin = new Thickness(0, 3, 0, 0),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
|
||
// Diff媛 ?ы븿??寃쎌슦 ?됱긽 ?섏씠?쇱씠???곸슜
|
||
var hasDiff = summaryText.Contains("--- ") && summaryText.Contains("+++ ");
|
||
UIElement fullContent;
|
||
|
||
if (hasDiff)
|
||
{
|
||
fullContent = BuildDiffView(summaryText);
|
||
}
|
||
else
|
||
{
|
||
fullContent = new TextBlock
|
||
{
|
||
Text = summaryText,
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
FontFamily = new FontFamily("Consolas"),
|
||
};
|
||
}
|
||
fullContent.Visibility = Visibility.Collapsed;
|
||
((FrameworkElement)fullContent).Margin = new Thickness(0, 4, 0, 0);
|
||
|
||
// ?쇱묠/?묎린 ?좉?
|
||
var expandIcon = new TextBlock
|
||
{
|
||
Text = "\uE70D", // ChevronDown
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 9,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||
Margin = new Thickness(6, 0, 0, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
header.Children.Add(expandIcon);
|
||
|
||
var isExpanded = false;
|
||
banner.MouseLeftButtonDown += (_, _) =>
|
||
{
|
||
isExpanded = !isExpanded;
|
||
fullContent.Visibility = isExpanded ? Visibility.Visible : Visibility.Collapsed;
|
||
summaryTb.Visibility = isExpanded ? Visibility.Collapsed : Visibility.Visible;
|
||
expandIcon.Text = isExpanded ? "\uE70E" : "\uE70D"; // ChevronUp : ChevronDown
|
||
};
|
||
|
||
sp.Children.Add(summaryTb);
|
||
sp.Children.Add(fullContent);
|
||
}
|
||
else
|
||
{
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = summaryText,
|
||
FontSize = 11.5,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5563")),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
Margin = new Thickness(0, 3, 0, 0),
|
||
});
|
||
}
|
||
}
|
||
|
||
// debug 紐⑤뱶: ToolInput ?뚮씪誘명꽣 ?쒖떆
|
||
if (logLevel == "debug" && !string.IsNullOrEmpty(evt.ToolInput))
|
||
{
|
||
sp.Children.Add(new Border
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8F8FC")),
|
||
CornerRadius = new CornerRadius(4),
|
||
Padding = new Thickness(8, 4, 8, 4),
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
Child = new TextBlock
|
||
{
|
||
Text = evt.ToolInput.Length > 500 ? evt.ToolInput[..500] + "?? : evt.ToolInput,
|
||
FontSize = 10,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#7C7F93")),
|
||
FontFamily = new FontFamily("Consolas"),
|
||
TextWrapping = TextWrapping.Wrap,
|
||
},
|
||
});
|
||
}
|
||
|
||
// ?뚯씪 寃쎈줈 諛곕꼫 (Claude ?ㅽ???
|
||
if (!string.IsNullOrEmpty(evt.FilePath))
|
||
{
|
||
var pathBorder = new Border
|
||
{
|
||
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F8FAFC")),
|
||
CornerRadius = new CornerRadius(4),
|
||
Padding = new Thickness(8, 4, 8, 4),
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
};
|
||
var pathPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||
pathPanel.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE8B7", // folder icon
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 10,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 4, 0),
|
||
});
|
||
pathPanel.Children.Add(new TextBlock
|
||
{
|
||
Text = evt.FilePath,
|
||
FontSize = 10.5,
|
||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")),
|
||
FontFamily = new FontFamily("Consolas"),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
});
|
||
|
||
// 鍮좊Ⅸ ?묒뾽 踰꾪듉?? var quickActions = BuildFileQuickActions(evt.FilePath);
|
||
pathPanel.Children.Add(quickActions);
|
||
|
||
pathBorder.Child = pathPanel;
|
||
sp.Children.Add(pathBorder);
|
||
}
|
||
|
||
banner.Child = sp;
|
||
|
||
// Total Stats 諛곕꼫 ?대┃ ???뚰겕?뚮줈??遺꾩꽍湲?蹂묐ぉ 遺꾩꽍 ???닿린
|
||
if (isTotalStats)
|
||
{
|
||
banner.Cursor = Cursors.Hand;
|
||
banner.ToolTip = "?대┃?섏뿬 蹂묐ぉ 遺꾩꽍 蹂닿린";
|
||
banner.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
OpenWorkflowAnalyzerIfEnabled();
|
||
_analyzerWindow?.SwitchToBottleneckTab();
|
||
_analyzerWindow?.Activate();
|
||
};
|
||
}
|
||
|
||
// ?섏씠?쒖씤 ?좊땲硫붿씠?? if (animate)
|
||
{
|
||
banner.Opacity = 0;
|
||
banner.BeginAnimation(UIElement.OpacityProperty,
|
||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||
}
|
||
|
||
MessagePanel.Children.Add(banner);
|
||
}
|
||
|
||
/// <summary>?뚯씪 鍮좊Ⅸ ?묒뾽 踰꾪듉 ?⑤꼸???앹꽦?⑸땲??</summary>
|
||
private StackPanel BuildFileQuickActions(string filePath)
|
||
{
|
||
var panel = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(6, 0, 0, 0),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
|
||
var accentColor = (Color)ColorConverter.ConvertFromString("#3B82F6");
|
||
var accentBrush = new SolidColorBrush(accentColor);
|
||
|
||
Border MakeBtn(string mdlIcon, string label, Action action)
|
||
{
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = mdlIcon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 9,
|
||
Foreground = accentBrush,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 3, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = label,
|
||
FontSize = 10,
|
||
Foreground = accentBrush,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
var btn = new Border
|
||
{
|
||
Child = sp,
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(4),
|
||
Padding = new Thickness(5, 2, 5, 2),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x15, 0x3B, 0x82, 0xF6)); };
|
||
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
btn.MouseLeftButtonUp += (_, _) => action();
|
||
return btn;
|
||
}
|
||
|
||
// ?꾨━酉?(吏???뺤옣?먮쭔)
|
||
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
||
if (_previewableExtensions.Contains(ext))
|
||
{
|
||
var path1 = filePath;
|
||
panel.Children.Add(MakeBtn("\uE8A1", "?꾨━酉?, () => ShowPreviewPanel(path1)));
|
||
}
|
||
|
||
// ?몃? ?닿린
|
||
var path2 = filePath;
|
||
panel.Children.Add(MakeBtn("\uE8A7", "?닿린", () =>
|
||
{
|
||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path2, UseShellExecute = true }); } catch { }
|
||
}));
|
||
|
||
// ?대뜑 ?닿린
|
||
var path3 = filePath;
|
||
panel.Children.Add(MakeBtn("\uED25", "?대뜑", () =>
|
||
{
|
||
try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path3}\""); } catch { }
|
||
}));
|
||
|
||
// 寃쎈줈 蹂듭궗
|
||
var path4 = filePath;
|
||
panel.Children.Add(MakeBtn("\uE8C8", "蹂듭궗", () =>
|
||
{
|
||
try
|
||
{
|
||
Clipboard.SetText(path4);
|
||
// 1.5珥??쇰뱶諛? "蹂듭궗?? ?쒖떆
|
||
if (panel.Children[^1] is Border lastBtn && lastBtn.Child is StackPanel lastSp)
|
||
{
|
||
var origLabel = lastSp.Children.OfType<TextBlock>().LastOrDefault();
|
||
if (origLabel != null)
|
||
{
|
||
var prev = origLabel.Text;
|
||
origLabel.Text = "蹂듭궗????;
|
||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) };
|
||
timer.Tick += (_, _) => { origLabel.Text = prev; timer.Stop(); };
|
||
timer.Start();
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}));
|
||
|
||
return panel;
|
||
}
|
||
|
||
// ??? ?묐떟 ?ъ깮????????????????????????????????????????????????????????
|
||
|
||
private async Task RegenerateLastAsync()
|
||
{
|
||
if (_isStreaming) return;
|
||
ChatConversation conv;
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation == null) return;
|
||
conv = _currentConversation;
|
||
}
|
||
|
||
// 留덉?留?assistant 硫붿떆吏 ?쒓굅
|
||
lock (_convLock)
|
||
{
|
||
if (conv.Messages.Count > 0 && conv.Messages[^1].Role == "assistant")
|
||
conv.Messages.RemoveAt(conv.Messages.Count - 1);
|
||
}
|
||
|
||
// UI?먯꽌 留덉?留?AI ?묐떟 ?쒓굅
|
||
if (MessagePanel.Children.Count > 0)
|
||
MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1);
|
||
|
||
// ?ъ쟾?? await SendRegenerateAsync(conv);
|
||
}
|
||
|
||
/// <summary>"?섏젙 ???ъ떆?? ???쇰뱶諛??낅젰 ?⑤꼸???쒖떆?섍퀬, ?ъ슜??吏?쒕? 異붽??섏뿬 ?ъ깮?깊빀?덈떎.</summary>
|
||
private void ShowRetryWithFeedbackInput()
|
||
{
|
||
if (_isStreaming) return;
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
var itemBg = TryFindResource("ItemBackground") as Brush
|
||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||
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 container = new Border
|
||
{
|
||
Margin = new Thickness(40, 4, 40, 8),
|
||
Padding = new Thickness(14, 10, 14, 10),
|
||
CornerRadius = new CornerRadius(12),
|
||
Background = itemBg,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
};
|
||
|
||
var stack = new StackPanel();
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = "?대뼸寃??섏젙?섎㈃ 醫뗭쓣吏 ?뚮젮二쇱꽭??",
|
||
FontSize = 12,
|
||
Foreground = secondaryText,
|
||
Margin = new Thickness(0, 0, 0, 6),
|
||
});
|
||
|
||
var textBox = new TextBox
|
||
{
|
||
MinHeight = 38,
|
||
MaxHeight = 80,
|
||
AcceptsReturn = true,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
FontSize = 13,
|
||
Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black,
|
||
Foreground = primaryText,
|
||
CaretBrush = primaryText,
|
||
BorderBrush = borderBrush,
|
||
BorderThickness = new Thickness(1),
|
||
Padding = new Thickness(10, 6, 10, 6),
|
||
};
|
||
stack.Children.Add(textBox);
|
||
|
||
var btnRow = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
Margin = new Thickness(0, 8, 0, 0),
|
||
};
|
||
|
||
var sendBtn = new Border
|
||
{
|
||
Background = accentBrush,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(14, 6, 14, 6),
|
||
Cursor = Cursors.Hand,
|
||
Margin = new Thickness(6, 0, 0, 0),
|
||
};
|
||
sendBtn.Child = new TextBlock { Text = "?ъ떆??, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White };
|
||
sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||
sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||
sendBtn.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
var feedback = textBox.Text.Trim();
|
||
if (string.IsNullOrEmpty(feedback)) return;
|
||
MessagePanel.Children.Remove(container);
|
||
_ = RetryWithFeedbackAsync(feedback);
|
||
};
|
||
|
||
var cancelBtn = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(12, 6, 12, 6),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
cancelBtn.Child = new TextBlock { Text = "痍⑥냼", FontSize = 12, Foreground = secondaryText };
|
||
cancelBtn.MouseLeftButtonUp += (_, _) => MessagePanel.Children.Remove(container);
|
||
|
||
btnRow.Children.Add(cancelBtn);
|
||
btnRow.Children.Add(sendBtn);
|
||
stack.Children.Add(btnRow);
|
||
container.Child = stack;
|
||
|
||
ApplyMessageEntryAnimation(container);
|
||
MessagePanel.Children.Add(container);
|
||
ForceScrollToEnd();
|
||
textBox.Focus();
|
||
}
|
||
|
||
/// <summary>?ъ슜???쇰뱶諛깃낵 ?④퍡 留덉?留??묐떟???ъ깮?깊빀?덈떎.</summary>
|
||
private async Task RetryWithFeedbackAsync(string feedback)
|
||
{
|
||
if (_isStreaming) return;
|
||
ChatConversation conv;
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation == null) return;
|
||
conv = _currentConversation;
|
||
}
|
||
|
||
// 留덉?留?assistant 硫붿떆吏 ?쒓굅
|
||
lock (_convLock)
|
||
{
|
||
if (conv.Messages.Count > 0 && conv.Messages[^1].Role == "assistant")
|
||
conv.Messages.RemoveAt(conv.Messages.Count - 1);
|
||
}
|
||
|
||
// UI?먯꽌 留덉?留?AI ?묐떟 ?쒓굅
|
||
if (MessagePanel.Children.Count > 0)
|
||
MessagePanel.Children.RemoveAt(MessagePanel.Children.Count - 1);
|
||
|
||
// ?쇰뱶諛깆쓣 ?ъ슜??硫붿떆吏濡?異붽?
|
||
var feedbackMsg = new ChatMessage
|
||
{
|
||
Role = "user",
|
||
Content = $"[?댁쟾 ?묐떟??????섏젙 ?붿껌] {feedback}\n\n???쇰뱶諛깆쓣 諛섏쁺?섏뿬 ?ㅼ떆 ?묒꽦?댁<?몄슂."
|
||
};
|
||
lock (_convLock) conv.Messages.Add(feedbackMsg);
|
||
|
||
// ?쇰뱶諛?硫붿떆吏 UI ?쒖떆
|
||
AddMessageBubble("user", $"[?섏젙 ?붿껌] {feedback}", true);
|
||
|
||
// ?ъ쟾?? await SendRegenerateAsync(conv);
|
||
}
|
||
|
||
private async Task SendRegenerateAsync(ChatConversation conv)
|
||
{
|
||
_isStreaming = true;
|
||
BtnSend.IsEnabled = false;
|
||
BtnSend.Visibility = Visibility.Collapsed;
|
||
BtnStop.Visibility = Visibility.Visible;
|
||
_streamCts = new CancellationTokenSource();
|
||
|
||
var assistantMsg = new ChatMessage { Role = "assistant", Content = "" };
|
||
lock (_convLock) conv.Messages.Add(assistantMsg);
|
||
|
||
var streamContainer = CreateStreamingContainer(out var streamText);
|
||
MessagePanel.Children.Add(streamContainer);
|
||
ForceScrollToEnd(); // ?묐떟 ?쒖옉 ??媛뺤젣 ?섎떒 ?대룞
|
||
|
||
var sb = new System.Text.StringBuilder();
|
||
_activeStreamText = streamText;
|
||
_cachedStreamContent = "";
|
||
_displayedLength = 0;
|
||
_cursorVisible = true;
|
||
_aiIconPulseStopped = false;
|
||
_cursorTimer.Start();
|
||
_typingTimer.Start();
|
||
_streamStartTime = DateTime.UtcNow;
|
||
_elapsedTimer.Start();
|
||
SetStatus("?먯씠?꾪듃 ?묒뾽 以?..", spinning: true);
|
||
|
||
try
|
||
{
|
||
List<ChatMessage> sendMessages;
|
||
lock (_convLock) sendMessages = conv.Messages.SkipLast(1).ToList();
|
||
if (!string.IsNullOrEmpty(conv.SystemCommand))
|
||
sendMessages.Insert(0, new ChatMessage { Role = "system", Content = conv.SystemCommand });
|
||
|
||
await foreach (var chunk in _llm.StreamAsync(sendMessages, _streamCts.Token))
|
||
{
|
||
sb.Append(chunk);
|
||
StopAiIconPulse();
|
||
_cachedStreamContent = sb.ToString();
|
||
await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
|
||
}
|
||
_cachedStreamContent = sb.ToString();
|
||
assistantMsg.Content = _cachedStreamContent;
|
||
|
||
// ??댄븨 ?좊땲硫붿씠?섏씠 ?⑥? 踰꾪띁瑜??뚯쭊???뚭퉴吏 ?湲?(理쒕? 600ms)
|
||
var drainStart2 = DateTime.UtcNow;
|
||
while (_displayedLength < _cachedStreamContent.Length
|
||
&& (DateTime.UtcNow - drainStart2).TotalMilliseconds < 600)
|
||
{
|
||
await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.Background);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
if (sb.Length == 0) sb.Append("(痍⑥냼??");
|
||
assistantMsg.Content = sb.ToString();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var errMsg = $"???ㅻ쪟: {ex.Message}";
|
||
sb.Clear(); sb.Append(errMsg);
|
||
assistantMsg.Content = errMsg;
|
||
AddRetryButton();
|
||
}
|
||
finally
|
||
{
|
||
_cursorTimer.Stop();
|
||
_elapsedTimer.Stop();
|
||
_typingTimer.Stop();
|
||
HideStickyProgress(); // ?먯씠?꾪듃 ?꾨줈洹몃젅??諛?+ ??대㉧ ?뺣━
|
||
StopRainbowGlow(); // ?덉씤蹂댁슦 湲濡쒖슦 醫낅즺
|
||
_activeStreamText = null;
|
||
_elapsedLabel = null;
|
||
_cachedStreamContent = "";
|
||
_isStreaming = false;
|
||
BtnSend.IsEnabled = true;
|
||
BtnStop.Visibility = Visibility.Collapsed;
|
||
BtnSend.Visibility = Visibility.Visible;
|
||
_streamCts?.Dispose();
|
||
_streamCts = null;
|
||
SetStatusIdle();
|
||
}
|
||
|
||
FinalizeStreamingContainer(streamContainer, streamText, assistantMsg.Content, assistantMsg);
|
||
AutoScrollIfNeeded();
|
||
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"???????ㅽ뙣: {ex.Message}"); }
|
||
_sessionState.RememberConversation(conv.Tab ?? _activeTab, conv.Id);
|
||
RefreshConversationList();
|
||
}
|
||
|
||
/// <summary>硫붿떆吏 踰꾨툝??MaxWidth瑜?李??덈퉬??鍮꾨??섏뿬 怨꾩궛?⑸땲??(理쒖냼 500, 理쒕? 1200).</summary>
|
||
private double GetMessageMaxWidth()
|
||
{
|
||
var scrollWidth = MessageScroll.ActualWidth;
|
||
if (scrollWidth < 100) scrollWidth = 700; // 珥덇린????湲곕낯媛? // 醫뚯슦 留덉쭊(40+80=120)??鍮쇨퀬 ?꾩껜??90%
|
||
var maxW = (scrollWidth - 120) * 0.90;
|
||
return Math.Clamp(maxW, 500, 1200);
|
||
}
|
||
|
||
private StackPanel CreateStreamingContainer(out TextBlock streamText)
|
||
{
|
||
var msgMaxWidth = GetMessageMaxWidth();
|
||
var container = new StackPanel
|
||
{
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
Width = msgMaxWidth,
|
||
MaxWidth = msgMaxWidth,
|
||
Margin = new Thickness(40, 8, 80, 8),
|
||
Opacity = 0,
|
||
RenderTransform = new TranslateTransform(0, 10)
|
||
};
|
||
|
||
// 而⑦뀒?대꼫 ?섏씠?쒖씤 + ?щ씪?대뱶 ?? container.BeginAnimation(UIElement.OpacityProperty,
|
||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(280)));
|
||
((TranslateTransform)container.RenderTransform).BeginAnimation(
|
||
TranslateTransform.YProperty,
|
||
new DoubleAnimation(10, 0, TimeSpan.FromMilliseconds(300))
|
||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
||
|
||
var headerGrid = new Grid { Margin = new Thickness(0, 0, 0, 4) };
|
||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
|
||
var aiIcon = new TextBlock
|
||
{
|
||
Text = "\uE8BD", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12,
|
||
Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
// AI ?꾩씠肄??꾩뒪 ?좊땲硫붿씠??(?묐떟 ?湲?以?
|
||
aiIcon.BeginAnimation(UIElement.OpacityProperty,
|
||
new DoubleAnimation(1.0, 0.35, TimeSpan.FromMilliseconds(700))
|
||
{ AutoReverse = true, RepeatBehavior = RepeatBehavior.Forever,
|
||
EasingFunction = new SineEase() });
|
||
_activeAiIcon = aiIcon;
|
||
Grid.SetColumn(aiIcon, 0);
|
||
headerGrid.Children.Add(aiIcon);
|
||
|
||
var (streamAgentName, _, _) = GetAgentIdentity();
|
||
var aiNameTb = new TextBlock
|
||
{
|
||
Text = streamAgentName, FontSize = 11, FontWeight = FontWeights.SemiBold,
|
||
Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||
Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center
|
||
};
|
||
Grid.SetColumn(aiNameTb, 1);
|
||
headerGrid.Children.Add(aiNameTb);
|
||
|
||
// ?ㅼ떆媛?寃쎄낵 ?쒓컙 (?ㅻ뜑 ?곗륫)
|
||
_elapsedLabel = new TextBlock
|
||
{
|
||
Text = "0s",
|
||
FontSize = 10.5,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Opacity = 0.5,
|
||
};
|
||
Grid.SetColumn(_elapsedLabel, 2);
|
||
headerGrid.Children.Add(_elapsedLabel);
|
||
|
||
container.Children.Add(headerGrid);
|
||
|
||
streamText = new TextBlock
|
||
{
|
||
Text = "\u258c", // 釉붾줉 而ㅼ꽌留??쒖떆 (泥?泥?겕 ??
|
||
FontSize = 13.5,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
TextWrapping = TextWrapping.Wrap, LineHeight = 22,
|
||
};
|
||
container.Children.Add(streamText);
|
||
return container;
|
||
}
|
||
|
||
// ??? ?ㅽ듃由щ컢 ?꾨즺 ??留덊겕?ㅼ슫 ?뚮뜑留곸쑝濡?援먯껜 ???????????????????????
|
||
|
||
private void FinalizeStreamingContainer(StackPanel container, TextBlock streamText, string finalContent, ChatMessage? message = null)
|
||
{
|
||
// ?ㅽ듃由щ컢 plaintext 釉붾줉 ?쒓굅
|
||
container.Children.Remove(streamText);
|
||
|
||
// 留덊겕?ㅼ슫 ?뚮뜑留? var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||
var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
|
||
|
||
var mdPanel = MarkdownRenderer.Render(finalContent, primaryText, secondaryText, accentBrush, codeBgBrush);
|
||
mdPanel.Margin = new Thickness(0, 0, 0, 4);
|
||
mdPanel.Opacity = 0;
|
||
container.Children.Add(mdPanel);
|
||
mdPanel.BeginAnimation(UIElement.OpacityProperty,
|
||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180)));
|
||
|
||
// ?≪뀡 踰꾪듉 諛?+ ?좏겙 ?쒖떆
|
||
var btnColor = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var capturedContent = finalContent;
|
||
var actionBar = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
Margin = new Thickness(0, 6, 0, 0)
|
||
};
|
||
actionBar.Children.Add(CreateActionButton("\uE8C8", "蹂듭궗", btnColor, () =>
|
||
{
|
||
try { Clipboard.SetText(capturedContent); } catch { }
|
||
}));
|
||
actionBar.Children.Add(CreateActionButton("\uE72C", "?ㅼ떆 ?앹꽦", btnColor, () => _ = RegenerateLastAsync()));
|
||
actionBar.Children.Add(CreateActionButton("\uE70F", "?섏젙 ???ъ떆??, btnColor, () => ShowRetryWithFeedbackInput()));
|
||
AddLinkedFeedbackButtons(actionBar, btnColor, message);
|
||
|
||
container.Children.Add(actionBar);
|
||
|
||
// 寃쎄낵 ?쒓컙 + ?좏겙 ?ъ슜??(?곗륫 ?섎떒, 蹂꾨룄 以?
|
||
var elapsed = DateTime.UtcNow - _streamStartTime;
|
||
var elapsedText = elapsed.TotalSeconds < 60
|
||
? $"{elapsed.TotalSeconds:0.#}s"
|
||
: $"{(int)elapsed.TotalMinutes}m {elapsed.Seconds}s";
|
||
|
||
var usage = _llm.LastTokenUsage;
|
||
// ?먯씠?꾪듃 猷⑦봽(Cowork/Code)?먯꽌???꾩쟻 ?좏겙 ?ъ슜, ?쇰컲 ??붿뿉?쒕뒗 留덉?留??몄텧 ?좏겙 ?ъ슜
|
||
var isAgentTab = _activeTab is "Cowork" or "Code";
|
||
var displayInput = isAgentTab && _agentCumulativeInputTokens > 0
|
||
? _agentCumulativeInputTokens
|
||
: usage?.PromptTokens ?? 0;
|
||
var displayOutput = isAgentTab && _agentCumulativeOutputTokens > 0
|
||
? _agentCumulativeOutputTokens
|
||
: usage?.CompletionTokens ?? 0;
|
||
|
||
if (displayInput > 0 || displayOutput > 0)
|
||
{
|
||
UpdateStatusTokens(displayInput, displayOutput);
|
||
Services.UsageStatisticsService.RecordTokens(displayInput, displayOutput);
|
||
}
|
||
string tokenText;
|
||
if (displayInput > 0 || displayOutput > 0)
|
||
tokenText = $"{FormatTokenCount(displayInput)} + {FormatTokenCount(displayOutput)} = {FormatTokenCount(displayInput + displayOutput)} tokens";
|
||
else if (usage != null)
|
||
tokenText = $"{FormatTokenCount(usage.PromptTokens)} + {FormatTokenCount(usage.CompletionTokens)} = {FormatTokenCount(usage.TotalTokens)} tokens";
|
||
else
|
||
tokenText = $"~{FormatTokenCount(EstimateTokenCount(finalContent))} tokens";
|
||
|
||
var metaText = new TextBlock
|
||
{
|
||
Text = $"{elapsedText} 쨌 {tokenText}",
|
||
FontSize = 10.5,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Right,
|
||
Margin = new Thickness(0, 6, 0, 0),
|
||
Opacity = 0.6,
|
||
};
|
||
container.Children.Add(metaText);
|
||
|
||
// Suggestion chips ??AI媛 踰덊샇 ?좏깮吏瑜??쒖떆??寃쎌슦 ?대┃ 媛??踰꾪듉 ?쒖떆
|
||
var chips = ParseSuggestionChips(finalContent);
|
||
if (chips.Count > 0)
|
||
{
|
||
var chipPanel = new WrapPanel
|
||
{
|
||
Margin = new Thickness(0, 8, 0, 4),
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
};
|
||
foreach (var (num, label) in chips)
|
||
{
|
||
var chipBorder = new Border
|
||
{
|
||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent,
|
||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(16),
|
||
Padding = new Thickness(14, 7, 14, 7),
|
||
Margin = new Thickness(0, 0, 8, 6),
|
||
Cursor = Cursors.Hand,
|
||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||
RenderTransform = new ScaleTransform(1, 1),
|
||
};
|
||
chipBorder.Child = new TextBlock
|
||
{
|
||
Text = $"{num}. {label}",
|
||
FontSize = 12.5,
|
||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||
};
|
||
|
||
var chipHover = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||
var chipNormal = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
|
||
chipBorder.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{ st.ScaleX = 1.02; st.ScaleY = 1.02; b.Background = chipHover; }
|
||
};
|
||
chipBorder.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{ st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = chipNormal; }
|
||
};
|
||
|
||
var capturedLabel = $"{num}. {label}";
|
||
var capturedPanel = chipPanel;
|
||
chipBorder.MouseLeftButtonDown += (_, _) =>
|
||
{
|
||
// 移??⑤꼸 ?쒓굅 (1?뚯슜)
|
||
if (capturedPanel.Parent is Panel parent)
|
||
parent.Children.Remove(capturedPanel);
|
||
// ?좏깮???듭뀡???ъ슜??硫붿떆吏濡??꾩넚
|
||
InputBox.Text = capturedLabel;
|
||
_ = SendMessageAsync();
|
||
};
|
||
chipPanel.Children.Add(chipBorder);
|
||
}
|
||
container.Children.Add(chipPanel);
|
||
}
|
||
}
|
||
|
||
/// <summary>AI ?묐떟?먯꽌 踰덊샇 ?좏깮吏瑜??뚯떛?⑸땲?? (1. xxx / 2. xxx ?⑦꽩)</summary>
|
||
private static List<(string Num, string Label)> ParseSuggestionChips(string content)
|
||
{
|
||
var chips = new List<(string, string)>();
|
||
if (string.IsNullOrEmpty(content)) return chips;
|
||
|
||
var lines = content.Split('\n');
|
||
// 留덉?留?踰덊샇 紐⑸줉 釉붾줉??李얠쓬 (?곗냽??踰덊샇 ?쇱씤)
|
||
var candidates = new List<(string, string)>();
|
||
var lastBlockStart = -1;
|
||
|
||
for (int i = 0; i < lines.Length; i++)
|
||
{
|
||
var line = lines[i].Trim();
|
||
// "1. xxx", "2) xxx", "??xxx" ??踰덊샇 ?⑦꽩
|
||
var m = System.Text.RegularExpressions.Regex.Match(line, @"^(\d+)[.\)]\s+(.+)$");
|
||
if (m.Success)
|
||
{
|
||
if (lastBlockStart < 0 || i == lastBlockStart + candidates.Count)
|
||
{
|
||
if (lastBlockStart < 0) { lastBlockStart = i; candidates.Clear(); }
|
||
candidates.Add((m.Groups[1].Value, m.Groups[2].Value.TrimEnd()));
|
||
}
|
||
else
|
||
{
|
||
// ?덈줈??釉붾줉 ?쒖옉
|
||
lastBlockStart = i;
|
||
candidates.Clear();
|
||
candidates.Add((m.Groups[1].Value, m.Groups[2].Value.TrimEnd()));
|
||
}
|
||
}
|
||
else if (!string.IsNullOrWhiteSpace(line))
|
||
{
|
||
// 踰덊샇 紐⑸줉???꾨땶 以꾩씠 ?섏삤硫?釉붾줉 由ъ뀑
|
||
lastBlockStart = -1;
|
||
candidates.Clear();
|
||
}
|
||
// 鍮?以꾩? 釉붾줉 ?좎? (踰덊샇 紐⑸줉 ?ъ씠 鍮?以??덉슜)
|
||
}
|
||
|
||
// 2媛??댁긽 ?좏깮吏, 10媛??댄븯???뚮쭔 chips濡??쒖떆
|
||
if (candidates.Count >= 2 && candidates.Count <= 10)
|
||
chips.AddRange(candidates);
|
||
|
||
return chips;
|
||
}
|
||
|
||
/// <summary>?좏겙 ?섎? k/m ?⑥쐞濡??щ㎎</summary>
|
||
private static string FormatTokenCount(int count) => count switch
|
||
{
|
||
>= 1_000_000 => $"{count / 1_000_000.0:0.#}m",
|
||
>= 1_000 => $"{count / 1_000.0:0.#}k",
|
||
_ => count.ToString(),
|
||
};
|
||
|
||
/// <summary>?좏겙 ??異붿젙 (?쒓뎅??3???좏겙, ?곸뼱~4???좏겙, ?쇳빀 ?됯퇏 ~3???좏겙)</summary>
|
||
private static int EstimateTokenCount(string text)
|
||
{
|
||
if (string.IsNullOrEmpty(text)) return 0;
|
||
// ?쒓뎅??臾몄옄 鍮꾩쑉???곕씪 媛以? int cjk = 0;
|
||
foreach (var c in text)
|
||
if (c >= 0xAC00 && c <= 0xD7A3 || c >= 0x3000 && c <= 0x9FFF) cjk++;
|
||
double ratio = text.Length > 0 ? (double)cjk / text.Length : 0;
|
||
double charsPerToken = 4.0 - ratio * 2.0; // ?곸뼱 4, ?쒓뎅??2
|
||
return Math.Max(1, (int)Math.Round(text.Length / charsPerToken));
|
||
}
|
||
|
||
// ??? ?앹꽦 以묒? ??????????????????????????????????????????????????????
|
||
|
||
private void StopGeneration()
|
||
{
|
||
_streamCts?.Cancel();
|
||
}
|
||
|
||
// ??? ????대낫?닿린 ??????????????????????????????????????????????????
|
||
|
||
// ??? ???遺꾧린 (Fork) ??????????????????????????????????????????????
|
||
|
||
private void ForkConversation(ChatConversation source, int atIndex)
|
||
{
|
||
var branchCount = _storage.LoadAllMeta()
|
||
.Count(m => m.ParentId == source.Id) + 1;
|
||
|
||
var fork = new ChatConversation
|
||
{
|
||
Title = $"{source.Title} (遺꾧린 {branchCount})",
|
||
Tab = source.Tab,
|
||
Category = source.Category,
|
||
WorkFolder = source.WorkFolder,
|
||
SystemCommand = source.SystemCommand,
|
||
ParentId = source.Id,
|
||
BranchLabel = $"遺꾧린 {branchCount}",
|
||
BranchAtIndex = atIndex,
|
||
};
|
||
|
||
// 遺꾧린 ?쒖젏源뚯???硫붿떆吏 蹂듭젣
|
||
for (int i = 0; i <= atIndex && i < source.Messages.Count; i++)
|
||
{
|
||
var m = source.Messages[i];
|
||
fork.Messages.Add(new ChatMessage
|
||
{
|
||
Role = m.Role,
|
||
Content = m.Content,
|
||
Timestamp = m.Timestamp,
|
||
});
|
||
}
|
||
|
||
try
|
||
{
|
||
_storage.Save(fork);
|
||
ShowToast($"遺꾧린 ?앹꽦: {fork.Title}");
|
||
|
||
// 遺꾧린 ??붾줈 ?꾪솚
|
||
lock (_convLock) _currentConversation = fork;
|
||
ChatTitle.Text = fork.Title;
|
||
RenderMessages();
|
||
RefreshConversationList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ShowToast($"遺꾧린 ?ㅽ뙣: {ex.Message}", "\uE783");
|
||
}
|
||
}
|
||
|
||
// ??? 而ㅻ㎤???붾젅???????????????????????????????????????????????????
|
||
|
||
private void OpenCommandPalette()
|
||
{
|
||
var palette = new CommandPaletteWindow(ExecuteCommand) { Owner = this };
|
||
palette.ShowDialog();
|
||
}
|
||
|
||
private void ExecuteCommand(string commandId)
|
||
{
|
||
switch (commandId)
|
||
{
|
||
case "tab:chat": TabChat.IsChecked = true; break;
|
||
case "tab:cowork": TabCowork.IsChecked = true; break;
|
||
case "tab:code": if (TabCode.IsEnabled) TabCode.IsChecked = true; break;
|
||
case "new_conversation": StartNewConversation(); break;
|
||
case "search_conversation": ToggleMessageSearch(); break;
|
||
case "change_model": BtnModelSelector_Click(this, new RoutedEventArgs()); break;
|
||
case "open_settings": BtnSettings_Click(this, new RoutedEventArgs()); break;
|
||
case "open_statistics": new StatisticsWindow().Show(); break;
|
||
case "change_folder": FolderPathLabel_Click(FolderPathLabel, null!); break;
|
||
case "toggle_devmode":
|
||
var llm = _settings.Settings.Llm;
|
||
llm.DevMode = !llm.DevMode;
|
||
_settings.Save();
|
||
UpdateAnalyzerButtonVisibility();
|
||
ShowToast(llm.DevMode ? "媛쒕컻??紐⑤뱶 耳쒖쭚" : "媛쒕컻??紐⑤뱶 爰쇱쭚");
|
||
break;
|
||
case "open_audit_log":
|
||
try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { }
|
||
break;
|
||
case "paste_clipboard":
|
||
try { var text = Clipboard.GetText(); if (!string.IsNullOrEmpty(text)) InputBox.Text += text; } catch { }
|
||
break;
|
||
case "export_conversation": ExportConversation(); break;
|
||
}
|
||
}
|
||
|
||
private void ExportConversation()
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv == null || conv.Messages.Count == 0) return;
|
||
|
||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||
{
|
||
FileName = $"{conv.Title}",
|
||
DefaultExt = ".md",
|
||
Filter = "Markdown (*.md)|*.md|JSON (*.json)|*.json|HTML (*.html)|*.html|PDF ?몄뇙??HTML (*.pdf.html)|*.pdf.html|Text (*.txt)|*.txt"
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
|
||
var ext = System.IO.Path.GetExtension(dlg.FileName).ToLowerInvariant();
|
||
string content;
|
||
|
||
if (ext == ".json")
|
||
{
|
||
content = System.Text.Json.JsonSerializer.Serialize(conv, new System.Text.Json.JsonSerializerOptions
|
||
{
|
||
WriteIndented = true,
|
||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||
});
|
||
}
|
||
else if (dlg.FileName.EndsWith(".pdf.html"))
|
||
{
|
||
// PDF ?몄뇙??HTML ??釉뚮씪?곗??먯꽌 ?먮룞?쇰줈 ?몄뇙 ??붿긽???쒖떆
|
||
content = PdfExportService.BuildHtml(conv);
|
||
System.IO.File.WriteAllText(dlg.FileName, content, System.Text.Encoding.UTF8);
|
||
PdfExportService.OpenInBrowser(dlg.FileName);
|
||
ShowToast("PDF ?몄뇙??HTML???앹꽦?섏뼱 釉뚮씪?곗??먯꽌 ?대졇?듬땲??);
|
||
return;
|
||
}
|
||
else if (ext == ".html")
|
||
{
|
||
content = ExportToHtml(conv);
|
||
}
|
||
else
|
||
{
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine($"# {conv.Title}");
|
||
sb.AppendLine($"_?앹꽦: {conv.CreatedAt:yyyy-MM-dd HH:mm} 쨌 二쇱젣: {conv.Category}_");
|
||
sb.AppendLine();
|
||
|
||
foreach (var msg in conv.Messages)
|
||
{
|
||
if (msg.Role == "system") continue;
|
||
var label = msg.Role == "user" ? "**?ъ슜??*" : "**AI**";
|
||
sb.AppendLine($"{label} ({msg.Timestamp:HH:mm})");
|
||
sb.AppendLine();
|
||
sb.AppendLine(msg.Content);
|
||
if (msg.AttachedFiles is { Count: > 0 })
|
||
{
|
||
sb.AppendLine();
|
||
sb.AppendLine("_泥⑤? ?뚯씪: " + string.Join(", ", msg.AttachedFiles.Select(System.IO.Path.GetFileName)) + "_");
|
||
}
|
||
sb.AppendLine();
|
||
sb.AppendLine("---");
|
||
sb.AppendLine();
|
||
}
|
||
content = sb.ToString();
|
||
}
|
||
|
||
System.IO.File.WriteAllText(dlg.FileName, content, System.Text.Encoding.UTF8);
|
||
}
|
||
|
||
private static string ExportToHtml(ChatConversation conv)
|
||
{
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine("<!DOCTYPE html><html><head><meta charset=\"utf-8\">");
|
||
sb.AppendLine($"<title>{System.Net.WebUtility.HtmlEncode(conv.Title)}</title>");
|
||
sb.AppendLine("<style>body{font-family:'Segoe UI',sans-serif;max-width:800px;margin:0 auto;padding:20px;background:#1a1a2e;color:#e0e0e0}");
|
||
sb.AppendLine(".msg{margin:12px 0;padding:12px 16px;border-radius:12px}.user{background:#2d2d5e;margin-left:60px}.ai{background:#1e1e3a;margin-right:60px}");
|
||
sb.AppendLine(".meta{font-size:11px;color:#888;margin-bottom:6px}.content{white-space:pre-wrap;line-height:1.6}");
|
||
sb.AppendLine("h1{text-align:center;color:#8b6dff}pre{background:#111;padding:12px;border-radius:8px;overflow-x:auto}</style></head><body>");
|
||
sb.AppendLine($"<h1>{System.Net.WebUtility.HtmlEncode(conv.Title)}</h1>");
|
||
sb.AppendLine($"<p style='text-align:center;color:#888'>?앹꽦: {conv.CreatedAt:yyyy-MM-dd HH:mm} 쨌 二쇱젣: {conv.Category}</p>");
|
||
|
||
foreach (var msg in conv.Messages)
|
||
{
|
||
if (msg.Role == "system") continue;
|
||
var cls = msg.Role == "user" ? "user" : "ai";
|
||
var label = msg.Role == "user" ? "?ъ슜?? : "AI";
|
||
sb.AppendLine($"<div class='msg {cls}'>");
|
||
sb.AppendLine($"<div class='meta'>{label} 쨌 {msg.Timestamp:HH:mm}</div>");
|
||
sb.AppendLine($"<div class='content'>{System.Net.WebUtility.HtmlEncode(msg.Content)}</div>");
|
||
sb.AppendLine("</div>");
|
||
}
|
||
|
||
sb.AppendLine("</body></html>");
|
||
return sb.ToString();
|
||
}
|
||
|
||
// ??? 踰꾪듉 ?대깽????????????????????????????????????????????????????????
|
||
|
||
private void ChatWindow_KeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
var mod = Keyboard.Modifiers;
|
||
|
||
// Ctrl ?⑥텞?? if (mod == ModifierKeys.Control)
|
||
{
|
||
switch (e.Key)
|
||
{
|
||
case Key.N: BtnNewChat_Click(this, new RoutedEventArgs()); e.Handled = true; break;
|
||
case Key.W: Close(); e.Handled = true; break;
|
||
case Key.E: ExportConversation(); e.Handled = true; break;
|
||
case Key.L: InputBox.Text = ""; InputBox.Focus(); e.Handled = true; break;
|
||
case Key.B: BtnToggleSidebar_Click(this, new RoutedEventArgs()); e.Handled = true; break;
|
||
case Key.M: BtnModelSelector_Click(this, new RoutedEventArgs()); e.Handled = true; break;
|
||
case Key.OemComma: BtnSettings_Click(this, new RoutedEventArgs()); e.Handled = true; break;
|
||
case Key.F: ToggleMessageSearch(); e.Handled = true; break;
|
||
case Key.D1: TabChat.IsChecked = true; e.Handled = true; break;
|
||
case Key.D2: TabCowork.IsChecked = true; e.Handled = true; break;
|
||
case Key.D3: if (TabCode.IsEnabled) TabCode.IsChecked = true; e.Handled = true; break;
|
||
}
|
||
}
|
||
|
||
// Ctrl+Shift ?⑥텞?? if (mod == (ModifierKeys.Control | ModifierKeys.Shift))
|
||
{
|
||
switch (e.Key)
|
||
{
|
||
case Key.C:
|
||
// 留덉?留?AI ?묐떟 蹂듭궗
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv != null)
|
||
{
|
||
var lastAi = conv.Messages.LastOrDefault(m => m.Role == "assistant");
|
||
if (lastAi != null)
|
||
try { Clipboard.SetText(lastAi.Content); } catch { }
|
||
}
|
||
e.Handled = true;
|
||
break;
|
||
case Key.R:
|
||
// 留덉?留??묐떟 ?ъ깮?? _ = RegenerateLastAsync();
|
||
e.Handled = true;
|
||
break;
|
||
case Key.D:
|
||
// 紐⑤뱺 ?????젣
|
||
BtnDeleteAll_Click(this, new RoutedEventArgs());
|
||
e.Handled = true;
|
||
break;
|
||
case Key.P:
|
||
// 而ㅻ㎤???붾젅?? OpenCommandPalette();
|
||
e.Handled = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Escape: 寃??諛??リ린 ?먮뒗 ?ㅽ듃由щ컢 以묒?
|
||
if (e.Key == Key.Escape)
|
||
{
|
||
if (MessageSearchBar.Visibility == Visibility.Visible) { CloseMessageSearch(); e.Handled = true; }
|
||
else if (_isStreaming) { StopGeneration(); e.Handled = true; }
|
||
}
|
||
|
||
// ?щ옒??紐낅졊 ?앹뾽 ??泥섎━
|
||
if (SlashPopup.IsOpen)
|
||
{
|
||
if (e.Key == Key.Escape)
|
||
{
|
||
SlashPopup.IsOpen = false;
|
||
_slashSelectedIndex = -1;
|
||
e.Handled = true;
|
||
}
|
||
else if (e.Key == Key.Up)
|
||
{
|
||
SlashPopup_ScrollByDelta(120); // ?꾨줈 1移? e.Handled = true;
|
||
}
|
||
else if (e.Key == Key.Down)
|
||
{
|
||
SlashPopup_ScrollByDelta(-120); // ?꾨옒濡?1移? e.Handled = true;
|
||
}
|
||
else if (e.Key == Key.Enter && _slashSelectedIndex >= 0)
|
||
{
|
||
e.Handled = true;
|
||
ExecuteSlashSelectedItem();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void BtnStop_Click(object sender, RoutedEventArgs e) => StopGeneration();
|
||
|
||
private void BtnPause_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
if (_agentLoop.IsPaused)
|
||
{
|
||
_agentLoop.Resume();
|
||
PauseIcon.Text = "\uE769"; // ?쇱떆?뺤? ?꾩씠肄? BtnPause.ToolTip = "?쇱떆?뺤?";
|
||
}
|
||
else
|
||
{
|
||
_ = _agentLoop.PauseAsync();
|
||
PauseIcon.Text = "\uE768"; // ?ъ깮 ?꾩씠肄? BtnPause.ToolTip = "?ш컻";
|
||
}
|
||
}
|
||
private void BtnExport_Click(object sender, RoutedEventArgs e) => ExportConversation();
|
||
|
||
// ??? 硫붿떆吏 ??寃??(Ctrl+F) ?????????????????????????????????????????
|
||
|
||
private List<int> _searchMatchIndices = new();
|
||
private int _searchCurrentIndex = -1;
|
||
|
||
private void ToggleMessageSearch()
|
||
{
|
||
if (MessageSearchBar.Visibility == Visibility.Visible)
|
||
CloseMessageSearch();
|
||
else
|
||
{
|
||
MessageSearchBar.Visibility = Visibility.Visible;
|
||
SearchTextBox.Focus();
|
||
SearchTextBox.SelectAll();
|
||
}
|
||
}
|
||
|
||
private void CloseMessageSearch()
|
||
{
|
||
MessageSearchBar.Visibility = Visibility.Collapsed;
|
||
SearchTextBox.Text = "";
|
||
SearchResultCount.Text = "";
|
||
_searchMatchIndices.Clear();
|
||
_searchCurrentIndex = -1;
|
||
// ?섏씠?쇱씠???쒓굅
|
||
ClearSearchHighlights();
|
||
}
|
||
|
||
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
|
||
{
|
||
var query = SearchTextBox.Text.Trim();
|
||
if (string.IsNullOrEmpty(query))
|
||
{
|
||
SearchResultCount.Text = "";
|
||
_searchMatchIndices.Clear();
|
||
_searchCurrentIndex = -1;
|
||
ClearSearchHighlights();
|
||
return;
|
||
}
|
||
|
||
// ?꾩옱 ??붿쓽 硫붿떆吏?먯꽌 寃?? ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv == null) return;
|
||
|
||
_searchMatchIndices.Clear();
|
||
for (int i = 0; i < conv.Messages.Count; i++)
|
||
{
|
||
if (conv.Messages[i].Content.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||
_searchMatchIndices.Add(i);
|
||
}
|
||
|
||
if (_searchMatchIndices.Count > 0)
|
||
{
|
||
_searchCurrentIndex = 0;
|
||
SearchResultCount.Text = $"1/{_searchMatchIndices.Count}";
|
||
HighlightSearchResult();
|
||
}
|
||
else
|
||
{
|
||
_searchCurrentIndex = -1;
|
||
SearchResultCount.Text = "寃곌낵 ?놁쓬";
|
||
}
|
||
}
|
||
|
||
private void SearchPrev_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_searchMatchIndices.Count == 0) return;
|
||
_searchCurrentIndex = (_searchCurrentIndex - 1 + _searchMatchIndices.Count) % _searchMatchIndices.Count;
|
||
SearchResultCount.Text = $"{_searchCurrentIndex + 1}/{_searchMatchIndices.Count}";
|
||
HighlightSearchResult();
|
||
}
|
||
|
||
private void SearchNext_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_searchMatchIndices.Count == 0) return;
|
||
_searchCurrentIndex = (_searchCurrentIndex + 1) % _searchMatchIndices.Count;
|
||
SearchResultCount.Text = $"{_searchCurrentIndex + 1}/{_searchMatchIndices.Count}";
|
||
HighlightSearchResult();
|
||
}
|
||
|
||
private void SearchClose_Click(object sender, RoutedEventArgs e) => CloseMessageSearch();
|
||
|
||
private void HighlightSearchResult()
|
||
{
|
||
if (_searchCurrentIndex < 0 || _searchCurrentIndex >= _searchMatchIndices.Count) return;
|
||
var msgIndex = _searchMatchIndices[_searchCurrentIndex];
|
||
|
||
// MessagePanel?먯꽌 ?대떦 硫붿떆吏 ?몃뜳?ㅼ쓽 ?먯떇 ?붿냼瑜?李얠븘 ?ㅽ겕濡? // 硫붿떆吏 ?⑤꼸???먯떇 ?섍? ???硫붿떆吏 ?섏? ?뺥솗???쇱튂?섏? ?딆쓣 ???덉쑝誘濡? // (諛곕꼫, 怨꾪쉷移대뱶 ???욎엫) BringIntoView濡???듭쟻 ?꾩튂 ?대룞
|
||
if (msgIndex < MessagePanel.Children.Count)
|
||
{
|
||
var element = MessagePanel.Children[msgIndex] as FrameworkElement;
|
||
element?.BringIntoView();
|
||
}
|
||
else if (MessagePanel.Children.Count > 0)
|
||
{
|
||
// 踰붿쐞 諛뽰씠硫?留덉?留??먯떇?쇰줈 ?대룞
|
||
(MessagePanel.Children[^1] as FrameworkElement)?.BringIntoView();
|
||
}
|
||
}
|
||
|
||
private void ClearSearchHighlights()
|
||
{
|
||
// ?꾩옱??BringIntoView 湲곕컲?대?濡?蹂꾨룄 ?섏씠?쇱씠???쒓굅 遺덊븘?? }
|
||
|
||
// ??? ?먮윭 蹂듦뎄 ?ъ떆??踰꾪듉 ??????????????????????????????????????????????
|
||
|
||
private void AddRetryButton()
|
||
{
|
||
Dispatcher.Invoke(() =>
|
||
{
|
||
var retryBorder = new Border
|
||
{
|
||
Background = new SolidColorBrush(Color.FromArgb(0x18, 0xEF, 0x44, 0x44)),
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(12, 8, 12, 8),
|
||
Margin = new Thickness(40, 4, 80, 4),
|
||
HorizontalAlignment = HorizontalAlignment.Left,
|
||
Cursor = System.Windows.Input.Cursors.Hand,
|
||
};
|
||
var retrySp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
retrySp.Children.Add(new TextBlock
|
||
{
|
||
Text = "\uE72C", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)),
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||
});
|
||
retrySp.Children.Add(new TextBlock
|
||
{
|
||
Text = "?ъ떆??, FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
retryBorder.Child = retrySp;
|
||
retryBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x44, 0x44)); };
|
||
retryBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xEF, 0x44, 0x44)); };
|
||
retryBorder.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null)
|
||
{
|
||
var lastIdx = _currentConversation.Messages.Count - 1;
|
||
if (lastIdx >= 0 && _currentConversation.Messages[lastIdx].Role == "assistant")
|
||
_currentConversation.Messages.RemoveAt(lastIdx);
|
||
}
|
||
}
|
||
_ = RegenerateLastAsync();
|
||
};
|
||
MessagePanel.Children.Add(retryBorder);
|
||
ForceScrollToEnd();
|
||
});
|
||
}
|
||
|
||
// ??? 硫붿떆吏 ?고겢由?而⑦뀓?ㅽ듃 硫붾돱 ???????????????????????????????????????
|
||
|
||
private void ShowMessageContextMenu(string content, string role)
|
||
{
|
||
var menu = CreateThemedContextMenu();
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
void AddItem(string icon, string label, Action action)
|
||
{
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = secondaryText,
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = label, FontSize = 12, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) };
|
||
mi.Click += (_, _) => action();
|
||
menu.Items.Add(mi);
|
||
}
|
||
|
||
// 蹂듭궗
|
||
AddItem("\uE8C8", "?띿뒪??蹂듭궗", () =>
|
||
{
|
||
try { Clipboard.SetText(content); ShowToast("蹂듭궗?섏뿀?듬땲??); } catch { }
|
||
});
|
||
|
||
// 留덊겕?ㅼ슫 蹂듭궗
|
||
AddItem("\uE943", "留덊겕?ㅼ슫 蹂듭궗", () =>
|
||
{
|
||
try { Clipboard.SetText(content); ShowToast("留덊겕?ㅼ슫?쇰줈 蹂듭궗??); } catch { }
|
||
});
|
||
|
||
// ?몄슜?섏뿬 ?듭옣
|
||
AddItem("\uE97A", "?몄슜?섏뿬 ?듭옣", () =>
|
||
{
|
||
var quote = content.Length > 200 ? content[..200] + "..." : content;
|
||
var lines = quote.Split('\n');
|
||
var quoted = string.Join("\n", lines.Select(l => $"> {l}"));
|
||
InputBox.Text = quoted + "\n\n";
|
||
InputBox.Focus();
|
||
InputBox.CaretIndex = InputBox.Text.Length;
|
||
});
|
||
|
||
menu.Items.Add(new Separator());
|
||
|
||
// ?ъ깮??(AI ?묐떟留?
|
||
if (role == "assistant")
|
||
{
|
||
AddItem("\uE72C", "?묐떟 ?ъ깮??, () => _ = RegenerateLastAsync());
|
||
}
|
||
|
||
// ???遺꾧린 (Fork)
|
||
AddItem("\uE8A5", "?ш린??遺꾧린", () =>
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv == null) return;
|
||
|
||
var idx = conv.Messages.FindLastIndex(m => m.Role == role && m.Content == content);
|
||
if (idx < 0) return;
|
||
|
||
ForkConversation(conv, idx);
|
||
});
|
||
|
||
menu.Items.Add(new Separator());
|
||
|
||
// ?댄썑 硫붿떆吏 紐⑤몢 ??젣
|
||
var msgContent = content;
|
||
var msgRole = role;
|
||
AddItem("\uE74D", "?댄썑 硫붿떆吏 紐⑤몢 ??젣", () =>
|
||
{
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
if (conv == null) return;
|
||
|
||
var idx = conv.Messages.FindLastIndex(m => m.Role == msgRole && m.Content == msgContent);
|
||
if (idx < 0) return;
|
||
|
||
var removeCount = conv.Messages.Count - idx;
|
||
if (MessageBox.Show($"??硫붿떆吏 ?ы븿 {removeCount}媛?硫붿떆吏瑜???젣?섏떆寃좎뒿?덇퉴?",
|
||
"硫붿떆吏 ??젣", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
|
||
return;
|
||
|
||
conv.Messages.RemoveRange(idx, removeCount);
|
||
try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"???????ㅽ뙣: {ex.Message}"); }
|
||
RenderMessages();
|
||
ShowToast($"{removeCount}媛?硫붿떆吏 ??젣??);
|
||
});
|
||
|
||
menu.IsOpen = true;
|
||
}
|
||
|
||
// ??? ???뚮┝ ??????????????????????????????????????????????????????
|
||
|
||
private static readonly string[] Tips =
|
||
[
|
||
"?뮕 ?묒뾽 ?대뜑??AX.md ?뚯씪??留뚮뱾硫?留ㅻ쾲 ?쒖뒪???꾨\?꾪듃???먮룞 二쇱엯?⑸땲?? ?꾨줈?앺듃 ?ㅺ퀎 ?먯튃?대굹 肄붾뵫 洹쒖튃??湲곕줉?섏꽭??",
|
||
"?뮕 Ctrl+1/2/3?쇰줈 Chat/Cowork/Code ??쓣 鍮좊Ⅴ寃??꾪솚?????덉뒿?덈떎.",
|
||
"?뮕 Ctrl+F濡??꾩옱 ?????硫붿떆吏瑜?寃?됲븷 ???덉뒿?덈떎.",
|
||
"?뮕 硫붿떆吏瑜??고겢由?븯硫?蹂듭궗, ?몄슜 ?듭옣, ?ъ깮?? ??젣瑜??????덉뒿?덈떎.",
|
||
"?뮕 肄붾뱶 釉붾줉???붾툝?대┃?섎㈃ ?꾩껜?붾㈃?쇰줈 蹂????덇퀬, ?뮶 踰꾪듉?쇰줈 ?뚯씪 ??μ씠 媛?ν빀?덈떎.",
|
||
"?뮕 Cowork ?먯씠?꾪듃媛 留뚮뱺 ?뚯씪? ?먮룞?쇰줈 ?좎쭨_?쒓컙 ?묐??ш? 遺숈뼱 ??뼱?곌린瑜?諛⑹??⑸땲??",
|
||
"?뮕 Code ??뿉??媛쒕컻 ?몄뼱瑜??좏깮?섎㈃ ?대떦 ?몄뼱 ?곗꽑?쇰줈 肄붾뱶瑜??앹꽦?⑸땲??",
|
||
"?뮕 ?뚯씪 ?먯깋湲??섎떒 諛?'?뚯씪' 踰꾪듉)?먯꽌 ?붾툝?대┃?쇰줈 ?꾨━酉? ?고겢由?쑝濡?愿由ы븷 ???덉뒿?덈떎.",
|
||
"?뮕 ?먯씠?꾪듃媛 怨꾪쉷???쒖떆?섎㈃ '?섏젙 ?붿껌'?쇰줈 諛⑺뼢??諛붽씀嫄곕굹 '痍⑥냼'濡?以묐떒?????덉뒿?덈떎.",
|
||
"?뮕 Code ??? 鍮뚮뱶/?뚯뒪?몃? ?먮룞?쇰줈 ?ㅽ뻾?⑸땲?? ?꾨줈?앺듃 ?대뜑瑜?癒쇱? ?좏깮?섏꽭??",
|
||
"?뮕 臾대뱶 媛ㅻ윭由ъ뿉??10媛吏 ?붿옄???쒗뵆由?以??먰븯???ㅽ??쇱쓣 誘몃━蹂닿린濡??좏깮?????덉뒿?덈떎.",
|
||
"?뮕 Git ?곕룞: Code ??뿉???먯씠?꾪듃媛 git status, diff, commit???섑뻾?⑸땲?? (push??吏곸젒)",
|
||
"?뮕 ?ㅼ젙 ??AX Agent ??怨듯넻?먯꽌 媛쒕컻??紐⑤뱶瑜?耳쒕㈃ ?먯씠?꾪듃 ?숈옉???ㅽ뀦蹂꾨줈 寃利앺븷 ???덉뒿?덈떎.",
|
||
"?뮕 ?몃젅???꾩씠肄??고겢由???'?ъ슜 ?듦퀎'?먯꽌 ???鍮덈룄? ?좏겙 ?ъ슜?됱쓣 ?뺤씤?????덉뒿?덈떎.",
|
||
"?뮕 ????쒕ぉ???대┃?섎㈃ ?대쫫??蹂寃쏀븷 ???덉뒿?덈떎.",
|
||
"?뮕 LLM ?ㅻ쪟 諛쒖깮 ??'?ъ떆?? 踰꾪듉???먮룞?쇰줈 ?섑??⑸땲??",
|
||
"?뮕 寃?됰??먯꽌 ????쒕ぉ肉??꾨땲??泥?硫붿떆吏 ?댁슜源뚯? 寃?됰맗?덈떎.",
|
||
"?뮕 ?꾨━???좏깮 ?꾩뿉????붽? 由ъ뀑?섏? ?딆뒿?덈떎. 吏꾪뻾 以묒씤 ??붿뿉???꾨━?뗭쓣 蹂寃쏀븷 ???덉뒿?덈떎.",
|
||
"?뮕 Shift+Enter濡??쇱? 寃??寃곌낵???뚯씪???덈뒗 ?대뜑瑜??????덉뒿?덈떎.",
|
||
"?뮕 理쒓렐 ?대뜑瑜??고겢由?븯硫?'?대뜑 ?닿린', '寃쎈줈 蹂듭궗', '紐⑸줉?먯꽌 ??젣'媛 媛?ν빀?덈떎.",
|
||
"?뮕 Cowork/Code ?먯씠?꾪듃 ?묒뾽 ?꾨즺 ???쒖뒪???몃젅?댁뿉 ?뚮┝???쒖떆?⑸땲??",
|
||
"?뮕 留덊겕?ㅼ슫 ?뚯씠釉? ?몄슜(>), 痍⑥냼??~~), 留곹겕([text](url))媛 紐⑤몢 ?뚮뜑留곷맗?덈떎.",
|
||
"?뮕 ???곗씠???대뜑瑜??뚰겕?ㅽ럹?댁뒪濡?吏?뺥븷 ?뚮뒗 諛섎뱶??諛깆뾽??癒쇱? 留뚮뱶?몄슂!",
|
||
"?뮕 ?쒕씪?대툕 猷⑦듃(C:\\, D:\\)???묒뾽怨듦컙?쇰줈 ?ㅼ젙?????놁뒿?덈떎. ?섏쐞 ?대뜑瑜??좏깮?섏꽭??",
|
||
];
|
||
private int _tipIndex;
|
||
private DispatcherTimer? _tipDismissTimer;
|
||
|
||
private void ShowRandomTip()
|
||
{
|
||
if (!_settings.Settings.Llm.ShowTips) return;
|
||
if (_activeTab != "Cowork" && _activeTab != "Code") return;
|
||
|
||
var tip = Tips[_tipIndex % Tips.Length];
|
||
_tipIndex++;
|
||
|
||
// ?좎뒪???ㅽ??쇰줈 ?쒖떆 (湲곗〈 ?좎뒪?몄? ?ㅻⅨ ?꾩튂/?됱긽)
|
||
ShowTip(tip);
|
||
}
|
||
|
||
private void ShowTip(string message)
|
||
{
|
||
_tipDismissTimer?.Stop();
|
||
|
||
ToastText.Text = message;
|
||
ToastIcon.Text = "\uE82F"; // ?꾧뎄 ?꾩씠肄? ToastBorder.Visibility = Visibility.Visible;
|
||
ToastBorder.BeginAnimation(UIElement.OpacityProperty,
|
||
new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(300)));
|
||
|
||
var duration = _settings.Settings.Llm.TipDurationSeconds;
|
||
if (duration <= 0) return; // 0?대㈃ ?섎룞 ?リ린 (?먮룞 ?щ씪吏??놁쓬)
|
||
|
||
_tipDismissTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(duration) };
|
||
_tipDismissTimer.Tick += (_, _) =>
|
||
{
|
||
_tipDismissTimer.Stop();
|
||
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
|
||
fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed;
|
||
ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
||
};
|
||
_tipDismissTimer.Start();
|
||
}
|
||
|
||
// ??? ?꾨줈?앺듃 臾몃㎘ ?뚯씪 (AX.md) ??????????????????????????????????
|
||
|
||
/// <summary>
|
||
/// ?묒뾽 ?대뜑??AX.md媛 ?덉쑝硫??댁슜???쎌뼱 ?쒖뒪???꾨\?꾪듃??二쇱엯?⑸땲??
|
||
/// Claude Code? ?숈씪???뚯씪紐??뺤떇???ъ슜?⑸땲??
|
||
/// </summary>
|
||
private static string LoadProjectContext(string workFolder)
|
||
{
|
||
if (string.IsNullOrEmpty(workFolder)) return "";
|
||
|
||
// AX.md ?먯깋 (?묒뾽 ?대뜑 ???곸쐞 ?대뜑 ??
|
||
var searchDir = workFolder;
|
||
for (int i = 0; i < 3; i++) // 理쒕? 3?④퀎 ?곸쐞源뚯?
|
||
{
|
||
if (string.IsNullOrEmpty(searchDir)) break;
|
||
var filePath = System.IO.Path.Combine(searchDir, "AX.md");
|
||
if (System.IO.File.Exists(filePath))
|
||
{
|
||
try
|
||
{
|
||
var content = System.IO.File.ReadAllText(filePath);
|
||
if (content.Length > 8000) content = content[..8000] + "\n... (8000??珥덇낵 ?앸왂)";
|
||
return $"\n## Project Context (from AX.md)\n{content}\n";
|
||
}
|
||
catch { }
|
||
}
|
||
searchDir = System.IO.Directory.GetParent(searchDir)?.FullName;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
// ??? 臾댁?媛?湲濡쒖슦 ?좊땲硫붿씠???????????????????????????????????????????
|
||
|
||
private DispatcherTimer? _rainbowTimer;
|
||
private DateTime _rainbowStartTime;
|
||
|
||
/// <summary>?낅젰李??뚮몢由ъ뿉 臾댁?媛?洹몃씪?곗씠???뚯쟾 ?좊땲硫붿씠?섏쓣 ?ъ깮?⑸땲??(3珥?.</summary>
|
||
private void PlayRainbowGlow()
|
||
{
|
||
if (!_settings.Settings.Llm.EnableChatRainbowGlow) return;
|
||
|
||
_rainbowTimer?.Stop();
|
||
_rainbowStartTime = DateTime.UtcNow;
|
||
|
||
// ?섏씠?쒖씤 (鍮좊Ⅴ寃?
|
||
InputGlowBorder.BeginAnimation(UIElement.OpacityProperty,
|
||
new System.Windows.Media.Animation.DoubleAnimation(0, 0.9, TimeSpan.FromMilliseconds(150)));
|
||
|
||
// 洹몃씪?곗씠???뚯쟾 ??대㉧ (~60fps) ???ㅽ듃由щ컢 醫낅즺源뚯? 吏?? _rainbowTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) };
|
||
_rainbowTimer.Tick += (_, _) =>
|
||
{
|
||
var elapsed = (DateTime.UtcNow - _rainbowStartTime).TotalMilliseconds;
|
||
|
||
// 洹몃씪?곗씠???ㅽ봽???뚯쟾
|
||
var shift = (elapsed / 1500.0) % 1.0; // 1.5珥덉뿉 1諛뷀?(?먮━寃?
|
||
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)
|
||
{
|
||
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(
|
||
InputGlowBorder.Opacity, 0, TimeSpan.FromMilliseconds(600));
|
||
fadeOut.Completed += (_, _) => InputGlowBorder.Opacity = 0;
|
||
InputGlowBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
||
}
|
||
}
|
||
|
||
// ??? ?좎뒪???뚮┝ ??????????????????????????????????????????????????????
|
||
|
||
private DispatcherTimer? _toastHideTimer;
|
||
|
||
private void ShowToast(string message, string icon = "\uE73E", int durationMs = 2000)
|
||
{
|
||
_toastHideTimer?.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)));
|
||
|
||
// ?먮룞 ?④린湲? _toastHideTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(durationMs) };
|
||
_toastHideTimer.Tick += (_, _) =>
|
||
{
|
||
_toastHideTimer.Stop();
|
||
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
|
||
fadeOut.Completed += (_, _) => ToastBorder.Visibility = Visibility.Collapsed;
|
||
ToastBorder.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
||
};
|
||
_toastHideTimer.Start();
|
||
}
|
||
|
||
// ??? ???二쇱젣 踰꾪듉 ??????????????????????????????????????????????????
|
||
|
||
/// <summary>?꾨━?뗭뿉?????二쇱젣 踰꾪듉???숈쟻?쇰줈 ?앹꽦?⑸땲??</summary>
|
||
private void BuildTopicButtons()
|
||
{
|
||
TopicButtonPanel.Children.Clear();
|
||
|
||
TopicButtonPanel.Visibility = Visibility.Visible;
|
||
|
||
// ??퀎 EmptyState ?띿뒪?? if (_activeTab == "Cowork" || _activeTab == "Code")
|
||
{
|
||
if (EmptyStateTitle != null) EmptyStateTitle.Text = "?묒뾽 ?좏삎???좏깮?섏꽭??;
|
||
if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
|
||
? "肄붾뵫 ?먯씠?꾪듃媛 肄붾뱶 遺꾩꽍, ?섏젙, 鍮뚮뱶, ?뚯뒪?몃? ?섑뻾?⑸땲??
|
||
: "?먯씠?꾪듃媛 ?곸꽭???곗씠?곕? ?묒꽦?⑸땲??;
|
||
}
|
||
else
|
||
{
|
||
if (EmptyStateTitle != null) EmptyStateTitle.Text = "???二쇱젣瑜??좏깮?섏꽭??;
|
||
if (EmptyStateDesc != null) EmptyStateDesc.Text = "二쇱젣??留욌뒗 ?꾨Ц ?꾨━?뗭씠 ?먮룞 ?곸슜?⑸땲??;
|
||
}
|
||
|
||
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
|
||
|
||
foreach (var preset in presets)
|
||
{
|
||
var capturedPreset = preset;
|
||
var btnColor = BrushFromHex(preset.Color);
|
||
|
||
var border = new Border
|
||
{
|
||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(14),
|
||
Padding = new Thickness(14, 12, 14, 12),
|
||
Margin = new Thickness(4, 4, 4, 8),
|
||
Cursor = Cursors.Hand,
|
||
Width = 120,
|
||
Height = 105,
|
||
ClipToBounds = true,
|
||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||
RenderTransform = new ScaleTransform(1, 1),
|
||
};
|
||
|
||
var stack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
||
|
||
// ?꾩씠肄?而⑦뀒?대꼫 (?먰삎 諛곌꼍 + ?꾩뒪 ?좊땲硫붿씠??
|
||
var iconCircle = new Border
|
||
{
|
||
Width = 40, Height = 40,
|
||
CornerRadius = new CornerRadius(20),
|
||
Background = new SolidColorBrush(((SolidColorBrush)btnColor).Color) { Opacity = 0.15 },
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 0, 10),
|
||
};
|
||
var iconTb = new TextBlock
|
||
{
|
||
Text = preset.Symbol,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 18,
|
||
Foreground = btnColor,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
iconCircle.Child = iconTb;
|
||
stack.Children.Add(iconCircle);
|
||
|
||
// ?쒕ぉ
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = preset.Label,
|
||
FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
});
|
||
|
||
// ?ㅻ챸
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = preset.Description,
|
||
FontSize = 9, TextWrapping = TextWrapping.Wrap,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
MaxHeight = 28,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
TextAlignment = TextAlignment.Center,
|
||
});
|
||
|
||
// 而ㅼ뒪? ?꾨━?? 醫뚯륫 ?곷떒 諭껋?
|
||
if (capturedPreset.IsCustom)
|
||
{
|
||
var grid = new Grid();
|
||
grid.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),
|
||
ToolTip = "而ㅼ뒪? ?꾨━??,
|
||
};
|
||
badge.Child = new TextBlock
|
||
{
|
||
Text = "\uE710", // + ?꾩씠肄? FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 8,
|
||
Foreground = btnColor,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
grid.Children.Add(badge);
|
||
border.Child = grid;
|
||
}
|
||
else
|
||
{
|
||
border.Child = stack;
|
||
}
|
||
|
||
// ?몃쾭 ?좊땲硫붿씠?????ㅼ???1.05x + 諛앷린 蹂寃? var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||
var normalBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
|
||
border.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{
|
||
st.ScaleX = 1.03; st.ScaleY = 1.03;
|
||
b.Background = hoverBg;
|
||
}
|
||
};
|
||
border.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{
|
||
st.ScaleX = 1.0; st.ScaleY = 1.0;
|
||
b.Background = normalBg;
|
||
}
|
||
};
|
||
|
||
// ?대┃ ???대떦 二쇱젣濡???????쒖옉
|
||
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 = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(14),
|
||
Padding = new Thickness(14, 12, 14, 12),
|
||
Margin = new Thickness(4, 4, 4, 8),
|
||
Cursor = Cursors.Hand,
|
||
Width = 120,
|
||
Height = 105,
|
||
ClipToBounds = true,
|
||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||
RenderTransform = new ScaleTransform(1, 1),
|
||
};
|
||
|
||
var etcStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
||
|
||
var etcIconCircle = new Border
|
||
{
|
||
Width = 40, Height = 40,
|
||
CornerRadius = new CornerRadius(20),
|
||
Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 0, 10),
|
||
};
|
||
etcIconCircle.Child = new TextBlock
|
||
{
|
||
Text = "\uE70F", // Edit ?꾩씠肄? FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 18,
|
||
Foreground = etcColor,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
etcStack.Children.Add(etcIconCircle);
|
||
|
||
etcStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "湲고?",
|
||
FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
});
|
||
etcStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "?꾨━???놁씠 ?먯쑀濡?쾶 ??뷀빀?덈떎",
|
||
FontSize = 9, TextWrapping = TextWrapping.Wrap,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
MaxHeight = 28,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
TextAlignment = TextAlignment.Center,
|
||
});
|
||
|
||
etcBorder.Child = etcStack;
|
||
|
||
var hoverBg2 = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||
var normalBg2 = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
|
||
etcBorder.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{ st.ScaleX = 1.03; st.ScaleY = 1.03; b.Background = hoverBg2; }
|
||
};
|
||
etcBorder.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{ st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = normalBg2; }
|
||
};
|
||
|
||
etcBorder.MouseLeftButtonDown += (_, _) =>
|
||
{
|
||
EmptyState.Visibility = Visibility.Collapsed;
|
||
InputBox.Focus();
|
||
};
|
||
TopicButtonPanel.Children.Add(etcBorder);
|
||
}
|
||
|
||
// ?? "+" 而ㅼ뒪? ?꾨━??異붽? 踰꾪듉 ??
|
||
{
|
||
var addColor = BrushFromHex("#6366F1");
|
||
var addBorder = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(14),
|
||
Padding = new Thickness(14, 12, 14, 12),
|
||
Margin = new Thickness(4, 4, 4, 8),
|
||
Cursor = Cursors.Hand,
|
||
Width = 120, Height = 105,
|
||
ClipToBounds = true,
|
||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||
BorderThickness = new Thickness(1.5),
|
||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||
RenderTransform = new ScaleTransform(1, 1),
|
||
};
|
||
// ?먯꽑 ?④낵瑜??꾪븳 Dashes
|
||
if (addBorder.BorderBrush is SolidColorBrush scb)
|
||
{
|
||
var dashPen = new Pen(scb, 1.5) { DashStyle = DashStyles.Dash };
|
||
}
|
||
|
||
var addStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
|
||
|
||
// + ?꾩씠肄? var plusIcon = new TextBlock
|
||
{
|
||
Text = "\uE710",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 24,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
Margin = new Thickness(0, 8, 0, 8),
|
||
};
|
||
addStack.Children.Add(plusIcon);
|
||
|
||
addStack.Children.Add(new TextBlock
|
||
{
|
||
Text = "?꾨━??異붽?",
|
||
FontSize = 12,
|
||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
});
|
||
|
||
addBorder.Child = addStack;
|
||
|
||
var hoverBg3 = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||
addBorder.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{ st.ScaleX = 1.03; st.ScaleY = 1.03; b.Background = hoverBg3; }
|
||
};
|
||
addBorder.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b && b.RenderTransform is ScaleTransform st)
|
||
{ st.ScaleX = 1.0; st.ScaleY = 1.0; b.Background = Brushes.Transparent; }
|
||
};
|
||
addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
|
||
TopicButtonPanel.Children.Add(addBorder);
|
||
}
|
||
}
|
||
|
||
// ??? 而ㅼ뒪? ?꾨━??愿由??????????????????????????????????????????????
|
||
|
||
/// <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;
|
||
lock (_convLock) hasMessages = _currentConversation?.Messages.Count > 0;
|
||
|
||
// ?낅젰????띿뒪?멸? ?덉쑝硫?湲곗〈 ??붾? ?좎? (?낅젰 ?댁슜 蹂댁〈)
|
||
bool hasInput = !string.IsNullOrEmpty(InputBox.Text);
|
||
bool keepConversation = hasMessages || hasInput;
|
||
|
||
if (!keepConversation)
|
||
{
|
||
// 硫붿떆吏???낅젰 ?띿뒪?몃룄 ?놁쑝硫???????쒖옉
|
||
StartNewConversation();
|
||
}
|
||
|
||
// ?꾨━???곸슜 (湲곗〈 ??붿뿉???꾨━??蹂寃?媛??
|
||
lock (_convLock)
|
||
{
|
||
if (_currentConversation != null)
|
||
{
|
||
_currentConversation.SystemCommand = preset.SystemPrompt;
|
||
_currentConversation.Category = preset.Category;
|
||
}
|
||
}
|
||
|
||
if (!keepConversation)
|
||
EmptyState.Visibility = Visibility.Collapsed;
|
||
|
||
InputBox.Focus();
|
||
|
||
if (!string.IsNullOrEmpty(preset.Placeholder))
|
||
{
|
||
_promptCardPlaceholder = preset.Placeholder;
|
||
if (!keepConversation) ShowPlaceholder();
|
||
}
|
||
|
||
if (keepConversation)
|
||
ShowToast($"?꾨━??蹂寃? {preset.Label}");
|
||
|
||
// Cowork ?? ?섎떒 諛?媛깆떊
|
||
if (_activeTab == "Cowork")
|
||
BuildBottomBar();
|
||
}
|
||
|
||
|
||
|
||
/// <summary>?좏깮???붿옄??臾대뱶 ??(HtmlSkill?먯꽌 ?ъ슜).</summary>
|
||
private string _selectedMood = null!; // Loaded ?대깽?몄뿉??珥덇린?? private string _selectedLanguage = "auto"; // Code ??媛쒕컻 ?몄뼱
|
||
private string _folderDataUsage = null!; // Loaded ?대깽?몄뿉??珥덇린??
|
||
/// <summary>?섎떒 諛붾? 援ъ꽦?⑸땲??(?щ㎎ + ?붿옄???쒕∼?ㅼ슫 踰꾪듉).</summary>
|
||
private void BuildBottomBar()
|
||
{
|
||
MoodIconPanel.Children.Clear();
|
||
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
// ?? ?щ㎎ 踰꾪듉 ??
|
||
var currentFormat = _settings.Settings.Llm.DefaultOutputFormat ?? "auto";
|
||
var formatLabel = GetFormatLabel(currentFormat);
|
||
var formatBtn = CreateFolderBarButton("\uE9F9", formatLabel, "蹂닿퀬???뺥깭 ?좏깮", "#8B5CF6");
|
||
formatBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowFormatMenu(); };
|
||
// Name ?깅줉 (Popup PlacementTarget??
|
||
try { RegisterName("BtnFormatMenu", formatBtn); } catch { try { UnregisterName("BtnFormatMenu"); RegisterName("BtnFormatMenu", formatBtn); } catch { } }
|
||
MoodIconPanel.Children.Add(formatBtn);
|
||
|
||
// 援щ텇?? 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 currentMood = TemplateService.AllMoods.FirstOrDefault(m => m.Key == _selectedMood);
|
||
var moodLabel = currentMood?.Label ?? "紐⑤뜕";
|
||
var moodIcon = currentMood?.Icon ?? "?뵹";
|
||
var moodBtn = CreateFolderBarButton(null, $"{moodIcon} {moodLabel}", "?붿옄??臾대뱶 ?좏깮");
|
||
moodBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowMoodMenu(); };
|
||
try { RegisterName("BtnMoodMenu", moodBtn); } catch { try { UnregisterName("BtnMoodMenu"); RegisterName("BtnMoodMenu", moodBtn); } catch { } }
|
||
MoodIconPanel.Children.Add(moodBtn);
|
||
|
||
// 援щ텇?? 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 fileBrowserBtn = CreateFolderBarButton("\uED25", "?뚯씪", "?뚯씪 ?먯깋湲??닿린/?リ린", "#D97706");
|
||
fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); };
|
||
MoodIconPanel.Children.Add(fileBrowserBtn);
|
||
|
||
// ?? ?ㅽ뻾 ?대젰 ?곸꽭??踰꾪듉 ??
|
||
AppendLogLevelButton();
|
||
|
||
// 援щ텇???쒖떆
|
||
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible;
|
||
}
|
||
|
||
/// <summary>Code ???섎떒 諛? 媛쒕컻 ?몄뼱 ?좏깮 + ?뚯씪 ?먯깋湲??좉?.</summary>
|
||
private void BuildCodeBottomBar()
|
||
{
|
||
MoodIconPanel.Children.Clear();
|
||
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
|
||
// 媛쒕컻 ?몄뼱 ?좏깮 踰꾪듉
|
||
var langLabel = _selectedLanguage switch
|
||
{
|
||
"python" => "?릫 Python",
|
||
"java" => "??Java",
|
||
"csharp" => "?뵹 C#",
|
||
"cpp" => "??C++",
|
||
"javascript" => "?뙋 JavaScript",
|
||
_ => "?뵩 ?먮룞 媛먯?",
|
||
};
|
||
var langBtn = CreateFolderBarButton(null, langLabel, "媛쒕컻 ?몄뼱 ?좏깮");
|
||
langBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowLanguageMenu(); };
|
||
try { RegisterName("BtnLangMenu", langBtn); } catch { try { UnregisterName("BtnLangMenu"); RegisterName("BtnLangMenu", langBtn); } catch { } }
|
||
MoodIconPanel.Children.Add(langBtn);
|
||
|
||
// 援щ텇?? 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 fileBrowserBtn = CreateFolderBarButton("\uED25", "?뚯씪", "?뚯씪 ?먯깋湲??닿린/?リ린", "#D97706");
|
||
fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); };
|
||
MoodIconPanel.Children.Add(fileBrowserBtn);
|
||
|
||
// ?? ?ㅽ뻾 ?대젰 ?곸꽭??踰꾪듉 ??
|
||
AppendLogLevelButton();
|
||
|
||
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible;
|
||
}
|
||
|
||
/// <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 ?? "simple";
|
||
var levelLabel = currentLevel switch
|
||
{
|
||
"debug" => "?붾쾭洹?,
|
||
"detailed" => "?곸꽭",
|
||
_ => "媛꾨왂",
|
||
};
|
||
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)[]
|
||
{
|
||
("simple", "Simple (媛꾨왂)", "?꾧뎄 寃곌낵留???以꾨줈 ?쒖떆"),
|
||
("detailed", "Detailed (?곸꽭)", "?꾧뎄 ?몄텧/寃곌낵 + ?묒씠???곸꽭"),
|
||
("debug", "Debug (?붾쾭洹?", "紐⑤뱺 ?뺣낫 + ?뚮씪誘명꽣 ?쒖떆"),
|
||
};
|
||
|
||
var current = _settings.Settings.Llm.AgentLogLevel ?? "simple";
|
||
|
||
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 iconColor = iconColorHex != null ? BrushFromHex(iconColorHex) : secondaryText;
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
|
||
if (mdlIcon != null)
|
||
{
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = mdlIcon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
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,
|
||
});
|
||
|
||
return new Border
|
||
{
|
||
Child = sp,
|
||
Background = Brushes.Transparent,
|
||
Padding = new Thickness(6, 4, 6, 4),
|
||
Cursor = Cursors.Hand,
|
||
ToolTip = tooltip,
|
||
};
|
||
}
|
||
|
||
|
||
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 = new FontFamily("Segoe MDL2 Assets"),
|
||
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();
|
||
BuildBottomBar();
|
||
};
|
||
|
||
FormatMenuItems.Children.Add(itemBorder);
|
||
}
|
||
|
||
// PlacementTarget???숈쟻 ?깅줉??踰꾪듉?쇰줈 ?ㅼ젙
|
||
if (FindName("BtnFormatMenu") is UIElement formatTarget)
|
||
FormatMenuPopup.PlacementTarget = formatTarget;
|
||
FormatMenuPopup.IsOpen = true;
|
||
}
|
||
|
||
/// <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();
|
||
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 = new FontFamily("Segoe MDL2 Assets"),
|
||
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 (FindName("BtnMoodMenu") is UIElement moodTarget)
|
||
MoodMenuPopup.PlacementTarget = moodTarget;
|
||
MoodMenuPopup.IsOpen = true;
|
||
}
|
||
|
||
/// <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()
|
||
{
|
||
if (string.IsNullOrEmpty(_promptCardPlaceholder)) return;
|
||
InputWatermark.Text = _promptCardPlaceholder;
|
||
InputWatermark.Visibility = Visibility.Visible;
|
||
InputBox.Text = "";
|
||
InputBox.Focus();
|
||
}
|
||
|
||
private void UpdateWatermarkVisibility()
|
||
{
|
||
// ?щ옒??移⑹씠 ?쒖꽦?붾릺???덉쑝硫??뚰꽣留덊겕 ?④린湲?(寃뱀묠 諛⑹?)
|
||
if (_activeSlashCmd != null)
|
||
{
|
||
InputWatermark.Visibility = Visibility.Collapsed;
|
||
return;
|
||
}
|
||
|
||
if (_promptCardPlaceholder != null && string.IsNullOrEmpty(InputBox.Text))
|
||
InputWatermark.Visibility = Visibility.Visible;
|
||
else
|
||
InputWatermark.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
private void ClearPromptCardPlaceholder()
|
||
{
|
||
_promptCardPlaceholder = null;
|
||
InputWatermark.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
private void BtnSettings_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (System.Windows.Application.Current is App app)
|
||
app.OpenSettingsFromChat();
|
||
}
|
||
|
||
// ??? ?꾨\?꾪듃 ?쒗뵆由??앹뾽 ????????????????????????????????????????????
|
||
|
||
private void BtnTemplateSelector_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var templates = _settings.Settings.Llm.PromptTemplates;
|
||
TemplateItems.Items.Clear();
|
||
|
||
if (templates == null || templates.Count == 0)
|
||
{
|
||
TemplateEmptyHint.Visibility = Visibility.Visible;
|
||
TemplatePopup.IsOpen = true;
|
||
return;
|
||
}
|
||
|
||
TemplateEmptyHint.Visibility = Visibility.Collapsed;
|
||
|
||
foreach (var tpl in templates)
|
||
{
|
||
var item = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 8, 10, 8),
|
||
Margin = new Thickness(2),
|
||
Cursor = System.Windows.Input.Cursors.Hand,
|
||
Tag = tpl.Content,
|
||
};
|
||
|
||
var stack = new StackPanel();
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = tpl.Name,
|
||
FontSize = 13,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = (Brush)FindResource("PrimaryText"),
|
||
});
|
||
var preview = tpl.Content.Length > 60 ? tpl.Content[..60] + "?? : tpl.Content;
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = preview,
|
||
FontSize = 11,
|
||
Foreground = (Brush)FindResource("SecondaryText"),
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
Margin = new Thickness(0, 2, 0, 0),
|
||
});
|
||
item.Child = stack;
|
||
|
||
item.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b) b.Background = (Brush)FindResource("ItemBackground");
|
||
};
|
||
item.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b) b.Background = Brushes.Transparent;
|
||
};
|
||
item.MouseLeftButtonUp += (s, _) =>
|
||
{
|
||
if (s is Border b && b.Tag is string content)
|
||
{
|
||
InputBox.Text = content;
|
||
InputBox.CaretIndex = InputBox.Text.Length;
|
||
InputBox.Focus();
|
||
TemplatePopup.IsOpen = false;
|
||
}
|
||
};
|
||
|
||
TemplateItems.Items.Add(item);
|
||
}
|
||
|
||
TemplatePopup.IsOpen = true;
|
||
}
|
||
|
||
// ??? 紐⑤뜽 ?꾪솚 ??????????????????????????????????????????????????????
|
||
|
||
// Gemini/Claude ?ъ쟾 ?뺤쓽 紐⑤뜽 紐⑸줉
|
||
private static readonly (string Id, string Label)[] GeminiModels =
|
||
{
|
||
("gemini-2.5-pro", "Gemini 2.5 Pro"),
|
||
("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
||
("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"),
|
||
("gemini-2.0-flash", "Gemini 2.0 Flash"),
|
||
("gemini-2.0-flash-lite", "Gemini 2.0 Flash Lite"),
|
||
};
|
||
private static readonly (string Id, string Label)[] ClaudeModels =
|
||
{
|
||
("claude-opus-4-6", "Claude Opus 4.6"),
|
||
("claude-sonnet-4-6", "Claude Sonnet 4.6"),
|
||
("claude-haiku-4-5-20251001", "Claude Haiku 4.5"),
|
||
("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5"),
|
||
("claude-opus-4-20250514", "Claude Opus 4"),
|
||
};
|
||
|
||
/// <summary>?꾩옱 ?좏깮??紐⑤뜽???쒖떆紐낆쓣 諛섑솚?⑸땲??</summary>
|
||
private string GetCurrentModelDisplayName()
|
||
{
|
||
var llm = _settings.Settings.Llm;
|
||
var service = llm.Service.ToLowerInvariant();
|
||
|
||
if (service is "ollama" or "vllm")
|
||
{
|
||
// ?깅줉 紐⑤뜽?먯꽌 蹂꾩묶 李얘린
|
||
var registered = llm.RegisteredModels
|
||
.FirstOrDefault(rm => rm.EncryptedModelName == llm.Model);
|
||
if (registered != null) return registered.Alias;
|
||
return string.IsNullOrEmpty(llm.Model) ? "(誘몄꽕??" : "?™™™?;
|
||
}
|
||
|
||
if (service == "gemini")
|
||
{
|
||
var m = GeminiModels.FirstOrDefault(g => g.Id == llm.Model);
|
||
return m.Label ?? llm.Model;
|
||
}
|
||
if (service == "claude")
|
||
{
|
||
var m = ClaudeModels.FirstOrDefault(c => c.Id == llm.Model);
|
||
return m.Label ?? llm.Model;
|
||
}
|
||
return string.IsNullOrEmpty(llm.Model) ? "(誘몄꽕??" : llm.Model;
|
||
}
|
||
|
||
private void UpdateModelLabel()
|
||
{
|
||
var service = _settings.Settings.Llm.Service.ToLowerInvariant();
|
||
var serviceLabel = service switch
|
||
{
|
||
"gemini" => "Gemini",
|
||
"claude" => "Claude",
|
||
"vllm" => "vLLM",
|
||
_ => "Ollama",
|
||
};
|
||
ModelLabel.Text = $"{serviceLabel} 쨌 {GetCurrentModelDisplayName()}";
|
||
}
|
||
|
||
private void BtnModelSelector_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var llm = _settings.Settings.Llm;
|
||
|
||
// ?앹뾽 ?닿린 ???먮낯 ?쒕퉬??紐⑤뜽 諛깆뾽 ??紐⑤뜽 誘몄꽑?????レ쑝硫?濡ㅻ갚
|
||
var originalService = llm.Service;
|
||
var originalModel = llm.Model;
|
||
var modelConfirmed = false;
|
||
|
||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255));
|
||
var checkColor = new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69));
|
||
var activeSvcBg = new SolidColorBrush(Color.FromArgb(0x18, 0x4B, 0x5E, 0xFC));
|
||
|
||
var popup = new Popup
|
||
{
|
||
StaysOpen = false,
|
||
AllowsTransparency = true,
|
||
PopupAnimation = PopupAnimation.Fade,
|
||
PlacementTarget = BtnModelSelector,
|
||
Placement = PlacementMode.Top,
|
||
};
|
||
|
||
var container = new Border
|
||
{
|
||
Background = bgBrush,
|
||
BorderBrush = borderBrush,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(6),
|
||
MinWidth = 240,
|
||
MaxHeight = 460,
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black
|
||
},
|
||
};
|
||
|
||
var scroll = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, MaxHeight = 440 };
|
||
var stack = new StackPanel();
|
||
|
||
// 紐⑤뜽 紐⑸줉???댁쓣 ?⑤꼸 (?쒕퉬??蹂寃????숈쟻 ?ш뎄??
|
||
var modelSection = new StackPanel();
|
||
|
||
Border CreateMenuItem(string text, bool isChecked, Action onClick, bool closeOnClick = true)
|
||
{
|
||
var item = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(8),
|
||
Padding = new Thickness(10, 7, 10, 7),
|
||
Margin = new Thickness(0, 1, 0, 1),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var g = new Grid();
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) });
|
||
|
||
var textTb = new TextBlock
|
||
{
|
||
Text = text, FontSize = 13, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
Grid.SetColumn(textTb, 0);
|
||
g.Children.Add(textTb);
|
||
|
||
if (isChecked)
|
||
{
|
||
var checkIcon = new Canvas { Width = 14, Height = 14, VerticalAlignment = VerticalAlignment.Center };
|
||
checkIcon.Children.Add(new System.Windows.Shapes.Path
|
||
{
|
||
Data = Geometry.Parse("M 2 7 L 5.5 10.5 L 12 4"),
|
||
Stroke = checkColor, StrokeThickness = 2,
|
||
StrokeStartLineCap = PenLineCap.Round, StrokeEndLineCap = PenLineCap.Round, StrokeLineJoin = PenLineJoin.Round,
|
||
});
|
||
Grid.SetColumn(checkIcon, 1);
|
||
g.Children.Add(checkIcon);
|
||
}
|
||
|
||
item.Child = g;
|
||
// ?몃쾭 ??諛곌꼍??+ ?댁쭩 ?뺣?
|
||
item.RenderTransformOrigin = new Point(0.5, 0.5);
|
||
item.RenderTransform = new ScaleTransform(1, 1);
|
||
item.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b) b.Background = hoverBg;
|
||
var st = item.RenderTransform as ScaleTransform;
|
||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||
};
|
||
item.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b) b.Background = Brushes.Transparent;
|
||
var st = item.RenderTransform as ScaleTransform;
|
||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||
};
|
||
item.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
if (closeOnClick) popup.IsOpen = false;
|
||
onClick();
|
||
};
|
||
return item;
|
||
}
|
||
|
||
Border CreateSeparator() => new()
|
||
{
|
||
Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4),
|
||
};
|
||
|
||
// 紐⑤뜽 紐⑸줉 鍮뚮뱶 ?⑥닔 (?쒕퉬??蹂寃????몄텧)
|
||
void RebuildModelList(string service)
|
||
{
|
||
modelSection.Children.Clear();
|
||
|
||
modelSection.Children.Add(new TextBlock
|
||
{
|
||
Text = "紐⑤뜽", FontSize = 11, Foreground = secondaryText,
|
||
Margin = new Thickness(10, 4, 0, 4), FontWeight = FontWeights.SemiBold,
|
||
});
|
||
|
||
if (service is "ollama" or "vllm")
|
||
{
|
||
var registered = llm.RegisteredModels.Where(rm => rm.Service == service).ToList();
|
||
if (registered.Count == 0)
|
||
{
|
||
modelSection.Children.Add(new TextBlock
|
||
{
|
||
Text = "?깅줉??紐⑤뜽 ?놁쓬 ???ㅼ젙?먯꽌 異붽?",
|
||
FontSize = 11.5, Foreground = secondaryText, FontStyle = FontStyles.Italic,
|
||
Margin = new Thickness(10, 4, 10, 4),
|
||
});
|
||
}
|
||
else
|
||
{
|
||
foreach (var rm in registered)
|
||
{
|
||
var capturedEnc = rm.EncryptedModelName;
|
||
modelSection.Children.Add(CreateMenuItem(rm.Alias, llm.Model == rm.EncryptedModelName, () =>
|
||
{
|
||
_settings.Settings.Llm.Model = capturedEnc;
|
||
_settings.Save();
|
||
modelConfirmed = true;
|
||
UpdateModelLabel();
|
||
}));
|
||
}
|
||
}
|
||
}
|
||
else if (service == "gemini")
|
||
{
|
||
foreach (var (id, label) in GeminiModels)
|
||
{
|
||
var capturedId = id;
|
||
modelSection.Children.Add(CreateMenuItem(label, llm.Model == id, () =>
|
||
{
|
||
_settings.Settings.Llm.Model = capturedId;
|
||
_settings.Save();
|
||
modelConfirmed = true;
|
||
UpdateModelLabel();
|
||
}));
|
||
}
|
||
}
|
||
else if (service == "claude")
|
||
{
|
||
foreach (var (id, label) in ClaudeModels)
|
||
{
|
||
var capturedId = id;
|
||
modelSection.Children.Add(CreateMenuItem(label, llm.Model == id, () =>
|
||
{
|
||
_settings.Settings.Llm.Model = capturedId;
|
||
_settings.Save();
|
||
modelConfirmed = true;
|
||
UpdateModelLabel();
|
||
}));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ?쒕퉬???ㅻ뜑
|
||
stack.Children.Add(new TextBlock
|
||
{
|
||
Text = "?쒕퉬??, FontSize = 11, Foreground = secondaryText,
|
||
Margin = new Thickness(10, 4, 0, 4), FontWeight = FontWeights.SemiBold,
|
||
});
|
||
|
||
// ?쒕퉬??踰꾪듉?ㅼ쓣 ?댁쓣 ?⑤꼸 (泥댄겕留덊겕 媛깆떊??
|
||
var serviceItems = new StackPanel();
|
||
var services = new[] { ("ollama", "Ollama"), ("vllm", "vLLM"), ("gemini", "Gemini"), ("claude", "Claude") };
|
||
|
||
void BuildServiceItems()
|
||
{
|
||
serviceItems.Children.Clear();
|
||
var currentSvc = llm.Service.ToLowerInvariant();
|
||
foreach (var (svc, label) in services)
|
||
{
|
||
var capturedSvc = svc;
|
||
serviceItems.Children.Add(CreateMenuItem(label, currentSvc == svc, () =>
|
||
{
|
||
// ?쒕퉬?ㅻ쭔 ?꾩떆 諛섏쁺 ??Save??紐⑤뜽 ?좏깮 ?쒖뿉留??몄텧
|
||
_settings.Settings.Llm.Service = capturedSvc;
|
||
UpdateModelLabel();
|
||
// ?쒕퉬??蹂寃???紐⑤뜽 紐⑸줉 + ?쒕퉬??泥댄겕留덊겕 媛깆떊 (?앹뾽? ?대┛ 梨꾨줈)
|
||
BuildServiceItems();
|
||
RebuildModelList(capturedSvc);
|
||
}, closeOnClick: false));
|
||
}
|
||
}
|
||
|
||
BuildServiceItems();
|
||
stack.Children.Add(serviceItems);
|
||
stack.Children.Add(CreateSeparator());
|
||
|
||
// 紐⑤뜽 紐⑸줉 珥덇린 鍮뚮뱶
|
||
RebuildModelList(llm.Service.ToLowerInvariant());
|
||
stack.Children.Add(modelSection);
|
||
|
||
// ?앹뾽???ロ옄 ??紐⑤뜽 誘몄꽑?앹씠硫??쒕퉬??紐⑤뜽 ?먮옒?濡?濡ㅻ갚
|
||
popup.Closed += (_, _) =>
|
||
{
|
||
if (!modelConfirmed)
|
||
{
|
||
_settings.Settings.Llm.Service = originalService;
|
||
_settings.Settings.Llm.Model = originalModel;
|
||
UpdateModelLabel();
|
||
}
|
||
};
|
||
|
||
scroll.Content = stack;
|
||
container.Child = scroll;
|
||
popup.Child = container;
|
||
popup.IsOpen = true;
|
||
}
|
||
|
||
private void BtnNewChat_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
StartNewConversation();
|
||
InputBox.Focus();
|
||
}
|
||
|
||
public void ResumeConversation(string conversationId)
|
||
{
|
||
var conv = _storage.Load(conversationId);
|
||
if (conv != null)
|
||
{
|
||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||
lock (_convLock) _currentConversation = conv;
|
||
_activeTab = conv.Tab ?? _activeTab;
|
||
_sessionState.RememberConversation(_activeTab, conv.Id);
|
||
ApplySessionTabSelection();
|
||
LoadConversationSettings();
|
||
UpdateChatTitle();
|
||
RefreshConversationList();
|
||
RenderMessages();
|
||
UpdateFolderBar();
|
||
}
|
||
InputBox.Focus();
|
||
}
|
||
|
||
public void StartNewAndFocus()
|
||
{
|
||
StartNewConversation();
|
||
InputBox.Focus();
|
||
}
|
||
|
||
private void BtnDeleteAll_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var result = CustomMessageBox.Show(
|
||
"??λ맂 紐⑤뱺 ????댁뿭????젣?섏떆寃좎뒿?덇퉴?\n???묒뾽? ?섎룎由????놁뒿?덈떎.",
|
||
"????꾩껜 ??젣",
|
||
MessageBoxButton.YesNo,
|
||
MessageBoxImage.Warning);
|
||
|
||
if (result != MessageBoxResult.Yes) return;
|
||
|
||
_storage.DeleteAll();
|
||
lock (_convLock) _sessionState.ClearCurrentConversation(_activeTab);
|
||
MessagePanel.Children.Clear();
|
||
EmptyState.Visibility = Visibility.Visible;
|
||
UpdateChatTitle();
|
||
RefreshConversationList();
|
||
}
|
||
|
||
// ??? 誘몃━蹂닿린 ?⑤꼸 (??湲곕컲) ?????????????????????????????????????????????
|
||
|
||
private static readonly HashSet<string> _previewableExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log",
|
||
};
|
||
|
||
/// <summary>?대젮 ?덈뒗 ?꾨━酉???紐⑸줉 (?뚯씪 寃쎈줈 湲곗?).</summary>
|
||
private readonly List<string> _previewTabs = new();
|
||
private string? _activePreviewTab;
|
||
|
||
private void TryShowPreview(string filePath)
|
||
{
|
||
if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath))
|
||
return;
|
||
|
||
// 蹂꾨룄 而ㅼ뒪? 李쎌쑝濡?誘몃━蹂닿린 (WebView2 HWND airspace 臾몄젣 洹쇰낯 ?닿껐)
|
||
PreviewWindow.ShowPreview(filePath, _selectedMood);
|
||
}
|
||
|
||
private void ShowPreviewPanel(string filePath)
|
||
{
|
||
// ??뿉 ?놁쑝硫?異붽?
|
||
if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase))
|
||
_previewTabs.Add(filePath);
|
||
|
||
_activePreviewTab = filePath;
|
||
|
||
// ?⑤꼸 ?닿린
|
||
if (PreviewColumn.Width.Value < 100)
|
||
{
|
||
PreviewColumn.Width = new GridLength(420);
|
||
SplitterColumn.Width = new GridLength(5);
|
||
}
|
||
PreviewPanel.Visibility = Visibility.Visible;
|
||
PreviewSplitter.Visibility = Visibility.Visible;
|
||
BtnPreviewToggle.Visibility = Visibility.Visible;
|
||
|
||
RebuildPreviewTabs();
|
||
LoadPreviewContent(filePath);
|
||
}
|
||
|
||
/// <summary>??諛?UI瑜??ㅼ떆 援ъ꽦?⑸땲??</summary>
|
||
private void RebuildPreviewTabs()
|
||
{
|
||
PreviewTabPanel.Children.Clear();
|
||
|
||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
|
||
foreach (var tabPath in _previewTabs)
|
||
{
|
||
var fileName = System.IO.Path.GetFileName(tabPath);
|
||
var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase);
|
||
|
||
var tabBorder = new Border
|
||
{
|
||
Background = isActive
|
||
? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF))
|
||
: Brushes.Transparent,
|
||
BorderBrush = isActive ? accentBrush : Brushes.Transparent,
|
||
BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0),
|
||
Padding = new Thickness(8, 6, 4, 6),
|
||
Cursor = Cursors.Hand,
|
||
MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100),
|
||
};
|
||
|
||
var tabContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||
|
||
// ?뚯씪紐? tabContent.Children.Add(new TextBlock
|
||
{
|
||
Text = fileName,
|
||
FontSize = 11,
|
||
Foreground = isActive ? primaryText : secondaryText,
|
||
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
MaxWidth = tabBorder.MaxWidth - 30,
|
||
ToolTip = tabPath,
|
||
});
|
||
|
||
// ?リ린 踰꾪듉 (x) ???쒖꽦 ??? ??긽 ?쒖떆, 鍮꾪솢????? ?몃쾭 ?쒖뿉留??쒖떆
|
||
var closeFg = isActive ? primaryText : secondaryText;
|
||
var closeBtnText = new TextBlock
|
||
{
|
||
Text = "\uE711",
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 10,
|
||
Foreground = closeFg,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
};
|
||
var closeBtn = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(3),
|
||
Padding = new Thickness(3, 2, 3, 2),
|
||
Margin = new Thickness(5, 0, 0, 0),
|
||
Cursor = Cursors.Hand,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
// 鍮꾪솢????? 珥덇린???④?, ?쒖꽦 ??? ??긽 ?쒖떆
|
||
Visibility = isActive ? Visibility.Visible : Visibility.Hidden,
|
||
Child = closeBtnText,
|
||
};
|
||
|
||
var closePath = tabPath;
|
||
closeBtn.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b)
|
||
{
|
||
b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50));
|
||
if (b.Child is TextBlock tb) tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60));
|
||
}
|
||
};
|
||
closeBtn.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b)
|
||
{
|
||
b.Background = Brushes.Transparent;
|
||
if (b.Child is TextBlock tb) tb.Foreground = closeFg;
|
||
}
|
||
};
|
||
closeBtn.Tag = "close"; // ?リ린 踰꾪듉 ?앸퀎?? closeBtn.MouseLeftButtonUp += (_, e) =>
|
||
{
|
||
e.Handled = true; // 遺紐????대┃ ?대깽??李⑤떒
|
||
ClosePreviewTab(closePath);
|
||
};
|
||
|
||
tabContent.Children.Add(closeBtn);
|
||
tabBorder.Child = tabContent;
|
||
|
||
// ???대┃ ???쒖꽦??(MouseLeftButtonUp ?ъ슜: ?リ린 踰꾪듉??PreviewMouseLeftButtonDown蹂대떎 ??쾶 ?ㅽ뻾?섏뼱 異⑸룎 諛⑹?)
|
||
var clickPath = tabPath;
|
||
tabBorder.MouseLeftButtonUp += (_, e) =>
|
||
{
|
||
if (e.Handled) return;
|
||
e.Handled = true;
|
||
_activePreviewTab = clickPath;
|
||
RebuildPreviewTabs();
|
||
LoadPreviewContent(clickPath);
|
||
};
|
||
|
||
// ?고겢由???而⑦뀓?ㅽ듃 硫붾돱
|
||
var ctxPath = tabPath;
|
||
tabBorder.MouseRightButtonUp += (_, e) =>
|
||
{
|
||
e.Handled = true;
|
||
ShowPreviewTabContextMenu(ctxPath);
|
||
};
|
||
|
||
// ?붾툝?대┃ ??蹂꾨룄 李쎌뿉??蹂닿린
|
||
var dblPath = tabPath;
|
||
tabBorder.MouseLeftButtonDown += (_, e) =>
|
||
{
|
||
if (e.Handled) return;
|
||
if (e.ClickCount == 2)
|
||
{
|
||
e.Handled = true;
|
||
OpenPreviewPopupWindow(dblPath);
|
||
}
|
||
};
|
||
|
||
// ?몃쾭 ?④낵 ??鍮꾪솢????뿉??諛곌꼍 媛뺤“ + ?リ린 踰꾪듉 ?쒖떆
|
||
var capturedIsActive = isActive;
|
||
var capturedCloseBtn = closeBtn;
|
||
tabBorder.MouseEnter += (s, _) =>
|
||
{
|
||
if (s is Border b && !capturedIsActive)
|
||
b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
||
// 鍮꾪솢????룄 ?몃쾭 ???リ린 踰꾪듉 ?쒖떆
|
||
if (!capturedIsActive)
|
||
capturedCloseBtn.Visibility = Visibility.Visible;
|
||
};
|
||
tabBorder.MouseLeave += (s, _) =>
|
||
{
|
||
if (s is Border b && !capturedIsActive)
|
||
b.Background = Brushes.Transparent;
|
||
// 鍮꾪솢?????몃쾭 ?댁젣 ???リ린 踰꾪듉 ?④?
|
||
if (!capturedIsActive)
|
||
capturedCloseBtn.Visibility = Visibility.Hidden;
|
||
};
|
||
|
||
PreviewTabPanel.Children.Add(tabBorder);
|
||
|
||
// ???ъ씠 援щ텇?? if (tabPath != _previewTabs[^1])
|
||
{
|
||
PreviewTabPanel.Children.Add(new Border
|
||
{
|
||
Width = 1, Height = 14,
|
||
Background = borderBrush,
|
||
Margin = new Thickness(0, 4, 0, 4),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ClosePreviewTab(string filePath)
|
||
{
|
||
_previewTabs.Remove(filePath);
|
||
|
||
if (_previewTabs.Count == 0)
|
||
{
|
||
HidePreviewPanel();
|
||
return;
|
||
}
|
||
|
||
// ?ロ엺 ??씠 ?쒖꽦 ??씠硫?留덉?留???쑝濡??꾪솚
|
||
if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_activePreviewTab = _previewTabs[^1];
|
||
LoadPreviewContent(_activePreviewTab);
|
||
}
|
||
|
||
RebuildPreviewTabs();
|
||
}
|
||
|
||
private async void LoadPreviewContent(string filePath)
|
||
{
|
||
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
||
|
||
// 紐⑤뱺 肄섑뀗痢??④린湲? PreviewWebView.Visibility = Visibility.Collapsed;
|
||
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||
PreviewEmpty.Visibility = Visibility.Collapsed;
|
||
|
||
if (!System.IO.File.Exists(filePath))
|
||
{
|
||
PreviewEmpty.Text = "?뚯씪??李얠쓣 ???놁뒿?덈떎";
|
||
PreviewEmpty.Visibility = Visibility.Visible;
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
switch (ext)
|
||
{
|
||
case ".html":
|
||
case ".htm":
|
||
await EnsureWebViewInitializedAsync();
|
||
PreviewWebView.Source = new Uri(filePath);
|
||
PreviewWebView.Visibility = Visibility.Visible;
|
||
break;
|
||
|
||
case ".csv":
|
||
LoadCsvPreview(filePath);
|
||
PreviewDataGrid.Visibility = Visibility.Visible;
|
||
break;
|
||
|
||
case ".md":
|
||
await EnsureWebViewInitializedAsync();
|
||
var mdText = System.IO.File.ReadAllText(filePath);
|
||
if (mdText.Length > 50000) mdText = mdText[..50000];
|
||
var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood);
|
||
PreviewWebView.NavigateToString(mdHtml);
|
||
PreviewWebView.Visibility = Visibility.Visible;
|
||
break;
|
||
|
||
case ".txt":
|
||
case ".json":
|
||
case ".xml":
|
||
case ".log":
|
||
var text = System.IO.File.ReadAllText(filePath);
|
||
if (text.Length > 50000) text = text[..50000] + "\n\n... (?댄썑 ?앸왂)";
|
||
PreviewTextBlock.Text = text;
|
||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||
break;
|
||
|
||
default:
|
||
PreviewEmpty.Text = "誘몃━蹂닿린?????녿뒗 ?뚯씪 ?뺤떇?낅땲??;
|
||
PreviewEmpty.Visibility = Visibility.Visible;
|
||
break;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
PreviewTextBlock.Text = $"誘몃━蹂닿린 ?ㅻ쪟: {ex.Message}";
|
||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||
}
|
||
}
|
||
|
||
private bool _webViewInitialized;
|
||
private static readonly string WebView2DataFolder =
|
||
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||
"AxCopilot", "WebView2");
|
||
|
||
private async Task EnsureWebViewInitializedAsync()
|
||
{
|
||
if (_webViewInitialized) return;
|
||
try
|
||
{
|
||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||
userDataFolder: WebView2DataFolder);
|
||
await PreviewWebView.EnsureCoreWebView2Async(env);
|
||
_webViewInitialized = true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Services.LogService.Warn($"WebView2 珥덇린???ㅽ뙣: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void LoadCsvPreview(string filePath)
|
||
{
|
||
try
|
||
{
|
||
var lines = System.IO.File.ReadAllLines(filePath);
|
||
if (lines.Length == 0) return;
|
||
|
||
var dt = new System.Data.DataTable();
|
||
var headers = ParseCsvLine(lines[0]);
|
||
foreach (var h in headers)
|
||
dt.Columns.Add(h);
|
||
|
||
var maxRows = Math.Min(lines.Length, 501);
|
||
for (int i = 1; i < maxRows; i++)
|
||
{
|
||
var vals = ParseCsvLine(lines[i]);
|
||
var row = dt.NewRow();
|
||
for (int j = 0; j < Math.Min(vals.Length, headers.Length); j++)
|
||
row[j] = vals[j];
|
||
dt.Rows.Add(row);
|
||
}
|
||
|
||
PreviewDataGrid.ItemsSource = dt.DefaultView;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
PreviewTextBlock.Text = $"CSV 濡쒕뱶 ?ㅻ쪟: {ex.Message}";
|
||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||
}
|
||
}
|
||
|
||
private static string[] ParseCsvLine(string line)
|
||
{
|
||
var fields = new System.Collections.Generic.List<string>();
|
||
var current = new System.Text.StringBuilder();
|
||
bool inQuotes = false;
|
||
|
||
for (int i = 0; i < line.Length; i++)
|
||
{
|
||
char c = line[i];
|
||
if (inQuotes)
|
||
{
|
||
if (c == '"' && i + 1 < line.Length && line[i + 1] == '"')
|
||
{
|
||
current.Append('"');
|
||
i++;
|
||
}
|
||
else if (c == '"')
|
||
inQuotes = false;
|
||
else
|
||
current.Append(c);
|
||
}
|
||
else
|
||
{
|
||
if (c == '"')
|
||
inQuotes = true;
|
||
else if (c == ',')
|
||
{
|
||
fields.Add(current.ToString());
|
||
current.Clear();
|
||
}
|
||
else
|
||
current.Append(c);
|
||
}
|
||
}
|
||
fields.Add(current.ToString());
|
||
return fields.ToArray();
|
||
}
|
||
|
||
private void HidePreviewPanel()
|
||
{
|
||
_previewTabs.Clear();
|
||
_activePreviewTab = null;
|
||
PreviewColumn.Width = new GridLength(0);
|
||
SplitterColumn.Width = new GridLength(0);
|
||
PreviewPanel.Visibility = Visibility.Collapsed;
|
||
PreviewSplitter.Visibility = Visibility.Collapsed;
|
||
PreviewWebView.Visibility = Visibility.Collapsed;
|
||
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||
try { if (_webViewInitialized) PreviewWebView.CoreWebView2?.NavigateToString("<html></html>"); } catch { }
|
||
}
|
||
|
||
/// <summary>?꾨━酉???諛??대┃ ??WebView2?먯꽌 ?ъ빱?ㅻ? ?뚯닔 (HWND airspace 臾몄젣 諛⑹?).</summary>
|
||
private void PreviewTabBar_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
// WebView2媛 ?ъ빱?ㅻ? ?↔퀬 ?덉쑝硫?WPF 踰꾪듉 ?대┃??臾댁떆?????덉쑝誘濡??ъ빱?ㅻ? 媛뺤젣 ?대룞
|
||
if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin)
|
||
{
|
||
var border = sender as Border;
|
||
border?.Focus();
|
||
}
|
||
}
|
||
|
||
private void BtnClosePreview_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
HidePreviewPanel();
|
||
BtnPreviewToggle.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (PreviewPanel.Visibility == Visibility.Visible)
|
||
{
|
||
// ?④린湲?(??? ?좎?)
|
||
PreviewPanel.Visibility = Visibility.Collapsed;
|
||
PreviewSplitter.Visibility = Visibility.Collapsed;
|
||
PreviewColumn.Width = new GridLength(0);
|
||
SplitterColumn.Width = new GridLength(0);
|
||
}
|
||
else if (_previewTabs.Count > 0)
|
||
{
|
||
// ?ㅼ떆 ?닿린
|
||
PreviewPanel.Visibility = Visibility.Visible;
|
||
PreviewSplitter.Visibility = Visibility.Visible;
|
||
PreviewColumn.Width = new GridLength(420);
|
||
SplitterColumn.Width = new GridLength(5);
|
||
RebuildPreviewTabs();
|
||
if (_activePreviewTab != null) LoadPreviewContent(_activePreviewTab);
|
||
}
|
||
}
|
||
|
||
private void BtnOpenExternal_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (string.IsNullOrEmpty(_activePreviewTab) || !System.IO.File.Exists(_activePreviewTab))
|
||
return;
|
||
|
||
try
|
||
{
|
||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||
{
|
||
FileName = _activePreviewTab,
|
||
UseShellExecute = true,
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"?몃? ?꾨줈洹몃옩 ?ㅽ뻾 ?ㅻ쪟: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>?꾨━酉????고겢由?而⑦뀓?ㅽ듃 硫붾돱瑜??쒖떆?⑸땲??</summary>
|
||
private Popup? _previewTabPopup;
|
||
|
||
private void ShowPreviewTabContextMenu(string filePath)
|
||
{
|
||
// 湲곗〈 ?앹뾽 ?リ린
|
||
if (_previewTabPopup != null) _previewTabPopup.IsOpen = false;
|
||
|
||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||
|
||
var stack = new StackPanel();
|
||
|
||
void AddItem(string icon, string iconColor, string label, Action action)
|
||
{
|
||
var itemBorder = new Border
|
||
{
|
||
Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(6),
|
||
Padding = new Thickness(10, 7, 16, 7),
|
||
Cursor = Cursors.Hand,
|
||
};
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 12, Foreground = string.IsNullOrEmpty(iconColor)
|
||
? secondaryText
|
||
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)),
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = label, FontSize = 13, Foreground = primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
itemBorder.Child = sp;
|
||
itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||
itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
itemBorder.MouseLeftButtonUp += (_, _) =>
|
||
{
|
||
_previewTabPopup!.IsOpen = false;
|
||
action();
|
||
};
|
||
stack.Children.Add(itemBorder);
|
||
}
|
||
|
||
void AddSeparator()
|
||
{
|
||
stack.Children.Add(new Border
|
||
{
|
||
Height = 1,
|
||
Background = borderBrush,
|
||
Margin = new Thickness(8, 3, 8, 3),
|
||
});
|
||
}
|
||
|
||
AddItem("\uE8A7", "#64B5F6", "?몃? ?꾨줈洹몃옩?쇰줈 ?닿린", () =>
|
||
{
|
||
try
|
||
{
|
||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||
{
|
||
FileName = filePath, UseShellExecute = true,
|
||
});
|
||
}
|
||
catch { }
|
||
});
|
||
|
||
AddItem("\uE838", "#FFB74D", "?뚯씪 ?꾩튂 ?닿린", () =>
|
||
{
|
||
try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); }
|
||
catch { }
|
||
});
|
||
|
||
AddItem("\uE8A7", "#81C784", "蹂꾨룄 李쎌뿉??蹂닿린", () => OpenPreviewPopupWindow(filePath));
|
||
|
||
AddSeparator();
|
||
|
||
AddItem("\uE8C8", "", "寃쎈줈 蹂듭궗", () =>
|
||
{
|
||
try { Clipboard.SetText(filePath); } catch { }
|
||
});
|
||
|
||
AddSeparator();
|
||
|
||
AddItem("\uE711", "#EF5350", "?????リ린", () => ClosePreviewTab(filePath));
|
||
|
||
if (_previewTabs.Count > 1)
|
||
{
|
||
AddItem("\uE8BB", "#EF5350", "?ㅻⅨ ??紐⑤몢 ?リ린", () =>
|
||
{
|
||
var keep = filePath;
|
||
_previewTabs.RemoveAll(p => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase));
|
||
_activePreviewTab = keep;
|
||
RebuildPreviewTabs();
|
||
LoadPreviewContent(keep);
|
||
});
|
||
}
|
||
|
||
var popupBorder = new Border
|
||
{
|
||
Background = bg,
|
||
BorderBrush = borderBrush,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(12),
|
||
Padding = new Thickness(4, 6, 4, 6),
|
||
MinWidth = 180,
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
BlurRadius = 16, Opacity = 0.4, ShadowDepth = 4,
|
||
Color = Colors.Black,
|
||
},
|
||
Child = stack,
|
||
};
|
||
|
||
_previewTabPopup = new Popup
|
||
{
|
||
Child = popupBorder,
|
||
Placement = PlacementMode.MousePoint,
|
||
StaysOpen = false,
|
||
AllowsTransparency = true,
|
||
PopupAnimation = PopupAnimation.Fade,
|
||
};
|
||
_previewTabPopup.IsOpen = true;
|
||
}
|
||
|
||
/// <summary>?꾨━酉곕? 蹂꾨룄 ?앹뾽 李쎌뿉???쎈땲??</summary>
|
||
private void OpenPreviewPopupWindow(string filePath)
|
||
{
|
||
if (!System.IO.File.Exists(filePath)) return;
|
||
|
||
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
||
var fileName = System.IO.Path.GetFileName(filePath);
|
||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
|
||
var win = new Window
|
||
{
|
||
Title = $"誘몃━蹂닿린 ??{fileName}",
|
||
Width = 900,
|
||
Height = 700,
|
||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||
Background = bg,
|
||
};
|
||
|
||
FrameworkElement content;
|
||
|
||
switch (ext)
|
||
{
|
||
case ".html":
|
||
case ".htm":
|
||
var wv = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||
wv.Loaded += async (_, _) =>
|
||
{
|
||
try
|
||
{
|
||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||
userDataFolder: WebView2DataFolder);
|
||
await wv.EnsureCoreWebView2Async(env);
|
||
wv.Source = new Uri(filePath);
|
||
}
|
||
catch { }
|
||
};
|
||
content = wv;
|
||
break;
|
||
|
||
case ".md":
|
||
var mdWv = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||
var mdMood = _selectedMood;
|
||
mdWv.Loaded += async (_, _) =>
|
||
{
|
||
try
|
||
{
|
||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||
userDataFolder: WebView2DataFolder);
|
||
await mdWv.EnsureCoreWebView2Async(env);
|
||
var mdSrc = System.IO.File.ReadAllText(filePath);
|
||
if (mdSrc.Length > 100000) mdSrc = mdSrc[..100000];
|
||
var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood);
|
||
mdWv.NavigateToString(html);
|
||
}
|
||
catch { }
|
||
};
|
||
content = mdWv;
|
||
break;
|
||
|
||
case ".csv":
|
||
var dg = new System.Windows.Controls.DataGrid
|
||
{
|
||
AutoGenerateColumns = true,
|
||
IsReadOnly = true,
|
||
Background = Brushes.Transparent,
|
||
Foreground = Brushes.White,
|
||
BorderThickness = new Thickness(0),
|
||
FontSize = 12,
|
||
};
|
||
try
|
||
{
|
||
var lines = System.IO.File.ReadAllLines(filePath);
|
||
if (lines.Length > 0)
|
||
{
|
||
var dt = new System.Data.DataTable();
|
||
var headers = ParseCsvLine(lines[0]);
|
||
foreach (var h in headers) dt.Columns.Add(h);
|
||
for (int i = 1; i < Math.Min(lines.Length, 1001); i++)
|
||
{
|
||
var vals = ParseCsvLine(lines[i]);
|
||
var row = dt.NewRow();
|
||
for (int j = 0; j < Math.Min(vals.Length, dt.Columns.Count); j++)
|
||
row[j] = vals[j];
|
||
dt.Rows.Add(row);
|
||
}
|
||
dg.ItemsSource = dt.DefaultView;
|
||
}
|
||
}
|
||
catch { }
|
||
content = dg;
|
||
break;
|
||
|
||
default:
|
||
var text = System.IO.File.ReadAllText(filePath);
|
||
if (text.Length > 100000) text = text[..100000] + "\n\n... (?댄썑 ?앸왂)";
|
||
var sv = new ScrollViewer
|
||
{
|
||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||
Padding = new Thickness(20),
|
||
Content = new TextBlock
|
||
{
|
||
Text = text,
|
||
TextWrapping = TextWrapping.Wrap,
|
||
FontFamily = new FontFamily("Consolas"),
|
||
FontSize = 13,
|
||
Foreground = fg,
|
||
},
|
||
};
|
||
content = sv;
|
||
break;
|
||
}
|
||
|
||
win.Content = content;
|
||
win.Show();
|
||
}
|
||
|
||
// ??? ?먯씠?꾪듃 ?ㅽ떚??吏꾪뻾瑜?諛???????????????????????????????????????????
|
||
|
||
private DateTime _progressStartTime;
|
||
private DispatcherTimer? _progressElapsedTimer;
|
||
|
||
private void UpdateAgentProgressBar(AgentEvent evt)
|
||
{
|
||
switch (evt.Type)
|
||
{
|
||
case AgentEventType.Planning when evt.Steps is { Count: > 0 }:
|
||
ShowStickyProgress(evt.Steps.Count);
|
||
break;
|
||
|
||
case AgentEventType.StepStart when evt.StepTotal > 0:
|
||
UpdateStickyProgress(evt.StepCurrent, evt.StepTotal, evt.Summary);
|
||
break;
|
||
|
||
case AgentEventType.Complete:
|
||
HideStickyProgress();
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void ShowStickyProgress(int totalSteps)
|
||
{
|
||
_progressStartTime = DateTime.Now;
|
||
AgentProgressBar.Visibility = Visibility.Visible;
|
||
ProgressIcon.Text = "\uE768"; // play
|
||
ProgressStepLabel.Text = $"?묒뾽 以鍮?以?.. (0/{totalSteps})";
|
||
ProgressPercent.Text = "0%";
|
||
ProgressElapsed.Text = "0:00";
|
||
ProgressFill.Width = 0;
|
||
|
||
// 寃쎄낵 ?쒓컙 ??대㉧
|
||
_progressElapsedTimer?.Stop();
|
||
_progressElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||
_progressElapsedTimer.Tick += (_, _) =>
|
||
{
|
||
var elapsed = DateTime.Now - _progressStartTime;
|
||
ProgressElapsed.Text = elapsed.TotalHours >= 1
|
||
? elapsed.ToString(@"h\:mm\:ss")
|
||
: elapsed.ToString(@"m\:ss");
|
||
};
|
||
_progressElapsedTimer.Start();
|
||
}
|
||
|
||
private void UpdateStickyProgress(int currentStep, int totalSteps, string stepDescription)
|
||
{
|
||
if (AgentProgressBar.Visibility != Visibility.Visible) return;
|
||
|
||
var pct = totalSteps > 0 ? (double)currentStep / totalSteps : 0;
|
||
ProgressStepLabel.Text = $"{stepDescription} ({currentStep}/{totalSteps})";
|
||
ProgressPercent.Text = $"{(int)(pct * 100)}%";
|
||
|
||
// ?꾨줈洹몃젅??諛??덈퉬 ?좊땲硫붿씠?? var parentBorder = ProgressFill.Parent as Border;
|
||
if (parentBorder != null)
|
||
{
|
||
var targetWidth = parentBorder.ActualWidth * pct;
|
||
var anim = new System.Windows.Media.Animation.DoubleAnimation(
|
||
ProgressFill.Width, targetWidth, TimeSpan.FromMilliseconds(300))
|
||
{
|
||
EasingFunction = new System.Windows.Media.Animation.QuadraticEase(),
|
||
};
|
||
ProgressFill.BeginAnimation(WidthProperty, anim);
|
||
}
|
||
}
|
||
|
||
private void HideStickyProgress()
|
||
{
|
||
_progressElapsedTimer?.Stop();
|
||
_progressElapsedTimer = null;
|
||
|
||
if (AgentProgressBar.Visibility != Visibility.Visible) return;
|
||
|
||
// ?꾨즺 ?쒖떆 ???섏씠?쒖븘?? ProgressIcon.Text = "\uE930"; // check
|
||
ProgressStepLabel.Text = "?묒뾽 ?꾨즺";
|
||
ProgressPercent.Text = "100%";
|
||
|
||
// ?꾨줈洹몃젅??諛?100%
|
||
var parentBorder = ProgressFill.Parent as Border;
|
||
if (parentBorder != null)
|
||
{
|
||
var anim = new System.Windows.Media.Animation.DoubleAnimation(
|
||
ProgressFill.Width, parentBorder.ActualWidth, TimeSpan.FromMilliseconds(200));
|
||
ProgressFill.BeginAnimation(WidthProperty, anim);
|
||
}
|
||
|
||
// 3珥????④린湲? var hideTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
|
||
hideTimer.Tick += (_, _) =>
|
||
{
|
||
hideTimer.Stop();
|
||
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
|
||
fadeOut.Completed += (_, _) =>
|
||
{
|
||
AgentProgressBar.Visibility = Visibility.Collapsed;
|
||
AgentProgressBar.Opacity = 1;
|
||
ProgressFill.BeginAnimation(WidthProperty, null);
|
||
ProgressFill.Width = 0;
|
||
};
|
||
AgentProgressBar.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
||
};
|
||
hideTimer.Start();
|
||
}
|
||
|
||
// ??? ?뚯씪 ?먯깋湲???????????????????????????????????????????????????????
|
||
|
||
private static readonly HashSet<string> _ignoredDirs = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
"bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode",
|
||
"__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build",
|
||
".cache", ".next", ".nuxt", "coverage", ".terraform",
|
||
};
|
||
|
||
private DispatcherTimer? _fileBrowserRefreshTimer;
|
||
|
||
private void ToggleFileBrowser()
|
||
{
|
||
if (FileBrowserPanel.Visibility == Visibility.Visible)
|
||
{
|
||
FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||
_settings.Settings.Llm.ShowFileBrowser = false;
|
||
}
|
||
else
|
||
{
|
||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||
_settings.Settings.Llm.ShowFileBrowser = true;
|
||
BuildFileTree();
|
||
}
|
||
_settings.Save();
|
||
}
|
||
|
||
private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) => BuildFileTree();
|
||
|
||
private void BtnFileBrowserOpenFolder_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var folder = GetCurrentWorkFolder();
|
||
if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) return;
|
||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = folder, UseShellExecute = true }); } catch { }
|
||
}
|
||
|
||
private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
private void BuildFileTree()
|
||
{
|
||
FileTreeView.Items.Clear();
|
||
var folder = GetCurrentWorkFolder();
|
||
if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder))
|
||
{
|
||
FileTreeView.Items.Add(new TreeViewItem { Header = "?묒뾽 ?대뜑瑜??좏깮?섏꽭??, IsEnabled = false });
|
||
return;
|
||
}
|
||
|
||
FileBrowserTitle.Text = $"?뚯씪 ?먯깋湲???{System.IO.Path.GetFileName(folder)}";
|
||
var count = 0;
|
||
PopulateDirectory(new System.IO.DirectoryInfo(folder), FileTreeView.Items, 0, ref count);
|
||
}
|
||
|
||
private void PopulateDirectory(System.IO.DirectoryInfo dir, ItemCollection items, int depth, ref int count)
|
||
{
|
||
if (depth > 4 || count > 200) return;
|
||
|
||
// ?붾젆?곕━
|
||
try
|
||
{
|
||
foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name))
|
||
{
|
||
if (count > 200) break;
|
||
if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.')) continue;
|
||
|
||
count++;
|
||
var dirItem = new TreeViewItem
|
||
{
|
||
Header = CreateFileTreeHeader("\uED25", subDir.Name, null),
|
||
Tag = subDir.FullName,
|
||
IsExpanded = depth < 1,
|
||
};
|
||
|
||
// 吏??濡쒕뵫: ?붾? ?먯떇 ???쇱튌 ???ㅼ젣 濡쒕뱶
|
||
if (depth < 3)
|
||
{
|
||
dirItem.Items.Add(new TreeViewItem { Header = "濡쒕뵫 以?.." }); // ?붾?
|
||
var capturedDir = subDir;
|
||
var capturedDepth = depth;
|
||
dirItem.Expanded += (s, _) =>
|
||
{
|
||
if (s is TreeViewItem ti && ti.Items.Count == 1 && ti.Items[0] is TreeViewItem d && d.Header?.ToString() == "濡쒕뵫 以?..")
|
||
{
|
||
ti.Items.Clear();
|
||
int c = 0;
|
||
PopulateDirectory(capturedDir, ti.Items, capturedDepth + 1, ref c);
|
||
}
|
||
};
|
||
}
|
||
else
|
||
{
|
||
PopulateDirectory(subDir, dirItem.Items, depth + 1, ref count);
|
||
}
|
||
|
||
items.Add(dirItem);
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
// ?뚯씪
|
||
try
|
||
{
|
||
foreach (var file in dir.GetFiles().OrderBy(f => f.Name))
|
||
{
|
||
if (count > 200) break;
|
||
count++;
|
||
|
||
var ext = file.Extension.ToLowerInvariant();
|
||
var icon = GetFileIcon(ext);
|
||
var size = FormatFileSize(file.Length);
|
||
|
||
var fileItem = new TreeViewItem
|
||
{
|
||
Header = CreateFileTreeHeader(icon, file.Name, size),
|
||
Tag = file.FullName,
|
||
};
|
||
|
||
// ?붾툝?대┃ ???꾨━酉? var capturedPath = file.FullName;
|
||
fileItem.MouseDoubleClick += (s, e) =>
|
||
{
|
||
e.Handled = true;
|
||
TryShowPreview(capturedPath);
|
||
};
|
||
|
||
// ?고겢由???而⑦뀓?ㅽ듃 硫붾돱 (MouseRightButtonUp?먯꽌 ?댁뼱??Popup??諛붾줈 ?ロ엳吏 ?딆쓬)
|
||
fileItem.MouseRightButtonUp += (s, e) =>
|
||
{
|
||
e.Handled = true;
|
||
if (s is TreeViewItem ti) ti.IsSelected = true;
|
||
ShowFileTreeContextMenu(capturedPath);
|
||
};
|
||
|
||
items.Add(fileItem);
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private static StackPanel CreateFileTreeHeader(string icon, string name, string? sizeText)
|
||
{
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = icon,
|
||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 11,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(0, 0, 5, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = name,
|
||
FontSize = 11.5,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
if (sizeText != null)
|
||
{
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = $" {sizeText}",
|
||
FontSize = 10,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80)),
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
}
|
||
return sp;
|
||
}
|
||
|
||
private static string GetFileIcon(string ext) => ext switch
|
||
{
|
||
".html" or ".htm" => "\uEB41",
|
||
".xlsx" or ".xls" => "\uE9F9",
|
||
".docx" or ".doc" => "\uE8A5",
|
||
".pdf" => "\uEA90",
|
||
".csv" => "\uE80A",
|
||
".md" => "\uE70B",
|
||
".json" or ".xml" => "\uE943",
|
||
".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".webp" => "\uEB9F",
|
||
".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" => "\uE943",
|
||
".bat" or ".cmd" or ".ps1" or ".sh" => "\uE756",
|
||
".txt" or ".log" => "\uE8A5",
|
||
_ => "\uE7C3",
|
||
};
|
||
|
||
private static string FormatFileSize(long bytes) => bytes switch
|
||
{
|
||
< 1024 => $"{bytes} B",
|
||
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
|
||
_ => $"{bytes / (1024.0 * 1024.0):F1} MB",
|
||
};
|
||
|
||
private void ShowFileTreeContextMenu(string filePath)
|
||
{
|
||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||
var hoverBg = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||
var dangerBrush = new SolidColorBrush(Color.FromRgb(0xE7, 0x4C, 0x3C));
|
||
|
||
var popup = new Popup
|
||
{
|
||
StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade,
|
||
Placement = PlacementMode.MousePoint,
|
||
};
|
||
var panel = new StackPanel { Margin = new Thickness(2) };
|
||
var container = new Border
|
||
{
|
||
Background = bg, BorderBrush = borderBrush, BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(10), Padding = new Thickness(6), MinWidth = 200,
|
||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||
{
|
||
BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
|
||
Color = Colors.Black, Direction = 270,
|
||
},
|
||
Child = panel,
|
||
};
|
||
popup.Child = container;
|
||
|
||
void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null)
|
||
{
|
||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||
FontSize = 13, Foreground = iconColor ?? secondaryText,
|
||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||
});
|
||
sp.Children.Add(new TextBlock
|
||
{
|
||
Text = label, FontSize = 12.5, Foreground = labelColor ?? primaryText,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
var item = new Border
|
||
{
|
||
Child = sp, Background = Brushes.Transparent,
|
||
CornerRadius = new CornerRadius(7), Cursor = Cursors.Hand,
|
||
Padding = new Thickness(10, 8, 14, 8), Margin = new Thickness(0, 1, 0, 1),
|
||
};
|
||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||
item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; action(); };
|
||
panel.Children.Add(item);
|
||
}
|
||
|
||
void AddSep()
|
||
{
|
||
panel.Children.Add(new Border
|
||
{
|
||
Height = 1, Margin = new Thickness(10, 4, 10, 4),
|
||
Background = borderBrush, Opacity = 0.3,
|
||
});
|
||
}
|
||
|
||
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
||
if (_previewableExtensions.Contains(ext))
|
||
AddItem("\uE8A1", "誘몃━蹂닿린", () => ShowPreviewPanel(filePath));
|
||
|
||
AddItem("\uE8A7", "?몃? ?꾨줈洹몃옩?쇰줈 ?닿린", () =>
|
||
{
|
||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filePath, UseShellExecute = true }); } catch { }
|
||
});
|
||
AddItem("\uED25", "?대뜑?먯꽌 蹂닿린", () =>
|
||
{
|
||
try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); } catch { }
|
||
});
|
||
AddItem("\uE8C8", "寃쎈줈 蹂듭궗", () =>
|
||
{
|
||
try { Clipboard.SetText(filePath); ShowToast("寃쎈줈 蹂듭궗??); } catch { }
|
||
});
|
||
|
||
AddSep();
|
||
|
||
// ?대쫫 蹂寃? AddItem("\uE8AC", "?대쫫 蹂寃?, () =>
|
||
{
|
||
var dir = System.IO.Path.GetDirectoryName(filePath) ?? "";
|
||
var oldName = System.IO.Path.GetFileName(filePath);
|
||
var dlg = new Views.InputDialog("?대쫫 蹂寃?, "???뚯씪 ?대쫫:", oldName) { Owner = this };
|
||
if (dlg.ShowDialog() == true && !string.IsNullOrWhiteSpace(dlg.ResponseText))
|
||
{
|
||
var newPath = System.IO.Path.Combine(dir, dlg.ResponseText.Trim());
|
||
try
|
||
{
|
||
System.IO.File.Move(filePath, newPath);
|
||
BuildFileTree();
|
||
ShowToast($"?대쫫 蹂寃? {dlg.ResponseText.Trim()}");
|
||
}
|
||
catch (Exception ex) { ShowToast($"?대쫫 蹂寃??ㅽ뙣: {ex.Message}", "\uE783"); }
|
||
}
|
||
});
|
||
|
||
// ??젣
|
||
AddItem("\uE74D", "??젣", () =>
|
||
{
|
||
var result = MessageBox.Show(
|
||
$"?뚯씪????젣?섏떆寃좎뒿?덇퉴?\n{System.IO.Path.GetFileName(filePath)}",
|
||
"?뚯씪 ??젣 ?뺤씤", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||
if (result == MessageBoxResult.Yes)
|
||
{
|
||
try
|
||
{
|
||
System.IO.File.Delete(filePath);
|
||
BuildFileTree();
|
||
ShowToast("?뚯씪 ??젣??);
|
||
}
|
||
catch (Exception ex) { ShowToast($"??젣 ?ㅽ뙣: {ex.Message}", "\uE783"); }
|
||
}
|
||
}, dangerBrush, dangerBrush);
|
||
|
||
// Dispatcher濡??댁뼱??MouseRightButtonUp ??諛붾줈 ?ロ엳吏 ?딆쓬
|
||
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; },
|
||
System.Windows.Threading.DispatcherPriority.Input);
|
||
}
|
||
|
||
/// <summary>?먯씠?꾪듃媛 ?뚯씪 ?앹꽦 ???뚯씪 ?먯깋湲곕? ?먮룞 ?덈줈怨좎묠?⑸땲??</summary>
|
||
private void RefreshFileTreeIfVisible()
|
||
{
|
||
if (FileBrowserPanel.Visibility != Visibility.Visible) return;
|
||
|
||
// ?붾컮?댁뒪: 500ms ??以묐났 ?몄텧 諛⑹?
|
||
_fileBrowserRefreshTimer?.Stop();
|
||
_fileBrowserRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||
_fileBrowserRefreshTimer.Tick += (_, _) =>
|
||
{
|
||
_fileBrowserRefreshTimer.Stop();
|
||
BuildFileTree();
|
||
};
|
||
_fileBrowserRefreshTimer.Start();
|
||
}
|
||
|
||
// ??? ?섎떒 ?곹깭諛???????????????????????????????????????????????????????
|
||
|
||
private System.Windows.Media.Animation.Storyboard? _statusSpinStoryboard;
|
||
|
||
private void UpdateStatusBar(AgentEvent evt)
|
||
{
|
||
var toolLabel = evt.ToolName switch
|
||
{
|
||
"file_read" or "document_read" => "?뚯씪 ?쎄린",
|
||
"file_write" => "?뚯씪 ?곌린",
|
||
"file_edit" => "?뚯씪 ?섏젙",
|
||
"html_create" => "HTML ?앹꽦",
|
||
"xlsx_create" => "Excel ?앹꽦",
|
||
"docx_create" => "Word ?앹꽦",
|
||
"csv_create" => "CSV ?앹꽦",
|
||
"md_create" => "Markdown ?앹꽦",
|
||
"folder_map" => "?대뜑 ?먯깋",
|
||
"glob" => "?뚯씪 寃??,
|
||
"grep" => "?댁슜 寃??,
|
||
"process" => "紐낅졊 ?ㅽ뻾",
|
||
_ => evt.ToolName,
|
||
};
|
||
|
||
switch (evt.Type)
|
||
{
|
||
case AgentEventType.Thinking:
|
||
SetStatus("?앷컖 以?..", spinning: true);
|
||
break;
|
||
case AgentEventType.Planning:
|
||
SetStatus($"怨꾪쉷 ?섎┰ 以???{evt.StepTotal}?④퀎", spinning: true);
|
||
break;
|
||
case AgentEventType.ToolCall:
|
||
SetStatus($"{toolLabel} ?ㅽ뻾 以?..", spinning: true);
|
||
break;
|
||
case AgentEventType.ToolResult:
|
||
SetStatus(evt.Success ? $"{toolLabel} ?꾨즺" : $"{toolLabel} ?ㅽ뙣", spinning: false);
|
||
break;
|
||
case AgentEventType.StepStart:
|
||
SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] {TruncateForStatus(evt.Summary)}", spinning: true);
|
||
break;
|
||
case AgentEventType.StepDone:
|
||
SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] ?④퀎 ?꾨즺", spinning: true);
|
||
break;
|
||
case AgentEventType.SkillCall:
|
||
SetStatus($"?ㅽ궗 ?ㅽ뻾 以? {TruncateForStatus(evt.Summary)}", spinning: true);
|
||
break;
|
||
case AgentEventType.Complete:
|
||
SetStatus("?묒뾽 ?꾨즺", spinning: false);
|
||
StopStatusAnimation();
|
||
break;
|
||
case AgentEventType.Error:
|
||
SetStatus("?ㅻ쪟 諛쒖깮", spinning: false);
|
||
StopStatusAnimation();
|
||
break;
|
||
case AgentEventType.Paused:
|
||
SetStatus("???쇱떆?뺤?", spinning: false);
|
||
break;
|
||
case AgentEventType.Resumed:
|
||
SetStatus("???ш컻??, spinning: true);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void SetStatus(string text, bool spinning)
|
||
{
|
||
if (StatusLabel != null) StatusLabel.Text = text;
|
||
_sessionState.SetRuntimeState(_isStreaming, text, spinning);
|
||
if (spinning) StartStatusAnimation();
|
||
else StopStatusAnimation();
|
||
UpdateRuntimeActivityIndicators();
|
||
}
|
||
|
||
private void StartStatusAnimation()
|
||
{
|
||
if (_statusSpinStoryboard != null) return;
|
||
|
||
var anim = new System.Windows.Media.Animation.DoubleAnimation
|
||
{
|
||
From = 0, To = 360,
|
||
Duration = TimeSpan.FromSeconds(2),
|
||
RepeatBehavior = System.Windows.Media.Animation.RepeatBehavior.Forever,
|
||
};
|
||
|
||
_statusSpinStoryboard = new System.Windows.Media.Animation.Storyboard();
|
||
System.Windows.Media.Animation.Storyboard.SetTarget(anim, StatusDiamond);
|
||
System.Windows.Media.Animation.Storyboard.SetTargetProperty(anim,
|
||
new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
|
||
_statusSpinStoryboard.Children.Add(anim);
|
||
_statusSpinStoryboard.Begin();
|
||
}
|
||
|
||
private void StopStatusAnimation()
|
||
{
|
||
_statusSpinStoryboard?.Stop();
|
||
_statusSpinStoryboard = null;
|
||
}
|
||
|
||
private void SetStatusIdle()
|
||
{
|
||
StopStatusAnimation();
|
||
if (StatusLabel != null) StatusLabel.Text = "?湲?以?;
|
||
if (StatusElapsed != null) StatusElapsed.Text = "";
|
||
if (StatusTokens != null) StatusTokens.Text = "";
|
||
_sessionState.SetRuntimeState(false, "?湲?以?, false);
|
||
UpdateRuntimeActivityIndicators();
|
||
}
|
||
|
||
private void UpdateStatusTokens(int inputTokens, int outputTokens)
|
||
{
|
||
if (StatusTokens == null) return;
|
||
var llm = _settings.Settings.Llm;
|
||
var (inCost, outCost) = Services.TokenEstimator.EstimateCost(
|
||
inputTokens, outputTokens, llm.Service, llm.Model);
|
||
var totalCost = inCost + outCost;
|
||
var costText = totalCost > 0 ? $" 쨌 {Services.TokenEstimator.FormatCost(totalCost)}" : "";
|
||
StatusTokens.Text = $"??Services.TokenEstimator.Format(inputTokens)} ??Services.TokenEstimator.Format(outputTokens)}{costText}";
|
||
}
|
||
|
||
private static string TruncateForStatus(string? text, int max = 40)
|
||
{
|
||
if (string.IsNullOrEmpty(text)) return "";
|
||
return text.Length <= max ? text : text[..max] + "??;
|
||
}
|
||
|
||
// ??? ?ы띁 ?????????????????????????????????????????????????????????????
|
||
private static DraftQueueItem CloneDraftQueueItem(DraftQueueItem item) => new()
|
||
{
|
||
Id = string.IsNullOrWhiteSpace(item.Id) ? Guid.NewGuid().ToString("N") : item.Id,
|
||
Text = item.Text ?? "",
|
||
Priority = NormalizeDraftPriority(item.Priority),
|
||
State = NormalizeDraftState(item.State),
|
||
AttemptCount = item.AttemptCount,
|
||
LastError = item.LastError,
|
||
CreatedAt = item.CreatedAt == default ? DateTime.Now : item.CreatedAt,
|
||
};
|
||
|
||
private static string NormalizeDraftPriority(string? priority)
|
||
=> priority is "now" or "later" ? priority : "next";
|
||
|
||
private static string NormalizeDraftState(string? state)
|
||
=> state is "running" or "failed" ? state : "queued";
|
||
|
||
private static int GetDraftPriorityRank(string? priority)
|
||
=> NormalizeDraftPriority(priority) switch
|
||
{
|
||
"now" => 0,
|
||
"next" => 1,
|
||
"later" => 2,
|
||
_ => 1,
|
||
};
|
||
|
||
private List<DraftQueueItem> GetConversationDraftQueueItems(ChatConversation? conv)
|
||
{
|
||
if (conv?.DraftQueueItems?.Count > 0)
|
||
return conv.DraftQueueItems.Select(CloneDraftQueueItem).ToList();
|
||
|
||
if (conv?.DraftQueue?.Count > 0)
|
||
{
|
||
return conv.DraftQueue
|
||
.Where(text => !string.IsNullOrWhiteSpace(text))
|
||
.Select(text => new DraftQueueItem { Text = text.Trim() })
|
||
.ToList();
|
||
}
|
||
|
||
return new List<DraftQueueItem>();
|
||
}
|
||
|
||
private List<DraftQueueItem> GetDraftQueueState()
|
||
{
|
||
if (DraftQueuePanel?.Tag is List<DraftQueueItem> queue)
|
||
return queue;
|
||
|
||
ChatConversation? conv;
|
||
lock (_convLock) conv = _currentConversation;
|
||
queue = GetConversationDraftQueueItems(conv);
|
||
if (DraftQueuePanel != null)
|
||
DraftQueuePanel.Tag = queue;
|
||
return queue;
|
||
}
|
||
|
||
private void RefreshDraftComposerUi()
|
||
{
|
||
var queue = GetDraftQueueState();
|
||
var prepared = InputBox?.Text?.Trim() ?? "";
|
||
|
||
if (DraftPreviewCard != null)
|
||
DraftPreviewCard.Visibility = string.IsNullOrWhiteSpace(prepared) ? Visibility.Collapsed : Visibility.Visible;
|
||
|
||
if (DraftPreviewText != null)
|
||
DraftPreviewText.Text = string.IsNullOrWhiteSpace(prepared)
|
||
? ""
|
||
: (prepared.Length > 140 ? prepared[..140] + "..." : prepared);
|
||
|
||
if (DraftQueuePanel == null)
|
||
return;
|
||
|
||
DraftQueuePanel.Children.Clear();
|
||
DraftQueuePanel.Visibility = queue.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||
|
||
for (var i = 0; i < queue.Count; i++)
|
||
{
|
||
var index = i;
|
||
var queuedItem = queue[i];
|
||
var queuedText = queuedItem.Text ?? "";
|
||
var card = new Border
|
||
{
|
||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
|
||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
||
BorderThickness = new Thickness(1),
|
||
CornerRadius = new CornerRadius(14),
|
||
Padding = new Thickness(12, 8, 12, 8),
|
||
Margin = new Thickness(0, 0, 0, 6),
|
||
};
|
||
|
||
var row = new Grid();
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
|
||
var previewStack = new StackPanel { Orientation = Orientation.Vertical };
|
||
var chipRow = new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(0, 0, 0, 4),
|
||
};
|
||
chipRow.Children.Add(new Border
|
||
{
|
||
Background = BrushFromHex(queuedItem.Priority switch
|
||
{
|
||
"now" => "#D1FAE5",
|
||
"later" => "#E5E7EB",
|
||
_ => "#DBEAFE",
|
||
}),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(7, 2, 7, 2),
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
Child = new TextBlock
|
||
{
|
||
Text = queuedItem.Priority switch
|
||
{
|
||
"now" => "吏湲?,
|
||
"later" => "?섏쨷",
|
||
_ => "?ㅼ쓬",
|
||
},
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = BrushFromHex(queuedItem.Priority switch
|
||
{
|
||
"now" => "#047857",
|
||
"later" => "#4B5563",
|
||
_ => "#1D4ED8",
|
||
}),
|
||
},
|
||
});
|
||
chipRow.Children.Add(new Border
|
||
{
|
||
Background = BrushFromHex(queuedItem.State == "failed" ? "#FEE2E2" : "#F3F4F6"),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(7, 2, 7, 2),
|
||
Child = new TextBlock
|
||
{
|
||
Text = queuedItem.State == "failed" ? "?ㅽ뙣" : queuedItem.AttemptCount > 0 ? $"?쒕룄 {queuedItem.AttemptCount}" : "?湲?,
|
||
FontSize = 11,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = BrushFromHex(queuedItem.State == "failed" ? "#B91C1C" : "#4B5563"),
|
||
},
|
||
});
|
||
previewStack.Children.Add(chipRow);
|
||
|
||
previewStack.Children.Add(new TextBlock
|
||
{
|
||
Text = queuedText.Length > 120 ? queuedText[..120] + "..." : queuedText,
|
||
FontSize = 12.5,
|
||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
});
|
||
|
||
if (!string.IsNullOrWhiteSpace(queuedItem.LastError))
|
||
{
|
||
previewStack.Children.Add(new TextBlock
|
||
{
|
||
Text = queuedItem.LastError!.Length > 90 ? queuedItem.LastError[..90] + "..." : queuedItem.LastError,
|
||
FontSize = 11.5,
|
||
Margin = new Thickness(0, 4, 0, 0),
|
||
Foreground = BrushFromHex("#B91C1C"),
|
||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||
});
|
||
}
|
||
|
||
Grid.SetColumn(previewStack, 0);
|
||
row.Children.Add(previewStack);
|
||
|
||
var bumpBtn = new Button
|
||
{
|
||
Style = TryFindResource("GhostBtn") as Style,
|
||
Content = "?곗꽑",
|
||
Padding = new Thickness(8, 3, 8, 3),
|
||
Margin = new Thickness(8, 0, 0, 0),
|
||
};
|
||
bumpBtn.Click += (_, _) =>
|
||
{
|
||
if (index < 0 || index >= queue.Count)
|
||
return;
|
||
|
||
queue[index].Priority = "now";
|
||
queue[index].State = "queued";
|
||
queue[index].LastError = null;
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
};
|
||
Grid.SetColumn(bumpBtn, 1);
|
||
row.Children.Add(bumpBtn);
|
||
|
||
var runBtn = new Button
|
||
{
|
||
Style = TryFindResource("GhostBtn") as Style,
|
||
Content = "?ㅽ뻾",
|
||
Padding = new Thickness(8, 3, 8, 3),
|
||
Margin = new Thickness(8, 0, 0, 0),
|
||
};
|
||
runBtn.Click += async (_, _) =>
|
||
{
|
||
if (_isStreaming || index < 0 || index >= queue.Count)
|
||
return;
|
||
|
||
var inputBox = InputBox;
|
||
if (inputBox == null)
|
||
return;
|
||
|
||
var next = queue[index];
|
||
queue.RemoveAt(index);
|
||
next.Priority = "now";
|
||
next.State = "queued";
|
||
next.LastError = null;
|
||
next.AttemptCount++;
|
||
next.State = "running";
|
||
_activeQueuedDraftItem = CloneDraftQueueItem(next);
|
||
inputBox.Text = next.Text;
|
||
inputBox.CaretIndex = inputBox.Text.Length;
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
await SendMessageAsync();
|
||
};
|
||
Grid.SetColumn(runBtn, 2);
|
||
row.Children.Add(runBtn);
|
||
|
||
var removeBtn = new Button
|
||
{
|
||
Style = TryFindResource("GhostBtn") as Style,
|
||
Content = "?쒓굅",
|
||
Padding = new Thickness(8, 3, 8, 3),
|
||
Margin = new Thickness(6, 0, 0, 0),
|
||
};
|
||
removeBtn.Click += (_, _) =>
|
||
{
|
||
if (index < 0 || index >= queue.Count)
|
||
return;
|
||
|
||
queue.RemoveAt(index);
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
};
|
||
Grid.SetColumn(removeBtn, 3);
|
||
row.Children.Add(removeBtn);
|
||
|
||
card.Child = row;
|
||
card.ToolTip = string.IsNullOrWhiteSpace(queuedItem.LastError)
|
||
? queuedText
|
||
: $"{queuedText}{Environment.NewLine}{queuedItem.LastError}";
|
||
DraftQueuePanel.Children.Add(card);
|
||
}
|
||
}
|
||
|
||
private async Task StartNextQueuedDraftIfAnyAsync()
|
||
{
|
||
if (_isStreaming || InputBox == null)
|
||
return;
|
||
|
||
var queue = GetDraftQueueState();
|
||
if (queue.Count == 0)
|
||
return;
|
||
|
||
if (!string.IsNullOrWhiteSpace(InputBox.Text))
|
||
return;
|
||
|
||
var next = queue
|
||
.OrderBy(item => GetDraftPriorityRank(item.Priority))
|
||
.ThenBy(item => item.CreatedAt)
|
||
.FirstOrDefault();
|
||
if (next == null)
|
||
return;
|
||
|
||
queue.RemoveAll(item => string.Equals(item.Id, next.Id, StringComparison.OrdinalIgnoreCase));
|
||
next.AttemptCount++;
|
||
next.State = "running";
|
||
next.LastError = null;
|
||
_activeQueuedDraftItem = CloneDraftQueueItem(next);
|
||
InputBox.Text = next.Text;
|
||
InputBox.CaretIndex = InputBox.Text.Length;
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
await SendMessageAsync();
|
||
}
|
||
|
||
private DraftQueueItem CreateDraftQueueItem(string text, string priority = "next")
|
||
=> new()
|
||
{
|
||
Id = Guid.NewGuid().ToString("N"),
|
||
Text = text,
|
||
Priority = NormalizeDraftPriority(priority),
|
||
State = "queued",
|
||
AttemptCount = 0,
|
||
CreatedAt = DateTime.Now,
|
||
};
|
||
|
||
private void RestoreFailedQueuedDraft(string text, Exception ex)
|
||
{
|
||
if (_activeQueuedDraftItem == null)
|
||
return;
|
||
|
||
var queue = GetDraftQueueState();
|
||
var failedItem = CloneDraftQueueItem(_activeQueuedDraftItem);
|
||
failedItem.Text = text;
|
||
failedItem.Priority = "now";
|
||
failedItem.State = "failed";
|
||
failedItem.AttemptCount = Math.Max(failedItem.AttemptCount, 1);
|
||
failedItem.LastError = ex.Message;
|
||
queue.Insert(0, failedItem);
|
||
_activeQueuedDraftItem = null;
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
}
|
||
|
||
private void BtnDraftEnqueue_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var inputBox = InputBox;
|
||
if (inputBox == null)
|
||
return;
|
||
|
||
var prepared = inputBox.Text?.Trim() ?? "";
|
||
if (string.IsNullOrWhiteSpace(prepared))
|
||
return;
|
||
|
||
var priority = Keyboard.Modifiers.HasFlag(ModifierKeys.Control) ? "now" : "next";
|
||
GetDraftQueueState().Add(CreateDraftQueueItem(prepared, priority));
|
||
inputBox.Clear();
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
inputBox.Focus();
|
||
}
|
||
|
||
private void BtnDraftEdit_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (InputBox == null)
|
||
return;
|
||
|
||
InputBox.Focus();
|
||
InputBox.CaretIndex = InputBox.Text?.Length ?? 0;
|
||
}
|
||
|
||
private void BtnDraftClear_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (InputBox == null)
|
||
return;
|
||
|
||
InputBox.Clear();
|
||
RefreshDraftComposerUi();
|
||
SaveConversationUiState();
|
||
InputBox.Focus();
|
||
}
|
||
|
||
private void BtnToggleExecutionLog_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
var hidden = Equals(BtnToggleExecutionLog.Tag, "hidden");
|
||
BtnToggleExecutionLog.Tag = hidden ? "visible" : "hidden";
|
||
|
||
if (ExecutionLogLabel != null)
|
||
ExecutionLogLabel.Text = hidden ? "?ㅽ뻾 濡쒓렇" : "?ㅽ뻾 濡쒓렇 ?④?";
|
||
|
||
if (ExecutionLogIcon != null)
|
||
ExecutionLogIcon.Text = hidden ? "\uE946" : "\uE8F8";
|
||
|
||
SaveConversationUiState();
|
||
RenderMessages();
|
||
}
|
||
|
||
private static System.Windows.Media.SolidColorBrush BrushFromHex(string hex)
|
||
{
|
||
var c = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(hex);
|
||
return new System.Windows.Media.SolidColorBrush(c);
|
||
}
|
||
|
||
private static Border CreateMetaChip(string text, string backgroundHex, string foregroundHex)
|
||
{
|
||
return new Border
|
||
{
|
||
Background = BrushFromHex(backgroundHex),
|
||
CornerRadius = new CornerRadius(10),
|
||
Padding = new Thickness(7, 2, 7, 2),
|
||
Margin = new Thickness(0, 0, 6, 0),
|
||
Child = new TextBlock
|
||
{
|
||
Text = text,
|
||
FontSize = 10.5,
|
||
FontWeight = FontWeights.SemiBold,
|
||
Foreground = BrushFromHex(foregroundHex),
|
||
},
|
||
};
|
||
}
|
||
}
|