diff --git a/README.md b/README.md index 75382d2..36cd371 100644 --- a/README.md +++ b/README.md @@ -1315,3 +1315,6 @@ MIT License - IBM 연결형 vLLM에서 `model_id` 또는 `mode`를 body에 넣지 말라는 응답이 오던 문제를 수정했다. [LlmService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.cs)에 IBM/CP4D 인증 + `/ml/v1/deployments/.../text/chat` 계열 엔드포인트를 감지하는 분기를 추가하고, 이 경우 일반 OpenAI 호환 body 대신 `messages + parameters` 형태의 IBM deployment chat body를 사용하도록 바꿨다. - 같은 파일에서 IBM deployment chat 경로는 `/v1/chat/completions`를 더 이상 강제로 붙이지 않고, 스트리밍 여부에 따라 `/text/chat` 또는 `/text/chat_stream` URL을 사용하도록 정리했다. 응답 파싱도 `results[].generated_text`, `output_text`, `choices[].message.content`를 함께 지원하게 확장했다. - [LlmService.ToolUse.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/LlmService.ToolUse.cs) 에서는 IBM deployment chat API가 감지되면 OpenAI function-calling body를 그대로 보내지 않고 `ToolCallNotSupportedException`으로 일반 응답 경로 폴백을 유도하도록 안전장치를 추가했다. +- 업데이트: 2026-04-06 18:09 (KST) + - 채팅 메시지의 좋아요/싫어요 토글을 다시 정리했다. [ChatWindow.MessageInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs) 에서 두 버튼이 각자 상태를 따로 들고 있던 구조를 없애고, 하나의 shared feedback 상태(`like/dislike/null`)를 기준으로 상호배타 토글되도록 재구성했다. + - 이제 `좋아요`도 즉시 색상/배경 상태가 바뀌고, `싫어요`를 다시 누르면 원래 상태(null)로 정상 해제된다. 버튼 시각 표현도 같은 glyph를 유지하되 active 색상과 라운드 chip 배경/테두리로 구분해, 특정 filled glyph가 보이지 않던 문제를 함께 줄였다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 505efa4..91e58fc 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4997,3 +4997,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 18:02 (KST) - Added IBM deployment-chat detection in `LlmService.cs` for vLLM registered models using `ibm_iam` or `cp4d_*` auth with `/ml/v1/deployments/...`-style endpoints. These requests now use IBM deployment chat URLs (`/text/chat` or `/text/chat_stream`) instead of appending `/v1/chat/completions`. - Document update: 2026-04-06 18:02 (KST) - Added an IBM deployment request body builder in `LlmService.cs` that omits OpenAI-style `model` and `stream` fields and sends `messages + parameters` instead. This directly addresses IBM responses complaining that `model_id` or `mode` must not be specified in the request body. - Document update: 2026-04-06 18:02 (KST) - Hardened vLLM response handling for IBM deployment endpoints by accepting `results[].generated_text`, `output_text`, and `choices[].message.content`, and by short-circuiting tool-use requests in `LlmService.ToolUse.cs` with a `ToolCallNotSupportedException` so IBM deployment chat connections do not receive an incompatible OpenAI function-calling payload. +- Document update: 2026-04-06 18:09 (KST) - Reworked message feedback toggles in `ChatWindow.MessageInteractions.cs`. `좋아요/싫어요` no longer keep separate local state with sibling reset callbacks; both buttons now derive from one shared `like/dislike/null` state and persist that single value back to the conversation/session. +- Document update: 2026-04-06 18:09 (KST) - The feedback buttons now use the same glyph in both idle/active states and express activation through color plus rounded chip background/border, which avoids cases where the like filled-glyph was visually missing and ensures pressing `싫어요` again properly returns the message to an unselected state. diff --git a/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs b/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs index 083d661..6ec98a5 100644 --- a/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs +++ b/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs @@ -11,87 +11,76 @@ namespace AxCopilot.Views; public partial class ChatWindow { - /// 좋아요/싫어요 토글 피드백 버튼 (상태 영구 저장) - private Button CreateFeedbackButton(string outline, string filled, string tooltip, - Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "", - Action? resetSibling = null, Action? registerReset = null) + /// 좋아요/싫어요 피드백 버튼을 생성합니다. + private Button CreateFeedbackButton( + string iconGlyph, + string tooltip, + Brush normalColor, + Brush activeColor, + Func isActive, + Action toggle) { var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var isActive = message?.Feedback == feedbackType; + var activeBackground = TryFindResource("ItemHoverBackground") as Brush + ?? new SolidColorBrush(Color.FromArgb(18, 255, 255, 255)); + var icon = new TextBlock { - Text = isActive ? filled : outline, + Text = iconGlyph, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, - Foreground = isActive ? activeColor : normalColor, + Foreground = normalColor, VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new ScaleTransform(1, 1) }; + + var chip = new Border + { + Background = Brushes.Transparent, + BorderBrush = Brushes.Transparent, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(8), + Padding = new Thickness(6, 4, 6, 4), + Margin = new Thickness(0, 0, 4, 0), + Child = icon + }; + var btn = new Button { - Content = icon, + Content = chip, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, - Padding = new Thickness(6, 4, 6, 4), - Margin = new Thickness(0, 0, 4, 0), + Padding = new Thickness(0), ToolTip = tooltip }; - registerReset?.Invoke(() => + void RefreshVisual() { - isActive = false; - icon.Text = outline; - icon.Foreground = normalColor; - }); + var active = isActive(); + icon.Foreground = active ? activeColor : normalColor; + chip.Background = active ? activeBackground : Brushes.Transparent; + chip.BorderBrush = active ? activeColor : Brushes.Transparent; + } - btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; }; - btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; }; + RefreshVisual(); + + 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) - { - try - { - var feedback = isActive ? feedbackType : null; - var session = ChatSession; - if (session != null) - { - lock (_convLock) - { - session.UpdateMessageFeedback(_activeTab, message, feedback, _storage); - _currentConversation = session.CurrentConversation; - } - } - else - { - message.Feedback = feedback; - ChatConversation? conv; - lock (_convLock) - { - conv = _currentConversation; - } - - if (conv != null) - { - _storage.Save(conv); - } - } - } - catch - { - } - } + toggle(); + RefreshVisual(); var scale = (ScaleTransform)icon.RenderTransform; var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250)) @@ -106,23 +95,77 @@ public partial class ChatWindow scale.BeginAnimation(ScaleTransform.ScaleXProperty, bounce); scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce); }; + return btn; } - /// 좋아요/싫어요 버튼을 상호 배타로 연결하여 추가 + /// 좋아요/싫어요 버튼을 상호배타 토글로 추가합니다. private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message) { - Action? resetLikeAction = null; - Action? resetDislikeAction = null; + string? currentFeedback = message?.Feedback; - 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); + void PersistFeedback() + { + if (message == null) + return; + + try + { + var feedback = string.IsNullOrWhiteSpace(currentFeedback) ? null : currentFeedback; + var session = ChatSession; + if (session != null) + { + lock (_convLock) + { + session.UpdateMessageFeedback(_activeTab, message, feedback, _storage); + _currentConversation = session.CurrentConversation; + } + } + else + { + message.Feedback = feedback; + ChatConversation? conv; + lock (_convLock) + { + conv = _currentConversation; + } + + if (conv != null) + _storage.Save(conv); + } + } + catch + { + } + } + + var likeBtn = CreateFeedbackButton( + "\uE8E1", + "좋아요", + btnColor, + new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), + () => string.Equals(currentFeedback, "like", StringComparison.OrdinalIgnoreCase), + () => + { + currentFeedback = string.Equals(currentFeedback, "like", StringComparison.OrdinalIgnoreCase) + ? null + : "like"; + PersistFeedback(); + }); + + var dislikeBtn = CreateFeedbackButton( + "\uE8E0", + "싫어요", + btnColor, + new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), + () => string.Equals(currentFeedback, "dislike", StringComparison.OrdinalIgnoreCase), + () => + { + currentFeedback = string.Equals(currentFeedback, "dislike", StringComparison.OrdinalIgnoreCase) + ? null + : "dislike"; + PersistFeedback(); + }); actionBar.Children.Add(likeBtn); actionBar.Children.Add(dislikeBtn); @@ -157,7 +200,7 @@ public partial class ChatWindow return new TextBlock { - Text = string.Join(" · ", parts), + Text = string.Join(" • ", parts), FontSize = 9.75, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, HorizontalAlignment = HorizontalAlignment.Left, @@ -386,7 +429,7 @@ public partial class ChatWindow } catch (Exception ex) { - Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); + Services.LogService.Debug($"편집 저장 실패: {ex.Message}"); } RefreshConversationList();