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();