Initial commit
This commit is contained in:
78
oAI/Utilities/Extensions/Color+Extensions.swift
Normal file
78
oAI/Utilities/Extensions/Color+Extensions.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Color+Extensions.swift
|
||||
// oAI
|
||||
//
|
||||
// Color scheme matching Python TUI dark theme
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
// MARK: - oAI Color Palette (Matching Python TUI)
|
||||
|
||||
static let oaiBackground = Color(hex: "#1e1e1e") // Main background
|
||||
static let oaiSurface = Color(hex: "#2d2d2d") // Cards, surfaces
|
||||
static let oaiPrimary = Color(hex: "#cccccc") // Primary text
|
||||
static let oaiSecondary = Color(hex: "#888888") // Secondary text
|
||||
static let oaiAccent = Color(hex: "#0a7aca") // Blue accent (assistant)
|
||||
static let oaiSuccess = Color(hex: "#90ee90") // Green (user messages)
|
||||
static let oaiError = Color(hex: "#ff6b6b") // Red (errors)
|
||||
static let oaiWarning = Color(hex: "#ffaa00") // Orange (warnings)
|
||||
static let oaiBorder = Color(hex: "#555555") // Borders, dividers
|
||||
|
||||
// MARK: - Message Role Colors
|
||||
|
||||
static func messageColor(for role: MessageRole) -> Color {
|
||||
switch role {
|
||||
case .user: return .oaiSuccess
|
||||
case .assistant: return .oaiAccent
|
||||
case .system: return .oaiSecondary
|
||||
}
|
||||
}
|
||||
|
||||
static func messageBackground(for role: MessageRole) -> Color {
|
||||
switch role {
|
||||
case .user: return .oaiSurface
|
||||
case .assistant: return .oaiBackground
|
||||
case .system: return Color(hex: "#2a2a2a")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Provider Colors
|
||||
|
||||
static func providerColor(_ provider: Settings.Provider) -> Color {
|
||||
switch provider {
|
||||
case .openrouter: return Color(hex: "#7c3aed") // Purple
|
||||
case .anthropic: return Color(hex: "#d4895a") // Orange
|
||||
case .openai: return Color(hex: "#10a37f") // Green
|
||||
case .ollama: return Color(hex: "#ffffff") // White
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hex Initializer
|
||||
|
||||
init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (255, 0, 0, 0)
|
||||
}
|
||||
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
87
oAI/Utilities/Extensions/String+Extensions.swift
Normal file
87
oAI/Utilities/Extensions/String+Extensions.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// String+Extensions.swift
|
||||
// oAI
|
||||
//
|
||||
// String utility extensions
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
// MARK: - Command Parsing
|
||||
|
||||
var isSlashCommand: Bool {
|
||||
hasPrefix("/")
|
||||
}
|
||||
|
||||
func parseCommand() -> (command: String, args: [String])? {
|
||||
guard isSlashCommand else { return nil }
|
||||
|
||||
let parts = self.split(separator: " ", omittingEmptySubsequences: true)
|
||||
.map(String.init)
|
||||
|
||||
guard let command = parts.first else { return nil }
|
||||
let args = Array(parts.dropFirst())
|
||||
|
||||
return (command, args)
|
||||
}
|
||||
|
||||
// MARK: - File Attachment Parsing
|
||||
|
||||
func parseFileAttachments() -> (cleanText: String, filePaths: [String]) {
|
||||
var cleanText = self
|
||||
var filePaths: [String] = []
|
||||
|
||||
// Pattern 1: @<filepath>
|
||||
let anglePattern = #"@<([^>]+)>"#
|
||||
if let regex = try? NSRegularExpression(pattern: anglePattern) {
|
||||
let matches = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
|
||||
for match in matches.reversed() {
|
||||
if let range = Range(match.range(at: 1), in: self) {
|
||||
let path = String(self[range])
|
||||
filePaths.insert(path, at: 0)
|
||||
}
|
||||
if let fullRange = Range(match.range, in: self) {
|
||||
cleanText.removeSubrange(fullRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: @filepath (starting with /, ~, ., or drive letter)
|
||||
let directPattern = #"@([~/.][\S]+|[A-Za-z]:[\\\/][\S]+)"#
|
||||
if let regex = try? NSRegularExpression(pattern: directPattern) {
|
||||
let matches = regex.matches(in: cleanText, range: NSRange(cleanText.startIndex..., in: cleanText))
|
||||
for match in matches.reversed() {
|
||||
if let range = Range(match.range(at: 1), in: cleanText) {
|
||||
let path = String(cleanText[range])
|
||||
if !filePaths.contains(path) {
|
||||
filePaths.insert(path, at: 0)
|
||||
}
|
||||
}
|
||||
if let fullRange = Range(match.range, in: cleanText) {
|
||||
cleanText.removeSubrange(fullRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (cleanText.trimmingCharacters(in: .whitespaces), filePaths)
|
||||
}
|
||||
|
||||
// MARK: - Token Estimation
|
||||
|
||||
func estimateTokens() -> Int {
|
||||
// Rough estimation: ~4 characters per token
|
||||
// This is approximate; Phase 2 will use proper tokenizer
|
||||
return max(1, count / 4)
|
||||
}
|
||||
|
||||
// MARK: - Truncation
|
||||
|
||||
func truncated(to length: Int, trailing: String = "...") -> String {
|
||||
if count <= length {
|
||||
return self
|
||||
}
|
||||
let endIndex = index(startIndex, offsetBy: length - trailing.count)
|
||||
return String(self[..<endIndex]) + trailing
|
||||
}
|
||||
}
|
||||
81
oAI/Utilities/Extensions/View+Extensions.swift
Normal file
81
oAI/Utilities/Extensions/View+Extensions.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// View+Extensions.swift
|
||||
// oAI
|
||||
//
|
||||
// SwiftUI view helpers and modifiers
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
// MARK: - Conditional Modifiers
|
||||
|
||||
@ViewBuilder
|
||||
func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
|
||||
if condition {
|
||||
transform(self)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func ifLet<Value, Transform: View>(_ value: Value?, transform: (Self, Value) -> Transform) -> some View {
|
||||
if let value = value {
|
||||
transform(self, value)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Platform-Specific Helpers
|
||||
|
||||
#if os(macOS)
|
||||
func onCommandReturn(perform action: @escaping () -> Void) -> some View {
|
||||
self
|
||||
// Note: onKeyPress modifiers don't work in command-line Swift build
|
||||
// This will be implemented when running in actual Xcode project
|
||||
// For now, using keyboard shortcuts in toolbar instead
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Common Styling
|
||||
|
||||
func oaiCardStyle() -> some View {
|
||||
self
|
||||
.background(Color.oaiSurface)
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.oaiBorder, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
func oaiButton() -> some View {
|
||||
self
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.oaiSurface)
|
||||
.foregroundColor(.oaiPrimary)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
|
||||
func oaiTextField() -> some View {
|
||||
self
|
||||
.padding(8)
|
||||
.background(Color.oaiBackground)
|
||||
.cornerRadius(6)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color.oaiBorder, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Frame Helpers
|
||||
|
||||
extension View {
|
||||
func frame(square size: CGFloat) -> some View {
|
||||
self.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user