Skip to content

Commit a57d081

Browse files
feat: Magic Conversion機能を選択テキストに対して拡張 (#148)
* feat: 選択したテキストの変換機能を追加 - InputStateに.transformSelectedTextを追加 - ClientActionにshowPromptInputWindowとtransformSelectedTextを追加 - UserActionにtransformSelectedTextを追加 - azooKeyMacInputControllerに選択したテキストを変換するための処理を実装 - PromptInputWindowを新規作成し、ユーザーがテキスト変換を行うためのUIを提供 * feat: プロンプト履歴機能を追加し、UIを改善 - プロンプト履歴を管理するためのPromptHistory構造体を追加 - PromptInputWindowに履歴ナビゲーション機能を実装 - UIデザインをモダン化し、ボタンスタイルを更新 - テキストフィールドのフォーカス処理を強化 - 履歴のピン留め機能を追加し、最近のプロンプトを表示 * feat: プロンプト入力ウィンドウのUIを改善し、履歴表示を強化 - プレビュー表示時に履歴を隠す機能を追加 - 履歴の表示数を最大10件に拡張 - UIのスクロール機能を追加し、履歴の可視性を向上 - UserDefaultsを使用して履歴を保存する処理を修正 * feat: プロンプト入力ウィンドウでのプレビューリクエストを自動化 - 履歴アイテムが選択された際に、テキスト設定後に自動的にプレビューをリクエストする処理を追加 - プレビュー表示時のユーザー体験を向上させるため、非同期でのリクエストを実装 * feat: プロンプト入力ウィンドウでのテキストフィールドフォーカス管理を強化 - テキストフィールドのフォーカス状態を管理するための通知を追加 - キーイベント処理を改善し、テキストフィールドがフォーカスされている場合の履歴ナビゲーションを最適化 - UIの透明度設定を変更し、ネイティブマテリアルバックを使用するように修正 * feat: プロンプト入力ウィンドウのテキストフィールドにカスタムキー処理を追加 - テキストフィールドに対する上下矢印キーの処理をカスタマイズし、履歴ナビゲーションを改善 - フォーカス管理を簡素化し、通知を通じてテキストフィールドのフォーカスをリクエストする機能を追加 - UIの透明度とスタイルを調整し、全体的なユーザー体験を向上 * feat: プロンプト履歴が空の場合にデフォルトのピン留めプロンプトを追加 - プロンプト履歴が空の際に、デフォルトのピン留めプロンプトを設定する機能を追加 - 新しいフォーマットで履歴を保存する処理を維持 * feat: 選択したテキストの前後コンテキストを取得する機能を追加 - 選択範囲の前後のテキストコンテキストを取得する`getContextAroundSelection`メソッドを追加 - テキスト変換時にコンテキスト情報をプロンプトに含めるように`transformSelectedText`と`getTransformationPreview`メソッドを修正 - デバッグメッセージを追加し、コンテキスト取得の詳細をログに記録 * feat: プロンプト履歴の保存形式を改善し、旧フォーマットからの移行を追加 - UserDefaultsからのプロンプト履歴の読み込み時に新フォーマットを優先し、旧フォーマットからの変換処理を実装 - 新フォーマットでの履歴保存処理を修正し、データの整合性を向上 * feat: プロンプト入力ウィンドウの新しいコンポーネントを追加 - カスタムテキストフィールドを実装し、キー操作による履歴ナビゲーションをサポート - モダンなボタンスタイルを追加し、UIの一貫性を向上 - プロンプト履歴アイテムの構造体を追加し、履歴管理機能を強化 - プロンプト入力ビューを新規作成し、ユーザー体験を向上させるためのインターフェースを提供 * feat: 選択したテキストの変換機能を拡張し、関連メソッドを整理 - `azooKeyMacInputController`クラス内のプロパティを修正し、`promptInputWindow`と`isPromptWindowVisible`を公開に変更 - 選択したテキストの前後コンテキストを取得する`getContextAroundSelection`メソッドを新しいファイルに移動し、関連機能を整理 - テキスト変換のプレビュー機能を強化し、ユーザー体験を向上させるための処理を追加 - APIリクエストのエラーハンドリングを改善し、デバッグメッセージを追加 * feat: 選択したテキストの変換処理を改善し、定数を追加 - `azooKeyMacInputController`クラスに定数を追加し、テキスト変換の設定を整理 - `getContextAroundSelection`メソッドのデフォルト引数を定数に変更 - テキスト置換処理を簡素化し、IMKを優先する方法に修正 - OpenAI APIリクエストのエラーハンドリングを強化し、デバッグメッセージを更新 * feat: OpenAIClient.swiftにInputMethodKitをインポート - OpenAIClient.swiftファイルにInputMethodKitを追加し、関連機能の拡張に備える * feat: PromptInputViewの背景スタイルを簡素化 - PromptInputView内の背景に使用されていたオーバーレイのストロークを削除し、デザインをシンプルに改善 - UIの一貫性を向上させるため、背景のマテリアルスタイルを調整 * feat: OpenAIClientのAPIリクエスト処理を改善し、デバッグメッセージのロギングを強化 - `sendRequest`メソッドの引数にロガーを追加し、デバッグメッセージの管理を改善 - `parseResponseData`メソッド内のデバッグメッセージをロガーを通じて出力するように変更 * feat: プロンプト履歴の管理を改善し、ピン留め状態を保持 - ピン留めされたプロンプトの表示順を最終使用日時でソート - 既存のプロンプトがある場合、ピン留め状態を保持しつつ最終使用日時を更新 - 無効なインデックスのリセット処理を追加し、履歴ナビゲーションの安定性を向上 * feat: PromptInputViewのテキストフィールドのフォーカス処理を改善 - テキストフィールドのフォーカスを確実にするため、遅延を設けて他のフォーカス変更を上書きする処理を追加 - 履歴選択をクリアする処理を強化し、ユーザー体験を向上 * feat: PromptInputViewにリロードボタンとプレビュー生成中の表示を追加 - プレビューをリロードするためのボタンを追加し、⌘Rショートカットを設定 - プレビュー生成中に進行状況を示すプログレスビューを表示する処理を実装 - プレビューが生成されるまでのユーザー体験を向上させるためのUI改善を実施 * feat: PromptInputViewのテキストを「AI Transform」から「Magic Conversion」に変更 * feat: ModernButtonStyles.swiftのボタンスタイルを改善 - ボタンの有効/無効状態に応じた色と透明度の調整を追加 - 押下時のアニメーションを有効状態に基づいて変更し、ユーザー体験を向上 * fix: プロンプト入力時の履歴選択状態をクリアし、無効ボタンの視覚的フィードバックを改善 - 履歴を選択した状態で新しいテキストを入力した際に履歴選択状態が残るバグを修正 - プロンプトが空の時のプレビューボタンの無効状態を視覚的に明確化 * feat: 不要なコメントを削除し、クラスの可視性を改善 - 複数のSwiftファイルから不要なコメントを削除 - `KeyHandlingTextField`と`PromptInputWindow`クラスを`private`から`final`に変更し、可視性を向上 - `PromptHistoryItem`構造体に`Sendable`プロトコルを追加し、スレッドセーフ性を強化 * feat: 履歴ナビゲーションの状態管理を改善 - テキストフィールドの編集時に履歴選択状態をクリアする条件を、ナビゲーション中でない場合に限定 - テキスト変更後にナビゲーションフラグをリセットする処理を追加し、ユーザー体験を向上 * feat: 不要なデバッグ用のprint文を削除し、コードをクリーンアップ - `azooKeyMacInputController`と`PromptInputView`から不要なprint文を削除し、可読性を向上 - 初期化時のウィンドウ表示を非表示に設定し、ユーザー体験を改善 * feat: PromptInputViewのUIを改善し、無駄な引数を削除 - 背景の影を削除し、クリップシェイプを追加してUIを整え - `onSubmit`、`onApply`、`onPreviewModeChanged`の引数を不要なものに変更し、コードの可読性を向上 * feat: PromptInputViewのプレースホルダーを日本語から英語に変更 - テキストフィールドのプレースホルダーを「例: フォーマルにして」から「example: formalize」に変更し、国際化対応を強化 * feat: PromptInputWindowのUIを改善し、ホスティングビューの角丸を追加 - `NSHostingView`に角丸を追加し、UIの見た目を向上 - `self.contentView`の設定を改善し、可読性を向上 * feat: AI変換時のコンテキストを含める設定を追加 - 新しいBoolConfigItemとして`IncludeContextInAITransform`を追加し、デフォルト値を`true`に設定 - `PromptInputView`にコンテキストを含めるトグルを追加し、ユーザーが設定を変更できるようにした - プレビュー生成時にコンテキストの有無を考慮するように`onPreview`メソッドを修正
1 parent b539aa1 commit a57d081

13 files changed

+1522
-25
lines changed

Core/Sources/Core/InputUtils/Actions/ClientAction.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public enum ClientAction {
5858
case submitReplaceSuggestionCandidate
5959
case hideReplaceSuggestionWindow
6060

61+
// Selected Text Transform
62+
case showPromptInputWindow
63+
case transformSelectedText(String, String) // (selectedText, prompt)
64+
6165
case stopComposition
6266
}
6367

Core/Sources/Core/InputUtils/Actions/UserAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public enum UserAction {
1414
case editSegment(Int)
1515
case suggest
1616
case forget
17+
case transformSelectedText
1718

1819
public enum NavigationDirection: Sendable, Equatable, Hashable {
1920
case up, down, right, left

Core/Sources/Core/InputUtils/InputState.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public enum InputState: Sendable, Hashable {
8383
} else {
8484
return (.fallthrough, .fallthrough)
8585
}
86-
case .unknown, .navigation, .backspace, .enter, .escape, .function, .editSegment, .tab, .forget:
86+
case .unknown, .navigation, .backspace, .enter, .escape, .function, .editSegment, .tab, .forget, .transformSelectedText:
8787
return (.fallthrough, .fallthrough)
8888
}
8989
case .composing:
@@ -136,7 +136,7 @@ public enum InputState: Sendable, Hashable {
136136
} else {
137137
return (.fallthrough, .fallthrough)
138138
}
139-
case .forget, .unknown, .tab:
139+
case .forget, .unknown, .tab, .transformSelectedText:
140140
return (.fallthrough, .fallthrough)
141141
}
142142
case .previewing:
@@ -179,7 +179,7 @@ public enum InputState: Sendable, Hashable {
179179
}
180180
case .editSegment(let count):
181181
return (.editSegment(count), .transition(.selecting))
182-
case .unknown, .suggest, .tab, .forget:
182+
case .unknown, .suggest, .tab, .forget, .transformSelectedText:
183183
return (.fallthrough, .fallthrough)
184184
}
185185
case .selecting:
@@ -248,7 +248,7 @@ public enum InputState: Sendable, Hashable {
248248
return (.consume, .fallthrough)
249249
case .英数:
250250
return (.commitMarkedTextAndSelectInputLanguage(.english), .transition(.none))
251-
case .unknown, .suggest, .tab:
251+
case .unknown, .suggest, .tab, .transformSelectedText:
252252
return (.fallthrough, .fallthrough)
253253
}
254254
case .replaceSuggestion:

azooKeyMac/Configs/BoolConfigItem.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ extension Config {
5555
static let `default` = false
5656
static var key: String = "dev.ensan.inputmethod.azooKeyMac.preference.enableOpenAiApiKey"
5757
}
58+
/// AI変換時にコンテキストを含めるかどうか
59+
struct IncludeContextInAITransform: BoolConfigItem {
60+
static let `default` = true
61+
static var key: String = "dev.ensan.inputmethod.azooKeyMac.preference.includeContextInAITransform"
62+
}
5863
}

azooKeyMac/Configs/StringConfigItem.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,9 @@ extension Config {
6666
static var `default`: String = "gpt-4o-mini"
6767
static var key: String = "dev.ensan.inputmethod.azooKeyMac.preference.OpenAiModelName"
6868
}
69+
70+
/// プロンプト履歴(JSON形式で保存)
71+
struct PromptHistory: StringConfigItem {
72+
static var key: String = "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory"
73+
}
6974
}

azooKeyMac/InputController/OpenAIClient.swift

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -233,23 +233,34 @@ enum OpenAIError: LocalizedError {
233233
var errorDescription: String? {
234234
switch self {
235235
case .invalidURL:
236-
return "Invalid URL"
236+
return "Could not connect to OpenAI service. Please check your internet connection."
237237
case .noServerResponse:
238-
return "No response from server"
239-
case .invalidResponseStatus(let code, let body):
240-
return "Invalid response from server. Status code: \(code), Response body: \(body)"
241-
case .parseError(let message):
242-
return "Parse error: \(message)"
243-
case .invalidResponseStructure(let received):
244-
return "Failed to parse response structure. Received: \(received)"
238+
return "OpenAI service is not responding. Please try again later."
239+
case .invalidResponseStatus(let code, _):
240+
switch code {
241+
case 401:
242+
return "OpenAI API key is invalid. Please check your API key in preferences."
243+
case 403:
244+
return "Access denied by OpenAI. Please check your API key permissions."
245+
case 429:
246+
return "OpenAI rate limit exceeded. Please wait a moment and try again."
247+
case 500...599:
248+
return "OpenAI service is temporarily unavailable. Please try again later."
249+
default:
250+
return "OpenAI request failed. Please try again later."
251+
}
252+
case .parseError:
253+
return "Could not understand OpenAI response. Please try again."
254+
case .invalidResponseStructure:
255+
return "Received unexpected response from OpenAI. Please try again."
245256
}
246257
}
247258
}
248259

249260
// OpenAI APIクライアント
250261
enum OpenAIClient {
251262
// APIリクエストを送信する静的メソッド
252-
static func sendRequest(_ request: OpenAIRequest, apiKey: String, segmentsManager: SegmentsManager) async throws -> [String] {
263+
static func sendRequest(_ request: OpenAIRequest, apiKey: String, logger: ((String) -> Void)? = nil) async throws -> [String] {
253264
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
254265
throw OpenAIError.invalidURL
255266
}
@@ -276,18 +287,18 @@ enum OpenAIClient {
276287
}
277288

278289
// レスポンスデータの解析
279-
return try parseResponseData(data, segmentsManager: segmentsManager)
290+
return try parseResponseData(data, logger: logger)
280291
}
281292

282293
// レスポンスデータのパースを行う静的メソッド
283-
private static func parseResponseData(_ data: Data, segmentsManager: SegmentsManager) throws -> [String] {
284-
segmentsManager.appendDebugMessage("Received JSON response")
294+
private static func parseResponseData(_ data: Data, logger: ((String) -> Void)? = nil) throws -> [String] {
295+
logger?("Received JSON response")
285296

286297
let jsonObject: Any
287298
do {
288299
jsonObject = try JSONSerialization.jsonObject(with: data)
289300
} catch {
290-
segmentsManager.appendDebugMessage("Failed to parse JSON response")
301+
logger?("Failed to parse JSON response")
291302
throw OpenAIError.parseError("Failed to parse response")
292303
}
293304

@@ -303,29 +314,78 @@ enum OpenAIClient {
303314
continue
304315
}
305316

306-
segmentsManager.appendDebugMessage("Raw content string: \(contentString)")
317+
logger?("Raw content string: \(contentString)")
307318

308319
guard let contentData = contentString.data(using: .utf8) else {
309-
segmentsManager.appendDebugMessage("Failed to convert `content` string to data")
320+
logger?("Failed to convert `content` string to data")
310321
continue
311322
}
312323

313324
do {
314325
guard let parsedContent = try JSONSerialization.jsonObject(with: contentData) as? [String: [String]],
315326
let predictions = parsedContent["predictions"] else {
316-
segmentsManager.appendDebugMessage("Failed to parse `content` as expected JSON dictionary: \(contentString)")
327+
logger?("Failed to parse `content` as expected JSON dictionary: \(contentString)")
317328
continue
318329
}
319330

320-
segmentsManager.appendDebugMessage("Parsed predictions: \(predictions)")
331+
logger?("Parsed predictions: \(predictions)")
321332
allPredictions.append(contentsOf: predictions)
322333
} catch {
323-
segmentsManager.appendDebugMessage("Error parsing JSON from `content`: \(error.localizedDescription)")
334+
logger?("Error parsing JSON from `content`: \(error.localizedDescription)")
324335
}
325336
}
326337

327338
return allPredictions
328339
}
340+
341+
// Simple text transformation method for AI Transform feature
342+
static func sendTextTransformRequest(prompt: String, modelName: String, apiKey: String) async throws -> String {
343+
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
344+
throw OpenAIError.invalidURL
345+
}
346+
347+
var request = URLRequest(url: url)
348+
request.httpMethod = "POST"
349+
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
350+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
351+
352+
let body: [String: Any] = [
353+
"model": modelName,
354+
"messages": [
355+
["role": "system", "content": "You are a helpful assistant that transforms text according to user instructions."],
356+
["role": "user", "content": prompt]
357+
],
358+
"max_tokens": 150,
359+
"temperature": 0.7
360+
]
361+
362+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
363+
364+
// Send async request
365+
let (data, response) = try await URLSession.shared.data(for: request)
366+
367+
// Validate response
368+
guard let httpResponse = response as? HTTPURLResponse else {
369+
throw OpenAIError.noServerResponse
370+
}
371+
372+
guard httpResponse.statusCode == 200 else {
373+
let responseBody = String(bytes: data, encoding: .utf8) ?? "Body is not encoded in UTF-8"
374+
throw OpenAIError.invalidResponseStatus(code: httpResponse.statusCode, body: responseBody)
375+
}
376+
377+
// Parse response data
378+
let jsonObject = try JSONSerialization.jsonObject(with: data)
379+
guard let jsonDict = jsonObject as? [String: Any],
380+
let choices = jsonDict["choices"] as? [[String: Any]],
381+
let firstChoice = choices.first,
382+
let message = firstChoice["message"] as? [String: Any],
383+
let content = message["content"] as? String else {
384+
throw OpenAIError.invalidResponseStructure(jsonObject)
385+
}
386+
387+
return content.trimmingCharacters(in: .whitespacesAndNewlines)
388+
}
329389
}
330390

331391
private enum ErrorUnion: Error {

0 commit comments

Comments
 (0)