Compare commits
3 Commits
7889189e41
...
94dc325df4
| Author | SHA1 | Date | |
|---|---|---|---|
| 94dc325df4 | |||
| 606ecbe6cd | |||
| e439fd144e |
10
README.md
10
README.md
@@ -1308,3 +1308,13 @@ MIT License
|
|||||||
- 업데이트: 2026-04-06 17:43 (KST)
|
- 업데이트: 2026-04-06 17:43 (KST)
|
||||||
- 추가로 앱 시작 시 [LauncherWindow](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs) 를 미리 생성하지 않고, 실제로 런처를 처음 열 때만 `EnsureLauncherCreated()`로 만들도록 바꿨다. 이로써 보이지 않는 상태의 런처 UI, 바인딩, 보조 타이머 준비 비용을 평소에는 지연시켰다.
|
- 추가로 앱 시작 시 [LauncherWindow](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs) 를 미리 생성하지 않고, 실제로 런처를 처음 열 때만 `EnsureLauncherCreated()`로 만들도록 바꿨다. 이로써 보이지 않는 상태의 런처 UI, 바인딩, 보조 타이머 준비 비용을 평소에는 지연시켰다.
|
||||||
- [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs) 에서 트레이 메뉴의 `PrepareForDisplay()` 사전 렌더 호출도 제거해, 사용하지도 않는 트레이 팝업 레이아웃 계산을 앱 시작 직후 강제로 하지 않도록 정리했다.
|
- [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs) 에서 트레이 메뉴의 `PrepareForDisplay()` 사전 렌더 호출도 제거해, 사용하지도 않는 트레이 팝업 레이아웃 계산을 앱 시작 직후 강제로 하지 않도록 정리했다.
|
||||||
|
- 업데이트: 2026-04-06 17:52 (KST)
|
||||||
|
- 런처 표시 체감 속도를 유지하기 위해 [LauncherWindow](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml.cs) 사전 생성은 다시 복원했다. 대신 무거운 후보를 색인으로 더 좁히기 위해, [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs)의 인덱스 워밍업 진입점을 런처 표시 시점이 아니라 실제 검색 시점으로 옮겼다.
|
||||||
|
- [LauncherViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.cs) 의 `SearchAsync(...)` 시작 시에만 `EnsureIndexWarmupStarted()`를 호출하도록 바꿔, 사용자가 런처를 단순 호출만 할 때는 전체 인덱스 스캔과 파일 감시가 돌지 않게 정리했다.
|
||||||
|
- 업데이트: 2026-04-06 18:02 (KST)
|
||||||
|
- 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가 보이지 않던 문제를 함께 줄였다.
|
||||||
|
|||||||
@@ -4992,3 +4992,10 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- Document update: 2026-04-06 17:35 (KST) - Changed launcher search warmup from eager startup work to on-demand initialization. `App.xaml.cs` now starts the first `IndexService.BuildAsync()` scan and `StartWatchers()` only when the launcher is actually shown through `ShowLauncherWindow()`, instead of running a full index scan and watcher hookup at boot even when the launcher is never opened.
|
- Document update: 2026-04-06 17:35 (KST) - Changed launcher search warmup from eager startup work to on-demand initialization. `App.xaml.cs` now starts the first `IndexService.BuildAsync()` scan and `StartWatchers()` only when the launcher is actually shown through `ShowLauncherWindow()`, instead of running a full index scan and watcher hookup at boot even when the launcher is never opened.
|
||||||
- Document update: 2026-04-06 17:43 (KST) - Deferred `LauncherWindow` construction itself. `App.xaml.cs` now keeps only `LauncherViewModel` alive at startup and creates the actual `LauncherWindow` through `EnsureLauncherCreated()` the first time the launcher is shown, rather than instantiating the full hidden window at boot.
|
- Document update: 2026-04-06 17:43 (KST) - Deferred `LauncherWindow` construction itself. `App.xaml.cs` now keeps only `LauncherViewModel` alive at startup and creates the actual `LauncherWindow` through `EnsureLauncherCreated()` the first time the launcher is shown, rather than instantiating the full hidden window at boot.
|
||||||
- Document update: 2026-04-06 17:43 (KST) - Removed the eager tray-menu warmup path (`PrepareForDisplay()`) from startup. This avoids doing popup layout/measure work for the tray menu until the user actually opens it, reducing idle desktop overhead further.
|
- Document update: 2026-04-06 17:43 (KST) - Removed the eager tray-menu warmup path (`PrepareForDisplay()`) from startup. This avoids doing popup layout/measure work for the tray menu until the user actually opens it, reducing idle desktop overhead further.
|
||||||
|
- Document update: 2026-04-06 17:52 (KST) - Restored eager `LauncherWindow` construction so launcher open latency stays low, but kept the tray-menu warmup removed. This keeps the launcher responsive while continuing to trim startup work elsewhere.
|
||||||
|
- Document update: 2026-04-06 17:52 (KST) - Moved index warmup from launcher-show time to actual search time. `LauncherViewModel.SearchAsync(...)` now triggers `App.EnsureIndexWarmupStarted()` only when the user performs a real search, so opening the launcher by itself no longer starts a full file scan and watcher bootstrap.
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ public partial class App : System.Windows.Application
|
|||||||
private System.Threading.Mutex? _singleInstanceMutex;
|
private System.Threading.Mutex? _singleInstanceMutex;
|
||||||
private InputListener? _inputListener;
|
private InputListener? _inputListener;
|
||||||
private LauncherWindow? _launcher;
|
private LauncherWindow? _launcher;
|
||||||
private LauncherViewModel? _launcherViewModel;
|
|
||||||
private NotifyIcon? _trayIcon;
|
private NotifyIcon? _trayIcon;
|
||||||
private Views.TrayMenuWindow? _trayMenu;
|
private Views.TrayMenuWindow? _trayMenu;
|
||||||
private SettingsService? _settings;
|
private SettingsService? _settings;
|
||||||
@@ -278,7 +277,11 @@ public partial class App : System.Windows.Application
|
|||||||
_pluginHost = pluginHost;
|
_pluginHost = pluginHost;
|
||||||
|
|
||||||
// ─── 런처 윈도우 ──────────────────────────────────────────────────────
|
// ─── 런처 윈도우 ──────────────────────────────────────────────────────
|
||||||
_launcherViewModel = new LauncherViewModel(commandResolver, settings);
|
var vm = new LauncherViewModel(commandResolver, settings);
|
||||||
|
_launcher = new LauncherWindow(vm)
|
||||||
|
{
|
||||||
|
OpenSettingsAction = OpenSettings
|
||||||
|
};
|
||||||
|
|
||||||
// ─── 클립보드 히스토리 초기화 (메시지 펌프 시작 직후 — 런처 표시 불필요) ──
|
// ─── 클립보드 히스토리 초기화 (메시지 펌프 시작 직후 — 런처 표시 불필요) ──
|
||||||
Dispatcher.BeginInvoke(
|
Dispatcher.BeginInvoke(
|
||||||
@@ -667,7 +670,7 @@ public partial class App : System.Windows.Application
|
|||||||
/// <summary>AX Agent 창 열기 (트레이 메뉴 등에서 호출).</summary>
|
/// <summary>AX Agent 창 열기 (트레이 메뉴 등에서 호출).</summary>
|
||||||
private Views.ChatWindow? _chatWindow;
|
private Views.ChatWindow? _chatWindow;
|
||||||
|
|
||||||
private void EnsureIndexWarmupStarted()
|
public void EnsureIndexWarmupStarted()
|
||||||
{
|
{
|
||||||
if (_indexService == null) return;
|
if (_indexService == null) return;
|
||||||
if (Interlocked.Exchange(ref _indexWarmupStarted, 1) == 1) return;
|
if (Interlocked.Exchange(ref _indexWarmupStarted, 1) == 1) return;
|
||||||
@@ -688,22 +691,10 @@ public partial class App : System.Windows.Application
|
|||||||
|
|
||||||
private void ShowLauncherWindow()
|
private void ShowLauncherWindow()
|
||||||
{
|
{
|
||||||
EnsureLauncherCreated();
|
|
||||||
if (_launcher == null) return;
|
if (_launcher == null) return;
|
||||||
EnsureIndexWarmupStarted();
|
|
||||||
_launcher.Show();
|
_launcher.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureLauncherCreated()
|
|
||||||
{
|
|
||||||
if (_launcher != null || _launcherViewModel == null) return;
|
|
||||||
|
|
||||||
_launcher = new LauncherWindow(_launcherViewModel)
|
|
||||||
{
|
|
||||||
OpenSettingsAction = OpenSettings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenAiChat()
|
private void OpenAiChat()
|
||||||
{
|
{
|
||||||
if (_settings == null) return;
|
if (_settings == null) return;
|
||||||
|
|||||||
@@ -431,13 +431,20 @@ public partial class LlmService
|
|||||||
List<ChatMessage> messages, IReadOnlyCollection<IAgentTool> tools, CancellationToken ct)
|
List<ChatMessage> messages, IReadOnlyCollection<IAgentTool> tools, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var activeService = ResolveService();
|
var activeService = ResolveService();
|
||||||
var body = BuildOpenAiToolBody(messages, tools);
|
|
||||||
|
|
||||||
// 등록 모델의 커스텀 엔드포인트 우선 사용 (ResolveServerInfo)
|
// 등록 모델의 커스텀 엔드포인트 우선 사용 (ResolveServerInfo)
|
||||||
var (resolvedEp, _, allowInsecureTls) = ResolveServerInfo();
|
var (resolvedEp, _, allowInsecureTls) = ResolveServerInfo();
|
||||||
var endpoint = string.IsNullOrEmpty(resolvedEp)
|
var endpoint = string.IsNullOrEmpty(resolvedEp)
|
||||||
? ResolveEndpointForService(activeService)
|
? ResolveEndpointForService(activeService)
|
||||||
: resolvedEp;
|
: resolvedEp;
|
||||||
|
var registered = GetActiveRegisteredModel();
|
||||||
|
if (UsesIbmDeploymentChatApi(activeService, registered, endpoint))
|
||||||
|
{
|
||||||
|
throw new ToolCallNotSupportedException(
|
||||||
|
"IBM 배포형 vLLM 연결은 OpenAI 도구 호출 형식과 다를 수 있어 일반 대화 경로로 폴백합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = BuildOpenAiToolBody(messages, tools);
|
||||||
|
|
||||||
var url = activeService.ToLowerInvariant() == "ollama"
|
var url = activeService.ToLowerInvariant() == "ollama"
|
||||||
? endpoint.TrimEnd('/') + "/api/chat"
|
? endpoint.TrimEnd('/') + "/api/chat"
|
||||||
|
|||||||
@@ -320,6 +320,103 @@ public partial class LlmService : IDisposable
|
|||||||
m.Alias == modelName));
|
m.Alias == modelName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Models.RegisteredModel? GetActiveRegisteredModel()
|
||||||
|
{
|
||||||
|
var llm = _settings.Settings.Llm;
|
||||||
|
return FindRegisteredModel(llm, ResolveService(), ResolveModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UsesIbmDeploymentChatApi(string service, Models.RegisteredModel? registered, string? endpoint)
|
||||||
|
{
|
||||||
|
if (!string.Equals(NormalizeServiceName(service), "vllm", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
if (registered == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var authType = (registered.AuthType ?? "").Trim().ToLowerInvariant();
|
||||||
|
if (authType is not ("ibm_iam" or "cp4d" or "cp4d_password" or "cp4d_api_key"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var normalizedEndpoint = (endpoint ?? "").Trim().ToLowerInvariant();
|
||||||
|
return normalizedEndpoint.Contains("/ml/") ||
|
||||||
|
normalizedEndpoint.Contains("/deployments/") ||
|
||||||
|
normalizedEndpoint.Contains("/text/chat");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildIbmDeploymentChatUrl(string endpoint, bool stream)
|
||||||
|
{
|
||||||
|
var trimmed = (endpoint ?? "").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmed))
|
||||||
|
throw new InvalidOperationException("IBM 배포형 vLLM 엔드포인트가 비어 있습니다.");
|
||||||
|
|
||||||
|
var normalized = trimmed.ToLowerInvariant();
|
||||||
|
if (normalized.Contains("/text/chat_stream"))
|
||||||
|
return stream ? trimmed : trimmed.Replace("/text/chat_stream", "/text/chat", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (normalized.Contains("/text/chat"))
|
||||||
|
return stream ? trimmed.Replace("/text/chat", "/text/chat_stream", StringComparison.OrdinalIgnoreCase) : trimmed;
|
||||||
|
if (normalized.Contains("/deployments/"))
|
||||||
|
return trimmed.TrimEnd('/') + (stream ? "/text/chat_stream" : "/text/chat");
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object BuildIbmDeploymentBody(List<ChatMessage> messages)
|
||||||
|
{
|
||||||
|
var msgs = new List<object>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(_systemPrompt))
|
||||||
|
msgs.Add(new { role = "system", content = _systemPrompt });
|
||||||
|
|
||||||
|
foreach (var m in messages)
|
||||||
|
{
|
||||||
|
if (m.Role == "system")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
msgs.Add(new
|
||||||
|
{
|
||||||
|
role = m.Role == "assistant" ? "assistant" : "user",
|
||||||
|
content = m.Content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
messages = msgs,
|
||||||
|
parameters = new
|
||||||
|
{
|
||||||
|
temperature = ResolveTemperature(),
|
||||||
|
max_new_tokens = ResolveOpenAiCompatibleMaxTokens()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractIbmDeploymentText(JsonElement root)
|
||||||
|
{
|
||||||
|
if (root.TryGetProperty("choices", out var choices) && choices.ValueKind == JsonValueKind.Array && choices.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var message = choices[0].TryGetProperty("message", out var choiceMessage) ? choiceMessage : default;
|
||||||
|
if (message.ValueKind == JsonValueKind.Object &&
|
||||||
|
message.TryGetProperty("content", out var content))
|
||||||
|
return content.GetString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var first = results[0];
|
||||||
|
if (first.TryGetProperty("generated_text", out var generatedText))
|
||||||
|
return generatedText.GetString() ?? "";
|
||||||
|
if (first.TryGetProperty("output_text", out var outputText))
|
||||||
|
return outputText.GetString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("generated_text", out var generated))
|
||||||
|
return generated.GetString() ?? "";
|
||||||
|
|
||||||
|
if (root.TryGetProperty("message", out var messageValue) && messageValue.ValueKind == JsonValueKind.String)
|
||||||
|
return messageValue.GetString() ?? "";
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 현재 활성 모델의 인증 헤더 값을 반환합니다.
|
/// 현재 활성 모델의 인증 헤더 값을 반환합니다.
|
||||||
/// IBM IAM / CP4D 인증인 경우 토큰을 자동 발급/캐싱하여 반환합니다.
|
/// IBM IAM / CP4D 인증인 경우 토큰을 자동 발급/캐싱하여 반환합니다.
|
||||||
@@ -606,8 +703,14 @@ public partial class LlmService : IDisposable
|
|||||||
var llm = _settings.Settings.Llm;
|
var llm = _settings.Settings.Llm;
|
||||||
var (endpoint, _, allowInsecureTls) = ResolveServerInfo();
|
var (endpoint, _, allowInsecureTls) = ResolveServerInfo();
|
||||||
var ep = string.IsNullOrEmpty(endpoint) ? llm.Endpoint : endpoint;
|
var ep = string.IsNullOrEmpty(endpoint) ? llm.Endpoint : endpoint;
|
||||||
var body = BuildOpenAiBody(messages, stream: false);
|
var registered = GetActiveRegisteredModel();
|
||||||
var url = ep.TrimEnd('/') + "/v1/chat/completions";
|
var usesIbmDeploymentApi = UsesIbmDeploymentChatApi("vllm", registered, ep);
|
||||||
|
var body = usesIbmDeploymentApi
|
||||||
|
? BuildIbmDeploymentBody(messages)
|
||||||
|
: BuildOpenAiBody(messages, stream: false);
|
||||||
|
var url = usesIbmDeploymentApi
|
||||||
|
? BuildIbmDeploymentChatUrl(ep, stream: false)
|
||||||
|
: ep.TrimEnd('/') + "/v1/chat/completions";
|
||||||
var json = JsonSerializer.Serialize(body);
|
var json = JsonSerializer.Serialize(body);
|
||||||
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, url)
|
using var req = new HttpRequestMessage(HttpMethod.Post, url)
|
||||||
@@ -621,6 +724,12 @@ public partial class LlmService : IDisposable
|
|||||||
return SafeParseJson(respBody, root =>
|
return SafeParseJson(respBody, root =>
|
||||||
{
|
{
|
||||||
TryParseOpenAiUsage(root);
|
TryParseOpenAiUsage(root);
|
||||||
|
if (usesIbmDeploymentApi)
|
||||||
|
{
|
||||||
|
var parsed = ExtractIbmDeploymentText(root);
|
||||||
|
return string.IsNullOrWhiteSpace(parsed) ? "(빈 응답)" : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
var choices = root.GetProperty("choices");
|
var choices = root.GetProperty("choices");
|
||||||
if (choices.GetArrayLength() == 0) return "(빈 응답)";
|
if (choices.GetArrayLength() == 0) return "(빈 응답)";
|
||||||
return choices[0].GetProperty("message").GetProperty("content").GetString() ?? "";
|
return choices[0].GetProperty("message").GetProperty("content").GetString() ?? "";
|
||||||
@@ -634,8 +743,14 @@ public partial class LlmService : IDisposable
|
|||||||
var llm = _settings.Settings.Llm;
|
var llm = _settings.Settings.Llm;
|
||||||
var (endpoint, _, allowInsecureTls) = ResolveServerInfo();
|
var (endpoint, _, allowInsecureTls) = ResolveServerInfo();
|
||||||
var ep = string.IsNullOrEmpty(endpoint) ? llm.Endpoint : endpoint;
|
var ep = string.IsNullOrEmpty(endpoint) ? llm.Endpoint : endpoint;
|
||||||
var body = BuildOpenAiBody(messages, stream: true);
|
var registered = GetActiveRegisteredModel();
|
||||||
var url = ep.TrimEnd('/') + "/v1/chat/completions";
|
var usesIbmDeploymentApi = UsesIbmDeploymentChatApi("vllm", registered, ep);
|
||||||
|
var body = usesIbmDeploymentApi
|
||||||
|
? BuildIbmDeploymentBody(messages)
|
||||||
|
: BuildOpenAiBody(messages, stream: true);
|
||||||
|
var url = usesIbmDeploymentApi
|
||||||
|
? BuildIbmDeploymentChatUrl(ep, stream: true)
|
||||||
|
: ep.TrimEnd('/') + "/v1/chat/completions";
|
||||||
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent(body) };
|
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent(body) };
|
||||||
await ApplyAuthHeaderAsync(req, ct);
|
await ApplyAuthHeaderAsync(req, ct);
|
||||||
@@ -657,12 +772,43 @@ public partial class LlmService : IDisposable
|
|||||||
{
|
{
|
||||||
using var doc = JsonDocument.Parse(data);
|
using var doc = JsonDocument.Parse(data);
|
||||||
TryParseOpenAiUsage(doc.RootElement);
|
TryParseOpenAiUsage(doc.RootElement);
|
||||||
var choices = doc.RootElement.GetProperty("choices");
|
if (usesIbmDeploymentApi)
|
||||||
if (choices.GetArrayLength() > 0)
|
|
||||||
{
|
{
|
||||||
var delta = choices[0].GetProperty("delta");
|
if (doc.RootElement.TryGetProperty("status", out var status) &&
|
||||||
if (delta.TryGetProperty("content", out var c))
|
string.Equals(status.GetString(), "error", StringComparison.OrdinalIgnoreCase))
|
||||||
text = c.GetString();
|
{
|
||||||
|
var detail = doc.RootElement.TryGetProperty("message", out var message)
|
||||||
|
? message.GetString()
|
||||||
|
: "IBM vLLM 스트리밍 오류";
|
||||||
|
throw new InvalidOperationException(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("results", out var results) &&
|
||||||
|
results.ValueKind == JsonValueKind.Array &&
|
||||||
|
results.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var first = results[0];
|
||||||
|
if (first.TryGetProperty("generated_text", out var generatedText))
|
||||||
|
text = generatedText.GetString();
|
||||||
|
else if (first.TryGetProperty("output_text", out var outputText))
|
||||||
|
text = outputText.GetString();
|
||||||
|
}
|
||||||
|
else if (doc.RootElement.TryGetProperty("choices", out var ibmChoices) && ibmChoices.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var delta = ibmChoices[0].GetProperty("delta");
|
||||||
|
if (delta.TryGetProperty("content", out var c))
|
||||||
|
text = c.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var choices = doc.RootElement.GetProperty("choices");
|
||||||
|
if (choices.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var delta = choices[0].GetProperty("delta");
|
||||||
|
if (delta.TryGetProperty("content", out var c))
|
||||||
|
text = c.GetString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
|
|||||||
@@ -280,6 +280,8 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
|||||||
|
|
||||||
private async Task SearchAsync(string query)
|
private async Task SearchAsync(string query)
|
||||||
{
|
{
|
||||||
|
(System.Windows.Application.Current as App)?.EnsureIndexWarmupStarted();
|
||||||
|
|
||||||
// CTS 취소는 setter에서 이미 처리됨. 새 토큰만 발급.
|
// CTS 취소는 setter에서 이미 처리됨. 새 토큰만 발급.
|
||||||
_searchCts = new CancellationTokenSource();
|
_searchCts = new CancellationTokenSource();
|
||||||
var ct = _searchCts.Token;
|
var ct = _searchCts.Token;
|
||||||
|
|||||||
@@ -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