Skip to content
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
25 changes: 21 additions & 4 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,7 +335,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 @@ -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
19 changes: 10 additions & 9 deletions fullmoon/Models/Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,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
132 changes: 132 additions & 0 deletions fullmoon/Views/Chat/CodeBlockView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import SwiftUI
import MarkdownUI
import Highlightr

private let languageMap: [String: String] = [
"js": "javascript",
"ts": "typescript",
"py": "python",
"rb": "ruby",
"shell": "bash",
"sh": "bash",
"swift: "swift",
"jsx": "javascript",
"tsx": "typescript",
"yml": "yaml",
"md": "markdown",
"cpp": "c++",
"objective-c": "objectivec",
"objc": "objectivec",
"golang": "go"
]

struct CodeBlockView: View {
let code: String
let language: String?
@Environment(\.colorScheme) var colorScheme
@State private var isCopied = false

private let highlightr = Highlightr()

var platformBackgroundColor: Color {
#if os(iOS)
return Color(UIColor.secondarySystemBackground)
#elseif os(visionOS)
return Color(UIColor.separator)
#elseif os(macOS)
return Color(NSColor.secondarySystemFill)
#endif
}

private func normalizeLanguage(_ language: String?) -> String? {
guard let language = language?.lowercased() else { return nil }
return languageMap[language] ?? language
}

var highlightedCode: NSAttributedString? {
guard let highlightr = highlightr else { return nil }
highlightr.setTheme(to: colorScheme == .dark ? "atom-one-dark" : "atom-one-light")
highlightr.theme.codeFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
return highlightr.highlight(code, as: normalizeLanguage(language))
}

func copyToClipboard() {
#if os(macOS)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(code, forType: .string)
#else
UIPasteboard.general.string = code
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
#endif

withAnimation {
isCopied = true
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
isCopied = false
}
}
}

var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
if let language {
Text(language)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Button(action: copyToClipboard) {
HStack(spacing: 4) {
Image(systemName: isCopied ? "checkmark" : "doc.on.doc")
.font(.system(size: 12))
Text(isCopied ? "Copied!" : "Copy")
.font(.caption)
}
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(.secondary.opacity(0.1))
.cornerRadius(6)
}
.buttonStyle(.plain)
}
.padding(.bottom, 2)

ScrollView(.horizontal, showsIndicators: false) {
if let highlightedCode {
Text(AttributedString(highlightedCode))
.textSelection(.enabled)
} else {
Text(code)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
}
}
}
.padding()
.background(platformBackgroundColor)
.cornerRadius(8)
}
}

#Preview {
VStack(spacing: 20) {
CodeBlockView(
code: "print(\"Hello, World!\")",
language: "swift"
)
CodeBlockView(
code: "function hello() {\n console.log('Hello World');\n}",
language: "js"
)
CodeBlockView(
code: "def hello():\n print('Hello World')",
language: "python"
)
}
.padding()
}
35 changes: 29 additions & 6 deletions fullmoon/Views/Chat/ConversationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
// Created by Xavier on 16/12/2024.
//

import MarkdownUI
import SwiftUI
import SwiftData
import MarkdownUI

extension TimeInterval {
var formatted: String {
Expand Down Expand Up @@ -82,6 +83,26 @@ struct MessageView: View {
.foregroundStyle(.secondary)
}

private func parseCodeBlocks(_ text: String) -> AttributedString {
var config = AttributedString.MarkdownParsingOptions()
config.interpretedSyntax = .inlineOnlyPreservingWhitespace
return (try? AttributedString(markdown: text, options: config)) ?? AttributedString(text)
}

private func markdownTheme(foregroundColor: Color) -> Theme {
Theme()
.text {
ForegroundColor(foregroundColor)
}
.code {
FontFamilyVariant(.monospaced)
FontSize(.em(0.85))
}
.codeBlock { content in
CodeBlockView(code: content.content, language: content.language)
}
}

var body: some View {
HStack {
if message.role == .user { Spacer() }
Expand All @@ -101,9 +122,7 @@ struct MessageView: View {
.foregroundStyle(.fill)
Markdown(thinking)
.textSelection(.enabled)
.markdownTextStyle {
ForegroundColor(.secondary)
}
.markdownTheme(markdownTheme(foregroundColor: .secondary))
}
.padding(.leading, 5)
}
Expand All @@ -121,12 +140,16 @@ struct MessageView: View {
if let afterThink {
Markdown(afterThink)
.textSelection(.enabled)
.markdownTheme(markdownTheme(foregroundColor: .primary))
}
}
.padding(.trailing, 48)
} else {
Markdown(message.content)
.textSelection(.enabled)
VStack(alignment: .leading, spacing: 8) {
Markdown(message.content)
.textSelection(.enabled)
.markdownTheme(markdownTheme(foregroundColor: .primary))
}
#if os(iOS) || os(visionOS)
.padding(.horizontal, 16)
.padding(.vertical, 12)
Expand Down