채팅 메시지 피드백 버튼 로직을 shared feedback 상태 기반으로 다시 구성해 좋아요와 싫어요가 모두 즉시 반응하고 상호배타적으로 동작하도록 정리했다. 좋아요는 활성 색상과 chip 배경으로 상태가 확실히 보이게 바꾸고, 싫어요를 다시 누르면 null 상태로 정상 해제되도록 수정했다. README와 DEVELOPMENT 문서를 2026-04-06 18:09 (KST) 기준으로 갱신했고 dotnet build 검증에서 경고 0 / 오류 0을 확인했다.
This commit is contained in:
@@ -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 연결형 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`를 함께 지원하게 확장했다.
|
- 같은 파일에서 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`으로 일반 응답 경로 폴백을 유도하도록 안전장치를 추가했다.
|
- [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가 보이지 않던 문제를 함께 줄였다.
|
||||||
|
|||||||
@@ -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 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) - 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: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.
|
||||||
|
|||||||
@@ -11,87 +11,76 @@ namespace AxCopilot.Views;
|
|||||||
|
|
||||||
public partial class ChatWindow
|
public partial class ChatWindow
|
||||||
{
|
{
|
||||||
/// <summary>좋아요/싫어요 토글 피드백 버튼 (상태 영구 저장)</summary>
|
/// <summary>좋아요/싫어요 피드백 버튼을 생성합니다.</summary>
|
||||||
private Button CreateFeedbackButton(string outline, string filled, string tooltip,
|
private Button CreateFeedbackButton(
|
||||||
Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "",
|
string iconGlyph,
|
||||||
Action? resetSibling = null, Action<Action>? registerReset = null)
|
string tooltip,
|
||||||
|
Brush normalColor,
|
||||||
|
Brush activeColor,
|
||||||
|
Func<bool> isActive,
|
||||||
|
Action toggle)
|
||||||
{
|
{
|
||||||
var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
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
|
var icon = new TextBlock
|
||||||
{
|
{
|
||||||
Text = isActive ? filled : outline,
|
Text = iconGlyph,
|
||||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||||
FontSize = 12,
|
FontSize = 12,
|
||||||
Foreground = isActive ? activeColor : normalColor,
|
Foreground = normalColor,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||||
RenderTransform = new ScaleTransform(1, 1)
|
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
|
var btn = new Button
|
||||||
{
|
{
|
||||||
Content = icon,
|
Content = chip,
|
||||||
Background = Brushes.Transparent,
|
Background = Brushes.Transparent,
|
||||||
BorderThickness = new Thickness(0),
|
BorderThickness = new Thickness(0),
|
||||||
Cursor = Cursors.Hand,
|
Cursor = Cursors.Hand,
|
||||||
Padding = new Thickness(6, 4, 6, 4),
|
Padding = new Thickness(0),
|
||||||
Margin = new Thickness(0, 0, 4, 0),
|
|
||||||
ToolTip = tooltip
|
ToolTip = tooltip
|
||||||
};
|
};
|
||||||
|
|
||||||
registerReset?.Invoke(() =>
|
void RefreshVisual()
|
||||||
{
|
{
|
||||||
isActive = false;
|
var active = isActive();
|
||||||
icon.Text = outline;
|
icon.Foreground = active ? activeColor : normalColor;
|
||||||
icon.Foreground = normalColor;
|
chip.Background = active ? activeBackground : Brushes.Transparent;
|
||||||
});
|
chip.BorderBrush = active ? activeColor : Brushes.Transparent;
|
||||||
|
}
|
||||||
|
|
||||||
btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; };
|
RefreshVisual();
|
||||||
btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; };
|
|
||||||
|
btn.MouseEnter += (_, _) =>
|
||||||
|
{
|
||||||
|
if (!isActive())
|
||||||
|
icon.Foreground = hoverBrush;
|
||||||
|
};
|
||||||
|
btn.MouseLeave += (_, _) =>
|
||||||
|
{
|
||||||
|
if (!isActive())
|
||||||
|
icon.Foreground = normalColor;
|
||||||
|
};
|
||||||
btn.Click += (_, _) =>
|
btn.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
isActive = !isActive;
|
toggle();
|
||||||
icon.Text = isActive ? filled : outline;
|
RefreshVisual();
|
||||||
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
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var scale = (ScaleTransform)icon.RenderTransform;
|
var scale = (ScaleTransform)icon.RenderTransform;
|
||||||
var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250))
|
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.ScaleXProperty, bounce);
|
||||||
scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce);
|
scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce);
|
||||||
};
|
};
|
||||||
|
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>좋아요/싫어요 버튼을 상호 배타로 연결하여 추가</summary>
|
/// <summary>좋아요/싫어요 버튼을 상호배타 토글로 추가합니다.</summary>
|
||||||
private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message)
|
private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message)
|
||||||
{
|
{
|
||||||
Action? resetLikeAction = null;
|
string? currentFeedback = message?.Feedback;
|
||||||
Action? resetDislikeAction = null;
|
|
||||||
|
|
||||||
var likeBtn = CreateFeedbackButton("\uE8E1", "\uEB51", "좋아요", btnColor,
|
void PersistFeedback()
|
||||||
new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), message, "like",
|
{
|
||||||
resetSibling: () => resetDislikeAction?.Invoke(),
|
if (message == null)
|
||||||
registerReset: reset => resetLikeAction = reset);
|
return;
|
||||||
var dislikeBtn = CreateFeedbackButton("\uE8E0", "\uEB50", "싫어요", btnColor,
|
|
||||||
new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), message, "dislike",
|
try
|
||||||
resetSibling: () => resetLikeAction?.Invoke(),
|
{
|
||||||
registerReset: reset => resetDislikeAction = reset);
|
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(likeBtn);
|
||||||
actionBar.Children.Add(dislikeBtn);
|
actionBar.Children.Add(dislikeBtn);
|
||||||
@@ -157,7 +200,7 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
return new TextBlock
|
return new TextBlock
|
||||||
{
|
{
|
||||||
Text = string.Join(" · ", parts),
|
Text = string.Join(" • ", parts),
|
||||||
FontSize = 9.75,
|
FontSize = 9.75,
|
||||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||||
HorizontalAlignment = HorizontalAlignment.Left,
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
@@ -386,7 +429,7 @@ public partial class ChatWindow
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Services.LogService.Debug($"대화 저장 실패: {ex.Message}");
|
Services.LogService.Debug($"편집 저장 실패: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshConversationList();
|
RefreshConversationList();
|
||||||
|
|||||||
Reference in New Issue
Block a user