Skip to content

Commit 58a47fd

Browse files
committed
Update GenAI API and implement folder picker UI
1 parent a0170fb commit 58a47fd

File tree

5 files changed

+212
-141
lines changed

5 files changed

+212
-141
lines changed
Loading

mobile/examples/phi-3/ios/LocalLLM/LocalLLM/ContentView.swift

+147-123
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33

44
import SwiftUI
55

6-
76
struct Message: Identifiable {
8-
let id = UUID()
9-
var text: String
10-
let isUser: Bool
7+
let id = UUID()
8+
var text: String
9+
let isUser: Bool
1110
}
1211

1312
struct ContentView: View {
@@ -18,140 +17,165 @@ struct ContentView: View {
1817
@State private var showAlert: Bool = false
1918
@State private var errorMessage: String = ""
2019

21-
private let generator = GenAIGenerator()
22-
23-
var body: some View {
24-
VStack {
25-
// ChatBubbles
26-
ScrollView {
27-
VStack(alignment: .leading, spacing: 20) {
28-
ForEach(messages) { message in
29-
ChatBubble(text: message.text, isUser: message.isUser)
30-
.padding(.horizontal, 20)
31-
}
32-
if !stats.isEmpty {
33-
Text(stats)
34-
.font(.footnote)
35-
.foregroundColor(.gray)
36-
.padding(.horizontal, 20)
37-
.padding(.top, 5)
38-
.multilineTextAlignment(.center)
39-
}
40-
}
41-
.padding(.top, 20)
42-
}
20+
private let generator = GenAIGenerator()
4321

44-
45-
// User input
46-
HStack {
47-
TextField("Type your message...", text: $userInput)
48-
.padding()
49-
.background(Color(.systemGray6))
50-
.cornerRadius(20)
51-
.padding(.horizontal)
52-
53-
Button(action: {
54-
// Check for non-empty input
55-
guard !userInput.trimmingCharacters(in: .whitespaces).isEmpty else { return }
56-
57-
messages.append(Message(text: userInput, isUser: true))
58-
messages.append(Message(text: "", isUser: false)) // Placeholder for AI response
59-
60-
61-
// clear previously generated tokens
62-
SharedTokenUpdater.shared.clearTokens()
63-
64-
let prompt = userInput
65-
userInput = ""
66-
isGenerating = true
67-
68-
69-
DispatchQueue.global(qos: .background).async {
70-
generator.generate(prompt)
71-
}
72-
}) {
73-
Image(systemName: "paperplane.fill")
74-
.foregroundColor(.white)
75-
.padding()
76-
.background(isGenerating ? Color.gray : Color.pastelGreen)
77-
.clipShape(Circle())
78-
.padding(.trailing, 10)
79-
}
80-
.disabled(isGenerating)
81-
}
82-
.padding(.bottom, 20)
83-
}
84-
.background(Color(.systemGroupedBackground))
85-
.edgesIgnoringSafeArea(.bottom)
86-
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationCompleted"))) { _ in
87-
isGenerating = false // Re-enable the button when token generation is complete
22+
var body: some View {
23+
VStack {
24+
// ChatBubbles
25+
ScrollView {
26+
VStack(alignment: .leading, spacing: 20) {
27+
ForEach(messages) { message in
28+
ChatBubble(text: message.text, isUser: message.isUser)
29+
.padding(.horizontal, 20)
30+
}
31+
if !stats.isEmpty {
32+
Text(stats)
33+
.font(.footnote)
34+
.foregroundColor(.gray)
35+
.padding(.horizontal, 20)
36+
.padding(.top, 5)
37+
.multilineTextAlignment(.center)
38+
}
8839
}
89-
.onReceive(SharedTokenUpdater.shared.$decodedTokens) { tokens in
90-
// update model response
91-
if let lastIndex = messages.lastIndex(where: { !$0.isUser }) {
92-
let combinedText = tokens.joined(separator: "")
93-
messages[lastIndex].text = combinedText
94-
}
95-
}
96-
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationStats"))) { notification in
97-
if let userInfo = notification.userInfo,
98-
let promptProcRate = userInfo["promptProcRate"] as? Double,
99-
let tokenGenRate = userInfo["tokenGenRate"] as? Double {
100-
stats = String(format: "Token generation rate: %.2f tokens/s. Prompt processing rate: %.2f tokens/s", tokenGenRate, promptProcRate)
101-
}
40+
.padding(.top, 20)
41+
}
42+
43+
HStack {
44+
Button(action: {
45+
showFolderPicker = true
46+
}) {
47+
HStack {
48+
Image(systemName: "folder")
49+
.resizable()
50+
.scaledToFit()
51+
.frame(width: 20, height: 20)
52+
}
53+
.padding()
54+
.background(Color.pastelGreen)
55+
.cornerRadius(10)
56+
.shadow(radius: 2)
57+
.padding(.leading, 10)
10258
}
103-
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationError"))) { notification in
104-
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String {
105-
errorMessage = error
106-
isGenerating = false
107-
showAlert = true
59+
.sheet(isPresented: $showFolderPicker) {
60+
FolderPicker { folderURL in
61+
if let folderURL = folderURL {
62+
let folderPath = folderURL.path
63+
print("Selected folder: \(folderPath)")
64+
DispatchQueue.global(qos: .background).async {
65+
generator.setModelFolderPath(folderPath)
66+
}
10867
}
68+
}
69+
}.help("Select a folder to set the model path")
70+
71+
TextField("Type your message...", text: $userInput)
72+
.padding()
73+
.background(Color(.systemGray6))
74+
.cornerRadius(20)
75+
.padding(.horizontal)
76+
77+
Button(action: {
78+
// Check for non-empty input
79+
guard !userInput.trimmingCharacters(in: .whitespaces).isEmpty else { return }
80+
81+
messages.append(Message(text: userInput, isUser: true))
82+
messages.append(Message(text: "", isUser: false)) // Placeholder for AI response
83+
84+
// clear previously generated tokens
85+
SharedTokenUpdater.shared.clearTokens()
86+
87+
let prompt = userInput
88+
userInput = ""
89+
isGenerating = true
90+
91+
DispatchQueue.global(qos: .background).async {
92+
generator.generate(prompt)
93+
}
94+
}) {
95+
Image(systemName: "paperplane.fill")
96+
.foregroundColor(.white)
97+
.padding()
98+
.background(isGenerating ? Color.gray : Color.pastelGreen)
99+
.clipShape(Circle())
109100
}
110-
.alert(isPresented: $showAlert) {
111-
Alert(
112-
title: Text("Error"),
113-
message: Text(errorMessage),
114-
dismissButton: .default(Text("OK"))
115-
)
116-
}
117-
101+
.disabled(isGenerating)
102+
}
103+
.padding(.bottom, 20)
104+
}
105+
.background(Color(.systemGroupedBackground))
106+
.edgesIgnoringSafeArea(.bottom)
107+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationCompleted"))) { _ in
108+
isGenerating = false // Re-enable the button when token generation is complete
109+
}
110+
.onReceive(SharedTokenUpdater.shared.$decodedTokens) { tokens in
111+
// update model response
112+
if let lastIndex = messages.lastIndex(where: { !$0.isUser }) {
113+
let combinedText = tokens.joined(separator: "")
114+
messages[lastIndex].text = combinedText
115+
}
116+
}
117+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationStats"))) { notification in
118+
if let userInfo = notification.userInfo,
119+
let promptProcRate = userInfo["promptProcRate"] as? Double,
120+
let tokenGenRate = userInfo["tokenGenRate"] as? Double
121+
{
122+
stats = String(
123+
format: "Token generation rate: %.2f tokens/s. Prompt processing rate: %.2f tokens/s", tokenGenRate,
124+
promptProcRate)
125+
}
126+
}
127+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("TokenGenerationError"))) { notification in
128+
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String {
129+
errorMessage = error
130+
isGenerating = false
131+
showAlert = true
132+
}
118133
}
134+
.alert(isPresented: $showAlert) {
135+
Alert(
136+
title: Text("Error"),
137+
message: Text(errorMessage),
138+
dismissButton: .default(Text("OK"))
139+
)
140+
}
141+
142+
}
119143
}
120144

121145
struct ChatBubble: View {
122-
var text: String
123-
var isUser: Bool
124-
125-
var body: some View {
126-
HStack {
127-
if isUser {
128-
Spacer()
129-
Text(text)
130-
.padding()
131-
.background(Color.pastelGreen)
132-
.foregroundColor(.white)
133-
.cornerRadius(25)
134-
.padding(.horizontal, 10)
135-
} else {
136-
Text(text)
137-
.padding()
138-
.background(Color(.systemGray5))
139-
.foregroundColor(.black)
140-
.cornerRadius(25)
141-
.padding(.horizontal, 10)
142-
Spacer()
143-
}
144-
}
146+
var text: String
147+
var isUser: Bool
148+
149+
var body: some View {
150+
HStack {
151+
if isUser {
152+
Spacer()
153+
Text(text)
154+
.padding()
155+
.background(Color.pastelGreen)
156+
.foregroundColor(.white)
157+
.cornerRadius(25)
158+
.padding(.horizontal, 10)
159+
} else {
160+
Text(text)
161+
.padding()
162+
.background(Color(.systemGray5))
163+
.foregroundColor(.black)
164+
.cornerRadius(25)
165+
.padding(.horizontal, 10)
166+
Spacer()
167+
}
145168
}
169+
}
146170
}
147171

148172
struct ContentView_Previews: PreviewProvider {
149-
static var previews: some View {
150-
ContentView()
151-
}
173+
static var previews: some View {
174+
ContentView()
175+
}
152176
}
153177

154178
// Extension for a pastel green color
155179
extension Color {
156-
static let pastelGreen = Color(red: 0.6, green: 0.9, blue: 0.6)
180+
static let pastelGreen = Color(red: 0.6, green: 0.9, blue: 0.6)
157181
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import SwiftUI
5+
import UIKit
6+
7+
struct FolderPicker: UIViewControllerRepresentable {
8+
var onPick: (URL?) -> Void
9+
10+
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
11+
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder])
12+
picker.allowsMultipleSelection = false
13+
picker.delegate = context.coordinator
14+
return picker
15+
}
16+
17+
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
18+
19+
func makeCoordinator() -> Coordinator {
20+
Coordinator(onPick: onPick)
21+
}
22+
23+
class Coordinator: NSObject, UIDocumentPickerDelegate {
24+
let onPick: (URL?) -> Void
25+
26+
init(onPick: @escaping (URL?) -> Void) {
27+
self.onPick = onPick
28+
}
29+
30+
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
31+
onPick(urls.first)
32+
}
33+
34+
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
35+
onPick(nil)
36+
}
37+
}
38+
}

mobile/examples/phi-3/ios/LocalLLM/LocalLLM/GenAIGenerator.h

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
1111

1212
@interface GenAIGenerator : NSObject
1313

14+
- (void)setModelFolderPath:(nonnull NSString*)modelPath;
1415
- (void)generate:(NSString *)input_user_question;
1516

1617
@end

0 commit comments

Comments
 (0)