Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2945061ca | |||
| d3b668bbfe | |||
| a0f708859d | |||
| 60c882000f | |||
| b91561d6df | |||
| 82da6641bb | |||
| 22cd854c37 | |||
| 5c85e34b41 |
@ -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;
|
||||||
|
|||||||
@ -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?) {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
12
README.md
12
README.md
@ -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)
|
||||||
|
|
||||||
<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.
|
||||||
|
|||||||
Reference in New Issue
Block a user