Skip to content

Commit a51d83b

Browse files
committed
Update GenAI API and implement folder picker UI
1 parent 1848479 commit a51d83b

File tree

5 files changed

+222
-148
lines changed

5 files changed

+222
-148
lines changed
Loading

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

+157-130
Original file line numberDiff line numberDiff line change
@@ -3,155 +3,182 @@
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 {
14-
@State private var userInput: String = ""
15-
@State private var messages: [Message] = [] // Store chat messages locally
16-
@State private var isGenerating: Bool = false // Track token generation state
17-
@State private var stats: String = "" // token genetation stats
18-
@State private var showAlert: Bool = false
19-
@State private var errorMessage: String = ""
20-
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-
}
13+
@State private var userInput: String = ""
14+
@State private var messages: [Message] = [] // Store chat messages locally
15+
@State private var isGenerating: Bool = false // Track token generation state
16+
@State private var stats: String = "" // token genetation stats
17+
@State private var showAlert: Bool = false
18+
@State private var errorMessage: String = ""
4319

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

121148
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-
}
149+
var text: String
150+
var isUser: Bool
151+
152+
var body: some View {
153+
HStack {
154+
if isUser {
155+
Spacer()
156+
Text(text)
157+
.padding()
158+
.background(Color.pastelGreen)
159+
.foregroundColor(.white)
160+
.cornerRadius(25)
161+
.padding(.horizontal, 10)
162+
} else {
163+
Text(text)
164+
.padding()
165+
.background(Color(.systemGray5))
166+
.foregroundColor(.black)
167+
.cornerRadius(25)
168+
.padding(.horizontal, 10)
169+
Spacer()
170+
}
145171
}
172+
}
146173
}
147174

148175
struct ContentView_Previews: PreviewProvider {
149-
static var previews: some View {
150-
ContentView()
151-
}
176+
static var previews: some View {
177+
ContentView()
178+
}
152179
}
153180

154181
// Extension for a pastel green color
155182
extension Color {
156-
static let pastelGreen = Color(red: 0.6, green: 0.9, blue: 0.6)
183+
static let pastelGreen = Color(red: 0.6, green: 0.9, blue: 0.6)
157184
}
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)