Skip to content

Commit

Permalink
Update GenAI API and implement folder picker UI
Browse files Browse the repository at this point in the history
  • Loading branch information
vraspar committed Dec 18, 2024
1 parent 1848479 commit a51d83b
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 148 deletions.
Binary file added mobile/examples/phi-3/ios/LocalLLM/IMG_1014.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
287 changes: 157 additions & 130 deletions mobile/examples/phi-3/ios/LocalLLM/LocalLLM/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,155 +3,182 @@

import SwiftUI


struct Message: Identifiable {
let id = UUID()
var text: String
let isUser: Bool
let id = UUID()
var text: String
let isUser: Bool
}

struct ContentView: View {
@State private var userInput: String = ""
@State private var messages: [Message] = [] // Store chat messages locally
@State private var isGenerating: Bool = false // Track token generation state
@State private var stats: String = "" // token genetation stats
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""

private let generator = GenAIGenerator()

var body: some View {
VStack {
// ChatBubbles
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ForEach(messages) { message in
ChatBubble(text: message.text, isUser: message.isUser)
.padding(.horizontal, 20)
}
if !stats.isEmpty {
Text(stats)
.font(.footnote)
.foregroundColor(.gray)
.padding(.horizontal, 20)
.padding(.top, 5)
.multilineTextAlignment(.center)
}
}
.padding(.top, 20)
}
@State private var userInput: String = ""
@State private var messages: [Message] = [] // Store chat messages locally
@State private var isGenerating: Bool = false // Track token generation state
@State private var stats: String = "" // token genetation stats
@State private var showAlert: Bool = false
@State private var errorMessage: String = ""


// User input
HStack {
TextField("Type your message...", text: $userInput)
.padding()
.background(Color(.systemGray6))
.cornerRadius(20)
.padding(.horizontal)

Button(action: {
// Check for non-empty input
guard !userInput.trimmingCharacters(in: .whitespaces).isEmpty else { return }

messages.append(Message(text: userInput, isUser: true))
messages.append(Message(text: "", isUser: false)) // Placeholder for AI response


// clear previously generated tokens
SharedTokenUpdater.shared.clearTokens()

let prompt = userInput
userInput = ""
isGenerating = true


DispatchQueue.global(qos: .background).async {
generator.generate(prompt)
}
}) {
Image(systemName: "paperplane.fill")
.foregroundColor(.white)
.padding()
.background(isGenerating ? Color.gray : Color.pastelGreen)
.clipShape(Circle())
.padding(.trailing, 10)
}
.disabled(isGenerating)
}
.padding(.bottom, 20)
}
.background(Color(.systemGroupedBackground))
.edgesIgnoringSafeArea(.bottom)
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationCompleted"))) { _ in
isGenerating = false // Re-enable the button when token generation is complete
}
.onReceive(SharedTokenUpdater.shared.$decodedTokens) { tokens in
// update model response
if let lastIndex = messages.lastIndex(where: { !$0.isUser }) {
let combinedText = tokens.joined(separator: "")
messages[lastIndex].text = combinedText
}
@State private var showFolderPicker: Bool = false
@State private var selectedFolderURL: URL?

private let generator = GenAIGenerator()

var body: some View {
VStack {
// ChatBubbles
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ForEach(messages) { message in
ChatBubble(text: message.text, isUser: message.isUser)
.padding(.horizontal, 20)
}
if !stats.isEmpty {
Text(stats)
.font(.footnote)
.foregroundColor(.gray)
.padding(.horizontal, 20)
.padding(.top, 5)
.multilineTextAlignment(.center)
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationStats"))) { notification in
if let userInfo = notification.userInfo,
let promptProcRate = userInfo["promptProcRate"] as? Double,
let tokenGenRate = userInfo["tokenGenRate"] as? Double {
stats = String(format: "Token generation rate: %.2f tokens/s. Prompt processing rate: %.2f tokens/s", tokenGenRate, promptProcRate)
}
.padding(.top, 20)
}

HStack {
Button(action: {
showFolderPicker = true
}) {
HStack {
Image(systemName: "folder")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
}
.padding()
.background(Color.pastelGreen)
.cornerRadius(10)
.shadow(radius: 2)
.padding(.leading, 10)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationError"))) { notification in
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String {
errorMessage = error
isGenerating = false
showAlert = true
.sheet(isPresented: $showFolderPicker) {
FolderPicker { folderURL in
if let folderURL = folderURL {
let folderPath = folderURL.path
print("Selected folder: \(folderPath)")
DispatchQueue.global(qos: .background).async {
generator.setModelFolderPath(folderPath)
}
}
}
}.help("Select a folder to set the model path")

TextField("Type your message...", text: $userInput)
.padding()
.background(Color(.systemGray6))
.cornerRadius(20)
.padding(.horizontal)

Button(action: {
// Check for non-empty input
guard !userInput.trimmingCharacters(in: .whitespaces).isEmpty else { return }

messages.append(Message(text: userInput, isUser: true))
messages.append(Message(text: "", isUser: false)) // Placeholder for AI response

// clear previously generated tokens
SharedTokenUpdater.shared.clearTokens()

let prompt = userInput
userInput = ""
isGenerating = true

DispatchQueue.global(qos: .background).async {
generator.generate(prompt)
}
}) {
Image(systemName: "paperplane.fill")
.foregroundColor(.white)
.padding()
.background(isGenerating ? Color.gray : Color.pastelGreen)
.clipShape(Circle())
}
.alert(isPresented: $showAlert) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("OK"))
)
}

.disabled(isGenerating)
}
.padding(.bottom, 20)
}
.background(Color(.systemGroupedBackground))
.edgesIgnoringSafeArea(.bottom)
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationCompleted"))) { _ in
isGenerating = false // Re-enable the button when token generation is complete
}
.onReceive(SharedTokenUpdater.shared.$decodedTokens) { tokens in
// update model response
if let lastIndex = messages.lastIndex(where: { !$0.isUser }) {
let combinedText = tokens.joined(separator: "")
messages[lastIndex].text = combinedText
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationStats"))) { notification in
if let userInfo = notification.userInfo,
let promptProcRate = userInfo["promptProcRate"] as? Double,
let tokenGenRate = userInfo["tokenGenRate"] as? Double
{
stats = String(
format: "Token generation rate: %.2f tokens/s. Prompt processing rate: %.2f tokens/s", tokenGenRate,
promptProcRate)
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationError"))) { notification in
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String {
errorMessage = error
isGenerating = false
showAlert = true
}
}
.alert(isPresented: $showAlert) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("OK"))
)
}

}
}

struct ChatBubble: View {
var text: String
var isUser: Bool

var body: some View {
HStack {
if isUser {
Spacer()
Text(text)
.padding()
.background(Color.pastelGreen)
.foregroundColor(.white)
.cornerRadius(25)
.padding(.horizontal, 10)
} else {
Text(text)
.padding()
.background(Color(.systemGray5))
.foregroundColor(.black)
.cornerRadius(25)
.padding(.horizontal, 10)
Spacer()
}
}
var text: String
var isUser: Bool

var body: some View {
HStack {
if isUser {
Spacer()
Text(text)
.padding()
.background(Color.pastelGreen)
.foregroundColor(.white)
.cornerRadius(25)
.padding(.horizontal, 10)
} else {
Text(text)
.padding()
.background(Color(.systemGray5))
.foregroundColor(.black)
.cornerRadius(25)
.padding(.horizontal, 10)
Spacer()
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
static var previews: some View {
ContentView()
}
}

// Extension for a pastel green color
extension Color {
static let pastelGreen = Color(red: 0.6, green: 0.9, blue: 0.6)
static let pastelGreen = Color(red: 0.6, green: 0.9, blue: 0.6)
}
38 changes: 38 additions & 0 deletions mobile/examples/phi-3/ios/LocalLLM/LocalLLM/FolderPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import SwiftUI
import UIKit

struct FolderPicker: UIViewControllerRepresentable {
var onPick: (URL?) -> Void

func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder])
picker.allowsMultipleSelection = false
picker.delegate = context.coordinator
return picker
}

func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator(onPick: onPick)
}

class Coordinator: NSObject, UIDocumentPickerDelegate {
let onPick: (URL?) -> Void

init(onPick: @escaping (URL?) -> Void) {
self.onPick = onPick
}

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
onPick(urls.first)
}

func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
onPick(nil)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN

@interface GenAIGenerator : NSObject

- (void)setModelFolderPath:(nonnull NSString*)modelPath;
- (void)generate:(NSString *)input_user_question;

@end
Expand Down
Loading

0 comments on commit a51d83b

Please sign in to comment.