Skip to content

Commit

Permalink
feat: allow input apikey and orgId
Browse files Browse the repository at this point in the history
store provided apikey and orgid in system keychain
  • Loading branch information
bsorrentino committed May 16, 2023
1 parent 48d2a5f commit 6cf6a56
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 43 deletions.
22 changes: 15 additions & 7 deletions AppSecureStorage/Sources/AppSecureStorage/AppSecureStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,34 @@
import SwiftUI

@propertyWrapper
struct AppSecureStorage: DynamicProperty {
@State private var value = ""
public struct AppSecureStorage: DynamicProperty {
@State private var value:String?
private let key: String
private let accessibility:KeychainItemAccessibility

var wrappedValue: String? {
public var wrappedValue: String? {
get {
KeychainWrapper.standard.string(forKey: key, withAccessibility: self.accessibility)
}

nonmutating set {
guard let newValue else {
if let newValue {
KeychainWrapper.standard.set( newValue, forKey: key, withAccessibility: self.accessibility)
}
else {
KeychainWrapper.standard.removeObject(forKey: key, withAccessibility: self.accessibility)
return
}
KeychainWrapper.standard.set( newValue, forKey: key, withAccessibility: self.accessibility)
value = newValue
}
}

init(_ key: String, accessibility:KeychainItemAccessibility = .whenUnlocked ) {
public var projectedValue: Binding<String> {
Binding(
get: { wrappedValue ?? "" },
set: { wrappedValue = $0 }
)
}
public init(_ key: String, accessibility:KeychainItemAccessibility = .whenUnlocked ) {
self.key = key
self.accessibility = accessibility
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ private let keychainItemAccessibilityLookup: [KeychainItemAccessibility:CFString
var lookup: [KeychainItemAccessibility:CFString] = [
.afterFirstUnlock: kSecAttrAccessibleAfterFirstUnlock,
.afterFirstUnlockThisDeviceOnly: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
.always: kSecAttrAccessibleAlways,
//.always: kSecAttrAccessibleAlways,
.whenPasscodeSetThisDeviceOnly: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.alwaysThisDeviceOnly : kSecAttrAccessibleAlwaysThisDeviceOnly,
//.alwaysThisDeviceOnly : kSecAttrAccessibleAlwaysThisDeviceOnly,
.whenUnlocked: kSecAttrAccessibleWhenUnlocked,
.whenUnlockedThisDeviceOnly: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
Expand Down
146 changes: 113 additions & 33 deletions PlantUML/PlantUML+OpenAI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import SwiftUI
import OpenAIKit
import AppSecureStorage

extension PlantUMLContentView {

Expand Down Expand Up @@ -78,24 +79,48 @@ class OpenAIService : ObservableObject {
}

@Published public var status: Status = .Ready
@AppSecureStorage("openaikey") var openAIKey:String?
@AppSecureStorage("openaiorg") var openAIOrg:String?

fileprivate var clipboard = LILOFixedSizeQueue<String>( maxSize: 10 )
fileprivate var prompt = LILOFixedSizeQueue<String>( maxSize: 10 )

lazy var openAI: OpenAI? = {

guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "OPENAI_API_KEY") as? String, !apiKey.isEmpty else {
// lazy var openAI: OpenAI? = {
//
// guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "OPENAI_API_KEY") as? String, !apiKey.isEmpty else {
// status = .Error("api key not found!")
// return nil
// }
// guard let orgId = Bundle.main.object(forInfoDictionaryKey: "OPENAI_ORG_ID") as? String, !orgId.isEmpty else {
// status = .Error("org id not found!")
// return nil
// }
//
// return OpenAI( Configuration(organizationId: orgId, apiKey: apiKey))
//
// }()

var openAI: OpenAI? {

guard let openAIKey else {
status = .Error("api key not found!")
return nil
}
guard let orgId = Bundle.main.object(forInfoDictionaryKey: "OPENAI_ORG_ID") as? String, !orgId.isEmpty else {
guard let openAIOrg else {
status = .Error("org id not found!")
return nil
}

return OpenAI( Configuration(organizationId: orgId, apiKey: apiKey))

}()


return OpenAI( Configuration(organizationId: openAIOrg, apiKey: openAIKey))

}

var isSettingValid:Bool {
guard let openAIKey, !openAIKey.isEmpty else { return false }
guard let openAIOrg, !openAIOrg.isEmpty else { return false }
return true
}

@MainActor
func generateEdit( input: String, instruction: String ) async -> String? {

Expand Down Expand Up @@ -133,16 +158,18 @@ class OpenAIService : ObservableObject {
struct OpenAIView : View {

enum Tab {
case Input
case Result
case Prompt
case Result
case PromptHistory
case Settings
}

@ObservedObject var service:OpenAIService
@Binding var result: String
@State var instruction:String = ""
@State private var tabs: Tab = .Input

@State private var tabs: Tab = .Prompt
@State private var hideOpenAISecrets = true

var isEditing:Bool {
if case .Editing = service.status {
return true
Expand All @@ -154,27 +181,42 @@ struct OpenAIView : View {

VStack(spacing:0) {
HStack(spacing: 10) {
Button( action: { tabs = .Input } ) {
Label( "OpenAI", systemImage: "")
}
Divider().frame(height: 20 )
Button( action: { tabs = .Prompt } ) {
Label( "Prompt", systemImage: "")
}
.disabled( !service.isSettingValid )

Divider().frame(height: 20 )
Button( action: { tabs = .PromptHistory } ) {
Label( "History", systemImage: "")
}
.disabled( !service.isSettingValid )

Divider().frame(height: 20 )
Button( action: { tabs = .Result } ) {
Label( "Result", systemImage: "")
}
.disabled( !service.isSettingValid )

Divider().frame(height: 20 )
Button( action: { tabs = .Settings } ) {
Label( "Settings", systemImage: "gearshape").labelStyle(.iconOnly)
}
}
if case .Input = tabs {
Input_Fragment
if case .Prompt = tabs {
Prompt_Fragment
.frame( minHeight: 100 )

}
if case .Result = tabs {
Result_Fragment
.disabled( !service.isSettingValid )
}
if case .Prompt = tabs {
Prompt_Fragment
if case .PromptHistory = tabs {
HistoryPrompts_Fragment
}
if case .Settings = tabs {
Settings_Fragment
}
}
.padding( EdgeInsets(top: 0, leading: 5, bottom: 5, trailing: 0))
Expand All @@ -183,10 +225,10 @@ struct OpenAIView : View {

}

// MARK: Input Extension
// MARK: Prompt Extension
extension OpenAIView {

var Input_Fragment: some View {
var Prompt_Fragment: some View {

ZStack(alignment: .topTrailing ) {

Expand Down Expand Up @@ -274,17 +316,22 @@ extension OpenAIView {

}

// MARK: Prompt Extension
// MARK: History Extension
extension OpenAIView {

var Prompt_Fragment: some View {
var HistoryPrompts_Fragment: some View {

List( service.prompt.elements, id: \.self ) { prompt in
HStack {
Text( prompt )
CopyToClipboardButton( value: prompt )
HStack {
List( service.prompt.elements, id: \.self ) { prompt in
HStack {
Text( prompt )
CopyToClipboardButton( value: prompt )
}
}
}
.border(.gray)
.padding()

}

}
Expand All @@ -293,16 +340,49 @@ extension OpenAIView {
extension OpenAIView {

var Result_Fragment: some View {

ScrollView {
Text( result )
.font( .system(size: 14.0, design: .monospaced) )
HStack {
Spacer()
ScrollView {
Text( result )
.font( .system(size: 14.0, design: .monospaced) )
.padding()
}
Spacer()
}
.border(.gray)
.padding()
}

}

// MARK: Settings Extension
extension OpenAIView {

var Settings_Fragment: some View {
Form {
Section {
SecureToggleField( "Api Key", value: service.$openAIKey, hidden: hideOpenAISecrets)
SecureToggleField( "Org Id", value: service.$openAIOrg, hidden: hideOpenAISecrets)

}
header: {
HStack {
Text("OpenAI Secrets")
HideToggleButton(hidden: $hideOpenAISecrets)
}
}
footer: {
HStack {
Spacer()
Text("these data will be stored in onboard secure keychain")
}
}
}

}

}

struct OpenAIView_Previews: PreviewProvider {

struct Item : RawRepresentable {
Expand Down
3 changes: 2 additions & 1 deletion PlantUML/PlantUMLContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Combine
import PlantUMLFramework
import PlantUMLKeyboard
import LineEditor

import AppSecureStorage
//
// [Managing Focus in SwiftUI List Views](https://peterfriese.dev/posts/swiftui-list-focus/)
//
Expand All @@ -34,6 +34,7 @@ struct PlantUMLContentView: View {
@State private var isEditorVisible = true
//@State private var isPreviewVisible = false
private var isDiagramVisible:Bool { !isEditorVisible}

@State var isOpenAIVisible = false

@State var keyboardTab: String = "general"
Expand Down
85 changes: 85 additions & 0 deletions PlantUML/View+Secure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// View+Secure.swift
// PlantUMLApp
//
// Created by Bartolomeo Sorrentino on 16/05/23.
//

import SwiftUI

public struct SecureToggleField : View {

var title:String
@Binding var value:String
var hidden:Bool

public init( _ title: String, value: Binding<String>, hidden: Bool ) {
self.title = title
self._value = value
self.hidden = hidden
}

public var body: some View {
Group {
if( hidden ) {
SecureField( title, text:$value)
}
else {
TextField( title, text:$value)
}
}
}
}


public struct HideToggleButton : View {

@Environment(\.colorScheme) var colorScheme: ColorScheme

@Binding var hidden:Bool

public init( hidden: Binding<Bool> ) {
self._hidden = hidden
}

public var body: some View {
Button( action: {
self.hidden.toggle()
}) {
Group {
if( self.hidden ) {
Image( systemName: "eye.slash")
}
else {
Image( systemName: "eye")
}
}
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
}
.buttonStyle(PlainButtonStyle())

}
}


struct View_Secure_Previews: PreviewProvider {

struct PasswordField : View {
@State var hidden = true
var body: some View {
HStack(spacing: 5) {
SecureToggleField( "give me password", value: .constant("test"), hidden: hidden)
.fixedSize()
.border(.green)
HideToggleButton( hidden: $hidden )
}
}

}
static var previews: some View {

PasswordField()

}
}

0 comments on commit 6cf6a56

Please sign in to comment.