8 Commits
v1.0 ... v1.2

Author SHA1 Message Date
f2945061ca bump version 2023-04-19 17:17:09 -04:00
d3b668bbfe implement errorDescription for OpenAIError 2023-04-19 17:14:44 -04:00
a0f708859d bump version 2023-04-19 14:45:21 -04:00
60c882000f add menu items to change API key and toggle GPT-4 2023-04-19 14:40:01 -04:00
b91561d6df add error alert 2023-04-18 19:42:42 -04:00
82da6641bb Update README.md 2023-04-18 13:14:50 -04:00
22cd854c37 Update README.md 2023-04-17 13:21:25 -04:00
5c85e34b41 Update README.md
fixes #3
2023-04-09 14:51:52 -04:00
9 changed files with 114 additions and 34 deletions

View File

@ -786,7 +786,7 @@
CODE_SIGN_ENTITLEMENTS = Cheetah/Cheetah.entitlements; CODE_SIGN_ENTITLEMENTS = Cheetah/Cheetah.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"Cheetah/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Cheetah/Preview Content\"";
DEVELOPMENT_TEAM = 5JL49Y835V; DEVELOPMENT_TEAM = 5JL49Y835V;
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
@ -799,7 +799,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = org.phrack.Cheetah; PRODUCT_BUNDLE_IDENTIFIER = org.phrack.Cheetah;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -816,7 +816,7 @@
CODE_SIGN_ENTITLEMENTS = Cheetah/Cheetah.entitlements; CODE_SIGN_ENTITLEMENTS = Cheetah/Cheetah.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"Cheetah/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Cheetah/Preview Content\"";
DEVELOPMENT_TEAM = 5JL49Y835V; DEVELOPMENT_TEAM = 5JL49Y835V;
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
@ -829,7 +829,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = org.phrack.Cheetah; PRODUCT_BUNDLE_IDENTIFIER = org.phrack.Cheetah;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;

View File

@ -24,6 +24,7 @@ class AppViewModel: ObservableObject {
@Published var analyzer: ConversationAnalyzer? @Published var analyzer: ConversationAnalyzer?
@Published var answerRequest = AnswerRequest.none @Published var answerRequest = AnswerRequest.none
@Published var errorDescription: String?
@Published var transcript: String? @Published var transcript: String?
@Published var answer: String? @Published var answer: String?
@ -67,35 +68,40 @@ struct CheetahApp: App {
// Install manifest needed for the browser extension to talk to ExtensionHelper // Install manifest needed for the browser extension to talk to ExtensionHelper
_ = try? installNativeMessagingManifest() _ = try? installNativeMessagingManifest()
do { while true {
for try await request in viewModel.$answerRequest.receive(on: RunLoop.main).values { do {
if let analyzer = viewModel.analyzer { for try await request in viewModel.$answerRequest.receive(on: RunLoop.main).values {
switch request { if let analyzer = viewModel.analyzer {
case .answerQuestion: switch request {
try await analyzer.answer() case .answerQuestion:
viewModel.answer = analyzer.context[.answer] try await analyzer.answer()
viewModel.codeAnswer = analyzer.context[.codeAnswer] viewModel.answer = analyzer.context[.answer]
viewModel.answerRequest = .none viewModel.codeAnswer = analyzer.context[.codeAnswer]
viewModel.answerRequest = .none
case .refineAnswer(let selection):
try await analyzer.answer(refine: true, selection: selection) case .refineAnswer(let selection):
viewModel.answer = analyzer.context[.answer] try await analyzer.answer(refine: true, selection: selection)
viewModel.codeAnswer = analyzer.context[.codeAnswer] viewModel.answer = analyzer.context[.answer]
viewModel.answerRequest = .none viewModel.codeAnswer = analyzer.context[.codeAnswer]
viewModel.answerRequest = .none
case .analyzeCode:
try await analyzer.analyzeCode(extensionState: extensionState) case .analyzeCode:
viewModel.answer = analyzer.context[.answer] try await analyzer.analyzeCode(extensionState: extensionState)
viewModel.answerRequest = .none viewModel.answer = analyzer.context[.answer]
viewModel.answerRequest = .none
case .none:
break case .none:
break
}
} }
} }
} catch let error as ErrorResult {
viewModel.errorDescription = error.message
viewModel.answerRequest = .none
} catch {
viewModel.errorDescription = error.localizedDescription
viewModel.answerRequest = .none
} }
} catch {
viewModel.answerRequest = .none
//TODO: handle error
} }
} }
@ -111,6 +117,34 @@ struct CheetahApp: App {
} }
.windowResizability(.contentSize) .windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
.commands {
CommandGroup(replacing: .appSettings) {
Button(action: {
viewModel.authToken = nil
resetAfterSettingsChanged()
}) {
Text("Change API Key…")
}
Button(action: {
if viewModel.useGPT4 == true {
viewModel.useGPT4 = false
} else {
viewModel.useGPT4 = true
}
resetAfterSettingsChanged()
}) {
Text("Use GPT-4")
if viewModel.useGPT4 == true {
Image(systemName: "checkmark")
}
}
}
}
}
func resetAfterSettingsChanged() {
viewModel.selectedDevice = nil
viewModel.analyzer = nil
} }
func setCaptureDevice(_ device: CaptureDevice?) { func setCaptureDevice(_ device: CaptureDevice?) {

View File

@ -77,6 +77,8 @@ class OpenAIExecutor {
let text = result.choices?.first?.text let text = result.choices?.first?.text
if let text = text { if let text = text {
log(completion: text) log(completion: text)
} else if let error = result.error {
throw error
} }
return text return text
} }
@ -87,6 +89,8 @@ class OpenAIExecutor {
let content = result.choices?.first?.message.content let content = result.choices?.first?.message.content
if let content = content { if let content = content {
log(completion: content) log(completion: content)
} else if let error = result.error {
throw error
} }
return content return content
} }

View File

@ -18,7 +18,7 @@ struct AuthTokenView: View {
} }
.privacySensitive() .privacySensitive()
.frame(width: 300) .frame(width: 300)
Toggle("Use GPT-4 (access required)", isOn: $toggleValue) Toggle("Use GPT-4 (API access required)", isOn: $toggleValue)
Button("Save") { Button("Save") {
storedToken = tokenValue storedToken = tokenValue
useGPT4 = toggleValue useGPT4 = toggleValue
@ -30,7 +30,7 @@ struct AuthTokenView: View {
} }
} }
struct APIKeyView_Previews: PreviewProvider { struct AuthTokenView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
return AuthTokenView( return AuthTokenView(
storedToken: Binding.constant(nil), storedToken: Binding.constant(nil),

View File

@ -7,6 +7,9 @@ struct CoachView: View {
@State var answer: String @State var answer: String
@State var answerSelection = NSRange() @State var answerSelection = NSRange()
@State var showError = false
@State var errorDescription = ""
init(viewModel: AppViewModel) { init(viewModel: AppViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
self.answer = viewModel.answer ?? "" self.answer = viewModel.answer ?? ""
@ -55,6 +58,17 @@ struct CoachView: View {
} }
} }
} }
.onReceive(viewModel.$errorDescription) {
if let error = $0 {
self.showError = true
self.errorDescription = error
}
}
.alert(errorDescription, isPresented: $showError) {
Button("OK", role: .cancel) {
self.showError = false
}
}
HStack { HStack {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
if let transcript = viewModel.transcript { if let transcript = viewModel.transcript {

View File

@ -6,7 +6,7 @@ struct ContentView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if viewModel.authToken != nil { if viewModel.authToken?.isEmpty == false {
VStack(spacing: 16) { VStack(spacing: 16) {
switch viewModel.downloadState { switch viewModel.downloadState {
case .pending: case .pending:

View File

@ -12,6 +12,12 @@ public struct OpenAI<T: Payload>: Codable {
public let choices: [T]? public let choices: [T]?
public let usage: UsageResult? public let usage: UsageResult?
public let data: [T]? public let data: [T]?
public let error: ErrorResult?
}
public struct ErrorResult: Codable, Error {
public let message: String
public let code: String
} }
public struct TextResult: Payload { public struct TextResult: Payload {

View File

@ -9,6 +9,15 @@ public enum OpenAIError: Error {
case decodingError(error: Error) case decodingError(error: Error)
} }
extension OpenAIError: LocalizedError {
public var errorDescription: String? {
switch self {
case .genericError(let error), .decodingError(let error):
return error.localizedDescription
}
}
}
public class OpenAISwift { public class OpenAISwift {
fileprivate(set) var token: String? fileprivate(set) var token: String?
fileprivate let config: Config fileprivate let config: Config
@ -132,6 +141,9 @@ extension OpenAISwift {
let res = try JSONDecoder().decode(OpenAI<MessageResult>.self, from: success) let res = try JSONDecoder().decode(OpenAI<MessageResult>.self, from: success)
completionHandler(.success(res)) completionHandler(.success(res))
} catch { } catch {
if let resp = String(data: success, encoding: .utf8) {
print("Failed to decode response:\n", resp)
}
completionHandler(.failure(.decodingError(error: error))) completionHandler(.failure(.decodingError(error: error)))
} }
case .failure(let failure): case .failure(let failure):

View File

@ -2,7 +2,7 @@
Cheetah is an AI-powered macOS app designed to assist users during remote software engineering interviews by providing real-time, discreet coaching and live coding platform integration. Cheetah is an AI-powered macOS app designed to assist users during remote software engineering interviews by providing real-time, discreet coaching and live coding platform integration.
![Quick demo video (1:28)](https://user-images.githubusercontent.com/106342593/229961889-489e2b36-f3e6-453a-9784-f160bc1c4f8d.mp4) [Quick demo video (1:28)](https://user-images.githubusercontent.com/106342593/229961889-489e2b36-f3e6-453a-9784-f160bc1c4f8d.mp4)
<img src="https://github.com/leetcode-mafia/cheetah/raw/91cc5b89864fe28476a7e2062ede2c8322c17896/cheetah.jpg" alt="Screenshot"> <img src="https://github.com/leetcode-mafia/cheetah/raw/91cc5b89864fe28476a7e2062ede2c8322c17896/cheetah.jpg" alt="Screenshot">
@ -17,6 +17,16 @@ Whisper runs locally on your system, utilizing Georgi Gerganov's [whisper.cpp](h
## Getting started ## Getting started
### Prerequisites
Requires macOS 13.1 or later.
SDL2 must be installed or the app will crash on launch:
```shell
brew install sdl2
```
### Audio driver setup ### Audio driver setup
For the best results, ensure the audio input captures both sides of the conversation. For the best results, ensure the audio input captures both sides of the conversation.