Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: use server (and some stuff to handle dall-e image models and o1- reasoning API models #98

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions fullmoon.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client.local</key>
<true/>
</dict>
</plist>
27 changes: 22 additions & 5 deletions fullmoon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0033E7FF2D4718D9001D469E /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 0033E7FE2D4718D9001D469E /* Highlightr */; };
860F26A42CBC31D6004E8D40 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 860F26A32CBC31D6004E8D40 /* MarkdownUI */; };
869B97622D0DD46B0078DF5A /* MLXLMCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 869B97612D0DD46B0078DF5A /* MLXLMCommon */; };
869B97642D0DD4D80078DF5A /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = 869B97632D0DD4D80078DF5A /* MLXLLM */; };
Expand Down Expand Up @@ -42,6 +43,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0033E7FF2D4718D9001D469E /* Highlightr in Frameworks */,
869B97642D0DD4D80078DF5A /* MLXLLM in Frameworks */,
869B97622D0DD46B0078DF5A /* MLXLMCommon in Frameworks */,
860F26A42CBC31D6004E8D40 /* MarkdownUI in Frameworks */,
Expand Down Expand Up @@ -98,6 +100,7 @@
860F26A32CBC31D6004E8D40 /* MarkdownUI */,
869B97612D0DD46B0078DF5A /* MLXLMCommon */,
869B97632D0DD4D80078DF5A /* MLXLLM */,
0033E7FE2D4718D9001D469E /* Highlightr */,
);
productName = fullmoon;
productReference = 860E9CCE2CB055B000C5BB52 /* fullmoon.app */;
Expand Down Expand Up @@ -130,6 +133,7 @@
packageReferences = (
860E9CE22CB0564600C5BB52 /* XCRemoteSwiftPackageReference "mlx-swift-examples" */,
860F26A22CBC31D6004E8D40 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
0033E7FD2D4718D9001D469E /* XCRemoteSwiftPackageReference "Highlightr" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 860E9CCF2CB055B000C5BB52 /* Products */;
Expand Down Expand Up @@ -285,7 +289,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"fullmoon/Preview Content\"";
DEVELOPMENT_TEAM = 2VT466P8NK;
DEVELOPMENT_TEAM = MMRT976ZJS;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -307,7 +311,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = me.mainfra.fullmoon;
PRODUCT_BUNDLE_IDENTIFIER = me.bleu.moon;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = NO;
SDKROOT = auto;
Expand All @@ -331,13 +335,13 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"fullmoon/Preview Content\"";
DEVELOPMENT_TEAM = 2VT466P8NK;
DEVELOPMENT_TEAM = MMRT976ZJS;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = fullmoon/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = fullmoon;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand All @@ -353,7 +357,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = me.mainfra.fullmoon;
PRODUCT_BUNDLE_IDENTIFIER = me.bleu.moon;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = NO;
SDKROOT = auto;
Expand Down Expand Up @@ -392,6 +396,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
0033E7FD2D4718D9001D469E /* XCRemoteSwiftPackageReference "Highlightr" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/raspu/Highlightr.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.2.1;
};
};
860E9CE22CB0564600C5BB52 /* XCRemoteSwiftPackageReference "mlx-swift-examples" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ml-explore/mlx-swift-examples/";
Expand All @@ -411,6 +423,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
0033E7FE2D4718D9001D469E /* Highlightr */ = {
isa = XCSwiftPackageProductDependency;
package = 0033E7FD2D4718D9001D469E /* XCRemoteSwiftPackageReference "Highlightr" */;
productName = Highlightr;
};
860F26A32CBC31D6004E8D40 /* MarkdownUI */ = {
isa = XCSwiftPackageProductDependency;
package = 860F26A22CBC31D6004E8D40 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
Expand Down
2 changes: 0 additions & 2 deletions fullmoon/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ struct ContentView: View {
}
}
.environmentObject(appManager)
.environment(llm)
.task {
if appManager.installedModels.count == 0 {
showOnboarding.toggle()
Expand Down Expand Up @@ -71,7 +70,6 @@ struct ContentView: View {
.sheet(isPresented: $showSettings) {
SettingsView(currentThread: $currentThread)
.environmentObject(appManager)
.environment(llm)
.presentationDragIndicator(.hidden)
.if(appManager.userInterfaceIdiom == .phone) { view in
view.presentationDetents([.medium])
Expand Down
5 changes: 5 additions & 0 deletions fullmoon/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
207 changes: 198 additions & 9 deletions fullmoon/Models/Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@ class AppManager: ObservableObject {
@AppStorage("shouldPlayHaptics") var shouldPlayHaptics = true
@AppStorage("numberOfVisits") var numberOfVisits = 0
@AppStorage("numberOfVisitsOfLastRequest") var numberOfVisitsOfLastRequest = 0
@AppStorage("isUsingServer") var isUsingServer = false
@AppStorage("serverAPIKey") var serverAPIKeyStorage = ""
@AppStorage("selectedServerId") var selectedServerIdString: String?

@Published var servers: [ServerConfig] = []
@Published var selectedServerId: UUID? {
didSet {
selectedServerIdString = selectedServerId?.uuidString
// Reset current model when switching servers
currentModelName = nil
}
}

private let serversKey = "savedServers"

var currentServerURL: String {
if let server = currentServer {
return server.url
}
return ""
}

var currentServerAPIKey: String {
if let server = currentServer {
return server.apiKey
}
return serverAPIKeyStorage
}

var currentServer: ServerConfig? {
guard let id = selectedServerId else { return nil }
return servers.first { $0.id == id }
}

var userInterfaceIdiom: LayoutType {
#if os(visionOS)
Expand All @@ -43,7 +76,34 @@ class AppManager: ObservableObject {
}
}

// Add a dictionary to cache models for each server
@AppStorage("cachedServerModels") private var cachedServerModelsData: Data?
@Published private(set) var cachedServerModels: [UUID: [String]] = [:] {
didSet {
// Save to UserDefaults whenever cache updates
if let encoded = try? JSONEncoder().encode(cachedServerModels) {
cachedServerModelsData = encoded
}
}
}

init() {
// First load saved servers
loadServers()

// Then restore selected server from saved ID string
if let savedIdString = selectedServerIdString,
let savedId = UUID(uuidString: savedIdString) {
selectedServerId = savedId
}

// If we have servers but no selection, select the first one
if selectedServerId == nil && !servers.isEmpty {
selectedServerId = servers.first?.id
}

// Finally load cached models
loadCachedModels()
loadInstalledModelsFromUserDefaults()
}

Expand Down Expand Up @@ -124,6 +184,128 @@ class AppManager: ObservableObject {
return "moonphase.new.moon" // New Moon (fallback)
}
}

func modelSource() -> ModelSource {
isUsingServer ? .server : .local
}

private func loadServers() {
if let data = UserDefaults.standard.data(forKey: serversKey),
let decodedServers = try? JSONDecoder().decode([ServerConfig].self, from: data) {
servers = decodedServers
}
}

func saveServers() {
if let encoded = try? JSONEncoder().encode(servers) {
UserDefaults.standard.set(encoded, forKey: serversKey)
}
}

// Update server saving to happen immediately when servers change
func addServer(_ server: ServerConfig) {
servers.append(server)
saveServers()

// Auto-select the first server if none is selected
if selectedServerId == nil {
selectedServerId = server.id
}
}

func removeServer(_ server: ServerConfig) {
servers.removeAll { $0.id == server.id }
saveServers()

// Clear selection if removed server was selected
if selectedServerId == server.id {
selectedServerId = servers.first?.id
}
}

func updateServer(_ server: ServerConfig) {
if let index = servers.firstIndex(where: { $0.id == server.id }) {
servers[index] = server
saveServers()
}
}

func addServerWithMetadata(_ server: ServerConfig) async {
var updatedServer = server

// Try to fetch server metadata
let metadata = await fetchServerMetadata(url: server.url)
if let title = metadata.title {
updatedServer.name = title
}

await MainActor.run {
addServer(updatedServer)
selectedServerId = updatedServer.id
}
}

private func fetchServerMetadata(url: String) async -> (title: String?, version: String?) {
guard var baseURL = URL(string: url) else { return (nil, nil) }
// Remove /v1 or other API paths to get base URL
baseURL = baseURL.deletingLastPathComponent()

do {
let (data, _) = try await URLSession.shared.data(from: baseURL)
if let html = String(data: data, encoding: .utf8) {
// Extract title from HTML metadata
let title = extractTitle(from: html)
let version = extractVersion(from: html)
return (title, version)
}
} catch {
print("Error fetching server metadata: \(error)")
}
return (nil, nil)
}

private func extractTitle(from html: String) -> String? {
// Basic title extraction - could be made more robust
if let titleRange = html.range(of: "<title>.*?</title>", options: .regularExpression) {
let title = html[titleRange]
.replacingOccurrences(of: "<title>", with: "")
.replacingOccurrences(of: "</title>", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
return title.isEmpty ? nil : title
}
return nil
}

private func extractVersion(from html: String) -> String? {
// Basic version extraction - could be made more robust
if let metaRange = html.range(of: "content=\".*?version.*?\"", options: .regularExpression) {
let version = html[metaRange]
.replacingOccurrences(of: "content=\"", with: "")
.replacingOccurrences(of: "\"", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
return version.isEmpty ? nil : version
}
return nil
}

private func loadCachedModels() {
if let data = cachedServerModelsData,
let decoded = try? JSONDecoder().decode([String: [String]].self, from: data) {
// Convert string keys back to UUIDs
cachedServerModels = Dictionary(uniqueKeysWithValues: decoded.compactMap { key, value in
guard let uuid = UUID(uuidString: key) else { return nil }
return (uuid, value)
})
}
}

func updateCachedModels(serverId: UUID, models: [String]) {
cachedServerModels[serverId] = models
}

func getCachedModels(for serverId: UUID) -> [String] {
return cachedServerModels[serverId] ?? []
}
}

enum Role: String, Codable {
Expand Down Expand Up @@ -153,23 +335,24 @@ class Message {
}

@Model
final class Thread: Sendable {
@Attribute(.unique) var id: UUID
var title: String?
final class Thread {
@Attribute(.unique) let id: UUID
var timestamp: Date

@Relationship var messages: [Message] = []

var sortedMessages: [Message] {
return messages.sorted { $0.timestamp < $1.timestamp }
}
var messages: [Message]

init() {
self.id = UUID()
self.timestamp = Date()
self.messages = []
}

var sortedMessages: [Message] {
messages.sorted { $0.timestamp < $1.timestamp }
}
}

extension Thread: @unchecked Sendable {}

enum AppTintColor: String, CaseIterable {
case monochrome, blue, brown, gray, green, indigo, mint, orange, pink, purple, red, teal, yellow

Expand Down Expand Up @@ -257,3 +440,9 @@ enum AppFontSize: String, CaseIterable {
}
}
}

enum ModelSource {
case local
case server
}

Loading