Some checks failed
Release Gate / gate (push) Has been cancelled
Claude/Codex/Slate/Nord/Ember 테마 팔레트를 다시 분리해 각 테마가 서로 다른 표면 위계와 인상을 갖도록 조정했다. 입력창 포커스 테두리를 AccentColor 고정이 아닌 테마별 InputFocusBorderColor로 변경하고, composer 및 메시지 버블 라운딩을 더 부드럽게 다듬었다. README와 DEVELOPMENT 문서를 2026-04-06 21:54 (KST) 기준으로 갱신했고 Release 빌드에서 경고 0, 오류 0을 확인했다.
343 lines
14 KiB
C#
343 lines
14 KiB
C#
using System;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using AxCopilot.Models;
|
|
using AxCopilot.Services;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ChatWindow
|
|
{
|
|
private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null)
|
|
{
|
|
var isUser = role == "user";
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var itemBg = TryFindResource("ItemBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
var hintBg = TryFindResource("HintBackground") as Brush
|
|
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
var userBubbleBg = hintBg;
|
|
var assistantBubbleBg = itemBg;
|
|
|
|
if (isUser)
|
|
{
|
|
var msgMaxWidth = GetMessageMaxWidth();
|
|
var wrapper = new StackPanel
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
Width = msgMaxWidth,
|
|
MaxWidth = msgMaxWidth,
|
|
Margin = new Thickness(0, 4, 0, 6),
|
|
};
|
|
|
|
var bubble = new Border
|
|
{
|
|
Background = userBubbleBg,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(12),
|
|
Padding = new Thickness(11, 7, 11, 7),
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
};
|
|
|
|
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var userCodeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
|
|
MarkdownRenderer.EnableFilePathHighlight =
|
|
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
|
MarkdownRenderer.EnableCodeSymbolHighlight = true;
|
|
bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, userCodeBgBrush);
|
|
}
|
|
else
|
|
{
|
|
MarkdownRenderer.EnableCodeSymbolHighlight = false;
|
|
bubble.Child = new TextBlock
|
|
{
|
|
Text = content,
|
|
TextAlignment = TextAlignment.Left,
|
|
FontSize = 12,
|
|
Foreground = primaryText,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
LineHeight = 18,
|
|
};
|
|
}
|
|
|
|
wrapper.Children.Add(bubble);
|
|
|
|
var userActionBar = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Opacity = 0.8,
|
|
Margin = new Thickness(0, 2, 0, 0),
|
|
};
|
|
var capturedUserContent = content;
|
|
var userBtnColor = secondaryText;
|
|
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, 1, 0, 0) };
|
|
userBottomBar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
userBottomBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
userBottomBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
var timestamp = message?.Timestamp ?? DateTime.Now;
|
|
Grid.SetColumn(userActionBar, 1);
|
|
userBottomBar.Children.Add(userActionBar);
|
|
|
|
var timestampText = new TextBlock
|
|
{
|
|
Text = timestamp.ToString("HH:mm"),
|
|
FontSize = 10.5,
|
|
Opacity = 0.52,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(8, 0, 2, 1),
|
|
};
|
|
Grid.SetColumn(timestampText, 2);
|
|
userBottomBar.Children.Add(timestampText);
|
|
wrapper.Children.Add(userBottomBar);
|
|
wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1;
|
|
wrapper.MouseLeave += (_, _) => userActionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, userActionBar) ? 1 : 0.8;
|
|
wrapper.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(userActionBar, bubble);
|
|
|
|
var userContent = content;
|
|
wrapper.MouseRightButtonUp += (_, re) =>
|
|
{
|
|
re.Handled = true;
|
|
ShowMessageContextMenu(userContent, "user");
|
|
};
|
|
|
|
if (animate)
|
|
ApplyMessageEntryAnimation(wrapper);
|
|
MessagePanel.Children.Add(wrapper);
|
|
return;
|
|
}
|
|
|
|
if (message != null && IsCompactionMetaMessage(message))
|
|
{
|
|
var compactCard = CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush);
|
|
if (animate)
|
|
ApplyMessageEntryAnimation(compactCard);
|
|
MessagePanel.Children.Add(compactCard);
|
|
return;
|
|
}
|
|
|
|
var assistantMaxWidth = GetMessageMaxWidth();
|
|
var container = new StackPanel
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
Width = assistantMaxWidth,
|
|
MaxWidth = assistantMaxWidth,
|
|
Margin = new Thickness(0, 6, 0, 6),
|
|
};
|
|
if (animate)
|
|
ApplyMessageEntryAnimation(container);
|
|
|
|
var (agentName, _, _) = GetAgentIdentity();
|
|
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 0, 0, 1.5) };
|
|
header.Children.Add(new TextBlock
|
|
{
|
|
Text = "\uE945",
|
|
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 10,
|
|
Foreground = secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
header.Children.Add(new TextBlock
|
|
{
|
|
Text = agentName,
|
|
FontSize = 11.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = secondaryText,
|
|
Margin = new Thickness(4, 0, 0, 0),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
container.Children.Add(header);
|
|
|
|
var contentCard = new Border
|
|
{
|
|
Background = assistantBubbleBg,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(12),
|
|
Padding = new Thickness(11, 8, 11, 8),
|
|
};
|
|
var contentStack = new StackPanel();
|
|
|
|
var app = System.Windows.Application.Current as App;
|
|
MarkdownRenderer.EnableFilePathHighlight =
|
|
app?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
|
|
MarkdownRenderer.EnableCodeSymbolHighlight =
|
|
string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase);
|
|
var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
|
|
if (IsBranchContextMessage(content))
|
|
{
|
|
var branchRun = GetAgentRunStateById(message?.MetaRunId) ?? GetLatestBranchContextRun();
|
|
var branchFiles = GetBranchContextFilePaths(message?.MetaRunId ?? branchRun?.RunId, 3);
|
|
var branchCard = new Border
|
|
{
|
|
Background = hintBg,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(14),
|
|
Padding = new Thickness(12, 10, 12, 10),
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
};
|
|
var branchStack = new StackPanel();
|
|
branchStack.Children.Add(new TextBlock
|
|
{
|
|
Text = "분기 컨텍스트",
|
|
FontSize = 11,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
});
|
|
branchStack.Children.Add(MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush));
|
|
|
|
if (branchFiles.Count > 0)
|
|
{
|
|
var filesWrap = new WrapPanel { Margin = new Thickness(0, 8, 0, 0) };
|
|
foreach (var path in branchFiles)
|
|
{
|
|
var fileButton = new Button
|
|
{
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(8, 3, 8, 3),
|
|
Margin = new Thickness(0, 0, 6, 6),
|
|
Cursor = Cursors.Hand,
|
|
ToolTip = path,
|
|
Content = new TextBlock
|
|
{
|
|
Text = System.IO.Path.GetFileName(path),
|
|
FontSize = 10,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
},
|
|
};
|
|
var capturedPath = path;
|
|
fileButton.Click += (_, _) => OpenRunFilePath(capturedPath);
|
|
filesWrap.Children.Add(fileButton);
|
|
}
|
|
branchStack.Children.Add(filesWrap);
|
|
}
|
|
|
|
if (branchRun != null)
|
|
{
|
|
var actionsWrap = new WrapPanel { Margin = new Thickness(0, 8, 0, 0) };
|
|
|
|
var followUpButton = new Button
|
|
{
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(8, 3, 8, 3),
|
|
Margin = new Thickness(0, 0, 6, 6),
|
|
Cursor = Cursors.Hand,
|
|
Content = new TextBlock
|
|
{
|
|
Text = "후속 작업 큐에 넣기",
|
|
FontSize = 10,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
},
|
|
};
|
|
var capturedBranchRun = branchRun;
|
|
followUpButton.Click += (_, _) => EnqueueFollowUpFromRun(capturedBranchRun);
|
|
actionsWrap.Children.Add(followUpButton);
|
|
|
|
var timelineButton = new Button
|
|
{
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(8, 3, 8, 3),
|
|
Margin = new Thickness(0, 0, 6, 6),
|
|
Cursor = Cursors.Hand,
|
|
Content = new TextBlock
|
|
{
|
|
Text = "관련 로그로 이동",
|
|
FontSize = 10,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
},
|
|
};
|
|
timelineButton.Click += (_, _) => ScrollToRunInTimeline(capturedBranchRun.RunId);
|
|
actionsWrap.Children.Add(timelineButton);
|
|
|
|
branchStack.Children.Add(actionsWrap);
|
|
}
|
|
|
|
branchCard.Child = branchStack;
|
|
contentStack.Children.Add(branchCard);
|
|
}
|
|
else
|
|
{
|
|
contentStack.Children.Add(MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush));
|
|
}
|
|
|
|
contentCard.Child = contentStack;
|
|
container.Children.Add(contentCard);
|
|
|
|
var actionBar = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
Margin = new Thickness(2, 2, 0, 0),
|
|
Opacity = 0.8,
|
|
};
|
|
var btnColor = secondaryText;
|
|
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.5,
|
|
Opacity = 0.52,
|
|
Foreground = btnColor,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(4, 0, 0, 1),
|
|
});
|
|
|
|
container.Children.Add(actionBar);
|
|
var assistantMeta = CreateAssistantMessageMetaText(message);
|
|
if (assistantMeta != null)
|
|
container.Children.Add(assistantMeta);
|
|
|
|
container.MouseEnter += (_, _) => actionBar.Opacity = 1;
|
|
container.MouseLeave += (_, _) => actionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, actionBar) ? 1 : 0.8;
|
|
container.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(actionBar, contentCard);
|
|
|
|
var aiContent = content;
|
|
container.MouseRightButtonUp += (_, re) =>
|
|
{
|
|
re.Handled = true;
|
|
ShowMessageContextMenu(aiContent, "assistant");
|
|
};
|
|
|
|
MessagePanel.Children.Add(container);
|
|
}
|
|
}
|