diff --git a/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj b/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj index 3508483..e8019e6 100644 --- a/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj +++ b/AIStudyApp/AIStudyApp.xcodeproj/project.pbxproj @@ -6,8 +6,19 @@ objectVersion = 77; objects = { +/* Begin PBXContainerItemProxy section */ + 7A1B2C3D4E5F60718293A40C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 05F6CD132FA886330043A7BC /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05F6CD1A2FA886330043A7BC; + remoteInfo = AIStudyApp; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 05F6CD1B2FA886330043A7BC /* AIStudyApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AIStudyApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7A1B2C3D4E5F60718293A401 /* AIStudyAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AIStudyAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -16,6 +27,11 @@ path = AIStudyApp; sourceTree = ""; }; + 7A1B2C3D4E5F60718293A402 /* AIStudyAppReaderTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AIStudyAppReaderTests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -26,6 +42,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A403 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -33,6 +56,7 @@ isa = PBXGroup; children = ( 05F6CD1D2FA886330043A7BC /* AIStudyApp */, + 7A1B2C3D4E5F60718293A402 /* AIStudyAppReaderTests */, 05F6CD1C2FA886330043A7BC /* Products */, ); sourceTree = ""; @@ -41,6 +65,7 @@ isa = PBXGroup; children = ( 05F6CD1B2FA886330043A7BC /* AIStudyApp.app */, + 7A1B2C3D4E5F60718293A401 /* AIStudyAppTests.xctest */, ); name = Products; sourceTree = ""; @@ -70,6 +95,29 @@ productReference = 05F6CD1B2FA886330043A7BC /* AIStudyApp.app */; productType = "com.apple.product-type.application"; }; + 7A1B2C3D4E5F60718293A404 /* AIStudyAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7A1B2C3D4E5F60718293A40A /* Build configuration list for PBXNativeTarget "AIStudyAppTests" */; + buildPhases = ( + 7A1B2C3D4E5F60718293A405 /* Sources */, + 7A1B2C3D4E5F60718293A403 /* Frameworks */, + 7A1B2C3D4E5F60718293A406 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7A1B2C3D4E5F60718293A40B /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7A1B2C3D4E5F60718293A402 /* AIStudyAppReaderTests */, + ); + name = AIStudyAppTests; + packageProductDependencies = ( + ); + productName = AIStudyAppTests; + productReference = 7A1B2C3D4E5F60718293A401 /* AIStudyAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -83,6 +131,10 @@ 05F6CD1A2FA886330043A7BC = { CreatedOnToolsVersion = 26.4.1; }; + 7A1B2C3D4E5F60718293A404 = { + CreatedOnToolsVersion = 26.4.1; + TestTargetID = 05F6CD1A2FA886330043A7BC; + }; }; }; buildConfigurationList = 05F6CD162FA886330043A7BC /* Build configuration list for PBXProject "AIStudyApp" */; @@ -102,6 +154,7 @@ projectRoot = ""; targets = ( 05F6CD1A2FA886330043A7BC /* AIStudyApp */, + 7A1B2C3D4E5F60718293A404 /* AIStudyAppTests */, ); }; /* End PBXProject section */ @@ -114,6 +167,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A406 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -124,8 +184,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A405 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 7A1B2C3D4E5F60718293A40B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05F6CD1A2FA886330043A7BC /* AIStudyApp */; + targetProxy = 7A1B2C3D4E5F60718293A40C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 05F6CD242FA886350043A7BC /* Debug */ = { isa = XCBuildConfiguration; @@ -258,6 +333,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = NO; HEADER_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/AIStudyApp/Core/Services", @@ -270,6 +346,8 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator", + "$(PROJECT_DIR)/AIStudyApp/Core/ZxDocumentRuntime.xcframework.bak/ios-arm64", + "$(PROJECT_DIR)/AIStudyApp/Core/ZxDocumentRuntime.xcframework.bak/ios-arm64-simulator", ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( "$(inherited)", @@ -309,6 +387,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = NO; HEADER_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/AIStudyApp/Core/Services", @@ -321,6 +400,8 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator", + "$(PROJECT_DIR)/AIStudyApp/Core/ZxDocumentRuntime.xcframework.bak/ios-arm64", + "$(PROJECT_DIR)/AIStudyApp/Core/ZxDocumentRuntime.xcframework.bak/ios-arm64-simulator", ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = ( "$(inherited)", @@ -347,6 +428,63 @@ }; name = Release; }; + 7A1B2C3D4E5F60718293A407 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 88FMP9VK6T; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.longde.AIStudyAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INCLUDE_PATHS = "$(inherited) $(PROJECT_DIR)/AIStudyApp/Core/Services"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AIStudyApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AIStudyApp"; + TEST_TARGET_NAME = AIStudyApp; + }; + name = Debug; + }; + 7A1B2C3D4E5F60718293A408 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 88FMP9VK6T; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.longde.AIStudyAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INCLUDE_PATHS = "$(inherited) $(PROJECT_DIR)/AIStudyApp/Core/Services"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AIStudyApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AIStudyApp"; + TEST_TARGET_NAME = AIStudyApp; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -368,6 +506,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7A1B2C3D4E5F60718293A40A /* Build configuration list for PBXNativeTarget "AIStudyAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7A1B2C3D4E5F60718293A407 /* Debug */, + 7A1B2C3D4E5F60718293A408 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 05F6CD132FA886330043A7BC /* Project object */; diff --git a/AIStudyApp/AIStudyApp.xcodeproj/project.xcworkspace/xcuserdata/Admin1.xcuserdatad/UserInterfaceState.xcuserstate b/AIStudyApp/AIStudyApp.xcodeproj/project.xcworkspace/xcuserdata/Admin1.xcuserdatad/UserInterfaceState.xcuserstate index ec57579..11a17a0 100644 Binary files a/AIStudyApp/AIStudyApp.xcodeproj/project.xcworkspace/xcuserdata/Admin1.xcuserdatad/UserInterfaceState.xcuserstate and b/AIStudyApp/AIStudyApp.xcodeproj/project.xcworkspace/xcuserdata/Admin1.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/AIStudyApp/AIStudyApp.xcodeproj/xcshareddata/xcschemes/AIStudyApp.xcscheme b/AIStudyApp/AIStudyApp.xcodeproj/xcshareddata/xcschemes/AIStudyApp.xcscheme index a716193..9e6f155 100644 --- a/AIStudyApp/AIStudyApp.xcodeproj/xcshareddata/xcschemes/AIStudyApp.xcscheme +++ b/AIStudyApp/AIStudyApp.xcodeproj/xcshareddata/xcschemes/AIStudyApp.xcscheme @@ -29,6 +29,17 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + : Decodable { - let success: Bool - let data: T - let timestamp: String? -} - -// MARK: - Pagination - -struct PaginationMeta: Codable { - let page: Int - let limit: Int - let total: Int -} - -struct PaginatedResponse: Decodable { - let data: [T] - let meta: PaginationMeta -} - // MARK: - Waitlist struct WaitlistEntry: Codable, Identifiable { @@ -40,194 +24,21 @@ struct WaitlistEntry: Codable, Identifiable { struct WaitlistCreateRequest: Codable { let email: String - let nickname: String? - let devices: [String]? - let interests: [String]? - let painpoint: String? - let willingBeta: Bool? - - init(email: String, nickname: String? = nil, devices: [String]? = nil, - interests: [String]? = nil, painpoint: String? = nil, willingBeta: Bool = true) { - self.email = email - self.nickname = nickname - self.devices = devices - self.interests = interests - self.painpoint = painpoint - self.willingBeta = willingBeta - } + let name: String? } struct WaitlistResponse: Codable { - let success: Bool? + let success: Bool let message: String? - let data: WaitlistEntry? + let entry: WaitlistEntry? } struct WaitlistStats: Codable { - let total: Int? - let today: Int? - let deviceBreakdown: [String: Int]? - let interestBreakdown: [String: Int]? + let total: Int + let today: Int } -// MARK: - Auth - -struct AuthResponse: Codable { - let accessToken: String - let refreshToken: String? - let user: AuthUser? -} - -struct AuthUser: Codable, Identifiable { - let id: String - let email: String? - let nickname: String? - let avatarUrl: String? - let role: String? - let status: String? - let onboardingCompleted: Bool? -} - -struct AppleAuthRequest: Codable { - let identityToken: String - let authorizationCode: String? - let fullName: AppleFullName? - let nonce: String? - - struct AppleFullName: Codable { - let givenName: String? - let familyName: String? - } -} - -struct RefreshRequest: Codable { - let refreshToken: String -} - -// MARK: - User Profile (matches GET /users/me with include: profile + preferences) - -struct UserProfileResponse: Codable, Identifiable { - let id: String - let email: String? - let nickname: String? - let avatarUrl: String? - let role: String? - let status: String? - let onboardingCompleted: Bool? - let createdAt: String? - let profile: UserProfileData? - let preferences: UserPreferences? -} - -struct UserProfileData: Codable { - let learningIdentity: String? - let learningDirection: String? - let bio: String? - let currentGoal: String? -} - -struct UserPreferences: Codable, Equatable { - let preferredMethods: [String]? - let defaultFocusMinutes: Int? - let aiSuggestionLevel: String? - let language: String? - let appearance: String? - let notificationEnabled: Bool? -} - -struct UpdateProfileRequest: Codable { - let nickname: String? - let avatarUrl: String? -} - -struct UpdatePreferencesRequest: Codable { - let preferredMethods: [String]? - let defaultFocusMinutes: Int? - let aiSuggestionLevel: String? - let language: String? - let appearance: String? - let notificationEnabled: Bool? -} - -struct UpdateProfileDataRequest: Codable { - let learningIdentity: String? - let learningDirection: String? - let bio: String? - let currentGoal: String? -} - -// MARK: - Knowledge Base (matches Prisma KnowledgeBase model) - -struct KnowledgeBase: Codable, Identifiable { - let id: String - let userId: String? - let title: String - let description: String? - let coverKey: String? - let coverUrl: String? - let coverType: String? - let visibility: String? - let isPinned: Bool? - let ownerType: String? - let isVerified: Bool? - let status: String? - let itemCount: Int? - let lastStudiedAt: String? - let createdAt: String? - let updatedAt: String? -} - -struct CreateKnowledgeBaseRequest: Codable { - let title: String - let description: String? - var coverKey: String? = nil -} - -struct KnowledgeBaseCoverUploadResult { - let fileId: String - let objectKey: String -} - -// MARK: - Knowledge Items (matches Prisma KnowledgeItem model) - - -struct GenerateKnowledgeResponse: Codable { - let items: [KnowledgeItem] - let count: Int - let sourceId: String -} - -struct KnowledgeItem: Codable, Identifiable, Hashable { - let id: String - let userId: String? - let knowledgeBaseId: String? - let parentId: String? - let itemType: String? - let title: String - let content: String? - let summary: String? - let sourceType: String? - let sourceRef: String? - let orderIndex: Int? - let status: String? - let createdAt: String? - let updatedAt: String? -} - -struct CreateKnowledgeItemRequest: Codable { - let knowledgeBaseId: String - let title: String - let content: String? - let itemType: String? -} - -struct UpdateKnowledgeItemRequest: Codable { - let title: String? - let content: String? - let summary: String? -} - -// MARK: - Active Recall (matches ActiveRecallQuestion / Answer models) +// MARK: - Active Recall struct ActiveRecallQuestion: Codable, Identifiable { let id: String @@ -252,7 +63,7 @@ struct SubmitAnswerRequest: Codable { let answerText: String } -// MARK: - AI Analysis (matches AiAnalysisResult model) +// MARK: - AI Analysis struct AIAnalysisRequest: Codable { let questionText: String? @@ -265,23 +76,23 @@ struct AIAnalysisRequest: Codable { struct AIAnalysisResult: Codable, Identifiable { let id: String let userId: String? - let summary: String? + let knowledgeItemId: String? + let questionText: String? + let userAnswer: String? + let analysis: String? + let score: Int? + let feedback: String? let masteryScore: Int? - let strengths: [String]? - let weaknesses: [String]? - let suggestions: [String]? - let nextActions: [String]? - let rawResult: AIAnalysisRawResult? let createdAt: String? } struct AIAnalysisRawResult: Codable { - let score: Double? - let analysis: String? - let focusItems: [String]? + let id: String + let resultJson: String? + let createdAt: String? } -// MARK: - Learning Session (matches Prisma LearningSession model) +// MARK: - Learning Session struct LearningSession: Codable, Identifiable { let id: String @@ -294,7 +105,7 @@ struct LearningSession: Codable, Identifiable { let endedAt: String? let durationSeconds: Int? let focusMinutes: Int? - let createdAt: String? + let totalActiveSeconds: Int? } struct CreateLearningSessionRequest: Codable { @@ -303,270 +114,101 @@ struct CreateLearningSessionRequest: Codable { let mode: String? } -// MARK: - Review (matches ReviewCard / ReviewLog models) +// MARK: - Review struct ReviewCard: Codable, Identifiable { let id: String let userId: String? let knowledgeItemId: String? - let frontText: String + let frontText: String? let backText: String? let difficulty: String? let status: String? - let nextReviewAt: String? + let scheduleState: String? let intervalDays: Int? - let easeFactor: Double? - let repetitionCount: Int? - let lapseCount: Int? -} - -struct SubmitReviewRequest: Codable { - let rating: String - let responseText: String? -} - -// MARK: - Focus Items (matches Prisma FocusItem model) - -struct FocusItem: Codable, Identifiable { - let id: String - let userId: String? - let knowledgeBaseId: String? - let knowledgeItemId: String? - let title: String - let reason: String? - let suggestion: String? - let priority: String? - let status: String? - let masteryScore: Int? - let dueAt: String? - let completedAt: String? + let ease: Double? + let nextReviewAt: String? let createdAt: String? } -// MARK: - Activity (matches DailyLearningActivity + summary aggregation) - -struct ActivitySummary: Codable { - let totalMinutes: Int? - let totalCardsReviewed: Int? - let activeDays: Int? - let dailyAverage: Int? -} - -struct ActivityHeatmap: Codable { - // Dictionary of "YYYY-MM-DD" -> durationSeconds +struct SubmitReviewRequest: Codable { + let difficulty: String? } // MARK: - Feedback struct FeedbackCreateRequest: Codable { - let category: String + let category: String? let content: String let email: String? - - init(category: String = "general", content: String, email: String? = nil) { - self.category = category - self.content = content - self.email = email - } } struct FeedbackData: Codable, Identifiable { - let id: String? + let id: String + let userId: String? let category: String? let content: String? let status: String? let createdAt: String? } -// MARK: - Notifications (matches Prisma Notification model) +// MARK: - Notifications struct NotificationItem: Codable, Identifiable { let id: String let userId: String? - let type: String + let type: String? let title: String let content: String? - let data: [String: String]? let readAt: String? let createdAt: String? } -// MARK: - Generic - -struct GenericSuccessResponse: Codable { - let success: Bool? - let message: String? -} - -struct BatchDeleteResponse: Codable { - let success: Bool? - let message: String? - let count: Int? -} - -// MARK: - Quiz - -struct Quiz: Codable, Identifiable { - let id: String - let knowledgeBaseId: String? - let title: String? - let description: String? - let questionCount: Int? - let sourceType: String? - let status: String? - let createdAt: String? - let questions: [QuizQuestion]? -} - -struct QuizQuestion: Codable, Identifiable { - let id: String - let quizId: String? - let type: String? - let stem: String? - let options: [String]? - let answer: String? - let explanation: String? - let orderIndex: Int? - - /// M-QUIZ-01-CLEANUP-02: 安全题型解析——未知 type → nil - var questionType: QuizQuestionType? { QuizQuestionType.from(rawValue: type) } -} - -struct QuizAttempt: Codable, Identifiable { - let id: String - let quizId: String? - let totalQuestions: Int? - let correctCount: Int? - let score: Int? - let startedAt: String? - let finishedAt: String? - let answers: [QuizAnswer]? - let quiz: QuizInfo? -} +// MARK: - Quiz (extras) struct QuizInfo: Codable { let title: String? } - struct QuizAnswer: Codable, Identifiable { - let id: String - let attemptId: String? - let questionId: String? - let userAnswer: String? - let isCorrect: Bool? - let answeredAt: String? - let question: QuizQuestion? + let id: String; let questionId: String?; let answer: String? } struct QuizGenerateRequest: Codable { let knowledgeBaseId: String + let sourceType: String? + let sourceId: String? let questionCount: Int } struct QuizSubmitRequest: Codable { - let attemptId: String - let answers: [QuizAnswerItem] + let attemptId: String; let answers: [QuizAnswerItem] } struct QuizAnswerItem: Codable { - let questionId: String - let answer: String + let questionId: String; let answer: String } struct QuizSubmitResponse: Codable { - let score: Int? - let correctCount: Int? - let totalQuestions: Int? - let finishedAt: String? + let score: Int?; let totalQuestions: Int?; let correctCount: Int? } -// M-QUIZ-01-03: Attempt 题目(不含 answer/explanation 直到提交后) struct QuizAttemptStartResponse: Codable { - let id: String - let quizId: String? - let totalQuestions: Int? + let id: String; let quizId: String?; let status: String? } -// MARK: - RAG Chat - -// MARK: - Chat Scope - -enum ChatScopeType: String, Codable { - case knowledgeBase = "knowledge_base" - case folder = "folder" - case material = "material" - case knowledgeItem = "knowledge_item" - case global = "global" -} - -struct ChatEntryContext: Hashable { - let scopeType: ChatScopeType - let scopeId: String? - let scopeName: String - let parentKnowledgeBaseId: String? - let createdFrom: String -} - -struct ChatSession: Codable, Identifiable { - let id: String - let userId: String? - let knowledgeBaseId: String? - let scopeType: String? - let scopeId: String? - let parentKnowledgeBaseId: String? - let title: String? - let createdFrom: String? - let isPinned: Bool? - let isArchived: Bool? - let isDeleted: Bool? - let modelMode: String? - let modelId: String? - let lastMessageAt: String? - let createdAt: String? - let updatedAt: String? -} - -struct ChatMessage: Codable, Identifiable { - let id: String - let sessionId: String? - let role: String - let content: String - let tokens: Int? - let scopeSnapshot: ChatScopeSnapshot? - let createdAt: String? - let citations: [ChatCitation]? -} +// MARK: - Chat (extras) struct ChatScopeSnapshot: Codable { let scopeType: String? let scopeId: String? let parentKnowledgeBaseId: String? -} - -struct ChatCitation: Codable, Identifiable { - let id: String - let chunkId: String? - let sourceId: String? - let sourceTitle: String? - let excerptText: String? - let pageNumber: Int? - let lineStart: Int? - let lineEnd: Int? -} - -struct CreateSessionRequest: Codable { - let scopeType: String - let scopeId: String? - let parentKnowledgeBaseId: String? - let createdFrom: String - let title: String? - let forceCreate: Bool? + let type: String? + let id: String? + let name: String? } struct UpdateChatSessionRequest: Codable { - var title: String? - var isPinned: Bool? - var isArchived: Bool? - var modelMode: String? - var modelId: String? + let title: String? + let isPinned: Bool? + let isArchived: Bool? } struct SendMessageRequest: Codable { @@ -574,55 +216,15 @@ struct SendMessageRequest: Codable { } struct SendMessageResponse: Codable { - let id: String? - let role: String? - let content: String? - let tokens: Int? - let blocked: Bool? - let message: ChatMessage? - let citations: [ChatCitation]? + let messageId: String? + let sessionId: String? } // MARK: - Document Import -struct CreateImportRequest: Codable { - let knowledgeBaseId: String - let fileName: String? - let sourceType: String? - let rawText: String? -} - -struct ImportStatusResponse: Codable { - let id: String - let jobId: String? - let status: String? - let fileName: String? - let knowledgeBaseId: String? - let errorMessage: String? - let progress: Int? - let itemCount: Int? -} - struct ParseResponse: Codable { - let id: String? - let jobId: String? - let status: String? -} - -// MARK: - Import Candidate - -struct ImportCandidate: Codable, Identifiable { - let id: String - let sourceId: String? - let title: String? - let content: String? - let status: String? - let confidence: Double? - let createdAt: String? -} - -struct BatchAcceptRequest: Codable { - let sourceId: String + let text: String? + let textLength: Int? } // MARK: - Knowledge Folder @@ -630,11 +232,10 @@ struct BatchAcceptRequest: Codable { struct KnowledgeFolder: Codable, Identifiable { let id: String let knowledgeBaseId: String? - let parentId: String? let name: String - let sortOrder: Int? + let parentId: String? + let orderIndex: Int? let createdAt: String? - let updatedAt: String? } struct CreateFolderRequest: Codable { @@ -642,54 +243,6 @@ struct CreateFolderRequest: Codable { let parentId: String? } -// MARK: - Knowledge Source - -struct KnowledgeSource: Codable, Identifiable { - let id: String - let knowledgeBaseId: String? - let fileId: String? - let title: String? - let originalFilename: String? - let type: String? - let mimeType: String? - let parseStatus: String? - let indexStatus: String? - let textLength: Int? - let createdAt: String? -} - -struct AddSourceRequest: Codable { - let fileId: String? - let type: String? - let title: String? - let originalFilename: String? - let mimeType: String? - let sizeBytes: Int? - let originalObjectKey: String? -} - -// MARK: - Learning Activity - -struct ActivityTrend: Codable { - let date: String? - let value: Double? - let label: String? -} - -struct ActivityStreak: Codable { - let currentStreak: Int? - let longestStreak: Int? - let lastActiveDate: String? -} - -struct ActivityRecommendation: Codable, Identifiable { - var id: String { title ?? UUID().uuidString } - let title: String? - let description: String? - let type: String? - let priority: String? -} - // MARK: - File Upload (COS presigned URL flow) struct FileUploadUrlRequest: Codable { @@ -699,8 +252,8 @@ struct FileUploadUrlRequest: Codable { } struct FileUploadUrlResponse: Codable { - let uploadId: String let uploadUrl: String + let uploadId: String let objectKey: String let bucket: String let region: String @@ -715,22 +268,15 @@ struct FileConfirmUploadRequest: Codable { struct FileConfirmUploadResponse: Codable { let id: String - let filename: String - let objectKey: String - let sizeBytes: Int } struct FileDetailResponse: Codable { - let file: FileInfo let downloadUrl: String } struct FileInfo: Codable { let id: String - let filename: String - let mimeType: String? - let sizeBytes: Int let objectKey: String? - let storagePath: String? - let createdAt: String? + let url: String? + let sizeBytes: Int? } diff --git a/AIStudyApp/AIStudyApp/Core/Models/Activity.swift b/AIStudyApp/AIStudyApp/Core/Models/Activity.swift new file mode 100644 index 0000000..bb58a88 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/Activity.swift @@ -0,0 +1,34 @@ +import Foundation + +struct ActivitySummary: Codable { + let dailyAverage: Int? + let totalMinutes: Int? + let totalCardsReviewed: Int? + let activeDays: Int? + let totalSessions: Int? + let totalEvents: Int? +} + +struct ActivityHeatmap: Codable { + let dates: [String: Int]? +} + +struct ActivityTrend: Codable, Identifiable { + var id: String { date } + let date: String + let value: Int +} + +struct ActivityStreak: Codable { + let currentStreak: Int? + let longestStreak: Int? + let lastActiveDate: String? +} + +struct ActivityRecommendation: Codable, Identifiable { + var id: String { title ?? UUID().uuidString } + let type: String? + let title: String? + let description: String? + let priority: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Auth.swift b/AIStudyApp/AIStudyApp/Core/Models/Auth.swift new file mode 100644 index 0000000..5e7ec3b --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/Auth.swift @@ -0,0 +1,30 @@ +import Foundation + +struct AuthResponse: Codable { + let accessToken: String + let refreshToken: String + let user: AuthUser? +} + +struct AuthUser: Codable, Identifiable { + let id: String + let email: String? + let nickname: String? + let avatar: String? +} + +struct AppleAuthRequest: Codable { + let identityToken: String + let authorizationCode: String? + let fullName: AppleFullName? + let nonce: String? + + struct AppleFullName: Codable { + let givenName: String? + let familyName: String? + } +} + +struct RefreshRequest: Codable { + let refreshToken: String +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Chat.swift b/AIStudyApp/AIStudyApp/Core/Models/Chat.swift new file mode 100644 index 0000000..ed185f2 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/Chat.swift @@ -0,0 +1,72 @@ +import Foundation + +// MARK: - Chat Session + +struct ChatSession: Codable, Identifiable { + let id: String + let title: String? + let scopeType: String? + let scopeId: String? + let parentKnowledgeBaseId: String? + let messageCount: Int? + let isPinned: Bool? + let createdAt: String? + let updatedAt: String? +} + +struct CreateSessionRequest: Codable { + let scopeType: String + let scopeId: String? + let parentKnowledgeBaseId: String? + let createdFrom: String? + let title: String? + let forceCreate: Bool? +} + +// MARK: - Chat Message + +struct ChatMessage: Codable, Identifiable { + let id: String + let sessionId: String? + let role: String? + let content: String + let toolCalls: String? + let citations: [ChatCitation]? + let scopeSnapshot: ChatScopeSnapshot? + let createdAt: String? +} + +struct ChatCitation: Codable { + let id: String + let title: String? + let content: String? +} + +struct SaveChatMessageAsKnowledgeResponse: Codable { + let success: Bool + let message: String + let taskId: String? + let sourceId: String? + let chatMessageId: String? + let aiJobId: String? + let status: String? + let knowledgeItemCount: Int? + let errorCode: String? + let errorMessage: String? +} + +struct ChatEntryContext: Codable, Hashable { + let scopeType: ChatScopeType + let scopeId: String? + let scopeName: String? + let parentKnowledgeBaseId: String? + let createdFrom: String? +} + +enum ChatScopeType: String, Codable { + case knowledgeBase = "knowledge_base" + case knowledgeItem = "knowledge_item" + case material + case folder + case global +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Common.swift b/AIStudyApp/AIStudyApp/Core/Models/Common.swift new file mode 100644 index 0000000..432d0c3 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/Common.swift @@ -0,0 +1,39 @@ +import Foundation + +// MARK: - API Envelope + +struct APIEnvelope: Decodable { + let success: Bool + let data: T + let timestamp: String? +} + +// MARK: - Pagination + +struct PaginationMeta: Codable { + let page: Int + let limit: Int + let total: Int + let totalPages: Int +} + +struct PaginatedResponse: Decodable { + let items: [T] + let total: Int + let page: Int + let limit: Int + let totalPages: Int +} + +// MARK: - Generic + +struct GenericSuccessResponse: Codable { + let success: Bool + let message: String? + let processed: Int? +} + +struct BatchDeleteResponse: Codable { + let deleted: Int + let message: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/FocusItem.swift b/AIStudyApp/AIStudyApp/Core/Models/FocusItem.swift new file mode 100644 index 0000000..0516690 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/FocusItem.swift @@ -0,0 +1,16 @@ +import Foundation + +struct FocusItem: Codable, Identifiable { + let id: String + let userId: String? + let knowledgeBaseId: String? + let title: String + let content: String? + let masteryScore: Int? + let priority: String? + let status: String? + let sourceType: String? + let sourceId: String? + let createdAt: String? + let updatedAt: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/KnowledgeBase.swift b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeBase.swift new file mode 100644 index 0000000..00255ad --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeBase.swift @@ -0,0 +1,31 @@ +import Foundation + +struct KnowledgeBase: Codable, Identifiable { + let id: String + let userId: String? + let title: String + let description: String? + let coverKey: String? + let coverUrl: String? + let coverType: String? + let visibility: String? + let isPinned: Bool? + let ownerType: String? + let isVerified: Bool? + let status: String? + let itemCount: Int? + let lastStudiedAt: String? + let createdAt: String? + let updatedAt: String? +} + +struct CreateKnowledgeBaseRequest: Codable { + let title: String + let description: String? + let coverKey: String? +} + +struct KnowledgeBaseCoverUploadResult { + let objectKey: String + let url: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/KnowledgeItem.swift b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeItem.swift new file mode 100644 index 0000000..195abf5 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeItem.swift @@ -0,0 +1,69 @@ +import Foundation + +struct GenerateKnowledgeResponse: Codable { + let taskId: String + let sourceId: String? + let chatMessageId: String? + let aiJobId: String? + let status: String + let knowledgeItemCount: Int? + let errorCode: String? + let errorMessage: String? + let message: String +} + +struct GenerateKnowledgeBatchResponse: Codable { + let knowledgeBaseId: String + let totalCount: Int + let queuedCount: Int + let waitingForParseCount: Int + let runningCount: Int + let tasks: [GenerateKnowledgeResponse] + let message: String +} + +extension GenerateKnowledgeResponse { + var asTaskStatus: KnowledgeGenerationTaskStatus { + KnowledgeGenerationTaskStatus( + taskId: taskId, + sourceId: sourceId, + chatMessageId: chatMessageId, + aiJobId: aiJobId, + status: status, + knowledgeItemCount: knowledgeItemCount, + errorCode: errorCode, + errorMessage: errorMessage, + message: message + ) + } +} + +struct KnowledgeItem: Codable, Identifiable, Hashable { + let id: String + let userId: String? + let knowledgeBaseId: String? + let parentId: String? + let itemType: String? + let title: String + let content: String? + let summary: String? + let sourceType: String? + let sourceRef: String? + let orderIndex: Int? + let status: String? + let createdAt: String? + let updatedAt: String? +} + +struct CreateKnowledgeItemRequest: Codable { + let knowledgeBaseId: String + let title: String + let content: String? + let itemType: String? +} + +struct UpdateKnowledgeItemRequest: Codable { + let title: String? + let content: String? + let summary: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/KnowledgeSource.swift b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeSource.swift new file mode 100644 index 0000000..48c85fe --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/KnowledgeSource.swift @@ -0,0 +1,47 @@ +import Foundation + +struct KnowledgeSource: Codable, Identifiable { + let id: String + let knowledgeBaseId: String? + let fileId: String? + let title: String? + let originalFilename: String? + let type: String? + let mimeType: String? + let parseStatus: String? + let indexStatus: String? + let learningStatus: String? + let textLength: Int? + let sizeBytes: Int? + let createdAt: String? + let updatedAt: String? +} + +struct KnowledgeGenerationTaskStatus: Codable, Identifiable { + let taskId: String + let sourceId: String? + let chatMessageId: String? + let aiJobId: String? + let status: String + let knowledgeItemCount: Int? + let errorCode: String? + let errorMessage: String? + let message: String + + var id: String { taskId } +} + +struct KnowledgeGenerationTaskListResponse: Codable { + let knowledgeBaseId: String + let items: [KnowledgeGenerationTaskStatus] +} + +struct AddSourceRequest: Codable { + let fileId: String? + let type: String? + let title: String? + let originalFilename: String? + let mimeType: String? + let sizeBytes: Int? + let originalObjectKey: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Quiz.swift b/AIStudyApp/AIStudyApp/Core/Models/Quiz.swift new file mode 100644 index 0000000..9c5c1a4 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/Quiz.swift @@ -0,0 +1,57 @@ +import Foundation + +struct Quiz: Codable, Identifiable { + let id: String + let userId: String? + let knowledgeBaseId: String? + let title: String? + let description: String? + let questionCount: Int? + let sourceType: String? + let sourceId: String? + let status: String? + let createdAt: String? + let updatedAt: String? +} + +struct QuizQuestion: Codable, Identifiable { + let id: String + let quizId: String? + let stem: String? + let questionText: String? + let questionType: String? + + var questionTypeEnum: QuizQuestionType? { + guard let t = questionType else { return nil } + return QuizQuestionType(rawValue: t) + } + let options: [String]? + let answer: String? + let correctAnswer: String? + let explanation: String? + let orderIndex: Int? +} + +struct QuizAttempt: Codable, Identifiable { + let id: String + let quizId: String? + let userId: String? + let score: Int? + let correctCount: Int? + let totalQuestions: Int? + let answers: [QuizAttemptAnswer]? + let completedAt: String? +} + +struct QuizAttemptAnswer: Codable, Identifiable { + let id: String + let questionId: String? + let question: QuizQuestion? + let userAnswer: String? + let isCorrect: Bool? +} + +struct SubmitQuizAnswerRequest: Codable { + let questionId: String + let answer: String +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/Reading.swift b/AIStudyApp/AIStudyApp/Core/Models/Reading.swift new file mode 100644 index 0000000..48f358c --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/Reading.swift @@ -0,0 +1,60 @@ +import Foundation + +// MARK: - Reading Position (from old ReadingAPI) + +struct ReadingPositionValue: Codable, Equatable, Hashable { + let type: String? + let pageNumber: UInt32? + let blockId: String? + let blockProgress: Float? + let scrollProgress: Float? + let lineNumber: UInt32? + let pageProgress: Float? + let overallProgress: Float? + let zoomScale: Float? + let offsetX: Float? + let offsetY: Float? + let chapterId: String? + let chapterProgress: Float? + let offset: Int? + let percent: Float? + let section: String? +} + +// MARK: - Continue Learning + +struct ContinueLearningResponse: Codable { + let type: String? + let materialId: String? + let title: String? + let lastPosition: ReadingPositionValue? + let lastProgress: Float? + let totalActiveSeconds: Int? + let lastReadAt: String? +} + +struct LearningSummaryResponse: Codable { + let todaySeconds: Int + let weekSeconds: Int + let markedReadCount: Int + let materialsReadCount: Int +} + +struct LearningTrendResponse: Codable { + let days: Int + let series: [TrendPoint] + struct TrendPoint: Codable { + let date: String + let value: Int + } +} + +struct LearningRecordsResponse: Codable { + let items: [RecordItem] + struct RecordItem: Codable, Identifiable { + let id: String + let recordType: String + let title: String + let durationSeconds: Int + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Models/ReadingEventUploadItem.swift b/AIStudyApp/AIStudyApp/Core/Models/ReadingEventUploadItem.swift index 6c4c008..a381f36 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/ReadingEventUploadItem.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/ReadingEventUploadItem.swift @@ -74,14 +74,56 @@ private struct PositionWrapper: Encodable { init(from pos: ReadingPosition) { switch pos { + case .block(let blockId, let blockProgress, let overallProgress): + raw = [ + "type": .string("Block"), + "blockId": .string(blockId), + "blockProgress": .float(blockProgress), + "overallProgress": .float(overallProgress) + ] case .markdown(let blockId, let scrollProgress): raw = ["type": .string("Markdown"), "blockId": .string(blockId), "scrollProgress": .float(scrollProgress)] case .text(let lineNumber, let scrollProgress): raw = ["type": .string("Text"), "lineNumber": .int(Int(lineNumber)), "scrollProgress": .float(scrollProgress)] case .pdf(let pageNumber, let pageProgress, let overallProgress): raw = ["type": .string("Pdf"), "pageNumber": .int(Int(pageNumber)), "pageProgress": .float(pageProgress), "overallProgress": .float(overallProgress)] + case .pdfViewport(let pageNumber, let anchorX, let anchorY, let zoomScale, let overallProgress): + raw = [ + "type": .string("PdfViewport"), + "pageNumber": .int(Int(pageNumber)), + "anchorX": .float(anchorX), + "anchorY": .float(anchorY), + "zoomScale": .float(zoomScale), + "overallProgress": .float(overallProgress) + ] case .image(let zoomScale, let offsetX, let offsetY): raw = ["type": .string("Image"), "zoomScale": .float(zoomScale), "offsetX": .float(offsetX), "offsetY": .float(offsetY)] + case .imageViewport(let zoomScale, let anchorX, let anchorY): + raw = ["type": .string("ImageViewport"), "zoomScale": .float(zoomScale), "anchorX": .float(anchorX), "anchorY": .float(anchorY)] + case .workbook(let sheetId, let topRow, let leftColumn, let selectedCell, let zoomScale, let overallProgress): + var value: [String: AnyCodableValue] = [ + "type": .string("Workbook"), + "sheetId": .string(sheetId), + "topRow": .int(Int(topRow)), + "leftColumn": .int(Int(leftColumn)), + "zoomScale": .float(zoomScale) + ] + if let selectedCell { + value["selectedCell"] = .string(selectedCell) + } + if let overallProgress { + value["overallProgress"] = .float(overallProgress) + } + raw = value + case .slide(let slideIndex, let zoomScale, let anchorX, let anchorY, let overallProgress): + raw = [ + "type": .string("Slide"), + "slideIndex": .int(Int(slideIndex)), + "zoomScale": .float(zoomScale), + "anchorX": .float(anchorX), + "anchorY": .float(anchorY), + "overallProgress": .float(overallProgress) + ] case .epub(let chapterId, let chapterProgress, let overallProgress): raw = ["type": .string("Epub"), "chapterId": .string(chapterId), "chapterProgress": .float(chapterProgress), "overallProgress": .float(overallProgress)] case .unknown: diff --git a/AIStudyApp/AIStudyApp/Core/Models/ReadingMaterialContext.swift b/AIStudyApp/AIStudyApp/Core/Models/ReadingMaterialContext.swift index d9da248..bd048d7 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/ReadingMaterialContext.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/ReadingMaterialContext.swift @@ -72,8 +72,8 @@ extension PreviewMode: Codable { // MARK: - ReadingMaterialContext /// iOS-side context that supplements Rust's materialId with business fields. -/// Rust only knows materialId; iOS adds readingTargetType, knowledgeBaseId, -/// material type metadata, and local file info for API upload. +/// Carries file metadata needed to build `ReadingMaterialRefV2` for +/// the Rust V2 reading session FFI. struct ReadingMaterialContext: Codable, Equatable { let readingTargetType: ReadingTargetType let materialId: String @@ -83,6 +83,13 @@ struct ReadingMaterialContext: Codable, Equatable { let materialType: MaterialType? let previewMode: PreviewMode? + // V2 material metadata (for ReadingMaterialRefV2) + let filePath: String + let fileName: String + let fileType: String // lowercase: "markdown", "pdf", "epub", etc. + let mimeType: String? + let fileSize: UInt64? + init( readingTargetType: ReadingTargetType, materialId: String, @@ -90,7 +97,12 @@ struct ReadingMaterialContext: Codable, Equatable { title: String? = nil, localFileURL: URL? = nil, materialType: MaterialType? = nil, - previewMode: PreviewMode? = nil + previewMode: PreviewMode? = nil, + filePath: String = "", + fileName: String = "", + fileType: String = "unknown", + mimeType: String? = nil, + fileSize: UInt64? = nil ) { self.readingTargetType = readingTargetType self.materialId = materialId @@ -99,5 +111,29 @@ struct ReadingMaterialContext: Codable, Equatable { self.localFileURL = localFileURL self.materialType = materialType self.previewMode = previewMode + self.filePath = filePath + self.fileName = fileName + self.fileType = fileType + self.mimeType = mimeType + self.fileSize = fileSize + } +} + +// MARK: - MaterialType → file_type string + +extension MaterialType { + /// Lowercase string matching `ReadingMaterialRefV2.fileType` Rust convention. + var fileTypeString: String { + switch self { + case .markdown: return "markdown" + case .text: return "text" + case .pdf: return "pdf" + case .image: return "image" + case .epub: return "epub" + case .word: return "word" + case .excel: return "excel" + case .powerPoint: return "powerpoint" + case .unknown: return "unknown" + } } } diff --git a/AIStudyApp/AIStudyApp/Core/Models/User.swift b/AIStudyApp/AIStudyApp/Core/Models/User.swift new file mode 100644 index 0000000..8e015fa --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Models/User.swift @@ -0,0 +1,69 @@ +import Foundation + +// MARK: - User Profile + +struct UserProfileResponse: Codable, Identifiable { + let id: String + let email: String? + let nickname: String? + let avatarUrl: String? + let role: String? + let status: String? + let onboardingCompleted: Bool? + let createdAt: String? + let profile: UserProfileData? + let preferences: UserPreferences? +} + +struct UserProfileData: Codable { + let learningIdentity: String? + let learningDirection: String? + let bio: String? + let currentGoal: String? +} + +struct UserPreferences: Codable, Equatable { + let preferredMethods: [String]? + let defaultFocusMinutes: Int? + let aiSuggestionLevel: String? + let language: String? + let appearance: String? + let notificationEnabled: Bool? +} + +struct UpdateProfileRequest: Codable { + let nickname: String? + let avatarUrl: String? +} + +struct UpdatePreferencesRequest: Codable { + let preferredMethods: [String]? + let defaultFocusMinutes: Int? + let aiSuggestionLevel: String? + let language: String? + let appearance: String? + let notificationEnabled: Bool? +} + +struct UpdateProfileDataRequest: Codable { + let learningIdentity: String? + let learningDirection: String? + let bio: String? + let currentGoal: String? +} + +// MARK: - Device + +struct UserDevice: Codable, Identifiable { + let id: String + let deviceName: String? + let platform: String? + let lastActiveAt: String? +} + +// MARK: - Notification Preferences + +struct NotificationPreferences: Codable { + let pushEnabled: Bool? + let emailEnabled: Bool? +} diff --git a/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift b/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift index f024375..2517f03 100644 --- a/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift +++ b/AIStudyApp/AIStudyApp/Core/Navigation/Route.swift @@ -24,17 +24,14 @@ enum Route: Hashable { case learningSession(taskTitle: String, taskType: String, taskColorHex: String) case studyHome - // Import - case importReview(sourceId: String) - // Quiz case quizList(knowledgeBaseId: String) case quizTake(quizId: String) case quizResult(quizId: String, attemptId: String) // Material Reader - case materialReader(materialId: String, filePath: String, materialType: MaterialType, knowledgeBaseId: String? = nil, title: String = "") - case materialDetail(knowledgeBaseId: String, fileName: String, fileType: MaterialType, fileSize: UInt64, filePath: String, uploadDate: String?) + case materialReader(materialId: String, filePath: String, materialType: MaterialType, knowledgeBaseId: String? = nil, title: String = "", resumePosition: ReadingPositionValue? = nil) + case materialDetail(knowledgeBaseId: String, sourceId: String, fileName: String, fileType: MaterialType, fileSize: UInt64, filePath: String, uploadDate: String?) // Membership case membership @@ -75,7 +72,6 @@ extension Route { case .feedbackForm: "FeedbackFormView" case .membership: "MembershipView" case .editProfile: "EditProfilePage" - case .importReview: "ImportReviewPage" case .quizList: "QuizListView" case .quizTake: "QuizTakerView" case .quizResult: "QuizResultView" @@ -116,13 +112,12 @@ extension Route { case .methodPreference: AnyView(MethodPreferenceView()) case .feedbackForm: AnyView(FeedbackFormView()) case .editProfile: AnyView(EditProfilePage()) - case .importReview(let sourceId): AnyView(ImportReviewPage(sourceId: sourceId)) case .quizList(let kbId): AnyView(QuizListView(knowledgeBaseId: kbId)) case .quizTake(let id): AnyView(QuizTakerView(quizId: id)) case .quizResult(let qid, let aid): AnyView(QuizResultView(quizId: qid, attemptId: aid)) - case .materialReader(let mid, let path, let mt, let kbId, let title): AnyView(MaterialReaderView(materialId: mid, filePath: path, materialType: mt, knowledgeBaseId: kbId, title: title)) - case .materialDetail(let kbId, let name, let type, let size, let path, let date): - AnyView(MaterialDetailView(knowledgeBaseId: kbId, fileName: name, fileType: type, fileSize: size, filePath: path, uploadDate: date)) + case .materialReader(let mid, let path, let mt, let kbId, let title, let rp): AnyView(MaterialReaderView(materialId: mid, filePath: path, materialType: mt, knowledgeBaseId: kbId, title: title, resumePosition: rp)) + case .materialDetail(let kbId, let sourceId, let name, let type, let size, let path, let date): + AnyView(MaterialDetailView(knowledgeBaseId: kbId, sourceId: sourceId, fileName: name, fileType: type, fileSize: size, filePath: path, uploadDate: date)) } }() diff --git a/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift b/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift index 3386c18..84796d9 100644 --- a/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift +++ b/AIStudyApp/AIStudyApp/Core/Network/APIClient.swift @@ -166,12 +166,19 @@ extension APIClient { private func decodeResponse(_ data: Data) throws -> T { let decoder = JSONDecoder() - // Try unwrapped response first (no envelope), then wrapped + // Check for error envelope first: { success: false, statusCode, message } + if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let success = json["success"] as? Bool, !success { + let message = json["message"] as? String ?? "请求失败" + let code = json["errorCode"] as? String + throw APIError.serverError(message, code: code) + } + // Try unwrapped response (no envelope) if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], json["data"] == nil && json["success"] == nil { return try decoder.decode(T.self, from: data) } - // Has envelope wrapper + // Has envelope wrapper: { success: true, data: ..., timestamp } do { let envelope = try decoder.decode(APIEnvelope.self, from: data) return envelope.data diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/AIAnalysisAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/AIAnalysisAPI.swift new file mode 100644 index 0000000..99d2a23 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/AIAnalysisAPI.swift @@ -0,0 +1,12 @@ +import Foundation + +@MainActor +final class AIAnalysisService { + static let shared = AIAnalysisService() + private let client = APIClient.shared + + func analyze(questionText: String, knowledgeItemContent: String, userAnswer: String) async throws -> AIAnalysisResult { + let body = AIAnalysisRequest(questionText: questionText, knowledgeItemContent: knowledgeItemContent, userAnswer: userAnswer, text: nil, type: nil) + return try await client.request("/ai-analysis", method: "POST", body: body) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/ActiveRecallAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/ActiveRecallAPI.swift new file mode 100644 index 0000000..112b130 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/ActiveRecallAPI.swift @@ -0,0 +1,19 @@ +import Foundation + +@MainActor +final class ActiveRecallService { + static let shared = ActiveRecallService() + private let client = APIClient.shared + + func questions(page: Int = 1, limit: Int = 20) async throws -> [ActiveRecallQuestion] { + try await client.request("/active-recalls", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } + + func submit(questionId: String, answerText: String) async throws -> ActiveRecallAnswer { + let body = SubmitAnswerRequest(answerText: answerText) + return try await client.request("/active-recalls/\(questionId)/submit", method: "POST", body: body) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/ActivityAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/ActivityAPI.swift new file mode 100644 index 0000000..4e7b8e6 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/ActivityAPI.swift @@ -0,0 +1,28 @@ +import Foundation + +@MainActor +final class ActivityService { + static let shared = ActivityService() + private let client = APIClient.shared + + func summary() async throws -> ActivitySummary { + try await client.request("/activity/summary") + } + + func heatmap() async throws -> [String: Int] { + try await client.request("/activity/heatmap") + } + + func trend(days: Int = 7) async throws -> [ActivityTrend] { + try await client.request("/activity/trend?days=\(days)") + } + + func streak() async throws -> ActivityStreak { + let offset = TimeZone.current.secondsFromGMT() / 60 + return try await client.request("/activity/streak?tzOffset=\(offset)") + } + + func recommendations() async throws -> [ActivityRecommendation] { + try await client.request("/activity/recommendations") + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/AuthAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/AuthAPI.swift new file mode 100644 index 0000000..b943ff7 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/AuthAPI.swift @@ -0,0 +1,25 @@ +import Foundation + +@MainActor +final class AuthService { + static let shared = AuthService() + private let client = APIClient.shared + + func appleLogin( + identityToken: String, + authorizationCode: String? = nil, + givenName: String?, + familyName: String?, + nonce: String? = nil + ) async throws -> AuthResponse { + let body = AppleAuthRequest( + identityToken: identityToken, + authorizationCode: authorizationCode, + fullName: givenName != nil + ? AppleAuthRequest.AppleFullName(givenName: givenName, familyName: familyName) + : nil, + nonce: nonce + ) + return try await client.request("/auth/apple", method: "POST", body: body) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/ChatAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/ChatAPI.swift new file mode 100644 index 0000000..b573ddc --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/ChatAPI.swift @@ -0,0 +1,102 @@ +import Foundation + +// MARK: - SSE Stream Types + +struct SSEChunk: Codable { + let type: String? + let delta: String? + let content: String? + let error: String? + let conversationId: String? + let toolCalls: String? + let citations: String? +} + +// MARK: - Service + +@MainActor +final class RagChatService { + static let shared = RagChatService() + private let client = APIClient.shared + + func createSession(ctx: ChatEntryContext, forceCreate: Bool = false) async throws -> ChatSession { + let body = CreateSessionRequest( + scopeType: ctx.scopeType.rawValue, scopeId: ctx.scopeId, + parentKnowledgeBaseId: ctx.parentKnowledgeBaseId, + createdFrom: ctx.createdFrom, title: nil, forceCreate: forceCreate ? true : nil + ) + return try await client.request("/rag-chat/sessions", method: "POST", body: body) + } + + func listSessions(scopeType: String? = nil, scopeId: String? = nil, parentKnowledgeBaseId: String? = nil) async throws -> [ChatSession] { + var items: [URLQueryItem] = [] + if let st = scopeType { items.append(URLQueryItem(name: "scopeType", value: st)) } + if let si = scopeId { items.append(URLQueryItem(name: "scopeId", value: si)) } + if let pk = parentKnowledgeBaseId { items.append(URLQueryItem(name: "parentKnowledgeBaseId", value: pk)) } + return try await client.request("/rag-chat/sessions", queryItems: items.isEmpty ? nil : items) + } + + func messages(sessionId: String) async throws -> [ChatMessage] { + try await client.request("/rag-chat/sessions/\(sessionId)/messages") + } + + func getMessages(sessionId: String) async throws -> [ChatMessage] { + try await messages(sessionId: sessionId) + } + + func updateSession(id: String, dto: UpdateChatSessionRequest) async throws -> ChatSession { + try await client.request("/rag-chat/sessions/\(id)", method: "PATCH", body: dto) + } + + func deleteSession(id: String) async throws -> GenericSuccessResponse { + try await client.request("/rag-chat/sessions/\(id)", method: "DELETE") + } + + func saveMessageAsKnowledge( + sessionId: String, + messageId: String, + knowledgeBaseId: String? = nil, + title: String? = nil, + summary: String? = nil + ) async throws -> SaveChatMessageAsKnowledgeResponse { + let body: [String: String?] = [ + "knowledgeBaseId": knowledgeBaseId, + "title": title, + "summary": summary + ] + return try await client.request( + "/rag-chat/sessions/\(sessionId)/messages/\(messageId)/save-as-knowledge", + method: "POST", + body: body + ) + } + + // MARK: - SSE Streaming + + func sendMessageStream(sessionId: String, content: String) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + let token = await client.getToken() ?? "" + let url = URL(string: "\(APIConfig.baseURL)/rag-chat/sessions/\(sessionId)/stream")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.httpBody = try? JSONEncoder().encode(SendMessageRequest(content: content)) + + do { + let (bytes, _) = try await URLSession.shared.bytes(for: request) + for try await line in bytes.lines { + guard let data = line.data(using: .utf8), + let chunk = try? JSONDecoder().decode(SSEChunk.self, from: data) else { continue } + continuation.yield(chunk) + if chunk.type == "done" || chunk.type == "error" { break } + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/FeedbackAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/FeedbackAPI.swift new file mode 100644 index 0000000..8c48697 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/FeedbackAPI.swift @@ -0,0 +1,12 @@ +import Foundation + +@MainActor +final class FeedbackService { + static let shared = FeedbackService() + private let client = APIClient.shared + + func submit(category: String = "general", content: String, email: String? = nil) async throws -> FeedbackData { + let body = FeedbackCreateRequest(category: category, content: content, email: email) + return try await client.request("/feedback", method: "POST", body: body) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/FocusItemAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/FocusItemAPI.swift new file mode 100644 index 0000000..44d6294 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/FocusItemAPI.swift @@ -0,0 +1,32 @@ +import Foundation + +struct FocusItemUpdateRequest: Codable { + let masteryScore: Int? + let title: String? +} + +struct FocusItemCompleteRequest: Codable { + let completed: Bool +} + +@MainActor +final class FocusItemService { + static let shared = FocusItemService() + private let client = APIClient.shared + + func list(page: Int = 1, limit: Int = 20) async throws -> [FocusItem] { + try await client.request("/focus-items", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } + + func update(id: String, masteryScore: Int? = nil, title: String? = nil) async throws -> FocusItem { + let body = FocusItemUpdateRequest(masteryScore: masteryScore, title: title) + return try await client.request("/focus-items/\(id)", method: "PATCH", body: body) + } + + func complete(id: String) async throws -> GenericSuccessResponse { + try await client.request("/focus-items/\(id)/complete", method: "POST") + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/ImportAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/ImportAPI.swift new file mode 100644 index 0000000..aa9f16d --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/ImportAPI.swift @@ -0,0 +1,38 @@ +import Foundation + +// MARK: - Document Import + +@MainActor +final class DocumentImportService { + static let shared = DocumentImportService() + private let client = APIClient.shared + + func status(importId: String) async throws -> ImportStatusResponse { + try await client.request("/document-import/\(importId)") + } + + func create(knowledgeBaseId: String, fileName: String? = nil, sourceType: String? = nil, rawText: String? = nil) async throws -> ImportStatusResponse { + let body: [String: String] = [ + "knowledgeBaseId": knowledgeBaseId, + "fileName": fileName ?? "", + "sourceType": sourceType ?? "", + "rawText": rawText ?? "", + ] + return try await client.request("/document-import", method: "POST", body: body) + } +} + +struct ImportStatusResponse: Codable { + let id: String + let status: String? + let progress: Int? + let errorMessage: String? +} + +struct CreateImportRequest: Codable { + let knowledgeBaseId: String + let fileId: String? + let sourceUrl: String? + let text: String? + let title: String? +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeBaseAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeBaseAPI.swift new file mode 100644 index 0000000..3babd56 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeBaseAPI.swift @@ -0,0 +1,56 @@ +import Foundation + +@MainActor +final class KnowledgeBaseService { + static let shared = KnowledgeBaseService() + private let client = APIClient.shared + + func list(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] { + try await client.request("/knowledge-bases", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } + + func create(title: String, description: String?, coverKey: String? = nil) async throws -> KnowledgeBase { + let body = CreateKnowledgeBaseRequest(title: title, description: description, coverKey: coverKey) + return try await client.request("/knowledge-bases", method: "POST", body: body) + } + + func detail(id: String) async throws -> KnowledgeBase { + try await client.request("/knowledge-bases/\(id)") + } + + func update(id: String, title: String?, description: String?) async throws -> KnowledgeBase { + let body = CreateKnowledgeBaseRequest(title: title ?? "", description: description, coverKey: nil) + return try await client.request("/knowledge-bases/\(id)", method: "PATCH", body: body) + } + + func delete(id: String) async throws -> GenericSuccessResponse { + try await client.request("/knowledge-bases/\(id)", method: "DELETE") + } + + func subscribe(kbId: String) async throws -> GenericSuccessResponse { + try await client.request("/knowledge-bases/\(kbId)/subscribe", method: "POST") + } + + func unsubscribe(kbId: String) async throws -> GenericSuccessResponse { + try await client.request("/knowledge-bases/\(kbId)/subscribe", method: "DELETE") + } + + func listSubscribed(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] { + try await client.request("/knowledge-bases/subscribed", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } + + func discover(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] { + try await client.request("/knowledge-bases", queryItems: [ + URLQueryItem(name: "visibility", value: "public"), + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } +} + diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeFolderAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeFolderAPI.swift new file mode 100644 index 0000000..20894a1 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeFolderAPI.swift @@ -0,0 +1,25 @@ +import Foundation + +@MainActor +final class KnowledgeFolderService { + static let shared = KnowledgeFolderService() + private let client = APIClient.shared + + func list(knowledgeBaseId: String) async throws -> [KnowledgeFolder] { + try await client.request("/knowledge-bases/\(knowledgeBaseId)/folders") + } + + func create(kbId: String, name: String, parentId: String? = nil) async throws -> KnowledgeFolder { + let body = CreateFolderRequest(name: name, parentId: parentId) + return try await client.request("/knowledge-bases/\(kbId)/folders", method: "POST", body: body) + } + + func update(kbId: String, folderId: String, name: String) async throws -> KnowledgeFolder { + let body = CreateFolderRequest(name: name, parentId: nil) + return try await client.request("/knowledge-bases/\(kbId)/folders/\(folderId)", method: "PATCH", body: body) + } + + func delete(kbId: String, folderId: String) async throws -> GenericSuccessResponse { + try await client.request("/knowledge-bases/\(kbId)/folders/\(folderId)", method: "DELETE") + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeItemAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeItemAPI.swift new file mode 100644 index 0000000..9e15f04 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeItemAPI.swift @@ -0,0 +1,36 @@ +import Foundation + +@MainActor +final class KnowledgeItemService { + static let shared = KnowledgeItemService() + private let client = APIClient.shared + + func list(knowledgeBaseId: String, sortBy: String? = nil, order: String? = nil) async throws -> [KnowledgeItem] { + var items = [URLQueryItem(name: "knowledgeBaseId", value: knowledgeBaseId)] + if let s = sortBy { items.append(URLQueryItem(name: "sortBy", value: s)) } + if let o = order { items.append(URLQueryItem(name: "order", value: o)) } + return try await client.request("/knowledge-items", queryItems: items) + } + + func detail(id: String) async throws -> KnowledgeItem { + try await client.request("/knowledge-items/\(id)") + } + + func create(knowledgeBaseId: String, title: String, content: String?, itemType: String? = nil) async throws -> KnowledgeItem { + let body = CreateKnowledgeItemRequest(knowledgeBaseId: knowledgeBaseId, title: title, content: content, itemType: itemType) + return try await client.request("/knowledge-items", method: "POST", body: body) + } + + func update(id: String, title: String?, content: String?, summary: String?) async throws -> KnowledgeItem { + let body = UpdateKnowledgeItemRequest(title: title, content: content, summary: summary) + return try await client.request("/knowledge-items/\(id)", method: "PATCH", body: body) + } + + func delete(id: String) async throws { + let _: GenericSuccessResponse = try await client.request("/knowledge-items/\(id)", method: "DELETE") + } + + func batchDelete(ids: [String]) async throws -> BatchDeleteResponse { + try await client.request("/knowledge-items/batch-delete", method: "POST", body: ["ids": ids]) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeSourceAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeSourceAPI.swift new file mode 100644 index 0000000..2b05c5b --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/KnowledgeSourceAPI.swift @@ -0,0 +1,67 @@ +import Foundation + +@MainActor +final class KnowledgeSourceService { + static let shared = KnowledgeSourceService() + private let client = APIClient.shared + + func list(kbId: String, sortBy: String? = nil, order: String? = nil) async throws -> [KnowledgeSource] { + var items = [URLQueryItem]() + if let s = sortBy { items.append(URLQueryItem(name: "sortBy", value: s)) } + if let o = order { items.append(URLQueryItem(name: "order", value: o)) } + return try await client.request("/knowledge-bases/\(kbId)/sources", queryItems: items.isEmpty ? nil : items) + } + + func detail(kbId: String, id: String) async throws -> KnowledgeSource { + try await client.request("/knowledge-bases/\(kbId)/sources/\(id)") + } + + func add( + kbId: String, + fileId: String? = nil, + type: String? = nil, + title: String? = nil, + originalFilename: String? = nil, + mimeType: String? = nil, + sizeBytes: Int? = nil, + originalObjectKey: String? = nil + ) async throws -> KnowledgeSource { + let body = AddSourceRequest( + fileId: fileId, type: type, title: title, + originalFilename: originalFilename, mimeType: mimeType, + sizeBytes: sizeBytes, originalObjectKey: originalObjectKey + ) + return try await client.request("/knowledge-bases/\(kbId)/sources", method: "POST", body: body) + } + + func delete(kbId: String, id: String) async throws -> GenericSuccessResponse { + try await client.request("/knowledge-bases/\(kbId)/sources/\(id)", method: "DELETE") + } + + func parse(kbId: String, sourceId: String) async throws -> ParseResponse { + try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/parse", method: "POST") + } + + func generateKnowledge(kbId: String, sourceId: String) async throws -> GenerateKnowledgeResponse { + try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/generate-knowledge", method: "POST") + } + + func generateKnowledgeBatch(kbId: String, sourceIds: [String]) async throws -> GenerateKnowledgeBatchResponse { + try await client.request( + "/knowledge-bases/\(kbId)/sources/generate-knowledge-batch", + method: "POST", + body: ["sourceIds": sourceIds] + ) + } + + func latestKnowledgeGenerationTask(kbId: String, sourceId: String) async throws -> KnowledgeGenerationTaskStatus? { + try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/knowledge-generation-task") + } + + func knowledgeGenerationTasks(kbId: String, sourceIds: [String] = []) async throws -> KnowledgeGenerationTaskListResponse { + let queryItems = sourceIds.isEmpty + ? nil + : [URLQueryItem(name: "sourceIds", value: sourceIds.joined(separator: ","))] + return try await client.request("/knowledge-bases/\(kbId)/sources/knowledge-generation-tasks", queryItems: queryItems) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/LearningSessionAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/LearningSessionAPI.swift new file mode 100644 index 0000000..ccf4a6d --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/LearningSessionAPI.swift @@ -0,0 +1,23 @@ +import Foundation + +@MainActor +final class LearningSessionService { + static let shared = LearningSessionService() + private let client = APIClient.shared + + func list(page: Int = 1, limit: Int = 20) async throws -> [LearningSession] { + try await client.request("/learning-sessions", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } + + func start(knowledgeBaseId: String? = nil, knowledgeItemId: String? = nil, mode: String? = nil) async throws -> LearningSession { + let body = CreateLearningSessionRequest(knowledgeBaseId: knowledgeBaseId, knowledgeItemId: knowledgeItemId, mode: mode) + return try await client.request("/learning-sessions", method: "POST", body: body) + } + + func end(id: String) async throws -> LearningSession { + try await client.request("/learning-sessions/\(id)/end", method: "POST") + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/MembershipAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/MembershipAPI.swift new file mode 100644 index 0000000..d5f9ee3 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/MembershipAPI.swift @@ -0,0 +1,27 @@ +import Foundation + +@MainActor +final class MembershipService { + static let shared = MembershipService() + private let client = APIClient.shared + + func myMembership() async throws -> MyMembershipResponse { + try await client.request("/membership/my") + } + + func plans() async throws -> MembershipPlanResponse { + try await client.request("/membership/plans") + } + + func getPlans() async throws -> MembershipPlanResponse { + try await plans() + } + + func submitTransaction(_ dto: TransactionSubmitBody) async throws -> GenericSuccessResponse { + try await client.request("/membership/apple/transactions", method: "POST", body: dto) + } + + func entitlements() async throws -> Entitlements { + try await client.request("/membership/entitlements") + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/NotificationAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/NotificationAPI.swift new file mode 100644 index 0000000..ae64cde --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/NotificationAPI.swift @@ -0,0 +1,39 @@ +import Foundation + +@MainActor +final class NotificationService { + static let shared = NotificationService() + private let client = APIClient.shared + + func list(page: Int = 1, limit: Int = 20) async throws -> [NotificationItem] { + try await client.request("/notifications", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } + + func markRead(id: String) async throws -> GenericSuccessResponse { + try await client.request("/notifications/\(id)/read", method: "POST") + } + + func markAllRead() async throws -> GenericSuccessResponse { + try await client.request("/notifications/read-all", method: "POST") + } + + func unreadCount() async throws -> Int { + let resp: UnreadCountResponse = try await client.request("/notifications/unread-count") + return resp.count + } + + func registerPushToken(_ token: String) async throws -> GenericSuccessResponse { + try await client.request("/notifications/push-token", method: "POST", body: ["token": token]) + } + + func removePushToken(_ token: String) async throws -> GenericSuccessResponse { + try await client.request("/notifications/push-tokens/\(token)", method: "DELETE") + } +} + +struct UnreadCountResponse: Codable { + let count: Int +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/QuizAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/QuizAPI.swift new file mode 100644 index 0000000..b4418c9 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/QuizAPI.swift @@ -0,0 +1,36 @@ +import Foundation + +@MainActor +final class QuizService { + static let shared = QuizService() + private let client = APIClient.shared + + func generate(knowledgeBaseId: String, sourceType: String? = nil, sourceId: String? = nil, questionCount: Int = 5) async throws -> Quiz { + let body = QuizGenerateRequest( + knowledgeBaseId: knowledgeBaseId, + sourceType: sourceType, + sourceId: sourceId, + questionCount: questionCount + ) + return try await client.request("/quizzes", method: "POST", body: body) + } + + func list(knowledgeBaseId: String? = nil) async throws -> [Quiz] { + var items: [URLQueryItem]? = nil + if let kb = knowledgeBaseId { items = [URLQueryItem(name: "knowledgeBaseId", value: kb)] } + return try await client.request("/quizzes", queryItems: items) + } + + func detail(id: String) async throws -> Quiz { try await client.request("/quizzes/\(id)") } + func createAttempt(quizId: String) async throws -> QuizAttemptStartResponse { try await client.request("/quizzes/\(quizId)/attempts", method: "POST") } + func getAttemptQuestions(attemptId: String) async throws -> [QuizQuestion] { try await client.request("/quizzes/attempts/\(attemptId)/questions") } + func start(quizId: String) async throws -> QuizAttempt { try await client.request("/quizzes/\(quizId)/start", method: "POST") } + + func submit(quizId: String, attemptId: String, answers: [QuizAnswerItem]) async throws -> QuizSubmitResponse { + let body = QuizSubmitRequest(attemptId: attemptId, answers: answers) + return try await client.request("/quizzes/\(quizId)/submit", method: "POST", body: body) + } + + func results(quizId: String, attemptId: String) async throws -> QuizAttempt { try await client.request("/quizzes/\(quizId)/results?attemptId=\(attemptId)") } + func history() async throws -> [QuizAttempt] { try await client.request("/quizzes/history/list") } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/ReadingAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/ReadingAPI.swift new file mode 100644 index 0000000..4561bbb --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/ReadingAPI.swift @@ -0,0 +1,57 @@ +import Foundation + +// MARK: - Reading Progress (DTOs) + +struct MaterialReadingProgressDTO: Codable { + let status: String? + let isMarkedRead: Bool? + let lastPosition: ReadingPositionValue? + let totalActiveSeconds: Int? + let sessionCount: Int? +} + +// MARK: - Service + +@MainActor +final class ReadingAPIService { + static let shared = ReadingAPIService() + private let client = APIClient.shared + + // Protocol-compatible: ReadingProgressServicing + func getReadingProgress(materialId: String, targetType: String) async throws -> MaterialReadingProgressDTO { + try await client.request("/materials/\(materialId)/reading-progress?readingTargetType=\(targetType)") + } + + // Convenience with default + func getReadingProgress(materialId: String, readingTargetType: String = "material") async throws -> MaterialReadingProgressDTO { + try await getReadingProgress(materialId: materialId, targetType: readingTargetType) + } + + func uploadReadingEvents(_ events: [ReadingEventUploadItem]) async throws -> GenericSuccessResponse { + let batch = ReadingEventBatchRequest(events: events) + return try await client.request("/learning/reading-events/batch", method: "POST", body: batch) + } + + func uploadReadingEvents(_ batch: ReadingEventBatchRequest) async throws -> GenericSuccessResponse { + try await client.request("/learning/reading-events/batch", method: "POST", body: batch) + } + + func getContinueLearning() async throws -> ContinueLearningResponse { + try await client.request("/learning/continue") + } + + func getLearningSummary() async throws -> LearningSummaryResponse { + try await client.request("/learning/summary") + } + + func getLearningTrend(days: Int = 7) async throws -> LearningTrendResponse { + try await client.request("/learning/trend?days=\(days)") + } + + func getLearningRecords(cursor: String? = nil, limit: Int = 10, type: String? = nil) async throws -> LearningRecordsResponse { + var items: [URLQueryItem] = [URLQueryItem(name: "limit", value: String(limit))] + if let c = cursor { items.append(URLQueryItem(name: "cursor", value: c)) } + if let t = type { items.append(URLQueryItem(name: "type", value: t)) } + return try await client.request("/learning/records", queryItems: items) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/ReviewAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/ReviewAPI.swift new file mode 100644 index 0000000..2f5d29d --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/ReviewAPI.swift @@ -0,0 +1,24 @@ +import Foundation + +@MainActor +final class ReviewService { + static let shared = ReviewService() + private let client = APIClient.shared + + func dueCards() async throws -> [ReviewCard] { + try await client.request("/reviews/due") + } + + func submit(id: String, rating: String, responseText: String? = nil) async throws -> ReviewCard { + var body: [String: String] = ["rating": rating] + if let text = responseText { body["responseText"] = text } + return try await client.request("/reviews/\(id)/submit", method: "POST", body: body) + } + + func history(page: Int = 1, limit: Int = 20) async throws -> [ReviewCard] { + try await client.request("/reviews/history", queryItems: [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + ]) + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/UserAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/UserAPI.swift new file mode 100644 index 0000000..aadb7e8 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/UserAPI.swift @@ -0,0 +1,53 @@ +import Foundation + +@MainActor +final class UserService { + static let shared = UserService() + private let client = APIClient.shared + + func myProfile() async throws -> UserProfileResponse { + try await client.request("/users/me") + } + + func updateProfile(_ dto: UpdateProfileRequest) async throws -> UserProfileResponse { + try await client.request("/users/me", method: "PATCH", body: dto) + } + + func updatePreferences(_ dto: UpdatePreferencesRequest) async throws -> UserPreferences { + try await client.request("/users/me/preferences", method: "PATCH", body: dto) + } + + func getProfileDetail() async throws -> UserProfileData { + try await client.request("/users/me/profile") + } + + func updateProfileDetail(_ dto: UpdateProfileDataRequest) async throws -> UserProfileData { + try await client.request("/users/me/profile", method: "PATCH", body: dto) + } + + func requestAccountDeletion() async throws -> GenericSuccessResponse { + try await client.request("/users/me/deletion-request", method: "POST") + } + + func cancelDeletion() async throws -> GenericSuccessResponse { + try await client.request("/users/me/deletion-request", method: "DELETE") + } + + func listDevices() async throws -> [UserDevice] { + try await client.request("/users/me/devices") + } + + func removeDevice(id: String) async throws -> GenericSuccessResponse { + try await client.request("/users/me/devices/\(id)", method: "DELETE") + } + + func fetchAssets() async throws -> AssetsResponse { + try await client.request("/users/me/assets-summary") + } + + func fetchStorage() async throws -> StorageResponse { + try await client.request("/users/me/storage") + } +} + + diff --git a/AIStudyApp/AIStudyApp/Core/Services/API/WaitlistAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/API/WaitlistAPI.swift new file mode 100644 index 0000000..1714e92 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/API/WaitlistAPI.swift @@ -0,0 +1,20 @@ +import Foundation + +@MainActor +final class WaitlistService { + static let shared = WaitlistService() + private let client = APIClient.shared + + func join(email: String, name: String? = nil) async throws -> WaitlistResponse { + let body = WaitlistCreateRequest(email: email, name: name) + return try await client.request("/waitlist", method: "POST", body: body) + } + + func list() async throws -> [WaitlistEntry] { + try await client.request("/waitlist") + } + + func stats() async throws -> WaitlistStats { + try await client.request("/waitlist/stats") + } +} diff --git a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift index cc0ec95..63a251a 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift @@ -1,684 +1,27 @@ // -// APIService.swift - 各模块 API 调用封装 +// APIService.swift — 各模块 API 调用封装 +// +// 所有 Service 类已拆分到 Core/Services/API/ 下独立文件: +// +// AuthAPI.swift → AuthService +// UserAPI.swift → UserService +// KnowledgeBaseAPI.swift → KnowledgeBaseService +// KnowledgeSourceAPI.swift → KnowledgeSourceService +// KnowledgeItemAPI.swift → KnowledgeItemService +// ActiveRecallAPI.swift → ActiveRecallService +// AIAnalysisAPI.swift → AIAnalysisService +// LearningSessionAPI.swift → LearningSessionService +// ReviewAPI.swift → ReviewService +// FocusItemAPI.swift → FocusItemService +// ActivityAPI.swift → ActivityService +// FeedbackAPI.swift → FeedbackService +// ChatAPI.swift → RagChatService +// QuizAPI.swift → QuizService +// NotificationAPI.swift → NotificationService +// MembershipAPI.swift → MembershipService +// ImportAPI.swift → DocumentImportService +// WaitlistAPI.swift → WaitlistService +// ReadingAPI.swift → ReadingAPIService // import Foundation - -// MARK: - Waitlist - -@MainActor -class WaitlistService { - static let shared = WaitlistService() - private let client = APIClient.shared - - func join(email: String, nickname: String?, devices: [String]?, - interests: [String]?, painpoint: String?, willingBeta: Bool = true) async throws -> WaitlistEntry { - let body = WaitlistCreateRequest(email: email, nickname: nickname, devices: devices, - interests: interests, painpoint: painpoint, willingBeta: willingBeta) - return try await client.request("/waitlist", method: "POST", body: body) - } - - func stats() async throws -> WaitlistStats { - return try await client.request("/waitlist/stats") - } -} - -// MARK: - Auth - -@MainActor -class AuthService { - static let shared = AuthService() - private let client = APIClient.shared - - func appleLogin(identityToken: String, authorizationCode: String? = nil, givenName: String?, familyName: String?, nonce: String? = nil) async throws -> AuthResponse { - let body = AppleAuthRequest( - identityToken: identityToken, - authorizationCode: authorizationCode, - fullName: givenName != nil - ? AppleAuthRequest.AppleFullName(givenName: givenName, familyName: familyName) - : nil, - nonce: nonce - ) - return try await client.request("/auth/apple", method: "POST", body: body) - } -} - -// MARK: - User - -@MainActor -class UserService { - static let shared = UserService() - private let client = APIClient.shared - - func myProfile() async throws -> UserProfileResponse { - return try await client.request("/users/me") - } - - func updateProfile(_ dto: UpdateProfileRequest) async throws -> UserProfileResponse { - return try await client.request("/users/me", method: "PATCH", body: dto) - } - - func updatePreferences(_ dto: UpdatePreferencesRequest) async throws -> UserPreferences { - return try await client.request("/users/me/preferences", method: "PATCH", body: dto) - } - - func getProfileDetail() async throws -> UserProfileData { - return try await client.request("/users/me/profile") - } - - func updateProfileDetail(_ dto: UpdateProfileDataRequest) async throws -> UserProfileData { - return try await client.request("/users/me/profile", method: "PATCH", body: dto) - } - - func requestAccountDeletion() async throws -> GenericSuccessResponse { - return try await client.request("/users/me/deletion-request", method: "POST") - } - - func cancelDeletion() async throws -> GenericSuccessResponse { - return try await client.request("/users/me/deletion-request", method: "DELETE") - } - - func listDevices() async throws -> [UserDevice] { - return try await client.request("/users/me/devices") - } - - func removeDevice(id: String) async throws -> GenericSuccessResponse { - return try await client.request("/users/me/devices/\(id)", method: "DELETE") - } - - func fetchAssets() async throws -> AssetsResponse { - return try await client.request("/users/me/assets-summary") - } - - func fetchStorage() async throws -> StorageResponse { - return try await client.request("/users/me/storage") - } -} - -struct UserDevice: Codable, Identifiable { - let id: String - let deviceName: String? - let deviceType: String? - let lastSeenAt: String? -} - -// MARK: - Knowledge Base - -@MainActor -class KnowledgeBaseService { - static let shared = KnowledgeBaseService() - private let client = APIClient.shared - - func list(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] { - return try await client.request("/knowledge-bases", queryItems: [ - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func create(title: String, description: String?, coverKey: String? = nil) async throws -> KnowledgeBase { - let body = CreateKnowledgeBaseRequest(title: title, description: description, coverKey: coverKey) - return try await client.request("/knowledge-bases", method: "POST", body: body) - } - - func detail(id: String) async throws -> KnowledgeBase { - return try await client.request("/knowledge-bases/\(id)") - } - - func update(id: String, title: String?, description: String?) async throws -> KnowledgeBase { - let body = CreateKnowledgeBaseRequest(title: title ?? "", description: description) - return try await client.request("/knowledge-bases/\(id)", method: "PATCH", body: body) - } - - func listSubscribed(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] { - return try await client.request("/knowledge-bases/subscribed", queryItems: [ - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func discover(page: Int = 1, limit: Int = 20) async throws -> [KnowledgeBase] { - return try await client.request("/knowledge-bases", queryItems: [ - URLQueryItem(name: "visibility", value: "public"), - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func subscribe(kbId: String) async throws -> GenericSuccessResponse { - return try await client.request("/knowledge-bases/\(kbId)/subscribe", method: "POST") - } - - func unsubscribe(kbId: String) async throws -> GenericSuccessResponse { - return try await client.request("/knowledge-bases/\(kbId)/subscribe", method: "DELETE") - } - - func delete(id: String) async throws -> GenericSuccessResponse { - return try await client.request("/knowledge-bases/\(id)", method: "DELETE") - } -} - -// MARK: - Knowledge Folder - -@MainActor -class KnowledgeFolderService { - static let shared = KnowledgeFolderService() - private let client = APIClient.shared - - func list(kbId: String) async throws -> [KnowledgeFolder] { - return try await client.request("/knowledge-bases/\(kbId)/folders") - } - - func create(kbId: String, name: String, parentId: String? = nil) async throws -> KnowledgeFolder { - let body = CreateFolderRequest(name: name, parentId: parentId) - return try await client.request("/knowledge-bases/\(kbId)/folders", method: "POST", body: body) - } - - func update(kbId: String, folderId: String, name: String) async throws -> KnowledgeFolder { - return try await client.request("/knowledge-bases/\(kbId)/folders/\(folderId)", method: "PATCH", body: ["name": name]) - } - - func delete(kbId: String, folderId: String) async throws -> GenericSuccessResponse { - return try await client.request("/knowledge-bases/\(kbId)/folders/\(folderId)", method: "DELETE") - } -} - -// MARK: - Knowledge Items - -@MainActor -class KnowledgeItemService { - static let shared = KnowledgeItemService() - private let client = APIClient.shared - - func list(knowledgeBaseId: String, sortBy: String? = nil, order: String? = nil) async throws -> [KnowledgeItem] { - var items = [URLQueryItem(name: "knowledgeBaseId", value: knowledgeBaseId)] - if let s = sortBy { items.append(URLQueryItem(name: "sortBy", value: s)) } - if let o = order { items.append(URLQueryItem(name: "order", value: o)) } - return try await client.request("/knowledge-items", queryItems: items) - } - - func detail(id: String) async throws -> KnowledgeItem { - return try await client.request("/knowledge-items/\(id)") - } - - func create(knowledgeBaseId: String, title: String, content: String?, itemType: String? = nil) async throws -> KnowledgeItem { - let body = CreateKnowledgeItemRequest( - knowledgeBaseId: knowledgeBaseId, - title: title, - content: content, - itemType: itemType - ) - return try await client.request("/knowledge-items", method: "POST", body: body) - } - - func update(id: String, title: String?, content: String?, summary: String?) async throws -> KnowledgeItem { - let body = UpdateKnowledgeItemRequest(title: title, content: content, summary: summary) - return try await client.request("/knowledge-items/\(id)", method: "PATCH", body: body) - } - - func delete(id: String) async throws { - let _: GenericSuccessResponse = try await client.request("/knowledge-items/\(id)", method: "DELETE") - } - - func batchDelete(ids: [String]) async throws -> BatchDeleteResponse { - return try await client.request("/knowledge-items/batch-delete", method: "POST", body: ["ids": ids]) - } -} - -// MARK: - Active Recall - -@MainActor -class ActiveRecallService { - static let shared = ActiveRecallService() - private let client = APIClient.shared - - func questions(page: Int = 1, limit: Int = 20) async throws -> [ActiveRecallQuestion] { - return try await client.request("/active-recalls", queryItems: [ - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func submit(questionId: String, answerText: String) async throws -> ActiveRecallAnswer { - let body = SubmitAnswerRequest(answerText: answerText) - return try await client.request("/active-recalls/\(questionId)/submit", method: "POST", body: body) - } -} - -// MARK: - AI Analysis - -@MainActor -class AIAnalysisService { - static let shared = AIAnalysisService() - private let client = APIClient.shared - - func analyze(questionText: String, knowledgeItemContent: String, userAnswer: String) async throws -> AIAnalysisResult { - let body = AIAnalysisRequest( - questionText: questionText, - knowledgeItemContent: knowledgeItemContent, - userAnswer: userAnswer, - text: nil, - type: nil - ) - return try await client.request("/ai-analysis", method: "POST", body: body) - } -} - -// MARK: - Learning Sessions - -@MainActor -class LearningSessionService { - static let shared = LearningSessionService() - private let client = APIClient.shared - - func list(page: Int = 1, limit: Int = 20) async throws -> [LearningSession] { - return try await client.request("/learning-sessions", queryItems: [ - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func start(knowledgeBaseId: String? = nil, knowledgeItemId: String? = nil, mode: String? = nil) async throws -> LearningSession { - let body = CreateLearningSessionRequest( - knowledgeBaseId: knowledgeBaseId, - knowledgeItemId: knowledgeItemId, - mode: mode - ) - return try await client.request("/learning-sessions", method: "POST", body: body) - } - - func end(id: String) async throws -> LearningSession { - return try await client.request("/learning-sessions/\(id)/end", method: "POST") - } - -} - -// MARK: - Review - -@MainActor -class ReviewService { - static let shared = ReviewService() - private let client = APIClient.shared - - func dueCards() async throws -> [ReviewCard] { - return try await client.request("/reviews/due") - } - - func submit(id: String, rating: String, responseText: String? = nil) async throws -> GenericSuccessResponse { - let body = SubmitReviewRequest(rating: rating, responseText: responseText) - return try await client.request("/reviews/\(id)/submit", method: "POST", body: body) - } - - func generateCards() async throws -> GenericSuccessResponse { - return try await client.request("/reviews/generate-cards", method: "POST") - } -} - -// MARK: - Focus Items - -@MainActor -class FocusItemService { - static let shared = FocusItemService() - private let client = APIClient.shared - - func list(page: Int = 1, limit: Int = 20) async throws -> [FocusItem] { - return try await client.request("/focus-items", queryItems: [ - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func update(id: String, masteryScore: Int? = nil, title: String? = nil) async throws -> FocusItem { - let body = FocusItemUpdateRequest(masteryScore: masteryScore, title: title) - return try await client.request("/focus-items/\(id)", method: "PATCH", body: body) - } - - func complete(id: String) async throws -> GenericSuccessResponse { - return try await client.request("/focus-items/\(id)/complete", method: "POST") - } -} - -// MARK: - Activity & Stats - -@MainActor -class ActivityService { - static let shared = ActivityService() - private let client = APIClient.shared - - func summary() async throws -> ActivitySummary { - return try await client.request("/activity/summary") - } - - func heatmap() async throws -> [String: Int] { - return try await client.request("/activity/heatmap") - } - - func trend(days: Int = 7) async throws -> [ActivityTrend] { - return try await client.request("/activity/trend?days=\(days)") - } - - func streak() async throws -> ActivityStreak { - return try await client.request("/activity/streak") - } - - func recommendations() async throws -> [ActivityRecommendation] { - return try await client.request("/activity/recommendations") - } -} - -// MARK: - Feedback - -@MainActor -class FeedbackService { - static let shared = FeedbackService() - private let client = APIClient.shared - - func submit(category: String = "general", content: String, email: String? = nil) async throws -> FeedbackData { - let body = FeedbackCreateRequest(category: category, content: content, email: email) - return try await client.request("/feedback", method: "POST", body: body) - } -} - -// MARK: - RAG Chat - -@MainActor -class RagChatService { - static let shared = RagChatService() - private let client = APIClient.shared - - func createSession(ctx: ChatEntryContext, forceCreate: Bool = false) async throws -> ChatSession { - let body = CreateSessionRequest( - scopeType: ctx.scopeType.rawValue, - scopeId: ctx.scopeId, - parentKnowledgeBaseId: ctx.parentKnowledgeBaseId, - createdFrom: ctx.createdFrom, - title: nil, - forceCreate: forceCreate ? true : nil - ) - print("[RagChat] POST /rag-chat/sessions — scopeType=\(body.scopeType), scopeId=\(body.scopeId ?? "nil"), parentKB=\(body.parentKnowledgeBaseId ?? "nil"), forceCreate=\(forceCreate)") - do { - let session = try await client.request("/rag-chat/sessions", method: "POST", body: body) as ChatSession - print("[RagChat] createSession OK — id=\(session.id), scopeType=\(session.scopeType ?? "nil")") - return session - } catch { - print("[RagChat] createSession FAILED: \(error)") - throw error - } - } - - func listSessions(scopeType: String? = nil, scopeId: String? = nil, parentKnowledgeBaseId: String? = nil) async throws -> [ChatSession] { - var items: [URLQueryItem] = [] - if let st = scopeType { items.append(URLQueryItem(name: "scopeType", value: st)) } - if let si = scopeId { items.append(URLQueryItem(name: "scopeId", value: si)) } - if let pk = parentKnowledgeBaseId { items.append(URLQueryItem(name: "parentKnowledgeBaseId", value: pk)) } - let page: PaginatedResponse = try await client.request("/rag-chat/sessions", queryItems: items.isEmpty ? nil : items) - return page.data - } - - func getMessages(sessionId: String) async throws -> [ChatMessage] { - return try await client.request("/rag-chat/sessions/\(sessionId)/messages") - } - - func sendMessage(sessionId: String, content: String) async throws -> SendMessageResponse { - let body = SendMessageRequest(content: content) - return try await client.request("/rag-chat/sessions/\(sessionId)/messages", method: "POST", body: body) - } - - func updateSession(id: String, dto: UpdateChatSessionRequest) async throws -> ChatSession { - return try await client.request("/rag-chat/sessions/\(id)", method: "PATCH", body: dto) - } - - func deleteSession(id: String) async throws { - let _: GenericSuccessResponse = try await client.request("/rag-chat/sessions/\(id)", method: "DELETE") - } - - struct SSEChunk: Decodable { - let type: String - let content: String? - } - - func sendMessageStream(sessionId: String, content: String) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - Task { - let token = await APIClient.shared.getToken() ?? "" - let url = URL(string: "\(APIConfig.baseURL)/rag-chat/sessions/\(sessionId)/stream")! - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.httpBody = try? JSONEncoder().encode(SendMessageRequest(content: content)) - - let session = URLSession(configuration: .default) - let (bytes, _) = try await session.bytes(for: request) - - // Read SSE lines properly — URLSession.bytes is character-based (UTF-8 safe) - var lineBuffer = "" - for try await line in bytes.lines { - if line.hasPrefix("data: "), let jsonData = line.dropFirst(6).data(using: .utf8) { - if let chunk = try? JSONDecoder().decode(SSEChunk.self, from: jsonData) { - continuation.yield(chunk) - if chunk.type == "done" || chunk.type == "error" { - continuation.finish() - return - } - } - } - } - continuation.finish() - } - } - } -} - -// MARK: - Document Import - -@MainActor -class DocumentImportService { - static let shared = DocumentImportService() - private let client = APIClient.shared - - func create(knowledgeBaseId: String, fileName: String? = nil, sourceType: String? = "file", rawText: String? = nil) async throws -> ImportStatusResponse { - let body = CreateImportRequest(knowledgeBaseId: knowledgeBaseId, fileName: fileName, sourceType: sourceType, rawText: rawText) - return try await client.request("/imports", method: "POST", body: body) - } - - func getStatus(id: String) async throws -> ImportStatusResponse { - return try await client.request("/imports/\(id)/status") - } -} - -// MARK: - Import Candidate - -@MainActor -class ImportCandidateService { - static let shared = ImportCandidateService() - private let client = APIClient.shared - - func listBySource(sourceId: String) async throws -> [ImportCandidate] { - return try await client.request("/knowledge-sources/\(sourceId)/import-candidates") - } - - func detail(id: String) async throws -> ImportCandidate { - return try await client.request("/import-candidates/\(id)") - } - - func update(id: String, title: String?, content: String?) async throws -> ImportCandidate { - var body: [String: String] = [:] - if let t = title { body["title"] = t } - if let c = content { body["content"] = c } - return try await client.request("/import-candidates/\(id)", method: "PATCH", body: body) - } - - func accept(id: String) async throws -> GenericSuccessResponse { - return try await client.request("/import-candidates/\(id)/accept", method: "POST") - } - - func reject(id: String) async throws -> GenericSuccessResponse { - return try await client.request("/import-candidates/\(id)/reject", method: "POST") - } - - func batchAccept(sourceId: String) async throws -> GenericSuccessResponse { - let body = BatchAcceptRequest(sourceId: sourceId) - return try await client.request("/import-candidates/batch-accept", method: "POST", body: body) - } -} - -// MARK: - Knowledge Source - -@MainActor -class KnowledgeSourceService { - static let shared = KnowledgeSourceService() - private let client = APIClient.shared - - func list(kbId: String, sortBy: String? = nil, order: String? = nil) async throws -> [KnowledgeSource] { - var items = [URLQueryItem]() - if let s = sortBy { items.append(URLQueryItem(name: "sortBy", value: s)) } - if let o = order { items.append(URLQueryItem(name: "order", value: o)) } - return try await client.request("/knowledge-bases/\(kbId)/sources", queryItems: items.isEmpty ? nil : items) - } - - func detail(kbId: String, id: String) async throws -> KnowledgeSource { - return try await client.request("/knowledge-bases/\(kbId)/sources/\(id)") - } - - func add(kbId: String, fileId: String? = nil, type: String? = nil, title: String? = nil, originalFilename: String? = nil, mimeType: String? = nil, sizeBytes: Int? = nil, originalObjectKey: String? = nil) async throws -> KnowledgeSource { - let body = AddSourceRequest(fileId: fileId, type: type, title: title, originalFilename: originalFilename, mimeType: mimeType, sizeBytes: sizeBytes, originalObjectKey: originalObjectKey) - return try await client.request("/knowledge-bases/\(kbId)/sources", method: "POST", body: body) - } - - func delete(kbId: String, id: String) async throws -> GenericSuccessResponse { - return try await client.request("/knowledge-bases/\(kbId)/sources/\(id)", method: "DELETE") - } - - func parse(kbId: String, sourceId: String) async throws -> ParseResponse { - return try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/parse", method: "POST") - } - - func generateKnowledge(kbId: String, sourceId: String) async throws -> GenerateKnowledgeResponse { - return try await client.request("/knowledge-bases/\(kbId)/sources/\(sourceId)/generate-knowledge", method: "POST") - } - -} - -// MARK: - Quiz - -@MainActor -class QuizService { - static let shared = QuizService() - private let client = APIClient.shared - - func generate(knowledgeBaseId: String, questionCount: Int = 5) async throws -> Quiz { - let body = QuizGenerateRequest(knowledgeBaseId: knowledgeBaseId, questionCount: questionCount) - return try await client.request("/quizzes", method: "POST", body: body) - } - - func list(knowledgeBaseId: String? = nil) async throws -> [Quiz] { - var items: [URLQueryItem]? = nil - if let kb = knowledgeBaseId { items = [URLQueryItem(name: "knowledgeBaseId", value: kb)] } - return try await client.request("/quizzes", queryItems: items) - } - - func detail(id: String) async throws -> Quiz { return try await client.request("/quizzes/\(id)") } - - // M-QUIZ-01-03: 使用新 Attempt API - func createAttempt(quizId: String) async throws -> QuizAttemptStartResponse { - return try await client.request("/quizzes/\(quizId)/attempts", method: "POST") - } - - // M-QUIZ-01-03: 按 Attempt 加载脱敏题目 - func getAttemptQuestions(attemptId: String) async throws -> [QuizQuestion] { - return try await client.request("/quizzes/attempts/\(attemptId)/questions") - } - - // 保留旧端点兼容 - func start(quizId: String) async throws -> QuizAttempt { return try await client.request("/quizzes/\(quizId)/start", method: "POST") } - - func submit(quizId: String, attemptId: String, answers: [QuizAnswerItem]) async throws -> QuizSubmitResponse { - let body = QuizSubmitRequest(attemptId: attemptId, answers: answers) - return try await client.request("/quizzes/\(quizId)/submit", method: "POST", body: body) - } - func results(quizId: String, attemptId: String) async throws -> QuizAttempt { return try await client.request("/quizzes/\(quizId)/results?attemptId=\(attemptId)") } - func history() async throws -> [QuizAttempt] { return try await client.request("/quizzes/history/list") } -} - -// MARK: - Notifications - -@MainActor -class NotificationService { - static let shared = NotificationService() - private let client = APIClient.shared - - func list(page: Int = 1, limit: Int = 20) async throws -> [NotificationItem] { - return try await client.request("/notifications", queryItems: [ - URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "limit", value: String(limit)), - ]) - } - - func markRead(id: String) async throws -> NotificationItem { - return try await client.request("/notifications/\(id)/read", method: "PATCH") - } - - func markAllRead() async throws -> GenericSuccessResponse { - return try await client.request("/notifications/read-all", method: "POST") - } - - func getPreferences() async throws -> NotificationPreferences { - return try await client.request("/notifications/preferences") - } - - func updatePreferences(_ dto: NotificationPreferences) async throws -> NotificationPreferences { - return try await client.request("/notifications/preferences", method: "PATCH", body: dto) - } - - func registerPushToken(_ token: String) async throws -> GenericSuccessResponse { - return try await client.request("/notifications/push-token", method: "POST", body: ["token": token]) - } - - func removePushToken(_ token: String) async throws -> GenericSuccessResponse { - return try await client.request("/notifications/push-tokens/\(token)", method: "DELETE") - } -} - -// MARK: - Membership (M-MEMBER-01-09) - -@MainActor -class MembershipService { - static let shared = MembershipService() - private let client = APIClient.shared - - func getPlans() async throws -> MembershipPlanResponse { - try await client.request("/membership/plans") - } - - func myMembership() async throws -> MyMembershipResponse { - try await client.request("/membership/me") - } - - func getUsage() async throws -> UsageSummary { - try await client.request("/membership/usage") - } - - func submitTransaction(signedTransactionInfo: String) async throws -> TransactionSubmitResponse { - try await client.request( - "/membership/apple/transactions", - method: "POST", - body: ["signedTransactionInfo": signedTransactionInfo] as [String: String] - ) - } -} - -struct NotificationPreferences: Codable { - var reviewReminder: Bool? - var newFeatures: Bool? - var systemNotice: Bool? -} - -struct FocusItemUpdateRequest: Codable { - let masteryScore: Int? - let title: String? -} - -struct FocusItemCompleteRequest: Codable { - let id: String -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/MaterialPathResolver.swift b/AIStudyApp/AIStudyApp/Core/Services/MaterialPathResolver.swift index dcafec9..08a7ab2 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/MaterialPathResolver.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/MaterialPathResolver.swift @@ -7,8 +7,19 @@ struct MaterialPathResolver { static func resolve(materialId: String, apiType: String?, title: String?) -> (filePath: String, materialType: MaterialType)? { let mt = materialTypeFrom(apiType: apiType, title: title) - // 1. knowledge_source: look up by source ID in local KnowledgeSourceService + // 1. knowledge_source: check actual download paths first (tmp/), then legacy paths if apiType == "knowledge_source" { + // Priority 1: tmp/ — where MaterialReaderView and KnowledgeDetailVM actually download to + let tmpDir = FileManager.default.temporaryDirectory + if let files = try? FileManager.default.contentsOfDirectory(at: tmpDir, includingPropertiesForKeys: nil) { + for f in files { + let name = f.lastPathComponent + if name.hasPrefix("source_\(materialId).") || name.hasPrefix("kd_\(materialId).") { + return (f.path, mt) + } + } + } + // Priority 2: legacy path Documents/knowledge_sources// if let path = resolveKnowledgeSource(id: materialId) { return (path, mt) } diff --git a/AIStudyApp/AIStudyApp/Core/Services/ReadingAPI.swift b/AIStudyApp/AIStudyApp/Core/Services/ReadingAPI.swift deleted file mode 100644 index c28f8c5..0000000 --- a/AIStudyApp/AIStudyApp/Core/Services/ReadingAPI.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation - -// MARK: - Reading API Service - -@MainActor -class ReadingAPIService { - static let shared = ReadingAPIService() - private let client = APIClient.shared - - /// POST /learning/reading-events/batch - func uploadReadingEvents(_ events: [ReadingEventUploadItem]) async throws -> ReadingEventBatchResponse { - let body = ReadingEventBatchRequest(events: events) - return try await client.request("/learning/reading-events/batch", method: "POST", body: body) - } - - /// GET /materials/:id/reading-progress - func getReadingProgress(materialId: String, targetType: String) async throws -> MaterialReadingProgressDTO { - return try await client.request( - "/materials/\(materialId)/reading-progress", - queryItems: [URLQueryItem(name: "readingTargetType", value: targetType)] - ) - } - - /// GET /learning/continue - func getContinueLearning() async throws -> ContinueLearningResponse { - return try await client.request("/learning/continue") - } - - /// GET /learning/summary - func getLearningSummary() async throws -> LearningSummaryResponse { - return try await client.request("/learning/summary") - } - - /// GET /learning/trend?days=7 - func getLearningTrend(days: Int = 7) async throws -> LearningTrendResponse { - return try await client.request( - "/learning/trend", - queryItems: [URLQueryItem(name: "days", value: String(days))] - ) - } - - /// GET /learning/records - func getLearningRecords(cursor: String? = nil, limit: Int = 20, type: String? = nil) async throws -> LearningRecordsResponse { - var items = [URLQueryItem(name: "limit", value: String(limit))] - if let c = cursor { items.append(URLQueryItem(name: "cursor", value: c)) } - if let t = type { items.append(URLQueryItem(name: "type", value: t)) } - return try await client.request("/learning/records", queryItems: items) - } -} - -// MARK: - Response DTOs - -/// Codable wrapper for API's lastPosition (JSON object matching the server's ReadingPosition schema). -struct ReadingPositionValue: Codable { - let type: String? - let blockId: String? - let lineNumber: UInt32? - let pageNumber: UInt32? - let chapterId: String? - let scrollProgress: Float? - let pageProgress: Float? - let overallProgress: Float? - let zoomScale: Float? - let offsetX: Float? - let offsetY: Float? - let chapterProgress: Float? -} - -struct MaterialReadingProgressDTO: Codable { - let status: String - let lastPosition: ReadingPositionValue? - let lastProgress: Float? - let totalActiveSeconds: Int - let isMarkedRead: Bool - let sessionCount: Int? - let firstOpenedAt: String? - let lastReadAt: String? -} - -struct ContinueLearningResponse: Codable { - let type: String? - let materialId: String? - let title: String? - let lastProgress: Float? - let totalActiveSeconds: Int? - let lastReadAt: String? -} - -struct LearningSummaryResponse: Codable { - let todaySeconds: Int - let weekSeconds: Int - let totalSeconds: Int - let activeDays: Int - let sessionsCount: Int - let materialsReadCount: Int - let markedReadCount: Int - let dailyAverageSeconds: Int -} - -struct LearningTrendResponse: Codable { - let days: Int - let series: [TrendPoint] - - struct TrendPoint: Codable { - let date: String - let value: Int - } -} - -struct LearningRecordsResponse: Codable { - let items: [RecordItem] - let nextCursor: String? - - struct RecordItem: Codable { - let id: String - let recordType: String - let title: String - let description: String? - let durationSeconds: Int - let occurredAt: String - let metadata: RecordMetadata? - let createdAt: String - } - - struct RecordMetadata: Codable { - let materialId: String? - let readingTargetType: String? - let knowledgeBaseId: String? - let totalActiveSeconds: Int? - } -} diff --git a/AIStudyApp/AIStudyApp/Core/Services/ReadingRuntimeAdapter.swift b/AIStudyApp/AIStudyApp/Core/Services/ReadingRuntimeAdapter.swift index 270b465..f56ddfc 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/ReadingRuntimeAdapter.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/ReadingRuntimeAdapter.swift @@ -16,17 +16,19 @@ struct ReadingRuntimeSession { protocol ReadingRuntimeAdapter { // ── Session Lifecycle ── - func startSession(material: ReadingMaterialRef, timestampMs: Int64) throws -> String + func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String func pauseSession(_ sessionId: String) throws func resumeSession(_ sessionId: String) throws - func closeSession(_ sessionId: String) throws + func closeSession(_ sessionId: String, timestampMs: Int64) throws // ── Event Push ── func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 func pushClosed(sessionId: String, materialId: String, delta: UInt32, timestampMs: Int64) throws -> ReadingEventV2 func pushPositionChanged(sessionId: String, materialId: String, position: ReadingPosition, timestampMs: Int64) throws -> ReadingEventV2 + func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 + func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 // ── Buffer Management ── @@ -36,12 +38,14 @@ protocol ReadingRuntimeAdapter { func markFailed(_ eventIds: [String]) -> UInt32 func reloadStaleEvents() -> UInt32 func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 + func runtimeComputeFileFingerprint(filePath: String) throws -> String + func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String } // MARK: - Default Implementation (Rust FFI) final class RustReadingRuntimeAdapter: ReadingRuntimeAdapter { - func startSession(material: ReadingMaterialRef, timestampMs: Int64) throws -> String { + func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String { let result = try startReadingSessionV2(material: material, timestampMs: timestampMs) return result } @@ -54,8 +58,8 @@ final class RustReadingRuntimeAdapter: ReadingRuntimeAdapter { try resumeReadingSessionV2(sessionId: sessionId) } - func closeSession(_ sessionId: String) throws { - try closeReadingSessionV2(sessionId: sessionId) + func closeSession(_ sessionId: String, timestampMs: Int64) throws { + try closeReadingSessionV2(sessionId: sessionId, timestampMs: timestampMs) } func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { @@ -70,10 +74,18 @@ final class RustReadingRuntimeAdapter: ReadingRuntimeAdapter { return try pushPositionChangedV2(sessionId: sessionId, materialId: materialId, position: position, timestampMs: timestampMs) } + func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 { + return try pushCheckpointChangedV2(sessionId: sessionId, materialId: materialId, checkpoint: checkpoint, timestampMs: timestampMs) + } + func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 { return try pushHeartbeatV2(sessionId: sessionId, materialId: materialId, activeSecondsDelta: delta, position: position, timestampMs: timestampMs) } + func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 { + return try pushHeartbeatCheckpointV2(sessionId: sessionId, materialId: materialId, activeSecondsDelta: delta, checkpoint: checkpoint, timestampMs: timestampMs) + } + func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { return try pushMarkedAsReadV2(sessionId: sessionId, materialId: materialId, timestampMs: timestampMs) } @@ -97,6 +109,19 @@ final class RustReadingRuntimeAdapter: ReadingRuntimeAdapter { func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { return cleanupStaleSessionsFfi(nowMs: nowMs, maxAgeMs: maxAgeMs) } + + func runtimeComputeFileFingerprint(filePath: String) throws -> String { + return try computeFileFingerprint(filePath: filePath) + } + + func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String { + return buildRepresentationFingerprint( + sourceFingerprint: sourceFingerprint, + rendererKind: rendererKind, + parserVersion: parserVersion, + schemaVersion: schemaVersion + ) + } } // MARK: - V1 Adapter (backward compatibility) @@ -112,7 +137,7 @@ final class RustReadingRuntimeAdapter: ReadingRuntimeAdapter { @MainActor final class V1ReadingRuntimeAdapter: ReadingRuntimeAdapter { - func startSession(material: ReadingMaterialRef, timestampMs: Int64) throws -> String { + func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String { let uuid = UUID().uuidString let event = ReadingEvent.materialOpened( materialId: material.materialId, @@ -126,7 +151,7 @@ final class V1ReadingRuntimeAdapter: ReadingRuntimeAdapter { func resumeSession(_ sessionId: String) throws { /* V1: no-op */ } - func closeSession(_ sessionId: String) throws { /* V1: no-op */ } + func closeSession(_ sessionId: String, timestampMs: Int64) throws { /* V1: no-op */ } func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { throw ReadingRuntimeAdapterError.v1ReturnTypeUnsupported @@ -140,10 +165,18 @@ final class V1ReadingRuntimeAdapter: ReadingRuntimeAdapter { throw ReadingRuntimeAdapterError.v1ReturnTypeUnsupported } + func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 { + throw ReadingRuntimeAdapterError.v1ReturnTypeUnsupported + } + func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 { throw ReadingRuntimeAdapterError.v1ReturnTypeUnsupported } + func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 { + throw ReadingRuntimeAdapterError.v1ReturnTypeUnsupported + } + func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { throw ReadingRuntimeAdapterError.v1ReturnTypeUnsupported } @@ -167,6 +200,14 @@ final class V1ReadingRuntimeAdapter: ReadingRuntimeAdapter { func reloadStaleEvents() -> UInt32 { return 0 } func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { return 0 } + + func runtimeComputeFileFingerprint(filePath: String) throws -> String { + throw ReadingRuntimeAdapterError.unavailable + } + + func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String { + return "\(rendererKind)-\(parserVersion)-\(schemaVersion)-\(sourceFingerprint)" + } } // MARK: - Noop Adapter (test / fallback) @@ -176,7 +217,7 @@ final class V1ReadingRuntimeAdapter: ReadingRuntimeAdapter { @MainActor final class NoopReadingRuntimeAdapter: ReadingRuntimeAdapter { - func startSession(material: ReadingMaterialRef, timestampMs: Int64) throws -> String { + func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String { return "noop-\(UUID().uuidString)" } @@ -184,7 +225,7 @@ final class NoopReadingRuntimeAdapter: ReadingRuntimeAdapter { func resumeSession(_ sessionId: String) throws { } - func closeSession(_ sessionId: String) throws { } + func closeSession(_ sessionId: String, timestampMs: Int64) throws { } func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { throw ReadingRuntimeAdapterError.unavailable @@ -198,10 +239,18 @@ final class NoopReadingRuntimeAdapter: ReadingRuntimeAdapter { throw ReadingRuntimeAdapterError.unavailable } + func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 { + throw ReadingRuntimeAdapterError.unavailable + } + func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 { throw ReadingRuntimeAdapterError.unavailable } + func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 { + throw ReadingRuntimeAdapterError.unavailable + } + func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { throw ReadingRuntimeAdapterError.unavailable } @@ -215,6 +264,14 @@ final class NoopReadingRuntimeAdapter: ReadingRuntimeAdapter { func reloadStaleEvents() -> UInt32 { return 0 } func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { return 0 } + + func runtimeComputeFileFingerprint(filePath: String) throws -> String { + throw ReadingRuntimeAdapterError.unavailable + } + + func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String { + return "\(rendererKind)-\(parserVersion)-\(schemaVersion)-\(sourceFingerprint)" + } } // MARK: - Runtime Error diff --git a/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift b/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift index 7774da8..a948d16 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift @@ -4,7 +4,12 @@ // swiftlint:disable all import Foundation -// C types imported via BridgingHeader.h +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(zx_documentFFI) +import zx_documentFFI +#endif private func uniffiEnsureZxDocumentFfiInitialized() {} @@ -32,7 +37,7 @@ fileprivate extension RustBuffer { } } -extension ForeignBytes { +fileprivate extension ForeignBytes { init(bufferPointer: UnsafeBufferPointer) { self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) } @@ -247,7 +252,7 @@ fileprivate let CALL_ERROR: Int8 = 1 fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 fileprivate let CALL_CANCELLED: Int8 = 3 -extension RustCallStatus { +fileprivate extension RustCallStatus { init() { self.init( code: CALL_SUCCESS, @@ -564,11 +569,20 @@ public struct DocumentInfo: Equatable, Hashable { public var fileSize: UInt64 public var pageCount: UInt32? public var wordCount: UInt32? + public var charCount: UInt32? + public var lineCount: UInt32? + public var imageWidth: UInt32? + public var imageHeight: UInt32? + public var imageFormat: String? + public var epubChapterCount: UInt32? + public var isSearchable: Bool + public var supportsPosition: Bool + public var supportsAnchor: Bool public var createdAt: String? // Default memberwise initializers are never public by default, so we // declare one manually. - public init(materialId: String, title: String, materialType: MaterialType, previewMode: PreviewMode, fileSize: UInt64, pageCount: UInt32?, wordCount: UInt32?, createdAt: String?) { + public init(materialId: String, title: String, materialType: MaterialType, previewMode: PreviewMode, fileSize: UInt64, pageCount: UInt32?, wordCount: UInt32?, charCount: UInt32?, lineCount: UInt32?, imageWidth: UInt32?, imageHeight: UInt32?, imageFormat: String?, epubChapterCount: UInt32?, isSearchable: Bool, supportsPosition: Bool, supportsAnchor: Bool, createdAt: String?) { self.materialId = materialId self.title = title self.materialType = materialType @@ -576,6 +590,15 @@ public struct DocumentInfo: Equatable, Hashable { self.fileSize = fileSize self.pageCount = pageCount self.wordCount = wordCount + self.charCount = charCount + self.lineCount = lineCount + self.imageWidth = imageWidth + self.imageHeight = imageHeight + self.imageFormat = imageFormat + self.epubChapterCount = epubChapterCount + self.isSearchable = isSearchable + self.supportsPosition = supportsPosition + self.supportsAnchor = supportsAnchor self.createdAt = createdAt } @@ -602,6 +625,15 @@ public struct FfiConverterTypeDocumentInfo: FfiConverterRustBuffer { fileSize: FfiConverterUInt64.read(from: &buf), pageCount: FfiConverterOptionUInt32.read(from: &buf), wordCount: FfiConverterOptionUInt32.read(from: &buf), + charCount: FfiConverterOptionUInt32.read(from: &buf), + lineCount: FfiConverterOptionUInt32.read(from: &buf), + imageWidth: FfiConverterOptionUInt32.read(from: &buf), + imageHeight: FfiConverterOptionUInt32.read(from: &buf), + imageFormat: FfiConverterOptionString.read(from: &buf), + epubChapterCount: FfiConverterOptionUInt32.read(from: &buf), + isSearchable: FfiConverterBool.read(from: &buf), + supportsPosition: FfiConverterBool.read(from: &buf), + supportsAnchor: FfiConverterBool.read(from: &buf), createdAt: FfiConverterOptionString.read(from: &buf) ) } @@ -614,6 +646,15 @@ public struct FfiConverterTypeDocumentInfo: FfiConverterRustBuffer { FfiConverterUInt64.write(value.fileSize, into: &buf) FfiConverterOptionUInt32.write(value.pageCount, into: &buf) FfiConverterOptionUInt32.write(value.wordCount, into: &buf) + FfiConverterOptionUInt32.write(value.charCount, into: &buf) + FfiConverterOptionUInt32.write(value.lineCount, into: &buf) + FfiConverterOptionUInt32.write(value.imageWidth, into: &buf) + FfiConverterOptionUInt32.write(value.imageHeight, into: &buf) + FfiConverterOptionString.write(value.imageFormat, into: &buf) + FfiConverterOptionUInt32.write(value.epubChapterCount, into: &buf) + FfiConverterBool.write(value.isSearchable, into: &buf) + FfiConverterBool.write(value.supportsPosition, into: &buf) + FfiConverterBool.write(value.supportsAnchor, into: &buf) FfiConverterOptionString.write(value.createdAt, into: &buf) } } @@ -758,6 +799,68 @@ public func FfiConverterTypeEpubMetadata_lower(_ value: EpubMetadata) -> RustBuf } +public struct EventBufferStateV2: Equatable, Hashable { + public var total: UInt32 + public var pending: UInt32 + public var exported: UInt32 + public var failed: UInt32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(total: UInt32, pending: UInt32, exported: UInt32, failed: UInt32) { + self.total = total + self.pending = pending + self.exported = exported + self.failed = failed + } + + + + +} + +#if compiler(>=6) +extension EventBufferStateV2: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeEventBufferStateV2: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EventBufferStateV2 { + return + try EventBufferStateV2( + total: FfiConverterUInt32.read(from: &buf), + pending: FfiConverterUInt32.read(from: &buf), + exported: FfiConverterUInt32.read(from: &buf), + failed: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: EventBufferStateV2, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.total, into: &buf) + FfiConverterUInt32.write(value.pending, into: &buf) + FfiConverterUInt32.write(value.exported, into: &buf) + FfiConverterUInt32.write(value.failed, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeEventBufferStateV2_lift(_ buf: RustBuffer) throws -> EventBufferStateV2 { + return try FfiConverterTypeEventBufferStateV2.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeEventBufferStateV2_lower(_ value: EventBufferStateV2) -> RustBuffer { + return FfiConverterTypeEventBufferStateV2.lower(value) +} + + public struct ImageMeta: Equatable, Hashable { public var width: UInt32 public var height: UInt32 @@ -998,24 +1101,104 @@ public func FfiConverterTypePdfPageText_lower(_ value: PdfPageText) -> RustBuffe } +public struct ReadingCheckpoint: Equatable, Hashable { + public var schemaVersion: UInt32 + public var materialId: String + public var representationFingerprint: String + public var rendererKind: RendererKind + public var capturedAtMs: UInt64 + public var deviceId: String + public var deviceSequence: UInt64 + public var position: ReadingPosition + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(schemaVersion: UInt32, materialId: String, representationFingerprint: String, rendererKind: RendererKind, capturedAtMs: UInt64, deviceId: String, deviceSequence: UInt64, position: ReadingPosition) { + self.schemaVersion = schemaVersion + self.materialId = materialId + self.representationFingerprint = representationFingerprint + self.rendererKind = rendererKind + self.capturedAtMs = capturedAtMs + self.deviceId = deviceId + self.deviceSequence = deviceSequence + self.position = position + } + + + + +} + +#if compiler(>=6) +extension ReadingCheckpoint: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingCheckpoint: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingCheckpoint { + return + try ReadingCheckpoint( + schemaVersion: FfiConverterUInt32.read(from: &buf), + materialId: FfiConverterString.read(from: &buf), + representationFingerprint: FfiConverterString.read(from: &buf), + rendererKind: FfiConverterTypeRendererKind.read(from: &buf), + capturedAtMs: FfiConverterUInt64.read(from: &buf), + deviceId: FfiConverterString.read(from: &buf), + deviceSequence: FfiConverterUInt64.read(from: &buf), + position: FfiConverterTypeReadingPosition.read(from: &buf) + ) + } + + public static func write(_ value: ReadingCheckpoint, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.schemaVersion, into: &buf) + FfiConverterString.write(value.materialId, into: &buf) + FfiConverterString.write(value.representationFingerprint, into: &buf) + FfiConverterTypeRendererKind.write(value.rendererKind, into: &buf) + FfiConverterUInt64.write(value.capturedAtMs, into: &buf) + FfiConverterString.write(value.deviceId, into: &buf) + FfiConverterUInt64.write(value.deviceSequence, into: &buf) + FfiConverterTypeReadingPosition.write(value.position, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingCheckpoint_lift(_ buf: RustBuffer) throws -> ReadingCheckpoint { + return try FfiConverterTypeReadingCheckpoint.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingCheckpoint_lower(_ value: ReadingCheckpoint) -> RustBuffer { + return FfiConverterTypeReadingCheckpoint.lower(value) +} + + public struct ReadingEventV2: Equatable, Hashable { public var eventId: String public var clientSessionId: String public var materialId: String public var eventType: ReadingEventTypeV2 public var position: ReadingPosition? + public var checkpoint: ReadingCheckpoint? public var activeSecondsDelta: UInt32 public var timestampMs: Int64 public var sequence: UInt64 // Default memberwise initializers are never public by default, so we // declare one manually. - public init(eventId: String, clientSessionId: String, materialId: String, eventType: ReadingEventTypeV2, position: ReadingPosition?, activeSecondsDelta: UInt32, timestampMs: Int64, sequence: UInt64) { + public init(eventId: String, clientSessionId: String, materialId: String, eventType: ReadingEventTypeV2, position: ReadingPosition?, checkpoint: ReadingCheckpoint?, activeSecondsDelta: UInt32, timestampMs: Int64, sequence: UInt64) { self.eventId = eventId self.clientSessionId = clientSessionId self.materialId = materialId self.eventType = eventType self.position = position + self.checkpoint = checkpoint self.activeSecondsDelta = activeSecondsDelta self.timestampMs = timestampMs self.sequence = sequence @@ -1042,6 +1225,7 @@ public struct FfiConverterTypeReadingEventV2: FfiConverterRustBuffer { materialId: FfiConverterString.read(from: &buf), eventType: FfiConverterTypeReadingEventTypeV2.read(from: &buf), position: FfiConverterOptionTypeReadingPosition.read(from: &buf), + checkpoint: FfiConverterOptionTypeReadingCheckpoint.read(from: &buf), activeSecondsDelta: FfiConverterUInt32.read(from: &buf), timestampMs: FfiConverterInt64.read(from: &buf), sequence: FfiConverterUInt64.read(from: &buf) @@ -1054,6 +1238,7 @@ public struct FfiConverterTypeReadingEventV2: FfiConverterRustBuffer { FfiConverterString.write(value.materialId, into: &buf) FfiConverterTypeReadingEventTypeV2.write(value.eventType, into: &buf) FfiConverterOptionTypeReadingPosition.write(value.position, into: &buf) + FfiConverterOptionTypeReadingCheckpoint.write(value.checkpoint, into: &buf) FfiConverterUInt32.write(value.activeSecondsDelta, into: &buf) FfiConverterInt64.write(value.timestampMs, into: &buf) FfiConverterUInt64.write(value.sequence, into: &buf) @@ -1126,26 +1311,108 @@ public func FfiConverterTypeReadingMaterialRef_lower(_ value: ReadingMaterialRef } +public struct ReadingMaterialRefV2: Equatable, Hashable { + public var materialId: String + public var filePath: String + public var fileName: String + public var fileType: String + public var mimeType: String? + public var fileSize: UInt64? + public var contentHash: String? + public var createdAtMs: Int64? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(materialId: String, filePath: String, fileName: String, fileType: String, mimeType: String?, fileSize: UInt64?, contentHash: String?, createdAtMs: Int64?) { + self.materialId = materialId + self.filePath = filePath + self.fileName = fileName + self.fileType = fileType + self.mimeType = mimeType + self.fileSize = fileSize + self.contentHash = contentHash + self.createdAtMs = createdAtMs + } + + + + +} + +#if compiler(>=6) +extension ReadingMaterialRefV2: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingMaterialRefV2: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingMaterialRefV2 { + return + try ReadingMaterialRefV2( + materialId: FfiConverterString.read(from: &buf), + filePath: FfiConverterString.read(from: &buf), + fileName: FfiConverterString.read(from: &buf), + fileType: FfiConverterString.read(from: &buf), + mimeType: FfiConverterOptionString.read(from: &buf), + fileSize: FfiConverterOptionUInt64.read(from: &buf), + contentHash: FfiConverterOptionString.read(from: &buf), + createdAtMs: FfiConverterOptionInt64.read(from: &buf) + ) + } + + public static func write(_ value: ReadingMaterialRefV2, into buf: inout [UInt8]) { + FfiConverterString.write(value.materialId, into: &buf) + FfiConverterString.write(value.filePath, into: &buf) + FfiConverterString.write(value.fileName, into: &buf) + FfiConverterString.write(value.fileType, into: &buf) + FfiConverterOptionString.write(value.mimeType, into: &buf) + FfiConverterOptionUInt64.write(value.fileSize, into: &buf) + FfiConverterOptionString.write(value.contentHash, into: &buf) + FfiConverterOptionInt64.write(value.createdAtMs, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingMaterialRefV2_lift(_ buf: RustBuffer) throws -> ReadingMaterialRefV2 { + return try FfiConverterTypeReadingMaterialRefV2.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingMaterialRefV2_lower(_ value: ReadingMaterialRefV2) -> RustBuffer { + return FfiConverterTypeReadingMaterialRefV2.lower(value) +} + + public struct ReadingSessionV2: Equatable, Hashable { public var clientSessionId: String - public var material: ReadingMaterialRef + public var material: ReadingMaterialRefV2 public var startedAtMs: Int64 + public var closedAtMs: Int64? public var lastEventAtMs: Int64 public var nextSequence: UInt64 public var totalActiveSeconds: UInt32 public var lastPosition: ReadingPosition? + public var lastCheckpoint: ReadingCheckpoint? public var status: ReadingSessionStatus // Default memberwise initializers are never public by default, so we // declare one manually. - public init(clientSessionId: String, material: ReadingMaterialRef, startedAtMs: Int64, lastEventAtMs: Int64, nextSequence: UInt64, totalActiveSeconds: UInt32, lastPosition: ReadingPosition?, status: ReadingSessionStatus) { + public init(clientSessionId: String, material: ReadingMaterialRefV2, startedAtMs: Int64, closedAtMs: Int64?, lastEventAtMs: Int64, nextSequence: UInt64, totalActiveSeconds: UInt32, lastPosition: ReadingPosition?, lastCheckpoint: ReadingCheckpoint?, status: ReadingSessionStatus) { self.clientSessionId = clientSessionId self.material = material self.startedAtMs = startedAtMs + self.closedAtMs = closedAtMs self.lastEventAtMs = lastEventAtMs self.nextSequence = nextSequence self.totalActiveSeconds = totalActiveSeconds self.lastPosition = lastPosition + self.lastCheckpoint = lastCheckpoint self.status = status } @@ -1166,24 +1433,28 @@ public struct FfiConverterTypeReadingSessionV2: FfiConverterRustBuffer { return try ReadingSessionV2( clientSessionId: FfiConverterString.read(from: &buf), - material: FfiConverterTypeReadingMaterialRef.read(from: &buf), + material: FfiConverterTypeReadingMaterialRefV2.read(from: &buf), startedAtMs: FfiConverterInt64.read(from: &buf), + closedAtMs: FfiConverterOptionInt64.read(from: &buf), lastEventAtMs: FfiConverterInt64.read(from: &buf), nextSequence: FfiConverterUInt64.read(from: &buf), totalActiveSeconds: FfiConverterUInt32.read(from: &buf), lastPosition: FfiConverterOptionTypeReadingPosition.read(from: &buf), + lastCheckpoint: FfiConverterOptionTypeReadingCheckpoint.read(from: &buf), status: FfiConverterTypeReadingSessionStatus.read(from: &buf) ) } public static func write(_ value: ReadingSessionV2, into buf: inout [UInt8]) { FfiConverterString.write(value.clientSessionId, into: &buf) - FfiConverterTypeReadingMaterialRef.write(value.material, into: &buf) + FfiConverterTypeReadingMaterialRefV2.write(value.material, into: &buf) FfiConverterInt64.write(value.startedAtMs, into: &buf) + FfiConverterOptionInt64.write(value.closedAtMs, into: &buf) FfiConverterInt64.write(value.lastEventAtMs, into: &buf) FfiConverterUInt64.write(value.nextSequence, into: &buf) FfiConverterUInt32.write(value.totalActiveSeconds, into: &buf) FfiConverterOptionTypeReadingPosition.write(value.lastPosition, into: &buf) + FfiConverterOptionTypeReadingCheckpoint.write(value.lastCheckpoint, into: &buf) FfiConverterTypeReadingSessionStatus.write(value.status, into: &buf) } } @@ -1491,6 +1762,16 @@ public enum DocumentError: Swift.Error, Equatable, Hashable, Foundation.Localize case IoError(message: String) + case SessionNotFound(message: String) + + case SessionAlreadyActive(message: String) + + case SessionNotActive(message: String) + + case BufferOverflow(message: String) + + case Unsupported(message: String) + @@ -1540,6 +1821,26 @@ public struct FfiConverterTypeDocumentError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) + case 6: return .SessionNotFound( + message: try FfiConverterString.read(from: &buf) + ) + + case 7: return .SessionAlreadyActive( + message: try FfiConverterString.read(from: &buf) + ) + + case 8: return .SessionNotActive( + message: try FfiConverterString.read(from: &buf) + ) + + case 9: return .BufferOverflow( + message: try FfiConverterString.read(from: &buf) + ) + + case 10: return .Unsupported( + message: try FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } @@ -1561,6 +1862,16 @@ public struct FfiConverterTypeDocumentError: FfiConverterRustBuffer { writeInt(&buf, Int32(4)) case .IoError(_ /* message is ignored*/): writeInt(&buf, Int32(5)) + case .SessionNotFound(_ /* message is ignored*/): + writeInt(&buf, Int32(6)) + case .SessionAlreadyActive(_ /* message is ignored*/): + writeInt(&buf, Int32(7)) + case .SessionNotActive(_ /* message is ignored*/): + writeInt(&buf, Int32(8)) + case .BufferOverflow(_ /* message is ignored*/): + writeInt(&buf, Int32(9)) + case .Unsupported(_ /* message is ignored*/): + writeInt(&buf, Int32(10)) } @@ -2213,14 +2524,24 @@ public func FfiConverterTypeReadingEventTypeV2_lower(_ value: ReadingEventTypeV2 public enum ReadingPosition: Equatable, Hashable { + case block(blockId: String, blockProgress: Float, overallProgress: Float + ) case markdown(blockId: String, scrollProgress: Float ) case text(lineNumber: UInt32, scrollProgress: Float ) case pdf(pageNumber: UInt32, pageProgress: Float, overallProgress: Float ) + case pdfViewport(pageNumber: UInt32, anchorX: Float, anchorY: Float, zoomScale: Float, overallProgress: Float + ) case image(zoomScale: Float, offsetX: Float, offsetY: Float ) + case imageViewport(zoomScale: Float, anchorX: Float, anchorY: Float + ) + case workbook(sheetId: String, topRow: UInt32, leftColumn: UInt32, selectedCell: String?, zoomScale: Float, overallProgress: Float? + ) + case slide(slideIndex: UInt32, zoomScale: Float, anchorX: Float, anchorY: Float, overallProgress: Float + ) case epub(chapterId: String, chapterProgress: Float, overallProgress: Float ) case unknown @@ -2245,22 +2566,37 @@ public struct FfiConverterTypeReadingPosition: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .markdown(blockId: try FfiConverterString.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf) + case 1: return .block(blockId: try FfiConverterString.read(from: &buf), blockProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) ) - case 2: return .text(lineNumber: try FfiConverterUInt32.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf) + case 2: return .markdown(blockId: try FfiConverterString.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf) ) - case 3: return .pdf(pageNumber: try FfiConverterUInt32.read(from: &buf), pageProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) + case 3: return .text(lineNumber: try FfiConverterUInt32.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf) ) - case 4: return .image(zoomScale: try FfiConverterFloat.read(from: &buf), offsetX: try FfiConverterFloat.read(from: &buf), offsetY: try FfiConverterFloat.read(from: &buf) + case 4: return .pdf(pageNumber: try FfiConverterUInt32.read(from: &buf), pageProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) ) - case 5: return .epub(chapterId: try FfiConverterString.read(from: &buf), chapterProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) + case 5: return .pdfViewport(pageNumber: try FfiConverterUInt32.read(from: &buf), anchorX: try FfiConverterFloat.read(from: &buf), anchorY: try FfiConverterFloat.read(from: &buf), zoomScale: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) ) - case 6: return .unknown + case 6: return .image(zoomScale: try FfiConverterFloat.read(from: &buf), offsetX: try FfiConverterFloat.read(from: &buf), offsetY: try FfiConverterFloat.read(from: &buf) + ) + + case 7: return .imageViewport(zoomScale: try FfiConverterFloat.read(from: &buf), anchorX: try FfiConverterFloat.read(from: &buf), anchorY: try FfiConverterFloat.read(from: &buf) + ) + + case 8: return .workbook(sheetId: try FfiConverterString.read(from: &buf), topRow: try FfiConverterUInt32.read(from: &buf), leftColumn: try FfiConverterUInt32.read(from: &buf), selectedCell: try FfiConverterOptionString.read(from: &buf), zoomScale: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterOptionFloat.read(from: &buf) + ) + + case 9: return .slide(slideIndex: try FfiConverterUInt32.read(from: &buf), zoomScale: try FfiConverterFloat.read(from: &buf), anchorX: try FfiConverterFloat.read(from: &buf), anchorY: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) + ) + + case 10: return .epub(chapterId: try FfiConverterString.read(from: &buf), chapterProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) + ) + + case 11: return .unknown default: throw UniffiInternalError.unexpectedEnumCase } @@ -2270,41 +2606,83 @@ public struct FfiConverterTypeReadingPosition: FfiConverterRustBuffer { switch value { - case let .markdown(blockId,scrollProgress): + case let .block(blockId,blockProgress,overallProgress): writeInt(&buf, Int32(1)) FfiConverterString.write(blockId, into: &buf) + FfiConverterFloat.write(blockProgress, into: &buf) + FfiConverterFloat.write(overallProgress, into: &buf) + + + case let .markdown(blockId,scrollProgress): + writeInt(&buf, Int32(2)) + FfiConverterString.write(blockId, into: &buf) FfiConverterFloat.write(scrollProgress, into: &buf) case let .text(lineNumber,scrollProgress): - writeInt(&buf, Int32(2)) + writeInt(&buf, Int32(3)) FfiConverterUInt32.write(lineNumber, into: &buf) FfiConverterFloat.write(scrollProgress, into: &buf) case let .pdf(pageNumber,pageProgress,overallProgress): - writeInt(&buf, Int32(3)) + writeInt(&buf, Int32(4)) FfiConverterUInt32.write(pageNumber, into: &buf) FfiConverterFloat.write(pageProgress, into: &buf) FfiConverterFloat.write(overallProgress, into: &buf) + case let .pdfViewport(pageNumber,anchorX,anchorY,zoomScale,overallProgress): + writeInt(&buf, Int32(5)) + FfiConverterUInt32.write(pageNumber, into: &buf) + FfiConverterFloat.write(anchorX, into: &buf) + FfiConverterFloat.write(anchorY, into: &buf) + FfiConverterFloat.write(zoomScale, into: &buf) + FfiConverterFloat.write(overallProgress, into: &buf) + + case let .image(zoomScale,offsetX,offsetY): - writeInt(&buf, Int32(4)) + writeInt(&buf, Int32(6)) FfiConverterFloat.write(zoomScale, into: &buf) FfiConverterFloat.write(offsetX, into: &buf) FfiConverterFloat.write(offsetY, into: &buf) + case let .imageViewport(zoomScale,anchorX,anchorY): + writeInt(&buf, Int32(7)) + FfiConverterFloat.write(zoomScale, into: &buf) + FfiConverterFloat.write(anchorX, into: &buf) + FfiConverterFloat.write(anchorY, into: &buf) + + + case let .workbook(sheetId,topRow,leftColumn,selectedCell,zoomScale,overallProgress): + writeInt(&buf, Int32(8)) + FfiConverterString.write(sheetId, into: &buf) + FfiConverterUInt32.write(topRow, into: &buf) + FfiConverterUInt32.write(leftColumn, into: &buf) + FfiConverterOptionString.write(selectedCell, into: &buf) + FfiConverterFloat.write(zoomScale, into: &buf) + FfiConverterOptionFloat.write(overallProgress, into: &buf) + + + case let .slide(slideIndex,zoomScale,anchorX,anchorY,overallProgress): + writeInt(&buf, Int32(9)) + FfiConverterUInt32.write(slideIndex, into: &buf) + FfiConverterFloat.write(zoomScale, into: &buf) + FfiConverterFloat.write(anchorX, into: &buf) + FfiConverterFloat.write(anchorY, into: &buf) + FfiConverterFloat.write(overallProgress, into: &buf) + + case let .epub(chapterId,chapterProgress,overallProgress): - writeInt(&buf, Int32(5)) + writeInt(&buf, Int32(10)) FfiConverterString.write(chapterId, into: &buf) FfiConverterFloat.write(chapterProgress, into: &buf) FfiConverterFloat.write(overallProgress, into: &buf) case .unknown: - writeInt(&buf, Int32(6)) + writeInt(&buf, Int32(11)) } } @@ -2334,6 +2712,8 @@ public enum ReadingSessionStatus: Equatable, Hashable { case active case paused case closed + case interrupted + case failed @@ -2361,6 +2741,10 @@ public struct FfiConverterTypeReadingSessionStatus: FfiConverterRustBuffer { case 3: return .closed + case 4: return .interrupted + + case 5: return .failed + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -2380,6 +2764,14 @@ public struct FfiConverterTypeReadingSessionStatus: FfiConverterRustBuffer { case .closed: writeInt(&buf, Int32(3)) + + case .interrupted: + writeInt(&buf, Int32(4)) + + + case .failed: + writeInt(&buf, Int32(5)) + } } } @@ -2400,6 +2792,108 @@ public func FfiConverterTypeReadingSessionStatus_lower(_ value: ReadingSessionSt } +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum RendererKind: Equatable, Hashable { + + case block + case pdf + case image + case workbook + case slide + case epub + case unknown + + + + + +} + +#if compiler(>=6) +extension RendererKind: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeRendererKind: FfiConverterRustBuffer { + typealias SwiftType = RendererKind + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RendererKind { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .block + + case 2: return .pdf + + case 3: return .image + + case 4: return .workbook + + case 5: return .slide + + case 6: return .epub + + case 7: return .unknown + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: RendererKind, into buf: inout [UInt8]) { + switch value { + + + case .block: + writeInt(&buf, Int32(1)) + + + case .pdf: + writeInt(&buf, Int32(2)) + + + case .image: + writeInt(&buf, Int32(3)) + + + case .workbook: + writeInt(&buf, Int32(4)) + + + case .slide: + writeInt(&buf, Int32(5)) + + + case .epub: + writeInt(&buf, Int32(6)) + + + case .unknown: + writeInt(&buf, Int32(7)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeRendererKind_lift(_ buf: RustBuffer) throws -> RendererKind { + return try FfiConverterTypeRendererKind.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeRendererKind_lower(_ value: RendererKind) -> RustBuffer { + return FfiConverterTypeRendererKind.lower(value) +} + + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -2424,6 +2918,78 @@ fileprivate struct FfiConverterOptionUInt32: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionUInt64: FfiConverterRustBuffer { + typealias SwiftType = UInt64? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterUInt64.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterUInt64.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer { + typealias SwiftType = Int64? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterInt64.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterInt64.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionFloat: FfiConverterRustBuffer { + typealias SwiftType = Float? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterFloat.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterFloat.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -2448,6 +3014,54 @@ fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeReadingCheckpoint: FfiConverterRustBuffer { + typealias SwiftType = ReadingCheckpoint? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeReadingCheckpoint.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeReadingCheckpoint.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeReadingSessionV2: FfiConverterRustBuffer { + typealias SwiftType = ReadingSessionV2? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeReadingSessionV2.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeReadingSessionV2.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -2703,6 +3317,16 @@ public func ackEventsV2(eventIds: [String]) -> UInt32 { ) }) } +public func buildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_build_representation_fingerprint( + FfiConverterString.lower(sourceFingerprint), + FfiConverterTypeRendererKind_lower(rendererKind), + FfiConverterString.lower(parserVersion), + FfiConverterUInt32.lower(schemaVersion),$0 + ) +}) +} public func cleanupStaleSessionsFfi(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { return try! FfiConverterUInt32.lift(try! rustCall() { uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi( @@ -2717,12 +3341,20 @@ public func clearExportedEvents(count: UInt32) {try! rustCall() { ) } } -public func closeReadingSessionV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { +public func closeReadingSessionV2(sessionId: String, timestampMs: Int64)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { uniffi_zx_document_ffi_fn_func_close_reading_session_v2( - FfiConverterString.lower(sessionId),$0 + FfiConverterString.lower(sessionId), + FfiConverterInt64.lower(timestampMs),$0 ) } } +public func computeFileFingerprint(filePath: String)throws -> String { + return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_compute_file_fingerprint( + FfiConverterString.lower(filePath),$0 + ) +}) +} public func createNoteAnchor(materialId: String, position: ReadingPosition?) -> NoteAnchor { return try! FfiConverterTypeNoteAnchor_lift(try! rustCall() { uniffi_zx_document_ffi_fn_func_create_note_anchor( @@ -2767,6 +3399,19 @@ public func extractPdfTextFfi(filePath: String)throws -> [PdfPageText] { ) }) } +public func getActiveSessionV2(materialId: String) -> ReadingSessionV2? { + return try! FfiConverterOptionTypeReadingSessionV2.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_get_active_session_v2( + FfiConverterString.lower(materialId),$0 + ) +}) +} +public func getEventBufferStateV2() -> EventBufferStateV2 { + return try! FfiConverterTypeEventBufferStateV2_lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2($0 + ) +}) +} public func getOfficePreviewConfigFfi(materialType: MaterialType, fileSize: UInt64)throws -> OfficePreviewConfig { return try FfiConverterTypeOfficePreviewConfig_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi( @@ -2775,6 +3420,13 @@ public func getOfficePreviewConfigFfi(materialType: MaterialType, fileSize: UInt ) }) } +public func getSessionV2Ffi(sessionId: String) -> ReadingSessionV2? { + return try! FfiConverterOptionTypeReadingSessionV2.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_get_session_v2_ffi( + FfiConverterString.lower(sessionId),$0 + ) +}) +} public func isOfficeTypeFfi(materialType: MaterialType) -> Bool { return try! FfiConverterBool.lift(try! rustCall() { uniffi_zx_document_ffi_fn_func_is_office_type_ffi( @@ -2789,6 +3441,18 @@ public func markEventsFailedV2(eventIds: [String]) -> UInt32 { ) }) } +public func markSessionFailedV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_mark_session_failed_v2( + FfiConverterString.lower(sessionId),$0 + ) +} +} +public func markSessionInterruptedV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_mark_session_interrupted_v2( + FfiConverterString.lower(sessionId),$0 + ) +} +} public func parseMarkdown(content: String)throws -> [DocumentBlock] { return try FfiConverterSequenceTypeDocumentBlock.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { uniffi_zx_document_ffi_fn_func_parse_markdown( @@ -2809,6 +3473,27 @@ public func pauseReadingSessionV2(sessionId: String)throws {try rustCallWithEr ) } } +public func pushCheckpointChangedV2(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_checkpoint_changed_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterTypeReadingCheckpoint_lower(checkpoint), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func pushHeartbeatCheckpointV2(sessionId: String, materialId: String, activeSecondsDelta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_heartbeat_checkpoint_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterUInt32.lower(activeSecondsDelta), + FfiConverterOptionTypeReadingCheckpoint.lower(checkpoint), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} public func pushHeartbeatV2(sessionId: String, materialId: String, activeSecondsDelta: UInt32, position: ReadingPosition?, timestampMs: Int64)throws -> ReadingEventV2 { return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { uniffi_zx_document_ffi_fn_func_push_heartbeat_v2( @@ -2952,10 +3637,17 @@ public func searchTextContent(content: String, query: String) -> [SearchResult] ) }) } -public func startReadingSessionV2(material: ReadingMaterialRef, timestampMs: Int64)throws -> String { +public func setSessionClosedAtMsV2(sessionId: String, timestampMs: Int64)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_set_session_closed_at_ms_v2( + FfiConverterString.lower(sessionId), + FfiConverterInt64.lower(timestampMs),$0 + ) +} +} +public func startReadingSessionV2(material: ReadingMaterialRefV2, timestampMs: Int64)throws -> String { return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { uniffi_zx_document_ffi_fn_func_start_reading_session_v2( - FfiConverterTypeReadingMaterialRef_lower(material), + FfiConverterTypeReadingMaterialRefV2_lower(material), FfiConverterInt64.lower(timestampMs),$0 ) }) diff --git a/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift.bak b/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift.bak new file mode 100644 index 0000000..c1e98fe --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/zx_document.swift.bak @@ -0,0 +1,3066 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +// swiftlint:disable all +import Foundation + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(zx_documentFFI) +import zx_documentFFI +#endif + +private func uniffiEnsureZxDocumentFfiInitialized() {} + +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func empty() -> RustBuffer { + RustBuffer(capacity: 0, len:0, data: nil) + } + + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { + try! rustCall { ffi_zx_document_ffi_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_zx_document_ffi_rustbuffer_free(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + let buf = UnsafeRawBufferPointer(start: rustBuffer.data, count: Int(rustBuffer.len)) + self.init(buf) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset...size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous to the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + let neverThrow: ((RustBuffer) throws -> Never)? = nil + return try makeRustCall(callback, errorHandler: neverThrow) +} + +private func rustCallWithError( + _ errorHandler: @escaping (RustBuffer) throws -> E, + _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: errorHandler) +} + +private func makeRustCall( + _ callback: (UnsafeMutablePointer) -> T, + errorHandler: ((RustBuffer) throws -> E)? +) throws -> T { + uniffiEnsureZxDocumentFfiInitialized() + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) + return returnedVal +} + +private func uniffiCheckCallStatus( + callStatus: RustCallStatus, + errorHandler: ((RustBuffer) throws -> E)? +) throws { + switch callStatus.code { + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } + + case CALL_UNEXPECTED_ERROR: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + case CALL_CANCELLED: + fatalError("Cancellation not supported yet") + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +private func uniffiTraitInterfaceCall( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> () +) { + do { + try writeReturn(makeCall()) + } catch let error { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} + +private func uniffiTraitInterfaceCallWithError( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> (), + lowerError: (E) -> RustBuffer +) { + do { + try writeReturn(makeCall()) + } catch let error as E { + callStatus.pointee.code = CALL_ERROR + callStatus.pointee.errorBuf = lowerError(error) + } catch { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} +// Initial value and increment amount for handles. +// These ensure that SWIFT handles always have the lowest bit set +fileprivate let UNIFFI_HANDLEMAP_INITIAL: UInt64 = 1 +fileprivate let UNIFFI_HANDLEMAP_DELTA: UInt64 = 2 + +fileprivate final class UniffiHandleMap: @unchecked Sendable { + // All mutation happens with this lock held, which is why we implement @unchecked Sendable. + private let lock = NSLock() + private var map: [UInt64: T] = [:] + private var currentHandle: UInt64 = UNIFFI_HANDLEMAP_INITIAL + + func insert(obj: T) -> UInt64 { + lock.withLock { + return doInsert(obj) + } + } + + // Low-level insert function, this assumes `lock` is held. + private func doInsert(_ obj: T) -> UInt64 { + let handle = currentHandle + currentHandle += UNIFFI_HANDLEMAP_DELTA + map[handle] = obj + return handle + } + + func get(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map[handle] else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + func clone(handle: UInt64) throws -> UInt64 { + try lock.withLock { + guard let obj = map[handle] else { + throw UniffiInternalError.unexpectedStaleHandle + } + return doInsert(obj) + } + } + + @discardableResult + func remove(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map.removeValue(forKey: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + var count: Int { + get { + map.count + } + } +} + + +// Public interface members begin here. + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt8: FfiConverterPrimitive { + typealias FfiType = UInt8 + typealias SwiftType = UInt8 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt8 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: UInt8, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt32: FfiConverterPrimitive { + typealias FfiType = UInt32 + typealias SwiftType = UInt32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt64: FfiConverterPrimitive { + typealias FfiType = UInt64 + typealias SwiftType = UInt64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterInt64: FfiConverterPrimitive { + typealias FfiType = Int64 + typealias SwiftType = Int64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int64, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterFloat: FfiConverterPrimitive { + typealias FfiType = Float + typealias SwiftType = Float + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Float { + return try lift(readFloat(&buf)) + } + + public static func write(_ value: Float, into buf: inout [UInt8]) { + writeFloat(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterBool : FfiConverter { + typealias FfiType = Int8 + typealias SwiftType = Bool + + public static func lift(_ value: Int8) throws -> Bool { + return value != 0 + } + + public static func lower(_ value: Bool) -> Int8 { + return value ? 1 : 0 + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Bool, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} + + +public struct DocumentInfo: Equatable, Hashable { + public var materialId: String + public var title: String + public var materialType: MaterialType + public var previewMode: PreviewMode + public var fileSize: UInt64 + public var pageCount: UInt32? + public var wordCount: UInt32? + public var charCount: UInt32? + public var lineCount: UInt32? + public var imageWidth: UInt32? + public var imageHeight: UInt32? + public var imageFormat: String? + public var epubChapterCount: UInt32? + public var isSearchable: Bool + public var supportsPosition: Bool + public var supportsAnchor: Bool + public var createdAt: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(materialId: String, title: String, materialType: MaterialType, previewMode: PreviewMode, fileSize: UInt64, pageCount: UInt32?, wordCount: UInt32?, charCount: UInt32?, lineCount: UInt32?, imageWidth: UInt32?, imageHeight: UInt32?, imageFormat: String?, epubChapterCount: UInt32?, isSearchable: Bool, supportsPosition: Bool, supportsAnchor: Bool, createdAt: String?) { + self.materialId = materialId + self.title = title + self.materialType = materialType + self.previewMode = previewMode + self.fileSize = fileSize + self.pageCount = pageCount + self.wordCount = wordCount + self.charCount = charCount + self.lineCount = lineCount + self.imageWidth = imageWidth + self.imageHeight = imageHeight + self.imageFormat = imageFormat + self.epubChapterCount = epubChapterCount + self.isSearchable = isSearchable + self.supportsPosition = supportsPosition + self.supportsAnchor = supportsAnchor + self.createdAt = createdAt + } + + + + +} + +#if compiler(>=6) +extension DocumentInfo: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocumentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocumentInfo { + return + try DocumentInfo( + materialId: FfiConverterString.read(from: &buf), + title: FfiConverterString.read(from: &buf), + materialType: FfiConverterTypeMaterialType.read(from: &buf), + previewMode: FfiConverterTypePreviewMode.read(from: &buf), + fileSize: FfiConverterUInt64.read(from: &buf), + pageCount: FfiConverterOptionUInt32.read(from: &buf), + wordCount: FfiConverterOptionUInt32.read(from: &buf), + charCount: FfiConverterOptionUInt32.read(from: &buf), + lineCount: FfiConverterOptionUInt32.read(from: &buf), + imageWidth: FfiConverterOptionUInt32.read(from: &buf), + imageHeight: FfiConverterOptionUInt32.read(from: &buf), + imageFormat: FfiConverterOptionString.read(from: &buf), + epubChapterCount: FfiConverterOptionUInt32.read(from: &buf), + isSearchable: FfiConverterBool.read(from: &buf), + supportsPosition: FfiConverterBool.read(from: &buf), + supportsAnchor: FfiConverterBool.read(from: &buf), + createdAt: FfiConverterOptionString.read(from: &buf) + ) + } + + public static func write(_ value: DocumentInfo, into buf: inout [UInt8]) { + FfiConverterString.write(value.materialId, into: &buf) + FfiConverterString.write(value.title, into: &buf) + FfiConverterTypeMaterialType.write(value.materialType, into: &buf) + FfiConverterTypePreviewMode.write(value.previewMode, into: &buf) + FfiConverterUInt64.write(value.fileSize, into: &buf) + FfiConverterOptionUInt32.write(value.pageCount, into: &buf) + FfiConverterOptionUInt32.write(value.wordCount, into: &buf) + FfiConverterOptionUInt32.write(value.charCount, into: &buf) + FfiConverterOptionUInt32.write(value.lineCount, into: &buf) + FfiConverterOptionUInt32.write(value.imageWidth, into: &buf) + FfiConverterOptionUInt32.write(value.imageHeight, into: &buf) + FfiConverterOptionString.write(value.imageFormat, into: &buf) + FfiConverterOptionUInt32.write(value.epubChapterCount, into: &buf) + FfiConverterBool.write(value.isSearchable, into: &buf) + FfiConverterBool.write(value.supportsPosition, into: &buf) + FfiConverterBool.write(value.supportsAnchor, into: &buf) + FfiConverterOptionString.write(value.createdAt, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDocumentInfo_lift(_ buf: RustBuffer) throws -> DocumentInfo { + return try FfiConverterTypeDocumentInfo.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDocumentInfo_lower(_ value: DocumentInfo) -> RustBuffer { + return FfiConverterTypeDocumentInfo.lower(value) +} + + +public struct EpubChapter: Equatable, Hashable { + public var chapterId: String + public var title: String + public var path: String + public var playOrder: UInt32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(chapterId: String, title: String, path: String, playOrder: UInt32) { + self.chapterId = chapterId + self.title = title + self.path = path + self.playOrder = playOrder + } + + + + +} + +#if compiler(>=6) +extension EpubChapter: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeEpubChapter: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EpubChapter { + return + try EpubChapter( + chapterId: FfiConverterString.read(from: &buf), + title: FfiConverterString.read(from: &buf), + path: FfiConverterString.read(from: &buf), + playOrder: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: EpubChapter, into buf: inout [UInt8]) { + FfiConverterString.write(value.chapterId, into: &buf) + FfiConverterString.write(value.title, into: &buf) + FfiConverterString.write(value.path, into: &buf) + FfiConverterUInt32.write(value.playOrder, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeEpubChapter_lift(_ buf: RustBuffer) throws -> EpubChapter { + return try FfiConverterTypeEpubChapter.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeEpubChapter_lower(_ value: EpubChapter) -> RustBuffer { + return FfiConverterTypeEpubChapter.lower(value) +} + + +public struct EpubMetadata: Equatable, Hashable { + public var title: String? + public var author: String? + public var chapterCount: UInt32 + public var fileSize: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(title: String?, author: String?, chapterCount: UInt32, fileSize: UInt64) { + self.title = title + self.author = author + self.chapterCount = chapterCount + self.fileSize = fileSize + } + + + + +} + +#if compiler(>=6) +extension EpubMetadata: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeEpubMetadata: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EpubMetadata { + return + try EpubMetadata( + title: FfiConverterOptionString.read(from: &buf), + author: FfiConverterOptionString.read(from: &buf), + chapterCount: FfiConverterUInt32.read(from: &buf), + fileSize: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: EpubMetadata, into buf: inout [UInt8]) { + FfiConverterOptionString.write(value.title, into: &buf) + FfiConverterOptionString.write(value.author, into: &buf) + FfiConverterUInt32.write(value.chapterCount, into: &buf) + FfiConverterUInt64.write(value.fileSize, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeEpubMetadata_lift(_ buf: RustBuffer) throws -> EpubMetadata { + return try FfiConverterTypeEpubMetadata.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeEpubMetadata_lower(_ value: EpubMetadata) -> RustBuffer { + return FfiConverterTypeEpubMetadata.lower(value) +} + + +public struct ImageMeta: Equatable, Hashable { + public var width: UInt32 + public var height: UInt32 + public var format: String + public var fileSize: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(width: UInt32, height: UInt32, format: String, fileSize: UInt64) { + self.width = width + self.height = height + self.format = format + self.fileSize = fileSize + } + + + + +} + +#if compiler(>=6) +extension ImageMeta: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeImageMeta: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ImageMeta { + return + try ImageMeta( + width: FfiConverterUInt32.read(from: &buf), + height: FfiConverterUInt32.read(from: &buf), + format: FfiConverterString.read(from: &buf), + fileSize: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: ImageMeta, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.width, into: &buf) + FfiConverterUInt32.write(value.height, into: &buf) + FfiConverterString.write(value.format, into: &buf) + FfiConverterUInt64.write(value.fileSize, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeImageMeta_lift(_ buf: RustBuffer) throws -> ImageMeta { + return try FfiConverterTypeImageMeta.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeImageMeta_lower(_ value: ImageMeta) -> RustBuffer { + return FfiConverterTypeImageMeta.lower(value) +} + + +public struct OfficePreviewConfig: Equatable, Hashable { + public var materialType: MaterialType + public var strategy: OfficePreviewStrategy + public var fileSize: UInt64 + public var supportsSearchAfterConversion: Bool + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(materialType: MaterialType, strategy: OfficePreviewStrategy, fileSize: UInt64, supportsSearchAfterConversion: Bool) { + self.materialType = materialType + self.strategy = strategy + self.fileSize = fileSize + self.supportsSearchAfterConversion = supportsSearchAfterConversion + } + + + + +} + +#if compiler(>=6) +extension OfficePreviewConfig: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeOfficePreviewConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OfficePreviewConfig { + return + try OfficePreviewConfig( + materialType: FfiConverterTypeMaterialType.read(from: &buf), + strategy: FfiConverterTypeOfficePreviewStrategy.read(from: &buf), + fileSize: FfiConverterUInt64.read(from: &buf), + supportsSearchAfterConversion: FfiConverterBool.read(from: &buf) + ) + } + + public static func write(_ value: OfficePreviewConfig, into buf: inout [UInt8]) { + FfiConverterTypeMaterialType.write(value.materialType, into: &buf) + FfiConverterTypeOfficePreviewStrategy.write(value.strategy, into: &buf) + FfiConverterUInt64.write(value.fileSize, into: &buf) + FfiConverterBool.write(value.supportsSearchAfterConversion, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeOfficePreviewConfig_lift(_ buf: RustBuffer) throws -> OfficePreviewConfig { + return try FfiConverterTypeOfficePreviewConfig.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeOfficePreviewConfig_lower(_ value: OfficePreviewConfig) -> RustBuffer { + return FfiConverterTypeOfficePreviewConfig.lower(value) +} + + +public struct PdfMetadata: Equatable, Hashable { + public var pageCount: UInt32 + public var title: String? + public var author: String? + public var fileSize: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(pageCount: UInt32, title: String?, author: String?, fileSize: UInt64) { + self.pageCount = pageCount + self.title = title + self.author = author + self.fileSize = fileSize + } + + + + +} + +#if compiler(>=6) +extension PdfMetadata: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypePdfMetadata: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PdfMetadata { + return + try PdfMetadata( + pageCount: FfiConverterUInt32.read(from: &buf), + title: FfiConverterOptionString.read(from: &buf), + author: FfiConverterOptionString.read(from: &buf), + fileSize: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: PdfMetadata, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.pageCount, into: &buf) + FfiConverterOptionString.write(value.title, into: &buf) + FfiConverterOptionString.write(value.author, into: &buf) + FfiConverterUInt64.write(value.fileSize, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePdfMetadata_lift(_ buf: RustBuffer) throws -> PdfMetadata { + return try FfiConverterTypePdfMetadata.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePdfMetadata_lower(_ value: PdfMetadata) -> RustBuffer { + return FfiConverterTypePdfMetadata.lower(value) +} + + +public struct PdfPageText: Equatable, Hashable { + public var pageNumber: UInt32 + public var text: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(pageNumber: UInt32, text: String) { + self.pageNumber = pageNumber + self.text = text + } + + + + +} + +#if compiler(>=6) +extension PdfPageText: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypePdfPageText: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PdfPageText { + return + try PdfPageText( + pageNumber: FfiConverterUInt32.read(from: &buf), + text: FfiConverterString.read(from: &buf) + ) + } + + public static func write(_ value: PdfPageText, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.pageNumber, into: &buf) + FfiConverterString.write(value.text, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePdfPageText_lift(_ buf: RustBuffer) throws -> PdfPageText { + return try FfiConverterTypePdfPageText.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePdfPageText_lower(_ value: PdfPageText) -> RustBuffer { + return FfiConverterTypePdfPageText.lower(value) +} + + +public struct ReadingEventV2: Equatable, Hashable { + public var eventId: String + public var clientSessionId: String + public var materialId: String + public var eventType: ReadingEventTypeV2 + public var position: ReadingPosition? + public var activeSecondsDelta: UInt32 + public var timestampMs: Int64 + public var sequence: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(eventId: String, clientSessionId: String, materialId: String, eventType: ReadingEventTypeV2, position: ReadingPosition?, activeSecondsDelta: UInt32, timestampMs: Int64, sequence: UInt64) { + self.eventId = eventId + self.clientSessionId = clientSessionId + self.materialId = materialId + self.eventType = eventType + self.position = position + self.activeSecondsDelta = activeSecondsDelta + self.timestampMs = timestampMs + self.sequence = sequence + } + + + + +} + +#if compiler(>=6) +extension ReadingEventV2: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingEventV2: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingEventV2 { + return + try ReadingEventV2( + eventId: FfiConverterString.read(from: &buf), + clientSessionId: FfiConverterString.read(from: &buf), + materialId: FfiConverterString.read(from: &buf), + eventType: FfiConverterTypeReadingEventTypeV2.read(from: &buf), + position: FfiConverterOptionTypeReadingPosition.read(from: &buf), + activeSecondsDelta: FfiConverterUInt32.read(from: &buf), + timestampMs: FfiConverterInt64.read(from: &buf), + sequence: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: ReadingEventV2, into buf: inout [UInt8]) { + FfiConverterString.write(value.eventId, into: &buf) + FfiConverterString.write(value.clientSessionId, into: &buf) + FfiConverterString.write(value.materialId, into: &buf) + FfiConverterTypeReadingEventTypeV2.write(value.eventType, into: &buf) + FfiConverterOptionTypeReadingPosition.write(value.position, into: &buf) + FfiConverterUInt32.write(value.activeSecondsDelta, into: &buf) + FfiConverterInt64.write(value.timestampMs, into: &buf) + FfiConverterUInt64.write(value.sequence, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingEventV2_lift(_ buf: RustBuffer) throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingEventV2_lower(_ value: ReadingEventV2) -> RustBuffer { + return FfiConverterTypeReadingEventV2.lower(value) +} + + +public struct ReadingMaterialRef: Equatable, Hashable { + public var materialId: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(materialId: String) { + self.materialId = materialId + } + + + + +} + +#if compiler(>=6) +extension ReadingMaterialRef: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingMaterialRef: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingMaterialRef { + return + try ReadingMaterialRef( + materialId: FfiConverterString.read(from: &buf) + ) + } + + public static func write(_ value: ReadingMaterialRef, into buf: inout [UInt8]) { + FfiConverterString.write(value.materialId, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingMaterialRef_lift(_ buf: RustBuffer) throws -> ReadingMaterialRef { + return try FfiConverterTypeReadingMaterialRef.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingMaterialRef_lower(_ value: ReadingMaterialRef) -> RustBuffer { + return FfiConverterTypeReadingMaterialRef.lower(value) +} + + +public struct ReadingSessionV2: Equatable, Hashable { + public var clientSessionId: String + public var material: ReadingMaterialRef + public var startedAtMs: Int64 + public var lastEventAtMs: Int64 + public var nextSequence: UInt64 + public var totalActiveSeconds: UInt32 + public var lastPosition: ReadingPosition? + public var status: ReadingSessionStatus + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(clientSessionId: String, material: ReadingMaterialRef, startedAtMs: Int64, lastEventAtMs: Int64, nextSequence: UInt64, totalActiveSeconds: UInt32, lastPosition: ReadingPosition?, status: ReadingSessionStatus) { + self.clientSessionId = clientSessionId + self.material = material + self.startedAtMs = startedAtMs + self.lastEventAtMs = lastEventAtMs + self.nextSequence = nextSequence + self.totalActiveSeconds = totalActiveSeconds + self.lastPosition = lastPosition + self.status = status + } + + + + +} + +#if compiler(>=6) +extension ReadingSessionV2: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingSessionV2: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingSessionV2 { + return + try ReadingSessionV2( + clientSessionId: FfiConverterString.read(from: &buf), + material: FfiConverterTypeReadingMaterialRef.read(from: &buf), + startedAtMs: FfiConverterInt64.read(from: &buf), + lastEventAtMs: FfiConverterInt64.read(from: &buf), + nextSequence: FfiConverterUInt64.read(from: &buf), + totalActiveSeconds: FfiConverterUInt32.read(from: &buf), + lastPosition: FfiConverterOptionTypeReadingPosition.read(from: &buf), + status: FfiConverterTypeReadingSessionStatus.read(from: &buf) + ) + } + + public static func write(_ value: ReadingSessionV2, into buf: inout [UInt8]) { + FfiConverterString.write(value.clientSessionId, into: &buf) + FfiConverterTypeReadingMaterialRef.write(value.material, into: &buf) + FfiConverterInt64.write(value.startedAtMs, into: &buf) + FfiConverterInt64.write(value.lastEventAtMs, into: &buf) + FfiConverterUInt64.write(value.nextSequence, into: &buf) + FfiConverterUInt32.write(value.totalActiveSeconds, into: &buf) + FfiConverterOptionTypeReadingPosition.write(value.lastPosition, into: &buf) + FfiConverterTypeReadingSessionStatus.write(value.status, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingSessionV2_lift(_ buf: RustBuffer) throws -> ReadingSessionV2 { + return try FfiConverterTypeReadingSessionV2.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingSessionV2_lower(_ value: ReadingSessionV2) -> RustBuffer { + return FfiConverterTypeReadingSessionV2.lower(value) +} + + +public struct SearchResult: Equatable, Hashable { + public var blockId: String + public var lineNumber: UInt32? + public var pageNumber: UInt32? + public var chapterId: String? + public var snippet: String + public var matchStart: UInt64 + public var matchEnd: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(blockId: String, lineNumber: UInt32?, pageNumber: UInt32?, chapterId: String?, snippet: String, matchStart: UInt64, matchEnd: UInt64) { + self.blockId = blockId + self.lineNumber = lineNumber + self.pageNumber = pageNumber + self.chapterId = chapterId + self.snippet = snippet + self.matchStart = matchStart + self.matchEnd = matchEnd + } + + + + +} + +#if compiler(>=6) +extension SearchResult: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeSearchResult: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SearchResult { + return + try SearchResult( + blockId: FfiConverterString.read(from: &buf), + lineNumber: FfiConverterOptionUInt32.read(from: &buf), + pageNumber: FfiConverterOptionUInt32.read(from: &buf), + chapterId: FfiConverterOptionString.read(from: &buf), + snippet: FfiConverterString.read(from: &buf), + matchStart: FfiConverterUInt64.read(from: &buf), + matchEnd: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: SearchResult, into buf: inout [UInt8]) { + FfiConverterString.write(value.blockId, into: &buf) + FfiConverterOptionUInt32.write(value.lineNumber, into: &buf) + FfiConverterOptionUInt32.write(value.pageNumber, into: &buf) + FfiConverterOptionString.write(value.chapterId, into: &buf) + FfiConverterString.write(value.snippet, into: &buf) + FfiConverterUInt64.write(value.matchStart, into: &buf) + FfiConverterUInt64.write(value.matchEnd, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeSearchResult_lift(_ buf: RustBuffer) throws -> SearchResult { + return try FfiConverterTypeSearchResult.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeSearchResult_lower(_ value: SearchResult) -> RustBuffer { + return FfiConverterTypeSearchResult.lower(value) +} + + +public struct TextStats: Equatable, Hashable { + public var lineCount: UInt32 + public var wordCount: UInt32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(lineCount: UInt32, wordCount: UInt32) { + self.lineCount = lineCount + self.wordCount = wordCount + } + + + + +} + +#if compiler(>=6) +extension TextStats: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTextStats: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TextStats { + return + try TextStats( + lineCount: FfiConverterUInt32.read(from: &buf), + wordCount: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: TextStats, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.lineCount, into: &buf) + FfiConverterUInt32.write(value.wordCount, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTextStats_lift(_ buf: RustBuffer) throws -> TextStats { + return try FfiConverterTypeTextStats.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTextStats_lower(_ value: TextStats) -> RustBuffer { + return FfiConverterTypeTextStats.lower(value) +} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum DocumentBlock: Equatable, Hashable { + + case heading(id: String, level: UInt8, text: String + ) + case paragraph(id: String, text: String + ) + case list(id: String, ordered: Bool, items: [String] + ) + case codeBlock(id: String, language: String?, code: String + ) + case quote(id: String, text: String + ) + case table(id: String, headers: [String], rows: [[String]] + ) + case imageBlock(id: String, src: String, alt: String? + ) + case horizontalRule(id: String + ) + + + + + +} + +#if compiler(>=6) +extension DocumentBlock: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocumentBlock: FfiConverterRustBuffer { + typealias SwiftType = DocumentBlock + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocumentBlock { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .heading(id: try FfiConverterString.read(from: &buf), level: try FfiConverterUInt8.read(from: &buf), text: try FfiConverterString.read(from: &buf) + ) + + case 2: return .paragraph(id: try FfiConverterString.read(from: &buf), text: try FfiConverterString.read(from: &buf) + ) + + case 3: return .list(id: try FfiConverterString.read(from: &buf), ordered: try FfiConverterBool.read(from: &buf), items: try FfiConverterSequenceString.read(from: &buf) + ) + + case 4: return .codeBlock(id: try FfiConverterString.read(from: &buf), language: try FfiConverterOptionString.read(from: &buf), code: try FfiConverterString.read(from: &buf) + ) + + case 5: return .quote(id: try FfiConverterString.read(from: &buf), text: try FfiConverterString.read(from: &buf) + ) + + case 6: return .table(id: try FfiConverterString.read(from: &buf), headers: try FfiConverterSequenceString.read(from: &buf), rows: try FfiConverterSequenceSequenceString.read(from: &buf) + ) + + case 7: return .imageBlock(id: try FfiConverterString.read(from: &buf), src: try FfiConverterString.read(from: &buf), alt: try FfiConverterOptionString.read(from: &buf) + ) + + case 8: return .horizontalRule(id: try FfiConverterString.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: DocumentBlock, into buf: inout [UInt8]) { + switch value { + + + case let .heading(id,level,text): + writeInt(&buf, Int32(1)) + FfiConverterString.write(id, into: &buf) + FfiConverterUInt8.write(level, into: &buf) + FfiConverterString.write(text, into: &buf) + + + case let .paragraph(id,text): + writeInt(&buf, Int32(2)) + FfiConverterString.write(id, into: &buf) + FfiConverterString.write(text, into: &buf) + + + case let .list(id,ordered,items): + writeInt(&buf, Int32(3)) + FfiConverterString.write(id, into: &buf) + FfiConverterBool.write(ordered, into: &buf) + FfiConverterSequenceString.write(items, into: &buf) + + + case let .codeBlock(id,language,code): + writeInt(&buf, Int32(4)) + FfiConverterString.write(id, into: &buf) + FfiConverterOptionString.write(language, into: &buf) + FfiConverterString.write(code, into: &buf) + + + case let .quote(id,text): + writeInt(&buf, Int32(5)) + FfiConverterString.write(id, into: &buf) + FfiConverterString.write(text, into: &buf) + + + case let .table(id,headers,rows): + writeInt(&buf, Int32(6)) + FfiConverterString.write(id, into: &buf) + FfiConverterSequenceString.write(headers, into: &buf) + FfiConverterSequenceSequenceString.write(rows, into: &buf) + + + case let .imageBlock(id,src,alt): + writeInt(&buf, Int32(7)) + FfiConverterString.write(id, into: &buf) + FfiConverterString.write(src, into: &buf) + FfiConverterOptionString.write(alt, into: &buf) + + + case let .horizontalRule(id): + writeInt(&buf, Int32(8)) + FfiConverterString.write(id, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDocumentBlock_lift(_ buf: RustBuffer) throws -> DocumentBlock { + return try FfiConverterTypeDocumentBlock.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDocumentBlock_lower(_ value: DocumentBlock) -> RustBuffer { + return FfiConverterTypeDocumentBlock.lower(value) +} + + + +public enum DocumentError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { + + + + case FileNotFound(message: String) + + case UnsupportedFormat(message: String) + + case ParseError(message: String) + + case InvalidEncoding(message: String) + + case IoError(message: String) + + case SessionNotFound(message: String) + + case SessionAlreadyActive(message: String) + + case SessionNotActive(message: String) + + case BufferOverflow(message: String) + + case Unsupported(message: String) + + + + + + + + public var errorDescription: String? { + String(reflecting: self) + } + +} + +#if compiler(>=6) +extension DocumentError: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocumentError: FfiConverterRustBuffer { + typealias SwiftType = DocumentError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocumentError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .FileNotFound( + message: try FfiConverterString.read(from: &buf) + ) + + case 2: return .UnsupportedFormat( + message: try FfiConverterString.read(from: &buf) + ) + + case 3: return .ParseError( + message: try FfiConverterString.read(from: &buf) + ) + + case 4: return .InvalidEncoding( + message: try FfiConverterString.read(from: &buf) + ) + + case 5: return .IoError( + message: try FfiConverterString.read(from: &buf) + ) + + case 6: return .SessionNotFound( + message: try FfiConverterString.read(from: &buf) + ) + + case 7: return .SessionAlreadyActive( + message: try FfiConverterString.read(from: &buf) + ) + + case 8: return .SessionNotActive( + message: try FfiConverterString.read(from: &buf) + ) + + case 9: return .BufferOverflow( + message: try FfiConverterString.read(from: &buf) + ) + + case 10: return .Unsupported( + message: try FfiConverterString.read(from: &buf) + ) + + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: DocumentError, into buf: inout [UInt8]) { + switch value { + + + + + case .FileNotFound(_ /* message is ignored*/): + writeInt(&buf, Int32(1)) + case .UnsupportedFormat(_ /* message is ignored*/): + writeInt(&buf, Int32(2)) + case .ParseError(_ /* message is ignored*/): + writeInt(&buf, Int32(3)) + case .InvalidEncoding(_ /* message is ignored*/): + writeInt(&buf, Int32(4)) + case .IoError(_ /* message is ignored*/): + writeInt(&buf, Int32(5)) + case .SessionNotFound(_ /* message is ignored*/): + writeInt(&buf, Int32(6)) + case .SessionAlreadyActive(_ /* message is ignored*/): + writeInt(&buf, Int32(7)) + case .SessionNotActive(_ /* message is ignored*/): + writeInt(&buf, Int32(8)) + case .BufferOverflow(_ /* message is ignored*/): + writeInt(&buf, Int32(9)) + case .Unsupported(_ /* message is ignored*/): + writeInt(&buf, Int32(10)) + + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDocumentError_lift(_ buf: RustBuffer) throws -> DocumentError { + return try FfiConverterTypeDocumentError.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDocumentError_lower(_ value: DocumentError) -> RustBuffer { + return FfiConverterTypeDocumentError.lower(value) +} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum MaterialType: Equatable, Hashable { + + case markdown + case text + case pdf + case image + case epub + case word + case excel + case powerPoint + case unknown + + + + + +} + +#if compiler(>=6) +extension MaterialType: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeMaterialType: FfiConverterRustBuffer { + typealias SwiftType = MaterialType + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MaterialType { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .markdown + + case 2: return .text + + case 3: return .pdf + + case 4: return .image + + case 5: return .epub + + case 6: return .word + + case 7: return .excel + + case 8: return .powerPoint + + case 9: return .unknown + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: MaterialType, into buf: inout [UInt8]) { + switch value { + + + case .markdown: + writeInt(&buf, Int32(1)) + + + case .text: + writeInt(&buf, Int32(2)) + + + case .pdf: + writeInt(&buf, Int32(3)) + + + case .image: + writeInt(&buf, Int32(4)) + + + case .epub: + writeInt(&buf, Int32(5)) + + + case .word: + writeInt(&buf, Int32(6)) + + + case .excel: + writeInt(&buf, Int32(7)) + + + case .powerPoint: + writeInt(&buf, Int32(8)) + + + case .unknown: + writeInt(&buf, Int32(9)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeMaterialType_lift(_ buf: RustBuffer) throws -> MaterialType { + return try FfiConverterTypeMaterialType.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeMaterialType_lower(_ value: MaterialType) -> RustBuffer { + return FfiConverterTypeMaterialType.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum NoteAnchor: Equatable, Hashable { + + case material(materialId: String, positionSnapshot: ReadingPosition? + ) + case markdownBlock(materialId: String, blockId: String, positionSnapshot: ReadingPosition? + ) + case textLine(materialId: String, lineNumber: UInt32, positionSnapshot: ReadingPosition? + ) + case pdfPage(materialId: String, pageNumber: UInt32, positionSnapshot: ReadingPosition? + ) + case image(materialId: String, positionSnapshot: ReadingPosition? + ) + case epubChapter(materialId: String, chapterId: String, positionSnapshot: ReadingPosition? + ) + case knowledgeItem(knowledgeItemId: String + ) + case searchResultAnchor(materialId: String, blockId: String?, lineNumber: UInt32?, pageNumber: UInt32?, chapterId: String?, snippet: String + ) + + + + + +} + +#if compiler(>=6) +extension NoteAnchor: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeNoteAnchor: FfiConverterRustBuffer { + typealias SwiftType = NoteAnchor + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> NoteAnchor { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .material(materialId: try FfiConverterString.read(from: &buf), positionSnapshot: try FfiConverterOptionTypeReadingPosition.read(from: &buf) + ) + + case 2: return .markdownBlock(materialId: try FfiConverterString.read(from: &buf), blockId: try FfiConverterString.read(from: &buf), positionSnapshot: try FfiConverterOptionTypeReadingPosition.read(from: &buf) + ) + + case 3: return .textLine(materialId: try FfiConverterString.read(from: &buf), lineNumber: try FfiConverterUInt32.read(from: &buf), positionSnapshot: try FfiConverterOptionTypeReadingPosition.read(from: &buf) + ) + + case 4: return .pdfPage(materialId: try FfiConverterString.read(from: &buf), pageNumber: try FfiConverterUInt32.read(from: &buf), positionSnapshot: try FfiConverterOptionTypeReadingPosition.read(from: &buf) + ) + + case 5: return .image(materialId: try FfiConverterString.read(from: &buf), positionSnapshot: try FfiConverterOptionTypeReadingPosition.read(from: &buf) + ) + + case 6: return .epubChapter(materialId: try FfiConverterString.read(from: &buf), chapterId: try FfiConverterString.read(from: &buf), positionSnapshot: try FfiConverterOptionTypeReadingPosition.read(from: &buf) + ) + + case 7: return .knowledgeItem(knowledgeItemId: try FfiConverterString.read(from: &buf) + ) + + case 8: return .searchResultAnchor(materialId: try FfiConverterString.read(from: &buf), blockId: try FfiConverterOptionString.read(from: &buf), lineNumber: try FfiConverterOptionUInt32.read(from: &buf), pageNumber: try FfiConverterOptionUInt32.read(from: &buf), chapterId: try FfiConverterOptionString.read(from: &buf), snippet: try FfiConverterString.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: NoteAnchor, into buf: inout [UInt8]) { + switch value { + + + case let .material(materialId,positionSnapshot): + writeInt(&buf, Int32(1)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterOptionTypeReadingPosition.write(positionSnapshot, into: &buf) + + + case let .markdownBlock(materialId,blockId,positionSnapshot): + writeInt(&buf, Int32(2)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterString.write(blockId, into: &buf) + FfiConverterOptionTypeReadingPosition.write(positionSnapshot, into: &buf) + + + case let .textLine(materialId,lineNumber,positionSnapshot): + writeInt(&buf, Int32(3)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterUInt32.write(lineNumber, into: &buf) + FfiConverterOptionTypeReadingPosition.write(positionSnapshot, into: &buf) + + + case let .pdfPage(materialId,pageNumber,positionSnapshot): + writeInt(&buf, Int32(4)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterUInt32.write(pageNumber, into: &buf) + FfiConverterOptionTypeReadingPosition.write(positionSnapshot, into: &buf) + + + case let .image(materialId,positionSnapshot): + writeInt(&buf, Int32(5)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterOptionTypeReadingPosition.write(positionSnapshot, into: &buf) + + + case let .epubChapter(materialId,chapterId,positionSnapshot): + writeInt(&buf, Int32(6)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterString.write(chapterId, into: &buf) + FfiConverterOptionTypeReadingPosition.write(positionSnapshot, into: &buf) + + + case let .knowledgeItem(knowledgeItemId): + writeInt(&buf, Int32(7)) + FfiConverterString.write(knowledgeItemId, into: &buf) + + + case let .searchResultAnchor(materialId,blockId,lineNumber,pageNumber,chapterId,snippet): + writeInt(&buf, Int32(8)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterOptionString.write(blockId, into: &buf) + FfiConverterOptionUInt32.write(lineNumber, into: &buf) + FfiConverterOptionUInt32.write(pageNumber, into: &buf) + FfiConverterOptionString.write(chapterId, into: &buf) + FfiConverterString.write(snippet, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeNoteAnchor_lift(_ buf: RustBuffer) throws -> NoteAnchor { + return try FfiConverterTypeNoteAnchor.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeNoteAnchor_lower(_ value: NoteAnchor) -> RustBuffer { + return FfiConverterTypeNoteAnchor.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum OfficePreviewStrategy: Equatable, Hashable { + + case platformPreview + case externalOpen + case serverConvertedPdf + case unsupported + + + + + +} + +#if compiler(>=6) +extension OfficePreviewStrategy: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeOfficePreviewStrategy: FfiConverterRustBuffer { + typealias SwiftType = OfficePreviewStrategy + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OfficePreviewStrategy { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .platformPreview + + case 2: return .externalOpen + + case 3: return .serverConvertedPdf + + case 4: return .unsupported + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: OfficePreviewStrategy, into buf: inout [UInt8]) { + switch value { + + + case .platformPreview: + writeInt(&buf, Int32(1)) + + + case .externalOpen: + writeInt(&buf, Int32(2)) + + + case .serverConvertedPdf: + writeInt(&buf, Int32(3)) + + + case .unsupported: + writeInt(&buf, Int32(4)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeOfficePreviewStrategy_lift(_ buf: RustBuffer) throws -> OfficePreviewStrategy { + return try FfiConverterTypeOfficePreviewStrategy.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeOfficePreviewStrategy_lower(_ value: OfficePreviewStrategy) -> RustBuffer { + return FfiConverterTypeOfficePreviewStrategy.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum PreviewMode: Equatable, Hashable { + + case nativeReader + case platformPreview + case externalOpen + case unsupported + + + + + +} + +#if compiler(>=6) +extension PreviewMode: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypePreviewMode: FfiConverterRustBuffer { + typealias SwiftType = PreviewMode + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PreviewMode { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .nativeReader + + case 2: return .platformPreview + + case 3: return .externalOpen + + case 4: return .unsupported + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: PreviewMode, into buf: inout [UInt8]) { + switch value { + + + case .nativeReader: + writeInt(&buf, Int32(1)) + + + case .platformPreview: + writeInt(&buf, Int32(2)) + + + case .externalOpen: + writeInt(&buf, Int32(3)) + + + case .unsupported: + writeInt(&buf, Int32(4)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePreviewMode_lift(_ buf: RustBuffer) throws -> PreviewMode { + return try FfiConverterTypePreviewMode.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypePreviewMode_lower(_ value: PreviewMode) -> RustBuffer { + return FfiConverterTypePreviewMode.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ReadingEvent: Equatable, Hashable { + + case materialOpened(materialId: String, timestampMs: Int64 + ) + case materialClosed(materialId: String, timestampMs: Int64, activeSeconds: UInt32 + ) + case positionChanged(materialId: String, position: ReadingPosition, timestampMs: Int64 + ) + case heartbeat(materialId: String, activeSeconds: UInt32, position: ReadingPosition?, timestampMs: Int64 + ) + case markedAsRead(materialId: String, timestampMs: Int64 + ) + + + + + +} + +#if compiler(>=6) +extension ReadingEvent: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingEvent: FfiConverterRustBuffer { + typealias SwiftType = ReadingEvent + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingEvent { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .materialOpened(materialId: try FfiConverterString.read(from: &buf), timestampMs: try FfiConverterInt64.read(from: &buf) + ) + + case 2: return .materialClosed(materialId: try FfiConverterString.read(from: &buf), timestampMs: try FfiConverterInt64.read(from: &buf), activeSeconds: try FfiConverterUInt32.read(from: &buf) + ) + + case 3: return .positionChanged(materialId: try FfiConverterString.read(from: &buf), position: try FfiConverterTypeReadingPosition.read(from: &buf), timestampMs: try FfiConverterInt64.read(from: &buf) + ) + + case 4: return .heartbeat(materialId: try FfiConverterString.read(from: &buf), activeSeconds: try FfiConverterUInt32.read(from: &buf), position: try FfiConverterOptionTypeReadingPosition.read(from: &buf), timestampMs: try FfiConverterInt64.read(from: &buf) + ) + + case 5: return .markedAsRead(materialId: try FfiConverterString.read(from: &buf), timestampMs: try FfiConverterInt64.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ReadingEvent, into buf: inout [UInt8]) { + switch value { + + + case let .materialOpened(materialId,timestampMs): + writeInt(&buf, Int32(1)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterInt64.write(timestampMs, into: &buf) + + + case let .materialClosed(materialId,timestampMs,activeSeconds): + writeInt(&buf, Int32(2)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterInt64.write(timestampMs, into: &buf) + FfiConverterUInt32.write(activeSeconds, into: &buf) + + + case let .positionChanged(materialId,position,timestampMs): + writeInt(&buf, Int32(3)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterTypeReadingPosition.write(position, into: &buf) + FfiConverterInt64.write(timestampMs, into: &buf) + + + case let .heartbeat(materialId,activeSeconds,position,timestampMs): + writeInt(&buf, Int32(4)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterUInt32.write(activeSeconds, into: &buf) + FfiConverterOptionTypeReadingPosition.write(position, into: &buf) + FfiConverterInt64.write(timestampMs, into: &buf) + + + case let .markedAsRead(materialId,timestampMs): + writeInt(&buf, Int32(5)) + FfiConverterString.write(materialId, into: &buf) + FfiConverterInt64.write(timestampMs, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingEvent_lift(_ buf: RustBuffer) throws -> ReadingEvent { + return try FfiConverterTypeReadingEvent.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingEvent_lower(_ value: ReadingEvent) -> RustBuffer { + return FfiConverterTypeReadingEvent.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ReadingEventTypeV2: Equatable, Hashable { + + case materialOpened + case materialClosed + case positionChanged + case heartbeat + case markedAsRead + + + + + +} + +#if compiler(>=6) +extension ReadingEventTypeV2: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingEventTypeV2: FfiConverterRustBuffer { + typealias SwiftType = ReadingEventTypeV2 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingEventTypeV2 { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .materialOpened + + case 2: return .materialClosed + + case 3: return .positionChanged + + case 4: return .heartbeat + + case 5: return .markedAsRead + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ReadingEventTypeV2, into buf: inout [UInt8]) { + switch value { + + + case .materialOpened: + writeInt(&buf, Int32(1)) + + + case .materialClosed: + writeInt(&buf, Int32(2)) + + + case .positionChanged: + writeInt(&buf, Int32(3)) + + + case .heartbeat: + writeInt(&buf, Int32(4)) + + + case .markedAsRead: + writeInt(&buf, Int32(5)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingEventTypeV2_lift(_ buf: RustBuffer) throws -> ReadingEventTypeV2 { + return try FfiConverterTypeReadingEventTypeV2.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingEventTypeV2_lower(_ value: ReadingEventTypeV2) -> RustBuffer { + return FfiConverterTypeReadingEventTypeV2.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ReadingPosition: Equatable, Hashable { + + case markdown(blockId: String, scrollProgress: Float + ) + case text(lineNumber: UInt32, scrollProgress: Float + ) + case pdf(pageNumber: UInt32, pageProgress: Float, overallProgress: Float + ) + case image(zoomScale: Float, offsetX: Float, offsetY: Float + ) + case epub(chapterId: String, chapterProgress: Float, overallProgress: Float + ) + case unknown + + + + + +} + +#if compiler(>=6) +extension ReadingPosition: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingPosition: FfiConverterRustBuffer { + typealias SwiftType = ReadingPosition + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingPosition { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .markdown(blockId: try FfiConverterString.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf) + ) + + case 2: return .text(lineNumber: try FfiConverterUInt32.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf) + ) + + case 3: return .pdf(pageNumber: try FfiConverterUInt32.read(from: &buf), pageProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) + ) + + case 4: return .image(zoomScale: try FfiConverterFloat.read(from: &buf), offsetX: try FfiConverterFloat.read(from: &buf), offsetY: try FfiConverterFloat.read(from: &buf) + ) + + case 5: return .epub(chapterId: try FfiConverterString.read(from: &buf), chapterProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf) + ) + + case 6: return .unknown + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ReadingPosition, into buf: inout [UInt8]) { + switch value { + + + case let .markdown(blockId,scrollProgress): + writeInt(&buf, Int32(1)) + FfiConverterString.write(blockId, into: &buf) + FfiConverterFloat.write(scrollProgress, into: &buf) + + + case let .text(lineNumber,scrollProgress): + writeInt(&buf, Int32(2)) + FfiConverterUInt32.write(lineNumber, into: &buf) + FfiConverterFloat.write(scrollProgress, into: &buf) + + + case let .pdf(pageNumber,pageProgress,overallProgress): + writeInt(&buf, Int32(3)) + FfiConverterUInt32.write(pageNumber, into: &buf) + FfiConverterFloat.write(pageProgress, into: &buf) + FfiConverterFloat.write(overallProgress, into: &buf) + + + case let .image(zoomScale,offsetX,offsetY): + writeInt(&buf, Int32(4)) + FfiConverterFloat.write(zoomScale, into: &buf) + FfiConverterFloat.write(offsetX, into: &buf) + FfiConverterFloat.write(offsetY, into: &buf) + + + case let .epub(chapterId,chapterProgress,overallProgress): + writeInt(&buf, Int32(5)) + FfiConverterString.write(chapterId, into: &buf) + FfiConverterFloat.write(chapterProgress, into: &buf) + FfiConverterFloat.write(overallProgress, into: &buf) + + + case .unknown: + writeInt(&buf, Int32(6)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingPosition_lift(_ buf: RustBuffer) throws -> ReadingPosition { + return try FfiConverterTypeReadingPosition.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingPosition_lower(_ value: ReadingPosition) -> RustBuffer { + return FfiConverterTypeReadingPosition.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ReadingSessionStatus: Equatable, Hashable { + + case active + case paused + case closed + case interrupted + case failed + + + + + +} + +#if compiler(>=6) +extension ReadingSessionStatus: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeReadingSessionStatus: FfiConverterRustBuffer { + typealias SwiftType = ReadingSessionStatus + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingSessionStatus { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .active + + case 2: return .paused + + case 3: return .closed + + case 4: return .interrupted + + case 5: return .failed + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ReadingSessionStatus, into buf: inout [UInt8]) { + switch value { + + + case .active: + writeInt(&buf, Int32(1)) + + + case .paused: + writeInt(&buf, Int32(2)) + + + case .closed: + writeInt(&buf, Int32(3)) + + + case .interrupted: + writeInt(&buf, Int32(4)) + + + case .failed: + writeInt(&buf, Int32(5)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingSessionStatus_lift(_ buf: RustBuffer) throws -> ReadingSessionStatus { + return try FfiConverterTypeReadingSessionStatus.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeReadingSessionStatus_lower(_ value: ReadingSessionStatus) -> RustBuffer { + return FfiConverterTypeReadingSessionStatus.lower(value) +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionUInt32: FfiConverterRustBuffer { + typealias SwiftType = UInt32? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterUInt32.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterUInt32.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { + typealias SwiftType = String? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterString.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterString.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeReadingPosition: FfiConverterRustBuffer { + typealias SwiftType = ReadingPosition? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeReadingPosition.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeReadingPosition.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceUInt32: FfiConverterRustBuffer { + typealias SwiftType = [UInt32] + + public static func write(_ value: [UInt32], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterUInt32.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [UInt32] { + let len: Int32 = try readInt(&buf) + var seq = [UInt32]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterUInt32.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer { + typealias SwiftType = [String] + + public static func write(_ value: [String], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterString.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String] { + let len: Int32 = try readInt(&buf) + var seq = [String]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterString.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeEpubChapter: FfiConverterRustBuffer { + typealias SwiftType = [EpubChapter] + + public static func write(_ value: [EpubChapter], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeEpubChapter.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [EpubChapter] { + let len: Int32 = try readInt(&buf) + var seq = [EpubChapter]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeEpubChapter.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypePdfPageText: FfiConverterRustBuffer { + typealias SwiftType = [PdfPageText] + + public static func write(_ value: [PdfPageText], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypePdfPageText.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [PdfPageText] { + let len: Int32 = try readInt(&buf) + var seq = [PdfPageText]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypePdfPageText.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeReadingEventV2: FfiConverterRustBuffer { + typealias SwiftType = [ReadingEventV2] + + public static func write(_ value: [ReadingEventV2], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeReadingEventV2.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [ReadingEventV2] { + let len: Int32 = try readInt(&buf) + var seq = [ReadingEventV2]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeReadingEventV2.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeSearchResult: FfiConverterRustBuffer { + typealias SwiftType = [SearchResult] + + public static func write(_ value: [SearchResult], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeSearchResult.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [SearchResult] { + let len: Int32 = try readInt(&buf) + var seq = [SearchResult]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeSearchResult.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeDocumentBlock: FfiConverterRustBuffer { + typealias SwiftType = [DocumentBlock] + + public static func write(_ value: [DocumentBlock], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeDocumentBlock.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [DocumentBlock] { + let len: Int32 = try readInt(&buf) + var seq = [DocumentBlock]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeDocumentBlock.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeReadingEvent: FfiConverterRustBuffer { + typealias SwiftType = [ReadingEvent] + + public static func write(_ value: [ReadingEvent], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeReadingEvent.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [ReadingEvent] { + let len: Int32 = try readInt(&buf) + var seq = [ReadingEvent]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeReadingEvent.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceSequenceString: FfiConverterRustBuffer { + typealias SwiftType = [[String]] + + public static func write(_ value: [[String]], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterSequenceString.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [[String]] { + let len: Int32 = try readInt(&buf) + var seq = [[String]]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterSequenceString.read(from: &buf)) + } + return seq + } +} +public func ackEventsV2(eventIds: [String]) -> UInt32 { + return try! FfiConverterUInt32.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_ack_events_v2( + FfiConverterSequenceString.lower(eventIds),$0 + ) +}) +} +public func cleanupStaleSessionsFfi(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { + return try! FfiConverterUInt32.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi( + FfiConverterInt64.lower(nowMs), + FfiConverterInt64.lower(maxAgeMs),$0 + ) +}) +} +public func clearExportedEvents(count: UInt32) {try! rustCall() { + uniffi_zx_document_ffi_fn_func_clear_exported_events( + FfiConverterUInt32.lower(count),$0 + ) +} +} +public func closeReadingSessionV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_close_reading_session_v2( + FfiConverterString.lower(sessionId),$0 + ) +} +} +public func createNoteAnchor(materialId: String, position: ReadingPosition?) -> NoteAnchor { + return try! FfiConverterTypeNoteAnchor_lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_create_note_anchor( + FfiConverterString.lower(materialId), + FfiConverterOptionTypeReadingPosition.lower(position),$0 + ) +}) +} +public func createNoteAnchorFromSearch(materialId: String, result: SearchResult) -> NoteAnchor { + return try! FfiConverterTypeNoteAnchor_lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_create_note_anchor_from_search( + FfiConverterString.lower(materialId), + FfiConverterTypeSearchResult_lower(result),$0 + ) +}) +} +public func detectMaterialType(filePath: String)throws -> MaterialType { + return try FfiConverterTypeMaterialType_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_detect_material_type( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func exportPendingEvents() -> [ReadingEvent] { + return try! FfiConverterSequenceTypeReadingEvent.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_export_pending_events($0 + ) +}) +} +public func exportPendingEventsV2(limit: UInt32, timestampMs: Int64) -> [ReadingEventV2] { + return try! FfiConverterSequenceTypeReadingEventV2.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_export_pending_events_v2( + FfiConverterUInt32.lower(limit), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func extractPdfTextFfi(filePath: String)throws -> [PdfPageText] { + return try FfiConverterSequenceTypePdfPageText.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func getOfficePreviewConfigFfi(materialType: MaterialType, fileSize: UInt64)throws -> OfficePreviewConfig { + return try FfiConverterTypeOfficePreviewConfig_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi( + FfiConverterTypeMaterialType_lower(materialType), + FfiConverterUInt64.lower(fileSize),$0 + ) +}) +} +public func isOfficeTypeFfi(materialType: MaterialType) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_is_office_type_ffi( + FfiConverterTypeMaterialType_lower(materialType),$0 + ) +}) +} +public func markEventsFailedV2(eventIds: [String]) -> UInt32 { + return try! FfiConverterUInt32.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_mark_events_failed_v2( + FfiConverterSequenceString.lower(eventIds),$0 + ) +}) +} +public func parseMarkdown(content: String)throws -> [DocumentBlock] { + return try FfiConverterSequenceTypeDocumentBlock.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_parse_markdown( + FfiConverterString.lower(content),$0 + ) +}) +} +public func parseText(content: String)throws -> [DocumentBlock] { + return try FfiConverterSequenceTypeDocumentBlock.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_parse_text( + FfiConverterString.lower(content),$0 + ) +}) +} +public func pauseReadingSessionV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_pause_reading_session_v2( + FfiConverterString.lower(sessionId),$0 + ) +} +} +public func pushHeartbeatV2(sessionId: String, materialId: String, activeSecondsDelta: UInt32, position: ReadingPosition?, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_heartbeat_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterUInt32.lower(activeSecondsDelta), + FfiConverterOptionTypeReadingPosition.lower(position), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func pushMarkedAsReadV2(sessionId: String, materialId: String, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func pushMaterialClosedV2(sessionId: String, materialId: String, activeSecondsDelta: UInt32, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_material_closed_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterUInt32.lower(activeSecondsDelta), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func pushMaterialOpenedV2(sessionId: String, materialId: String, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_material_opened_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func pushPositionChangedV2(sessionId: String, materialId: String, position: ReadingPosition, timestampMs: Int64)throws -> ReadingEventV2 { + return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_push_position_changed_v2( + FfiConverterString.lower(sessionId), + FfiConverterString.lower(materialId), + FfiConverterTypeReadingPosition_lower(position), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func pushReadingEvent(event: ReadingEvent) {try! rustCall() { + uniffi_zx_document_ffi_fn_func_push_reading_event( + FfiConverterTypeReadingEvent_lower(event),$0 + ) +} +} +public func readEpubChaptersFfi(filePath: String)throws -> [EpubChapter] { + return try FfiConverterSequenceTypeEpubChapter.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_read_epub_chapters_ffi( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func readEpubMetadataFfi(filePath: String)throws -> EpubMetadata { + return try FfiConverterTypeEpubMetadata_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_read_epub_metadata_ffi( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func readImageMeta(filePath: String)throws -> ImageMeta { + return try FfiConverterTypeImageMeta_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_read_image_meta( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func readPdfMetadataFfi(filePath: String)throws -> PdfMetadata { + return try FfiConverterTypePdfMetadata_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_read_pdf_metadata_ffi( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func readTextStats(filePath: String)throws -> TextStats { + return try FfiConverterTypeTextStats_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_read_text_stats( + FfiConverterString.lower(filePath),$0 + ) +}) +} +public func reloadStaleEventsV2() -> UInt32 { + return try! FfiConverterUInt32.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_reload_stale_events_v2($0 + ) +}) +} +public func restorePositionFromAnchor(anchor: NoteAnchor) -> ReadingPosition? { + return try! FfiConverterOptionTypeReadingPosition.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_restore_position_from_anchor( + FfiConverterTypeNoteAnchor_lower(anchor),$0 + ) +}) +} +public func resumeReadingSessionV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_resume_reading_session_v2( + FfiConverterString.lower(sessionId),$0 + ) +} +} +public func searchEpubChaptersFfi(chapterIds: [String], chapterTexts: [String], query: String) -> [SearchResult] { + return try! FfiConverterSequenceTypeSearchResult.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_search_epub_chapters_ffi( + FfiConverterSequenceString.lower(chapterIds), + FfiConverterSequenceString.lower(chapterTexts), + FfiConverterString.lower(query),$0 + ) +}) +} +public func searchMarkdownBlocks(blocks: [DocumentBlock], query: String) -> [SearchResult] { + return try! FfiConverterSequenceTypeSearchResult.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_search_markdown_blocks( + FfiConverterSequenceTypeDocumentBlock.lower(blocks), + FfiConverterString.lower(query),$0 + ) +}) +} +public func searchPdfPages(pageNumbers: [UInt32], pageTexts: [String], query: String) -> [SearchResult] { + return try! FfiConverterSequenceTypeSearchResult.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_search_pdf_pages( + FfiConverterSequenceUInt32.lower(pageNumbers), + FfiConverterSequenceString.lower(pageTexts), + FfiConverterString.lower(query),$0 + ) +}) +} +public func searchTextContent(content: String, query: String) -> [SearchResult] { + return try! FfiConverterSequenceTypeSearchResult.lift(try! rustCall() { + uniffi_zx_document_ffi_fn_func_search_text_content( + FfiConverterString.lower(content), + FfiConverterString.lower(query),$0 + ) +}) +} +public func startReadingSessionV2(material: ReadingMaterialRef, timestampMs: Int64)throws -> String { + return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) { + uniffi_zx_document_ffi_fn_func_start_reading_session_v2( + FfiConverterTypeReadingMaterialRef_lower(material), + FfiConverterInt64.lower(timestampMs),$0 + ) +}) +} +public func updateReadingPosition(materialId: String, position: ReadingPosition) {try! rustCall() { + uniffi_zx_document_ffi_fn_func_update_reading_position( + FfiConverterString.lower(materialId), + FfiConverterTypeReadingPosition_lower(position),$0 + ) +} +} + +// swiftlint:enable all \ No newline at end of file diff --git a/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.h b/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.h index 04d1257..4073836 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.h +++ b/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.h @@ -248,6 +248,11 @@ typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureRes uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_BUILD_REPRESENTATION_FINGERPRINT +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_BUILD_REPRESENTATION_FINGERPRINT +RustBuffer uniffi_zx_document_ffi_fn_func_build_representation_fingerprint(RustBuffer source_fingerprint, RustBuffer renderer_kind, RustBuffer parser_version, uint32_t schema_version, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status @@ -260,7 +265,12 @@ void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCa #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2 #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2 -void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status +void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_COMPUTE_FILE_FINGERPRINT +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_COMPUTE_FILE_FINGERPRINT +RustBuffer uniffi_zx_document_ffi_fn_func_compute_file_fingerprint(RustBuffer file_path, RustCallStatus *_Nonnull out_status ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR @@ -292,6 +302,17 @@ RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limi #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_ACTIVE_SESSION_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_ACTIVE_SESSION_V2 +RustBuffer uniffi_zx_document_ffi_fn_func_get_active_session_v2(RustBuffer material_id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_EVENT_BUFFER_STATE_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_EVENT_BUFFER_STATE_V2 +RustBuffer uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2(RustCallStatus *_Nonnull out_status + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI @@ -299,6 +320,11 @@ RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_p RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_SESSION_V2_FFI +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_SESSION_V2_FFI +RustBuffer uniffi_zx_document_ffi_fn_func_get_session_v2_ffi(RustBuffer session_id, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status @@ -309,6 +335,16 @@ int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_typ uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_FAILED_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_FAILED_V2 +void uniffi_zx_document_ffi_fn_func_mark_session_failed_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_INTERRUPTED_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_INTERRUPTED_V2 +void uniffi_zx_document_ffi_fn_func_mark_session_interrupted_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status @@ -324,6 +360,16 @@ RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCal void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_CHECKPOINT_CHANGED_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_CHECKPOINT_CHANGED_V2 +RustBuffer uniffi_zx_document_ffi_fn_func_push_checkpoint_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer checkpoint, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2 +RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_checkpoint_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer checkpoint, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2 #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2 RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status @@ -415,6 +461,11 @@ RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbe RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SET_SESSION_CLOSED_AT_MS_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SET_SESSION_CLOSED_AT_MS_V2 +void uniffi_zx_document_ffi_fn_func_set_session_closed_at_ms_v2(RustBuffer session_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2 #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2 RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status @@ -689,6 +740,12 @@ void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStat #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2 uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_BUILD_REPRESENTATION_FINGERPRINT +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_BUILD_REPRESENTATION_FINGERPRINT +uint16_t uniffi_zx_document_ffi_checksum_func_build_representation_fingerprint(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI @@ -707,6 +764,12 @@ uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2 uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_COMPUTE_FILE_FINGERPRINT +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_COMPUTE_FILE_FINGERPRINT +uint16_t uniffi_zx_document_ffi_checksum_func_compute_file_fingerprint(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR @@ -743,12 +806,30 @@ uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_ACTIVE_SESSION_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_ACTIVE_SESSION_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_get_active_session_v2(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_EVENT_BUFFER_STATE_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_EVENT_BUFFER_STATE_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_get_event_buffer_state_v2(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_SESSION_V2_FFI +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_SESSION_V2_FFI +uint16_t uniffi_zx_document_ffi_checksum_func_get_session_v2_ffi(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI @@ -761,6 +842,18 @@ uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2 uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_FAILED_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_FAILED_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_mark_session_failed_v2(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_INTERRUPTED_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_INTERRUPTED_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_mark_session_interrupted_v2(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN @@ -779,6 +872,18 @@ uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2 uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_CHECKPOINT_CHANGED_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_CHECKPOINT_CHANGED_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_push_checkpoint_changed_v2(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_checkpoint_v2(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2 @@ -887,6 +992,12 @@ uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void #define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SET_SESSION_CLOSED_AT_MS_V2 +#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SET_SESSION_CLOSED_AT_MS_V2 +uint16_t uniffi_zx_document_ffi_checksum_func_set_session_closed_at_ms_v2(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2 diff --git a/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.modulemap b/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.modulemap new file mode 100644 index 0000000..f7cb771 --- /dev/null +++ b/AIStudyApp/AIStudyApp/Core/Services/zx_documentFFI.modulemap @@ -0,0 +1,7 @@ +module zx_documentFFI { + header "zx_documentFFI.h" + export * + use "Darwin" + use "_Builtin_stdbool" + use "_Builtin_stdint" +} \ No newline at end of file diff --git a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/Info.plist b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/Info.plist index 612f37c..4526e4c 100644 --- a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/Info.plist +++ b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/Info.plist @@ -8,12 +8,13 @@ BinaryPath libzx_document_ffi.a LibraryIdentifier - ios-arm64-simulator + ios-arm64_x86_64-simulator LibraryPath libzx_document_ffi.a SupportedArchitectures arm64 + x86_64 SupportedPlatform ios diff --git a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/Headers/module.modulemap b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/Headers/module.modulemap deleted file mode 100644 index 9a0bd4e..0000000 --- a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/Headers/module.modulemap +++ /dev/null @@ -1 +0,0 @@ -module zx_documentFFI { header "zx_documentFFI.h"; export * } diff --git a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/Headers/zx_documentFFI.h b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/Headers/zx_documentFFI.h deleted file mode 100644 index 04d1257..0000000 --- a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/Headers/zx_documentFFI.h +++ /dev/null @@ -1,910 +0,0 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! - -#pragma once - -#include -#include -#include - -// The following structs are used to implement the lowest level -// of the FFI, and thus useful to multiple uniffied crates. -// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. -#ifdef UNIFFI_SHARED_H - // We also try to prevent mixing versions of shared uniffi header structs. - // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 - #ifndef UNIFFI_SHARED_HEADER_V4 - #error Combining helper code from multiple versions of uniffi is not supported - #endif // ndef UNIFFI_SHARED_HEADER_V4 -#else -#define UNIFFI_SHARED_H -#define UNIFFI_SHARED_HEADER_V4 -// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ -// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ - -typedef struct RustBuffer -{ - uint64_t capacity; - uint64_t len; - uint8_t *_Nullable data; -} RustBuffer; - -typedef struct ForeignBytes -{ - int32_t len; - const uint8_t *_Nullable data; -} ForeignBytes; - -// Error definitions -typedef struct RustCallStatus { - int8_t code; - RustBuffer errorBuf; -} RustCallStatus; - -// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ -// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ -#endif // def UNIFFI_SHARED_H -#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK -#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK -typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK -typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t - ); - -#endif -#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE -#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE -typedef void (*UniffiCallbackInterfaceFree)(uint64_t - ); - -#endif -#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE -#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE -typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT -typedef struct UniffiForeignFutureDroppedCallbackStruct { - uint64_t handle; - UniffiForeignFutureDroppedCallback _Nonnull free; -} UniffiForeignFutureDroppedCallbackStruct; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8 -typedef struct UniffiForeignFutureResultU8 { - uint8_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultU8; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8 -typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8 -typedef struct UniffiForeignFutureResultI8 { - int8_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultI8; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8 -typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16 -typedef struct UniffiForeignFutureResultU16 { - uint16_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultU16; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16 -typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16 -typedef struct UniffiForeignFutureResultI16 { - int16_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultI16; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16 -typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32 -typedef struct UniffiForeignFutureResultU32 { - uint32_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultU32; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32 -typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32 -typedef struct UniffiForeignFutureResultI32 { - int32_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultI32; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32 -typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64 -typedef struct UniffiForeignFutureResultU64 { - uint64_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultU64; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64 -typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64 -typedef struct UniffiForeignFutureResultI64 { - int64_t returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultI64; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64 -typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32 -typedef struct UniffiForeignFutureResultF32 { - float returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultF32; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32 -typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64 -typedef struct UniffiForeignFutureResultF64 { - double returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultF64; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64 -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64 -typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64 - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER -typedef struct UniffiForeignFutureResultRustBuffer { - RustBuffer returnValue; - RustCallStatus callStatus; -} UniffiForeignFutureResultRustBuffer; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER -typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer - ); - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID -typedef struct UniffiForeignFutureResultVoid { - RustCallStatus callStatus; -} UniffiForeignFutureResultVoid; - -#endif -#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID -#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID -typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid - ); - -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_ACK_EVENTS_V2 -uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI -uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEAR_EXPORTED_EVENTS -void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2 -void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR -RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH -RustBuffer uniffi_zx_document_ffi_fn_func_create_note_anchor_from_search(RustBuffer material_id, RustBuffer result, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_DETECT_MATERIAL_TYPE -RustBuffer uniffi_zx_document_ffi_fn_func_detect_material_type(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS -RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events(RustCallStatus *_Nonnull out_status - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXPORT_PENDING_EVENTS_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limit, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI -RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI -RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI -int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_EVENTS_FAILED_V2 -uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN -RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_TEXT -RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PAUSE_READING_SESSION_V2 -void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MARKED_AS_READ_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_CLOSED_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_push_material_closed_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_MATERIAL_OPENED_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_push_material_opened_v2(RustBuffer session_id, RustBuffer material_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_POSITION_CHANGED_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_push_position_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_READING_EVENT -void uniffi_zx_document_ffi_fn_func_push_reading_event(RustBuffer event, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_CHAPTERS_FFI -RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_chapters_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_EPUB_METADATA_FFI -RustBuffer uniffi_zx_document_ffi_fn_func_read_epub_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_IMAGE_META -RustBuffer uniffi_zx_document_ffi_fn_func_read_image_meta(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_PDF_METADATA_FFI -RustBuffer uniffi_zx_document_ffi_fn_func_read_pdf_metadata_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_READ_TEXT_STATS -RustBuffer uniffi_zx_document_ffi_fn_func_read_text_stats(RustBuffer file_path, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RELOAD_STALE_EVENTS_V2 -uint32_t uniffi_zx_document_ffi_fn_func_reload_stale_events_v2(RustCallStatus *_Nonnull out_status - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESTORE_POSITION_FROM_ANCHOR -RustBuffer uniffi_zx_document_ffi_fn_func_restore_position_from_anchor(RustBuffer anchor, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_RESUME_READING_SESSION_V2 -void uniffi_zx_document_ffi_fn_func_resume_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_EPUB_CHAPTERS_FFI -RustBuffer uniffi_zx_document_ffi_fn_func_search_epub_chapters_ffi(RustBuffer chapter_ids, RustBuffer chapter_texts, RustBuffer query, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_MARKDOWN_BLOCKS -RustBuffer uniffi_zx_document_ffi_fn_func_search_markdown_blocks(RustBuffer blocks, RustBuffer query, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_PDF_PAGES -RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbers, RustBuffer page_texts, RustBuffer query, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SEARCH_TEXT_CONTENT -RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2 -RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_UPDATE_READING_POSITION -void uniffi_zx_document_ffi_fn_func_update_reading_position(RustBuffer material_id, RustBuffer position, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_ALLOC -RustBuffer ffi_zx_document_ffi_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FROM_BYTES -RustBuffer ffi_zx_document_ffi_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_FREE -void ffi_zx_document_ffi_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUSTBUFFER_RESERVE -RustBuffer ffi_zx_document_ffi_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U8 -void ffi_zx_document_ffi_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U8 -void ffi_zx_document_ffi_rust_future_cancel_u8(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U8 -void ffi_zx_document_ffi_rust_future_free_u8(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U8 -uint8_t ffi_zx_document_ffi_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I8 -void ffi_zx_document_ffi_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I8 -void ffi_zx_document_ffi_rust_future_cancel_i8(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I8 -void ffi_zx_document_ffi_rust_future_free_i8(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I8 -int8_t ffi_zx_document_ffi_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U16 -void ffi_zx_document_ffi_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U16 -void ffi_zx_document_ffi_rust_future_cancel_u16(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U16 -void ffi_zx_document_ffi_rust_future_free_u16(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U16 -uint16_t ffi_zx_document_ffi_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I16 -void ffi_zx_document_ffi_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I16 -void ffi_zx_document_ffi_rust_future_cancel_i16(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I16 -void ffi_zx_document_ffi_rust_future_free_i16(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I16 -int16_t ffi_zx_document_ffi_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U32 -void ffi_zx_document_ffi_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U32 -void ffi_zx_document_ffi_rust_future_cancel_u32(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U32 -void ffi_zx_document_ffi_rust_future_free_u32(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U32 -uint32_t ffi_zx_document_ffi_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I32 -void ffi_zx_document_ffi_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I32 -void ffi_zx_document_ffi_rust_future_cancel_i32(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I32 -void ffi_zx_document_ffi_rust_future_free_i32(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I32 -int32_t ffi_zx_document_ffi_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_U64 -void ffi_zx_document_ffi_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_U64 -void ffi_zx_document_ffi_rust_future_cancel_u64(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_U64 -void ffi_zx_document_ffi_rust_future_free_u64(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_U64 -uint64_t ffi_zx_document_ffi_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_I64 -void ffi_zx_document_ffi_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_I64 -void ffi_zx_document_ffi_rust_future_cancel_i64(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_I64 -void ffi_zx_document_ffi_rust_future_free_i64(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_I64 -int64_t ffi_zx_document_ffi_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F32 -void ffi_zx_document_ffi_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F32 -void ffi_zx_document_ffi_rust_future_cancel_f32(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F32 -void ffi_zx_document_ffi_rust_future_free_f32(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F32 -float ffi_zx_document_ffi_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_F64 -void ffi_zx_document_ffi_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_F64 -void ffi_zx_document_ffi_rust_future_cancel_f64(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_F64 -void ffi_zx_document_ffi_rust_future_free_f64(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64 -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_F64 -double ffi_zx_document_ffi_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_RUST_BUFFER -void ffi_zx_document_ffi_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_RUST_BUFFER -void ffi_zx_document_ffi_rust_future_cancel_rust_buffer(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_RUST_BUFFER -void ffi_zx_document_ffi_rust_future_free_rust_buffer(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_RUST_BUFFER -RustBuffer ffi_zx_document_ffi_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_POLL_VOID -void ffi_zx_document_ffi_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_CANCEL_VOID -void ffi_zx_document_ffi_rust_future_cancel_void(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_FREE_VOID -void ffi_zx_document_ffi_rust_future_free_void(uint64_t handle -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_RUST_FUTURE_COMPLETE_VOID -void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_cleanup_stale_sessions_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEAR_EXPORTED_EVENTS -uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR -uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR_FROM_SEARCH -uint16_t uniffi_zx_document_ffi_checksum_func_create_note_anchor_from_search(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_DETECT_MATERIAL_TYPE -uint16_t uniffi_zx_document_ffi_checksum_func_detect_material_type(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS -uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXPORT_PENDING_EVENTS_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN -uint16_t uniffi_zx_document_ffi_checksum_func_parse_markdown(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_TEXT -uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MARKED_AS_READ_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_push_marked_as_read_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_CLOSED_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_push_material_closed_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_MATERIAL_OPENED_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_push_material_opened_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_POSITION_CHANGED_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_push_position_changed_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_READING_EVENT -uint16_t uniffi_zx_document_ffi_checksum_func_push_reading_event(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_CHAPTERS_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_chapters_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_EPUB_METADATA_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_read_epub_metadata_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_IMAGE_META -uint16_t uniffi_zx_document_ffi_checksum_func_read_image_meta(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_PDF_METADATA_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_read_pdf_metadata_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_READ_TEXT_STATS -uint16_t uniffi_zx_document_ffi_checksum_func_read_text_stats(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RELOAD_STALE_EVENTS_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_reload_stale_events_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESTORE_POSITION_FROM_ANCHOR -uint16_t uniffi_zx_document_ffi_checksum_func_restore_position_from_anchor(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_RESUME_READING_SESSION_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_resume_reading_session_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_EPUB_CHAPTERS_FFI -uint16_t uniffi_zx_document_ffi_checksum_func_search_epub_chapters_ffi(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_MARKDOWN_BLOCKS -uint16_t uniffi_zx_document_ffi_checksum_func_search_markdown_blocks(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_PDF_PAGES -uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT -uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2 -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2 -uint16_t uniffi_zx_document_ffi_checksum_func_start_reading_session_v2(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION -#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_UPDATE_READING_POSITION -uint16_t uniffi_zx_document_ffi_checksum_func_update_reading_position(void - -); -#endif -#ifndef UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION -#define UNIFFI_FFIDEF_FFI_ZX_DOCUMENT_FFI_UNIFFI_CONTRACT_VERSION -uint32_t ffi_zx_document_ffi_uniffi_contract_version(void - -); -#endif - diff --git a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/libzx_document_ffi.a b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/libzx_document_ffi.a deleted file mode 100644 index 832324a..0000000 Binary files a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64-simulator/libzx_document_ffi.a and /dev/null differ diff --git a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a index 05df97d..e762a9c 100644 Binary files a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a and b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a differ diff --git a/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64_x86_64-simulator/libzx_document_ffi.a b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64_x86_64-simulator/libzx_document_ffi.a new file mode 100644 index 0000000..e1166df Binary files /dev/null and b/AIStudyApp/AIStudyApp/Core/ZxDocumentRuntime.xcframework/ios-arm64_x86_64-simulator/libzx_document_ffi.a differ diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift index bd07890..00f5998 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift @@ -48,7 +48,14 @@ struct AIChatPage: View { } ForEach(vm.messages) { m in - MessageCard(message: m, onRegenerate: { Task { vm.send() } }) + MessageCard( + message: m, + canSaveAsKnowledge: m.role == .ai && vm.saveTargetKnowledgeBaseId != nil, + onRegenerate: { Task { vm.send() } }, + onSaveAsKnowledge: { + Task { await saveMessageAsKnowledge(m) } + } + ) .id(m.id) .padding(.horizontal, 20) } @@ -133,7 +140,7 @@ struct AIChatPage: View { } label: { Label("删除", systemImage: "trash") } Button { Task { - try? await RagChatService.shared.updateSession(id: s.id, dto: UpdateChatSessionRequest(isArchived: true)) + try? await RagChatService.shared.updateSession(id: s.id, dto: UpdateChatSessionRequest(title: nil, isPinned: nil, isArchived: true)) sessions.removeAll { $0.id == s.id } } } label: { Label("归档", systemImage: "archivebox") } @@ -142,10 +149,13 @@ struct AIChatPage: View { .swipeActions(edge: .leading, allowsFullSwipe: true) { Button { Task { - try? await RagChatService.shared.updateSession(id: s.id, dto: UpdateChatSessionRequest(isPinned: !(s.isPinned ?? false))) + try? await RagChatService.shared.updateSession(id: s.id, dto: UpdateChatSessionRequest(title: nil, isPinned: !(s.isPinned ?? false), isArchived: nil)) if let updated = try? await RagChatService.shared.listSessions() { sessions = updated } } - } label: { Label(s.isPinned == true ? "取消置顶" : "置顶", systemImage: s.isPinned == true ? "pin.slash" : "pin") } + } label: { + let pinned = s.isPinned == true + Label(pinned ? "取消置顶" : "置顶", systemImage: pinned ? "pin.slash" : "pin") + } .tint(Color.zxPurple) } } @@ -175,6 +185,43 @@ struct AIChatPage: View { } } + private func saveMessageAsKnowledge(_ message: AIMessage) async { + guard let knowledgeBaseId = vm.saveTargetKnowledgeBaseId else { + ZXToastManager.shared.error("当前范围不支持保存为知识点") + return + } + guard let currentSessionId = vm.currentSessionId else { + ZXToastManager.shared.error("当前对话尚未就绪") + return + } + + let content = message.content.trimmingCharacters(in: .whitespacesAndNewlines) + guard !content.isEmpty else { + ZXToastManager.shared.error("内容为空,无法保存") + return + } + + let scopeName = vm.saveTargetScopeName ?? "AI 对话" + let titleSeed = content + .components(separatedBy: .newlines) + .first? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "AI 对话知识点" + let title = String(titleSeed.prefix(40)).isEmpty ? "AI 对话知识点" : String(titleSeed.prefix(40)) + + do { + let result = try await RagChatService.shared.saveMessageAsKnowledge( + sessionId: currentSessionId, + messageId: message.id, + knowledgeBaseId: knowledgeBaseId, + title: title, + summary: nil + ) + ZXToastManager.shared.success(result.message) + } catch { + ZXToastManager.shared.error("保存失败") + } + } + private func scopeIcon(for type: String?) -> String { switch type { case "knowledge_base": return "books.vertical" @@ -199,7 +246,9 @@ struct AIChatPage: View { struct MessageCard: View { let message: AIMessage + let canSaveAsKnowledge: Bool let onRegenerate: () -> Void + let onSaveAsKnowledge: () -> Void @State private var thinkingExpanded = true var body: some View { @@ -232,6 +281,11 @@ struct MessageCard: View { Button { UIPasteboard.general.string = message.content } label: { Label("复制", systemImage: "doc.on.doc").font(.system(size: 12)).foregroundColor(Color.zxF04) } + if canSaveAsKnowledge { + Button { onSaveAsKnowledge() } label: { + Label("存为知识点", systemImage: "square.and.arrow.down").font(.system(size: 12)).foregroundColor(Color.zxF04) + } + } Button { onRegenerate() } label: { Label("重新生成", systemImage: "arrow.clockwise").font(.system(size: 12)).foregroundColor(Color.zxF04) } diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift index 7759fbc..a527a5f 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift @@ -2,14 +2,14 @@ import Combine import Foundation struct AIMessage: Identifiable { - let id: UUID + let id: String let role: AIMessageRole let content: String let thinkingContent: String? let citations: [ChatCitation]? let isStreaming: Bool - init(role: AIMessageRole, content: String, thinkingContent: String? = nil, citations: [ChatCitation]? = nil, isStreaming: Bool = false, id: UUID = UUID()) { + init(role: AIMessageRole, content: String, thinkingContent: String? = nil, citations: [ChatCitation]? = nil, isStreaming: Bool = false, id: String = UUID().uuidString) { self.id = id self.role = role self.content = content @@ -61,6 +61,24 @@ final class AIChatViewModel: ObservableObject { } } + var saveTargetKnowledgeBaseId: String? { + if let kbId = entryContext?.parentKnowledgeBaseId, !kbId.isEmpty { + return kbId + } + if entryContext?.scopeType == .knowledgeBase { + return entryContext?.scopeId + } + return nil + } + + var saveTargetScopeName: String? { + entryContext?.scopeName + } + + var currentSessionId: String? { + sessionId + } + func load() async { guard sessionId == nil else { return } @@ -127,7 +145,7 @@ final class AIChatViewModel: ObservableObject { inputText = "" isSending = true - let placeholderId = UUID() + let placeholderId = UUID().uuidString messages.append(AIMessage(role: .ai, content: "", isStreaming: true, id: placeholderId)) Task { @@ -167,6 +185,7 @@ final class AIChatViewModel: ObservableObject { messages[idx] = AIMessage(role: .ai, content: "发送失败: \(error.localizedDescription)") } } + await loadSession(sid) isSending = false } } @@ -203,7 +222,12 @@ final class AIChatViewModel: ObservableObject { messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于\(scopePrefix)内容回答问题。")] } else { messages = msgs.map { m in - AIMessage(role: m.role == "user" ? .user : .ai, content: m.content, citations: m.citations) + AIMessage( + role: m.role == "user" ? .user : .ai, + content: m.content, + citations: m.citations, + id: m.id + ) } } } catch { diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift b/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift index 94de38a..7a10bda 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/ActivityViewModel.swift @@ -64,39 +64,24 @@ class ActivityViewModel: ObservableObject { self.activityService = activityService self.focusService = focusService self.readingService = readingService + // 初始化时立即加载,不等待 onAppear + Task { await loadAll() } } func loadAll() async { isLoading = true errorMessage = nil - async let s = try? activityService.summary() - async let f = try? focusService.listAll() - async let h = try? activityService.heatmap() - async let st = try? activityService.streak() - async let t = try? activityService.trend(days: 7) - async let r = try? activityService.recommendations() - async let cr = try? readingService.getContinueLearning() - async let ls = try? readingService.getLearningSummary() - async let ltr = try? readingService.getLearningTrend(days: 7) - async let recs = try? readingService.getLearningRecords(limit: 10, type: "reading") - - let (summaryResult, focusResult, heatmapResult, streakResult, trendResult, recResult, continueResult, lsResult, ltrResult, recordsResult) = await (s, f, h, st, t, r, cr, ls, ltr, recs) - - summary = summaryResult - learningSummary = lsResult - readingTrend = ltrResult - focusItems = focusResult ?? [] - heatmap = heatmapResult ?? [:] - streak = streakResult - trends = trendResult ?? [] - recommendations = recResult ?? [] - continueReading = continueResult - recentRecords = recordsResult?.items ?? [] - - if summary == nil { - errorMessage = "加载分析数据失败,请下拉刷新重试" - } + Task { summary = try? await activityService.summary() } + Task { focusItems = (try? await focusService.listAll()) ?? [] } + Task { heatmap = (try? await activityService.heatmap()) ?? [:] } + Task { streak = try? await activityService.streak() } + Task { trends = (try? await activityService.trend(days: 7)) ?? [] } + Task { recommendations = (try? await activityService.recommendations()) ?? [] } + Task { continueReading = try? await readingService.getContinueLearning() } + Task { learningSummary = try? await readingService.getLearningSummary() } + Task { readingTrend = try? await readingService.getLearningTrend(days: 7) } + Task { recentRecords = (try? await readingService.getLearningRecords(limit: 10, type: "reading"))?.items ?? [] } isLoading = false } @@ -104,27 +89,15 @@ class ActivityViewModel: ObservableObject { func refresh() async { errorMessage = nil - async let s = try? activityService.summary() - async let f = try? focusService.listAll() - async let h = try? activityService.heatmap() - async let st = try? activityService.streak() - async let t = try? activityService.trend(days: 7) - async let r = try? activityService.recommendations() - async let cr = try? readingService.getContinueLearning() - async let ls = try? readingService.getLearningSummary() - async let ltr = try? readingService.getLearningTrend(days: 7) - async let recs = try? readingService.getLearningRecords(limit: 10, type: "reading") - - let (summaryResult, focusResult, heatmapResult, streakResult, trendResult, recResult, continueResult, lsResult, ltrResult, recordsResult) = await (s, f, h, st, t, r, cr, ls, ltr, recs) - - summary = summaryResult - learningSummary = lsResult - readingTrend = ltrResult - focusItems = focusResult ?? [] - heatmap = heatmapResult ?? [:] - streak = streakResult - trends = trendResult ?? [] - recommendations = recResult ?? [] - continueReading = continueResult + Task { summary = try? await activityService.summary() } + Task { focusItems = (try? await focusService.listAll()) ?? [] } + Task { heatmap = (try? await activityService.heatmap()) ?? [:] } + Task { streak = try? await activityService.streak() } + Task { trends = (try? await activityService.trend(days: 7)) ?? [] } + Task { recommendations = (try? await activityService.recommendations()) ?? [] } + Task { continueReading = try? await readingService.getContinueLearning() } + Task { learningSummary = try? await readingService.getLearningSummary() } + Task { readingTrend = try? await readingService.getLearningTrend(days: 7) } + Task { recentRecords = (try? await readingService.getLearningRecords(limit: 10, type: "reading"))?.items ?? [] } } } diff --git a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift index 1d02b7d..89147c3 100644 --- a/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Analysis/AnalysisHomeView.swift @@ -2,6 +2,7 @@ import SwiftUI struct AnalysisHomeView: View { @StateObject private var viewModel = ActivityViewModel() + var body: some View { ZStack { Color.zxCanvas.ignoresSafeArea() @@ -166,7 +167,7 @@ struct AnalysisHomeView: View { .refreshable { await viewModel.refresh() } } } - .task { await viewModel.loadAll() } + .refreshable { await viewModel.refresh() } .navigationDestination(for: Route.self) { $0.destination } } diff --git a/AIStudyApp/AIStudyApp/Features/Library/FolderManageView.swift b/AIStudyApp/AIStudyApp/Features/Library/FolderManageView.swift index 83e56da..a370ccd 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/FolderManageView.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/FolderManageView.swift @@ -96,7 +96,7 @@ struct FolderManageView: View { private func load() async { isLoading = true - folders = (try? await KnowledgeFolderService.shared.list(kbId: knowledgeBaseId)) ?? [] + folders = (try? await KnowledgeFolderService.shared.list(knowledgeBaseId: knowledgeBaseId)) ?? [] isLoading = false } } diff --git a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift index 89eeb49..9c791d8 100644 --- a/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift +++ b/AIStudyApp/AIStudyApp/Features/Library/LibrarySubpages.swift @@ -139,26 +139,46 @@ struct LibraryDetailPage: View { @State private var showManageView = false @State private var showDeleteConfirm = false @State private var isDeleting = false - @State private var isSelectMode = false - @State private var selectedIds: Set = [] - @State private var showBatchDeleteConfirm = false - @State private var detailTab = 1 + // 批量选择 — 每个 Tab 独立 + @State private var sourceSelectMode = false + @State private var selectedSourceIds: Set = [] + @State private var showSourceDeleteConfirm = false + @State private var sourceToDelete: KnowledgeSource? + @State private var showSourceBatchDelete = false + @State private var showSourceBatchGenerate = false + @State private var isBatchGeneratingSources = false + + @State private var itemSelectMode = false + @State private var selectedItemIds: Set = [] + @State private var showItemBatchDelete = false + + @State private var detailTab = 0 // 0=资料来源, 1=知识点 private func appear() async { - print("[LibraryDetail] appear() START, kbId=\(knowledgeBaseId)") viewModel.knowledgeBase = try? await KnowledgeBaseService.shared.detail(id: knowledgeBaseId) - print("[LibraryDetail] KB loaded: \(viewModel.knowledgeBase?.title ?? "nil")") await loadSources() - print("[LibraryDetail] appear() DONE") } - @State private var sortOption = 0 + @State private var sortOption = 1 @State private var showFolderManager = false @State private var sources: [KnowledgeSource] = [] @State private var sourceReadingStatus: [String: String] = [:] + @State private var sourceTaskStatusMap: [String: KnowledgeGenerationTaskStatus] = [:] @State private var isLoadingSources = true + @State private var sourceTaskPollingTask: Task? - private var allSelected: Bool { - !viewModel.items.isEmpty && selectedIds.count == viewModel.items.count + private var sourceAllSelected: Bool { + !sources.isEmpty && selectedSourceIds.count == sources.count + } + private var itemAllSelected: Bool { + !viewModel.items.isEmpty && selectedItemIds.count == viewModel.items.count + } + // 当前 Tab 是否在选择模式 + private var isInSelectMode: Bool { + detailTab == 0 ? sourceSelectMode : itemSelectMode + } + // 当前选中的数量 + private var currentSelectedCount: Int { + detailTab == 0 ? selectedSourceIds.count : selectedItemIds.count } var body: some View { @@ -185,126 +205,20 @@ struct LibraryDetailPage: View { .padding(.horizontal, 20).padding(.top, 8) } + // Tab 切换 + Picker("", selection: $detailTab) { + Text("资料来源").tag(0) + Text("知识点").tag(1) + } + .pickerStyle(.segmented) + .padding(.horizontal, 20).padding(.vertical, 8) ScrollView { VStack(spacing: 0) { - if detailTab == 0 { - if viewModel.isLoading && viewModel.items.isEmpty { - VStack(spacing: 12) { - ProgressView() - Text("加载中…").font(.system(size: 14)).foregroundColor(Color.zxF04) - } - .frame(maxWidth: .infinity).padding(.top, 80) - } - ForEach(viewModel.items) { item in - let icon = fileTypeIcon(for: item) - let type = fileTypeText(for: item) - let date = formatShortDate(item.updatedAt) - let progress = progressFor(item) - if isSelectMode { - Button { - if selectedIds.contains(item.id) { selectedIds.remove(item.id) } - else { selectedIds.insert(item.id) } - } label: { - HStack(spacing: 10) { - Image(systemName: selectedIds.contains(item.id) ? "checkmark.circle" : "circle") - .font(.system(size: 20)) - .foregroundColor(selectedIds.contains(item.id) ? Color.zxPrimary : Color.zxF03) - itemRow(icon: icon, title: item.title, type: type, date: date, progress: progress) - } - } - .foregroundColor(.primary) - } else { - NavigationLink(value: Route.knowledgeDetail(item: item)) { - itemRow(icon: icon, title: item.title, type: type, date: date, progress: progress) - } - .contextMenu { - Button { - isSelectMode = true - selectedIds.insert(item.id) - } label: { - Label("多选", systemImage: "checkmark.circle") - } - Button { - // TODO: rename - } label: { - Label("重命名", image: "icon-pencil") - } - Button { - // TODO: move to folder - } label: { - Label("移动到", image: "icon-folder") - } - Divider() - Button(role: .destructive) { - Task { - await viewModel.batchDeleteItems(ids: [item.id]) - await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) - } - } label: { - Label("删除", image: "icon-trash") - } - } - } - } - if viewModel.items.isEmpty && !viewModel.isLoading { - Text("暂无知识点").font(.system(size: 14)).foregroundColor(Color.zxF03).padding(.top, 40) - } - if viewModel.hasMore { - ZXLoadMoreFooter { await viewModel.loadMore(knowledgeBaseId: knowledgeBaseId) } - } + if detailTab == 1 { + itemTabContent() } else { - if isLoadingSources { - VStack(spacing: 12) { - ProgressView().tint(Color.zxPurple) - Text("加载中…").font(.system(size: 14)).foregroundColor(Color.zxF04) - }.padding(.top, 80) - } else if sources.isEmpty { - Text("暂无资料来源").font(.system(size: 14)).foregroundColor(Color.zxF03).padding(.top, 40) - } else { - ForEach(sources) { src in - NavigationLink(value: Route.materialReader( - materialId: src.id, - filePath: "", - materialType: materialTypeFromMime(src.mimeType, filename: src.originalFilename), - knowledgeBaseId: knowledgeBaseId, - title: src.originalFilename ?? src.title ?? "未命名" - )) { - HStack(spacing: 12) { - Image(systemName: src.type == "file" ? "doc" : "link") - .font(.system(size: 18)).foregroundColor(Color.zxPurple) - .frame(width: 36, height: 36) - VStack(alignment: .leading, spacing: 2) { - Text(src.originalFilename ?? src.title ?? "未命名") - .font(.system(size: 15, weight: .medium)).foregroundColor(Color.zxF0).lineLimit(1) - if let len = src.textLength, len > 0 { - Text("\(len) 字") - .font(.system(size: 13)).foregroundColor(Color.zxF04) - } - // Reading status badge - if let status = sourceReadingStatus[src.id], status != "not_started" { - Text(status == "read" ? "已读" : "阅读中") - .font(.system(size: 10, weight: .medium)) - .foregroundColor(status == "read" ? Color.green : Color.zxPurple) - .padding(.horizontal, 6).padding(.vertical, 2) - .background((status == "read" ? Color.green : Color.zxPurple).opacity(0.1)) - .clipShape(Capsule()) - } - } - Spacer() - Button { - Task { await deleteSource(src) } - } label: { - Image("icon-trash").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03) - } - } - .padding(.vertical, 14) - .overlay(alignment: .bottom) { - Color.zxHairline.frame(height: 0.5) - } - } - } - } + sourceTabContent() } } .padding(.horizontal, 20).padding(.bottom, 80) @@ -315,33 +229,59 @@ struct LibraryDetailPage: View { } .navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar) .onChange(of: detailTab) { _, newTab in - if newTab == 1 && sources.isEmpty { Task { await loadSources() } } + // 切换 Tab 时退出选择模式 + sourceSelectMode = false; selectedSourceIds.removeAll() + itemSelectMode = false; selectedItemIds.removeAll() + if newTab == 0 && sources.isEmpty { Task { await loadSources() } } } .toolbar { - if isSelectMode { + if isInSelectMode { + // ── 选择模式 ── ToolbarItem(placement: .topBarLeading) { - Button("取消") { isSelectMode = false; selectedIds.removeAll() } - } - ToolbarItem(placement: .topBarTrailing) { - Button { selectedIds = allSelected ? [] : Set(viewModel.items.map(\.id)) } label: { - Text(allSelected ? "取消全选" : "全选").font(.system(size: 14)) + Button("取消") { + if detailTab == 0 { sourceSelectMode = false; selectedSourceIds.removeAll() } + else { itemSelectMode = false; selectedItemIds.removeAll() } } } ToolbarItem(placement: .topBarTrailing) { Button { - showBatchDeleteConfirm = true + if detailTab == 0 { + selectedSourceIds = sourceAllSelected ? [] : Set(sources.map(\.id)) + } else { + selectedItemIds = itemAllSelected ? [] : Set(viewModel.items.map(\.id)) + } } label: { - Image("icon-trash").resizable().scaledToFit().frame(width: 18, height: 18).foregroundColor(selectedIds.isEmpty ? Color.zxF03 : Color.zxCoral) + let all = detailTab == 0 ? sourceAllSelected : itemAllSelected + Text(all ? "取消全选(\(currentSelectedCount))" : "全选(\(currentSelectedCount))").font(.system(size: 14)) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + if detailTab == 0 { showSourceBatchGenerate = true } + else { showItemBatchDelete = true } + } label: { + Image(systemName: detailTab == 0 ? "brain.head.profile" : "trash") + .font(.system(size: 18)) + .foregroundColor(currentSelectedCount == 0 ? Color.zxF03 : (detailTab == 0 ? Color.zxPurple : Color.zxCoral)) + } + .disabled(currentSelectedCount == 0) + } + if detailTab == 0 { + ToolbarItem(placement: .topBarTrailing) { + Button { + showSourceBatchDelete = true + } label: { + Image("icon-trash").resizable().scaledToFit().frame(width: 18, height: 18) + .foregroundColor(currentSelectedCount == 0 ? Color.zxF03 : Color.zxCoral) + } + .disabled(currentSelectedCount == 0) } - .disabled(selectedIds.isEmpty) } } else { + // ── 普通模式 ── ToolbarItem(placement: .topBarTrailing) { HStack(spacing: 4) { Menu { - Button { sortOption = 0 } label: { - Label("默认排序", systemImage: sortOption == 0 ? "checkmark" : "") - } Button { sortOption = 1 } label: { Label("文件名排序", systemImage: sortOption == 1 ? "checkmark" : "") } @@ -372,8 +312,17 @@ struct LibraryDetailPage: View { NavigationLink(value: Route.quizList(knowledgeBaseId: knowledgeBaseId)) { Label("答题测验", image: "icon-pencil") } + if detailTab == 0 { + Button { + sourceSelectMode = true + selectedSourceIds.removeAll() + } label: { + Label("批量生成知识点", systemImage: "brain.head.profile") + } + } Button { - isSelectMode = true + if detailTab == 0 { sourceSelectMode = true } + else { itemSelectMode = true } } label: { Label("批量选择", systemImage: "checkmark.circle") } @@ -407,19 +356,49 @@ struct LibraryDetailPage: View { .sheet(isPresented: $showManageView) { KnowledgeBaseManageView(knowledgeBaseId: knowledgeBaseId) } - .alert("批量删除", isPresented: $showBatchDeleteConfirm) { + .alert("删除资料来源", isPresented: $showSourceDeleteConfirm) { + Button("取消", role: .cancel) { sourceToDelete = nil } + Button("删除", role: .destructive) { + if let src = sourceToDelete { Task { await deleteSource(src) } } + sourceToDelete = nil + } + } message: { + Text("确定要删除「\(sourceToDelete?.originalFilename ?? sourceToDelete?.title ?? "")」吗?关联文件也将被删除。") + } + .alert("批量删除资料来源", isPresented: $showSourceBatchDelete) { Button("取消", role: .cancel) {} - Button("删除 \(selectedIds.count) 个", role: .destructive) { + Button("删除 \(selectedSourceIds.count) 个", role: .destructive) { Task { - await viewModel.batchDeleteItems(ids: Array(selectedIds)) - isSelectMode = false - selectedIds.removeAll() + for id in selectedSourceIds { await deleteSource(byId: id) } + selectedSourceIds.removeAll(); sourceSelectMode = false + await loadSources() } } } message: { - Text("确定要删除选中的 \(selectedIds.count) 个知识点吗?此操作不可恢复。") + Text("确定要删除选中的 \(selectedSourceIds.count) 个资料来源吗?关联文件也将被删除。此操作不可恢复。") + } + .alert("批量生成知识点", isPresented: $showSourceBatchGenerate) { + Button("取消", role: .cancel) {} + Button(isBatchGeneratingSources ? "提交中..." : "生成 \(selectedSourceIds.count) 个") { + Task { await generateKnowledgeForSelectedSources() } + } + .disabled(selectedSourceIds.isEmpty || isBatchGeneratingSources) + } message: { + Text("选中的资料会提交到异步队列。已解析的资料会立即入队,未解析完成的资料会先进入等待状态。") + } + .alert("批量删除知识点", isPresented: $showItemBatchDelete) { + Button("取消", role: .cancel) {} + Button("删除 \(selectedItemIds.count) 个", role: .destructive) { + Task { + await viewModel.batchDeleteItems(ids: Array(selectedItemIds)) + selectedItemIds.removeAll(); itemSelectMode = false + } + } + } message: { + Text("确定要删除选中的 \(selectedItemIds.count) 个知识点吗?此操作不可恢复。") } .task { await appear() } + .onDisappear { stopSourceTaskPolling() } .sheet(isPresented: $showFolderManager) { FolderManageView(knowledgeBaseId: knowledgeBaseId) } @@ -611,6 +590,26 @@ struct LibraryDetailPage: View { } } + private func formatFileSize(_ bytes: Int) -> String { + if bytes >= 1_048_576 { return String(format: "%.1f MB", Double(bytes) / 1_048_576) } + if bytes >= 1024 { return String(format: "%.1f KB", Double(bytes) / 1024) } + return "\(bytes) B" + } + private func formatSourceDate(_ iso: String?) -> String? { + guard let iso else { return nil } + let df = ISO8601DateFormatter() + df.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let date = df.date(from: iso) ?? { + df.formatOptions = [.withInternetDateTime] + return df.date(from: iso) + }() + guard let date else { return nil } + let cal = Calendar.current + let fmt = DateFormatter() + fmt.dateFormat = cal.isDate(date, equalTo: Date(), toGranularity: .year) ? "M/d" : "yyyy/M/d" + return fmt.string(from: date) + } + private func formatShortDate(_ iso: String?) -> String { guard let iso else { return "" } let formatter = ISO8601DateFormatter() @@ -650,27 +649,327 @@ struct LibraryDetailPage: View { private func loadSources(sortBy: String? = nil, order: String? = nil) async { isLoadingSources = true - print("[LibraryDetail] loadSources START, kbId=\(knowledgeBaseId)") do { sources = try await KnowledgeSourceService.shared.list(kbId: knowledgeBaseId, sortBy: sortBy, order: order) - print("[LibraryDetail] loadSources OK, count=\(sources.count)") - for src in sources { - print("[LibraryDetail] source: id=\(src.id), title=\(src.title ?? "-"), parseStatus=\(src.parseStatus ?? "-")") - } + await refreshSourceTaskStatuses() } catch { print("[LibraryDetail] loadSources ERROR: \(error.localizedDescription)") - print("[LibraryDetail] Full error: \(error)") } isLoadingSources = false + restartSourceTaskPollingIfNeeded() } private func deleteSource(_ src: KnowledgeSource) async { + await deleteSource(byId: src.id) + } + private func deleteSource(byId id: String) async { do { - let _ = try await KnowledgeSourceService.shared.delete(kbId: knowledgeBaseId, id: src.id) - sources.removeAll { $0.id == src.id } + let _ = try await KnowledgeSourceService.shared.delete(kbId: knowledgeBaseId, id: id) + sources.removeAll { $0.id == id } ZXToastManager.shared.success("已删除") } catch { ZXToastManager.shared.error("删除失败") } } + + private func generateKnowledgeForSelectedSources() async { + guard !selectedSourceIds.isEmpty, !isBatchGeneratingSources else { return } + isBatchGeneratingSources = true + defer { isBatchGeneratingSources = false } + do { + let result = try await KnowledgeSourceService.shared.generateKnowledgeBatch( + kbId: knowledgeBaseId, + sourceIds: Array(selectedSourceIds) + ) + let queued = result.queuedCount + let waiting = result.waitingForParseCount + let message = waiting > 0 + ? "已提交 \(result.totalCount) 个任务,\(queued) 个已入队,\(waiting) 个等待解析" + : "已提交 \(result.totalCount) 个知识点生成任务" + ZXToastManager.shared.success(message) + selectedSourceIds.removeAll() + sourceSelectMode = false + await loadSources() + restartSourceTaskPollingIfNeeded() + } catch { + ZXToastManager.shared.error("批量生成失败") + } + } + + private func sourceStatusBadge(for source: KnowledgeSource) -> (text: String, color: Color)? { + let effectiveStatus = sourceTaskStatusMap[source.id]?.status ?? source.learningStatus + switch effectiveStatus { + case "waiting_for_parse": + return ("等待解析", Color.orange) + case "queued": + return ("已入队", Color.zxPurple) + case "running": + return ("生成中", Color.blue) + case "completed": + return ("已生成", Color.green) + case "failed": + return ("生成失败", Color.zxCoral) + default: + break + } + + switch source.parseStatus { + case "pending", "processing", "queued": + return ("解析中", Color.orange) + case "failed": + return ("解析失败", Color.zxCoral) + default: + return nil + } + } + + private func statusBadge(text: String, color: Color) -> some View { + Text(text) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(color) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(color.opacity(0.12)) + .clipShape(Capsule()) + } + + private func readingStatusBadge(_ status: String) -> some View { + let isRead = status == "read" + let color = isRead ? Color.green : Color.zxPurple + let label = isRead ? "已读" : "阅读中" + + return Text(label) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(color) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(color.opacity(0.1)) + .clipShape(Capsule()) + } + + private func refreshSourceTaskStatuses() async { + do { + let response = try await KnowledgeSourceService.shared.knowledgeGenerationTasks( + kbId: knowledgeBaseId, + sourceIds: sources.map(\.id) + ) + sourceTaskStatusMap = Dictionary( + uniqueKeysWithValues: response.items.compactMap { item in + guard let sourceId = item.sourceId else { return nil } + return (sourceId, item) + } + ) + } catch { + print("[LibraryDetail] refreshSourceTaskStatuses ERROR: \(error.localizedDescription)") + } + } + + private func restartSourceTaskPollingIfNeeded() { + stopSourceTaskPolling() + let hasActiveTask = sourceTaskStatusMap.values.contains { isKnowledgeTaskActive($0.status) } + guard hasActiveTask else { return } + + sourceTaskPollingTask = Task { + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 3_000_000_000) + if Task.isCancelled { break } + await refreshSourceTaskStatuses() + let stillActive = sourceTaskStatusMap.values.contains { isKnowledgeTaskActive($0.status) } + if !stillActive { break } + do { + sources = try await KnowledgeSourceService.shared.list(kbId: knowledgeBaseId) + } catch { + print("[LibraryDetail] polling source refresh ERROR: \(error.localizedDescription)") + } + } + sourceTaskPollingTask = nil + } + } + + private func stopSourceTaskPolling() { + sourceTaskPollingTask?.cancel() + sourceTaskPollingTask = nil + } + + private func isKnowledgeTaskActive(_ status: String) -> Bool { + ["waiting_for_parse", "queued", "running"].contains(status) + } + + @ViewBuilder + private func itemTabContent() -> some View { + if viewModel.isLoading && viewModel.items.isEmpty { + VStack(spacing: 12) { + ProgressView() + Text("加载中…").font(.system(size: 14)).foregroundColor(Color.zxF04) + } + .frame(maxWidth: .infinity) + .padding(.top, 80) + } + + ForEach(viewModel.items) { item in + itemRowView(item) + } + + if viewModel.items.isEmpty && !viewModel.isLoading { + Text("暂无知识点") + .font(.system(size: 14)) + .foregroundColor(Color.zxF03) + .padding(.top, 40) + } + + if viewModel.hasMore { + ZXLoadMoreFooter { await viewModel.loadMore(knowledgeBaseId: knowledgeBaseId) } + } + } + + @ViewBuilder + private func itemRowView(_ item: KnowledgeItem) -> some View { + let icon = fileTypeIcon(for: item) + let type = fileTypeText(for: item) + let date = formatShortDate(item.updatedAt) + let progress = progressFor(item) + + if itemSelectMode { + Button { + if selectedItemIds.contains(item.id) { selectedItemIds.remove(item.id) } + else { selectedItemIds.insert(item.id) } + } label: { + HStack(spacing: 10) { + Image(systemName: selectedItemIds.contains(item.id) ? "checkmark.circle" : "circle") + .font(.system(size: 20)) + .foregroundColor(selectedItemIds.contains(item.id) ? Color.zxPurple : Color.zxF03) + itemRow(icon: icon, title: item.title, type: type, date: date, progress: progress) + } + } + .foregroundColor(.primary) + } else { + NavigationLink(value: Route.knowledgeDetail(item: item)) { + itemRow(icon: icon, title: item.title, type: type, date: date, progress: progress) + } + .contextMenu { + Button { + itemSelectMode = true + selectedItemIds.insert(item.id) + } label: { + Label("多选", systemImage: "checkmark.circle") + } + Button { + } label: { + Label("重命名", image: "icon-pencil") + } + Button { + } label: { + Label("移动到", image: "icon-folder") + } + Divider() + Button(role: .destructive) { + Task { + await viewModel.batchDeleteItems(ids: [item.id]) + await viewModel.refresh(knowledgeBaseId: knowledgeBaseId) + } + } label: { + Label("删除", image: "icon-trash") + } + } + } + } + + @ViewBuilder + private func sourceTabContent() -> some View { + if isLoadingSources { + VStack(spacing: 12) { + ProgressView().tint(Color.zxPurple) + Text("加载中…").font(.system(size: 14)).foregroundColor(Color.zxF04) + } + .padding(.top, 80) + } else if sources.isEmpty { + Text("暂无资料来源").font(.system(size: 14)).foregroundColor(Color.zxF03).padding(.top, 40) + } else { + ForEach(sources) { src in + sourceRowView(src) + } + } + } + + @ViewBuilder + private func sourceRowView(_ src: KnowledgeSource) -> some View { + if sourceSelectMode { + Button { + if selectedSourceIds.contains(src.id) { selectedSourceIds.remove(src.id) } + else { selectedSourceIds.insert(src.id) } + } label: { + sourceSelectRow(src) + } + .foregroundColor(.primary) + } else { + NavigationLink(value: Route.materialReader( + materialId: src.id, + filePath: "", + materialType: materialTypeFromMime(src.mimeType, filename: src.originalFilename), + knowledgeBaseId: knowledgeBaseId, + title: src.originalFilename ?? src.title ?? "未命名" + )) { + sourceNavigationRow(src) + } + } + } + + private func sourceSelectRow(_ src: KnowledgeSource) -> some View { + HStack(spacing: 12) { + Image(systemName: selectedSourceIds.contains(src.id) ? "checkmark.circle.fill" : "circle") + .font(.system(size: 20)) + .foregroundColor(selectedSourceIds.contains(src.id) ? Color.zxPurple : Color.zxF03) + Image(systemName: src.type == "file" ? "doc" : "link") + .font(.system(size: 18)).foregroundColor(Color.zxPurple) + .frame(width: 36, height: 36) + sourceMetaColumn(src) + Spacer() + } + .padding(.vertical, 14) + .overlay(alignment: .bottom) { Color.zxHairline.frame(height: 0.5) } + } + + private func sourceNavigationRow(_ src: KnowledgeSource) -> some View { + HStack(spacing: 12) { + Image(systemName: src.type == "file" ? "doc" : "link") + .font(.system(size: 18)).foregroundColor(Color.zxPurple) + .frame(width: 36, height: 36) + sourceMetaColumn(src) + Spacer() + Button { + sourceToDelete = src + showSourceDeleteConfirm = true + } label: { + Image("icon-trash").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03) + } + } + .padding(.vertical, 14) + .overlay(alignment: .bottom) { Color.zxHairline.frame(height: 0.5) } + } + + private func sourceMetaColumn(_ src: KnowledgeSource) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(src.originalFilename ?? src.title ?? "未命名") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(Color.zxF0) + .lineLimit(1) + HStack(spacing: 8) { + if let len = src.textLength, len > 0 { + Text("\(len) 字").font(.system(size: 12)).foregroundColor(Color.zxF04) + } + if let size = src.sizeBytes, size > 0 { + Text(formatFileSize(size)).font(.system(size: 12)).foregroundColor(Color.zxF04) + } + if let date = formatSourceDate(src.updatedAt ?? src.createdAt) { + Text(date).font(.system(size: 12)).foregroundColor(Color.zxF04) + } + } + if let badge = sourceStatusBadge(for: src) { + statusBadge(text: badge.text, color: badge.color) + } + if let status = sourceReadingStatus[src.id], status != "not_started" { + readingStatusBadge(status) + } + } + } } struct ZXCardRow: View { let icon: String; let title: String; let desc: String; let status: String; let c: Color var body: some View { HStack(spacing: 12) { Image(systemName: icon).font(.system(size: 18)).foregroundColor(c).frame(width: 40, height: 40).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 10)); VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF03) }; Spacer(); Text(status).font(.system(size: 10, weight: .semibold)).foregroundColor(c).padding(.horizontal, 8).padding(.vertical, 2).background(c.opacity(0.12)).clipShape(Capsule()) } @@ -1111,7 +1410,15 @@ struct KnowledgeDetailPage: View { }.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.zxCanvas) case .ready: if let path = vm.localPath, let mt = vm.detectedType { - MaterialReaderView(materialId: item.id, filePath: path, materialType: mt, title: item.title, showQuickLook: $showQuickLook, showNoteSheet: $showNoteSheet) + MaterialReaderView( + materialId: item.id, + filePath: path, + materialType: mt, + knowledgeBaseId: item.knowledgeBaseId, + title: item.title, + showQuickLook: $showQuickLook, + showNoteSheet: $showNoteSheet + ) } else if let content = item.content { // Plain text content — render directly ZStack { Color.zxCanvas.ignoresSafeArea() @@ -1350,137 +1657,6 @@ struct ZXImportRow: View { let icon: String; let title: String; let desc: String ; VStack(alignment: .leading, spacing: 2) { Text(title).font(.system(size: 16, weight: .semibold)).foregroundColor(Color.zxF0); Text(desc).font(.system(size: 12)).foregroundColor(Color.zxF04) }; Spacer(); Image("icon-chevron-right").resizable().scaledToFit().frame(width: 16, height: 16).foregroundColor(Color.zxF03) }.padding(16).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 16)).overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.zxBorder006, lineWidth: 1)) } } -// MARK: - Import Review - -struct ImportReviewPage: View { - let sourceId: String - @Environment(\.dismiss) private var dismiss - @State private var candidates: [ImportCandidate] = [] - @State private var isLoading = true - @State private var isProcessing = false - @State private var errorMessage: String? - @State private var expandedId: String? - - var body: some View { - ZStack { Color.zxBg0.ignoresSafeArea(); VStack(spacing: 0) { - if isLoading { - VStack(spacing: 12) { ProgressView().tint(Color.zxPurple); Text("加载候选...").font(.system(size: 14)).foregroundColor(Color.zxF04) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else if candidates.isEmpty { - VStack(spacing: 12) { - Image(systemName: "checkmark.circle").font(.system(size: 36)).foregroundColor(Color.zxGreen) - Text("没有待审批的候选").font(.system(size: 14)).foregroundColor(Color.zxF04) - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - ScrollView { - VStack(spacing: 12) { - HStack { - Text("\(candidates.count) 个候选知识点").font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxF04) - Spacer() - Button { - Task { await batchAccept() } - } label: { - Text("全部接受").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxPrimary) - } - .disabled(isProcessing) - } - ForEach(candidates) { c in - VStack(spacing: 0) { - Button { - withAnimation { expandedId = expandedId == c.id ? nil : c.id } - } label: { - HStack(spacing: 12) { - if let conf = c.confidence { - Text("\(Int(conf * 100))%").font(.system(size: 10, weight: .bold)) - .foregroundColor(conf > 0.7 ? Color.zxGreen : Color.zxAmber) - .padding(.horizontal, 6).padding(.vertical, 2) - .background((conf > 0.7 ? Color.zxGreen : Color.zxAmber).opacity(0.12)) - .clipShape(Capsule()) - } - VStack(alignment: .leading, spacing: 4) { - Text(c.title ?? "无标题").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF0).lineLimit(1) - if let content = c.content { - Text(content).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(2) - } - } - Spacer() - Image(systemName: expandedId == c.id ? "chevron.up" : "chevron.down").font(.system(size: 12)).foregroundColor(Color.zxF03) - } - .padding(12).background(Color.zxFill003).clipShape(RoundedRectangle(cornerRadius: 14)) - } - .foregroundColor(.primary) - - if expandedId == c.id { - HStack(spacing: 8) { - Button { - Task { await rejectCandidate(c) } - } label: { - Label("拒绝", systemImage: "xmark").font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.zxCoral).frame(maxWidth: .infinity).frame(height: 40) - - - } - Button { - Task { await acceptCandidate(c) } - } label: { - Label("接受", systemImage: "checkmark").font(.system(size: 14, weight: .medium)) - .foregroundColor(Color.zxGreen).frame(maxWidth: .infinity).frame(height: 40) - - - } - } - .padding(.horizontal, 12).padding(.top, 8) - } - } - } - } - .padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100) - } - .scrollIndicators(.hidden) - } - }} - .navigationTitle("候选审批").navigationBarTitleDisplayMode(.inline).toolbarBackground(.hidden, for: .navigationBar).toolbar(.hidden, for: .tabBar) - .disabled(isProcessing) - .task { await load() } - } - - private func load() async { - isLoading = true; errorMessage = nil - do { candidates = try await ImportCandidateService.shared.listBySource(sourceId: sourceId) } catch { errorMessage = error.localizedDescription } - isLoading = false - } - - private func acceptCandidate(_ c: ImportCandidate) async { - isProcessing = true - do { - let _ = try await ImportCandidateService.shared.accept(id: c.id) - candidates.removeAll { $0.id == c.id } - ZXToastManager.shared.success("已接受") - } catch { ZXToastManager.shared.error("操作失败") } - isProcessing = false - } - - private func rejectCandidate(_ c: ImportCandidate) async { - isProcessing = true - do { - let _ = try await ImportCandidateService.shared.reject(id: c.id) - candidates.removeAll { $0.id == c.id } - ZXToastManager.shared.success("已拒绝") - } catch { ZXToastManager.shared.error("操作失败") } - isProcessing = false - } - - private func batchAccept() async { - isProcessing = true - do { - let _ = try await ImportCandidateService.shared.batchAccept(sourceId: sourceId) - candidates.removeAll() - ZXToastManager.shared.success("全部接受") - } catch { ZXToastManager.shared.error("操作失败") } - isProcessing = false - } -} - struct EditKnowledgePage: View { let item: KnowledgeItem @State private var title: String; @State private var content: String diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialDetailView.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialDetailView.swift index 485edaa..cbee544 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialDetailView.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialDetailView.swift @@ -91,8 +91,8 @@ final class MaterialDetailViewModel: ObservableObject { quizCount = quizzesR?.count ?? 0 if let p = progR { readingStatus = p.status - readingSeconds = p.totalActiveSeconds - isMarkedRead = p.isMarkedRead + readingSeconds = p.totalActiveSeconds ?? 0 + isMarkedRead = p.isMarkedRead ?? false } aiStatus = itemCount > 0 ? .ready : .processing @@ -104,23 +104,28 @@ final class MaterialDetailViewModel: ObservableObject { struct MaterialDetailView: View { @StateObject private var vm: MaterialDetailViewModel + @State private var isGeneratingQuiz = false + @State private var generatedQuizId: String? + @State private var showGeneratedQuiz = false let knowledgeBaseId: String + let sourceId: String let fileName: String let fileType: MaterialType let fileSize: UInt64 let filePath: String let uploadDate: String? - init(knowledgeBaseId: String, fileName: String, fileType: MaterialType, + init(knowledgeBaseId: String, sourceId: String, fileName: String, fileType: MaterialType, fileSize: UInt64, filePath: String, uploadDate: String? = nil) { self.knowledgeBaseId = knowledgeBaseId + self.sourceId = sourceId self.fileName = fileName self.fileType = fileType self.fileSize = fileSize self.filePath = filePath self.uploadDate = uploadDate - _vm = StateObject(wrappedValue: MaterialDetailViewModel(knowledgeBaseId: knowledgeBaseId)) + _vm = StateObject(wrappedValue: MaterialDetailViewModel(knowledgeBaseId: knowledgeBaseId, materialId: sourceId)) } var body: some View { @@ -146,6 +151,13 @@ struct MaterialDetailView: View { .toolbar(.hidden, for: .tabBar) .toolbarBackground(.hidden, for: .navigationBar) .task { await vm.load() } + .navigationDestination(isPresented: $showGeneratedQuiz) { + if let quizId = generatedQuizId { + QuizTakerView(quizId: quizId) + } else { + EmptyView() + } + } } // MARK: - File section @@ -300,6 +312,27 @@ struct MaterialDetailView: View { statBox(count: vm.quizCount, label: "测验", icon: "list.bullet.clipboard", color: Color.zxAccent) } + Button { + Task { await generateSourceQuiz() } + } label: { + HStack(spacing: 10) { + if isGeneratingQuiz { + ProgressView().tint(.white) + } else { + Image(systemName: "sparkles.rectangle.stack") + .font(.system(size: 14, weight: .semibold)) + } + Text("基于当前资料生成测验") + .font(.system(size: 14, weight: .semibold)) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 46) + .background(ZXGradient.brand) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + .disabled(isGeneratingQuiz) + if vm.aiStatus == .processing { Text("知习正在整理知识点,完成后可开始学习和自测") .font(.system(size: 12)).foregroundColor(Color.zxF03) @@ -322,6 +355,24 @@ struct MaterialDetailView: View { .background(Color.zxSurface).clipShape(RoundedRectangle(cornerRadius: 14)) .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.zxBorder008, lineWidth: 1)) } + + private func generateSourceQuiz() async { + isGeneratingQuiz = true + defer { isGeneratingQuiz = false } + + do { + let quiz = try await QuizService.shared.generate( + knowledgeBaseId: knowledgeBaseId, + sourceType: "source", + sourceId: sourceId, + questionCount: 5 + ) + generatedQuizId = quiz.id + showGeneratedQuiz = true + } catch { + ZXToastManager.shared.error("生成失败") + } + } } // MARK: - File size formatter diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift index 639f8e0..3849317 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/MaterialReaderView.swift @@ -94,28 +94,40 @@ final class MaterialReaderViewModel: ObservableObject { // MARK: - Main View struct MaterialReaderView: View { + private enum BootstrapState: Equatable { + case idle + case bootstrapping + case ready + case failed(String) + } + @StateObject private var vm: MaterialReaderViewModel @Binding var showQuickLook: Bool @Binding var showNoteSheet: Bool @State private var scrollProgress: CGFloat = 0 - @State private var hasRestoredPosition = false - @State private var restoreBlockId: String? + @State private var bootstrapState: BootstrapState = .idle + @State private var pendingRestoreBlockId: String? + @State private var isInitialRestoreComplete = false + @State private var isProgrammaticScrollInFlight = false @State private var actualContentHeight: CGFloat = 1 @State private var isMarkedRead = false @State private var readingStatus: String? @State private var readingProgressSeconds: Int = 0 @State private var apiRestorePosition: ReadingPosition? + @State private var localRestoreCheckpoint: ReadingCheckpoint? @State private var flushTimer: Timer? @Environment(\.scenePhase) private var scenePhase @State private var isParsing = false @State private var parseMessage = "" + @State private var showParseOverlay = false + @State private var latestKnowledgeTask: KnowledgeGenerationTaskStatus? + @State private var knowledgeTaskPollingTask: Task? private let title: String private let knowledgeBaseId: String? - // V2 session manager (primary) + V1 collector (fallback) + // V2 session manager private let sessionManager = ReadingRuntimeSessionManager.shared - private let collector = ReadingEventCollector.shared private let positionStore = ReadingPositionStore.shared private let readingAPI = ReadingAPIService.shared @@ -124,13 +136,16 @@ struct MaterialReaderView: View { sessionManager.state == .active || sessionManager.state == .paused } - init(materialId: String, filePath: String, materialType: MaterialType, knowledgeBaseId: String? = nil, title: String = "", showQuickLook: Binding = .constant(false), showNoteSheet: Binding = .constant(false)) { + init(materialId: String, filePath: String, materialType: MaterialType, knowledgeBaseId: String? = nil, title: String = "", resumePosition: ReadingPositionValue? = nil, showQuickLook: Binding = .constant(false), showNoteSheet: Binding = .constant(false)) { self.title = title self.knowledgeBaseId = knowledgeBaseId self._showQuickLook = showQuickLook self._showNoteSheet = showNoteSheet _vm = StateObject(wrappedValue: MaterialReaderViewModel( materialId: materialId, filePath: filePath, materialType: materialType)) + if let rp = resumePosition { + _apiRestorePosition = State(initialValue: ReadingPositionAdapter.fromValue(rp)) + } } var body: some View { @@ -147,7 +162,7 @@ struct MaterialReaderView: View { VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle").font(.system(size: 40)).foregroundColor(Color.zxCoral) Text(msg).font(.system(size: 14)).foregroundColor(Color.zxF04) - Button("重试") { Task { await vm.load() } } + Button("重试") { Task { await bootstrapReader(forceReload: true) } } .font(.system(size: 14, weight: .medium)).foregroundColor(Color.zxPrimary) .padding(.horizontal, 24).padding(.vertical, 10) .background(Color.zxPrimarySoft).clipShape(RoundedRectangle(cornerRadius: 10)) @@ -171,56 +186,41 @@ struct MaterialReaderView: View { .toolbarBackground(.hidden, for: .navigationBar) .toolbar { ToolbarItem(placement: .topBarTrailing) { - HStack(spacing: 16) { - Button { - markAsRead() - } label: { + HStack(spacing: 4) { + Button { markAsRead() } label: { Image(systemName: isMarkedRead ? "checkmark.circle.fill" : "checkmark.circle") .font(.system(size: 16)) .foregroundColor(isMarkedRead ? Color.green : Color.zxF05) } - NavigationLink(value: Route.aiChat(context: ChatEntryContext( - scopeType: .material, - scopeId: vm.materialId, - scopeName: title, - parentKnowledgeBaseId: knowledgeBaseId, - createdFrom: "material_reader" - ))) { - Image(systemName: "bubble.left.and.bubble.right") - .font(.system(size: 16)) - .foregroundColor(Color.zxF05) - } - if let kbId = knowledgeBaseId { - NavigationLink(value: Route.materialDetail( - knowledgeBaseId: kbId, - fileName: title.isEmpty ? "资料" : title, - fileType: vm.materialType, - fileSize: 0, - filePath: vm.filePath, - uploadDate: nil - )) { - Image(systemName: "info.circle") - .font(.system(size: 16)) - .foregroundColor(Color.zxF05) - } - - NavigationLink(value: Route.learningSession( - taskTitle: title.isEmpty ? "学习资料" : title, - taskType: "study", - taskColorHex: "#3D7FFB" - )) { - Image(systemName: "arrow.triangle.2.circlepath") - .font(.system(size: 16)) - .foregroundColor(Color.zxF05) - } - - Button { - Task { await triggerParse(kbId: kbId, sourceId: vm.materialId) } + Menu { + NavigationLink(value: Route.aiChat(context: ChatEntryContext( + scopeType: .material, scopeId: vm.materialId, scopeName: title, + parentKnowledgeBaseId: knowledgeBaseId, createdFrom: "material_reader" + ))) { + Label("AI 对话", systemImage: "bubble.left.and.bubble.right") + } + NavigationLink(value: Route.materialDetail( + knowledgeBaseId: kbId, sourceId: vm.materialId, fileName: title.isEmpty ? "资料" : title, + fileType: vm.materialType, fileSize: 0, filePath: vm.filePath, uploadDate: nil + )) { + Label("资料详情", systemImage: "info.circle") + } + NavigationLink(value: Route.learningSession( + taskTitle: title.isEmpty ? "学习资料" : title, taskType: "study", taskColorHex: "#3D7FFB" + )) { + Label("学习会话", systemImage: "arrow.triangle.2.circlepath") + } + Button { + Task { await generateKnowledgePoints(kbId: kbId, sourceId: vm.materialId) } + } label: { + Label(generateKnowledgeMenuTitle, systemImage: "brain.head.profile") + } + .disabled(isKnowledgeTaskActive) } label: { - Image(systemName: "brain.head.profile") - .font(.system(size: 16)) + Image(systemName: "ellipsis.circle") + .font(.system(size: 20)) .foregroundColor(Color.zxF05) } } @@ -228,9 +228,9 @@ struct MaterialReaderView: View { } } .overlay(alignment: .bottom) { - if isParsing { + if showParseOverlay { HStack(spacing: 8) { - ProgressView().tint(.white) + if isParsing { ProgressView().tint(.white) } Text(parseMessage).font(.system(size: 13, weight: .medium)).foregroundColor(.white) } .padding(.horizontal, 16).padding(.vertical, 10) @@ -240,29 +240,11 @@ struct MaterialReaderView: View { } } .task { - if vm.filePath.isEmpty || !FileManager.default.fileExists(atPath: vm.filePath) { - if let kbId = knowledgeBaseId { - print("[MaterialReader] No local file, downloading from COS: \(vm.materialId)") - do { - let source = try await KnowledgeSourceService.shared.detail(kbId: kbId, id: vm.materialId) - if let fileId = source.fileId { - let downloadUrl = try await FileUploadService.shared.getDownloadUrl(fileId: fileId) - let url = URL(string: downloadUrl)! - let (data, _) = try await URLSession.shared.data(from: url) - let ext = source.originalFilename?.split(separator: ".").last.map(String.init) ?? "md" - let localURL = FileManager.default.temporaryDirectory - .appendingPathComponent("source_\(vm.materialId).\(ext)") - try data.write(to: localURL) - print("[MaterialReader] Downloaded to: \(localURL.path), size: \(data.count)") - vm.filePath = localURL.path - } - } catch { - print("[MaterialReader] COS download failed: \(error.localizedDescription)") - } - } - } - await vm.load() + await bootstrapReader() + await refreshKnowledgeTaskStatus() + restartKnowledgeTaskPollingIfNeeded() } + .onDisappear { stopKnowledgeTaskPolling() } .sheet(isPresented: $showQuickLook) { QuickLookPreview(url: URL(fileURLWithPath: vm.filePath)) } @@ -274,13 +256,13 @@ struct MaterialReaderView: View { ) } .onAppear { - openReadingSession() - hasRestoredPosition = false // 定时发送阅读事件(每 2 分钟),防止长时间阅读不退出导致数据丢失 - flushTimer = Timer.scheduledTimer(withTimeInterval: 120, repeats: true) { _ in - Task { - ReadingEventUploadPipeline.shared.exportAndEnqueue() - await ReadingEventUploadPipeline.shared.flush() + if flushTimer == nil { + flushTimer = Timer.scheduledTimer(withTimeInterval: 120, repeats: true) { _ in + Task { + await ReadingEventUploadPipeline.shared.exportAndEnqueue() + await ReadingEventUploadPipeline.shared.flush() + } } } } @@ -292,7 +274,7 @@ struct MaterialReaderView: View { // 2. 导出所有事件(含 material_closed)+ 发送到后端 // 3. 发送完成后再清理 context Task { - ReadingEventUploadPipeline.shared.exportAndEnqueue() + await ReadingEventUploadPipeline.shared.exportAndEnqueue() await ReadingEventUploadPipeline.shared.flush() sessionManager.cleanup() } @@ -306,7 +288,7 @@ struct MaterialReaderView: View { } // Flush: export → enqueue Task { - ReadingEventUploadPipeline.shared.exportAndEnqueue() + await ReadingEventUploadPipeline.shared.exportAndEnqueue() await ReadingEventUploadPipeline.shared.flush() } case .active: @@ -320,10 +302,12 @@ struct MaterialReaderView: View { break } } - .onChange(of: vm.loadingState) { _, newState in - if newState == .loaded, !hasRestoredPosition { - restorePosition() - hasRestoredPosition = true + .onChange(of: apiRestorePosition) { _, newPos in + if newPos != nil, + vm.loadingState == .loaded, + !isInitialRestoreComplete, + pendingRestoreBlockId == nil { + restoreInitialPositionIfNeeded() } } } @@ -393,13 +377,12 @@ struct MaterialReaderView: View { scrollProgress = min(1, max(0, -offset / max(actualContentHeight, 1))) reportScrollPosition() } - .onChange(of: restoreBlockId) { _, target in - if let id = target { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - withAnimation { scrollProxy.scrollTo(id, anchor: .top) } - } - restoreBlockId = nil - } + .onAppear { + consumePendingRestore(using: scrollProxy) + } + .onChange(of: pendingRestoreBlockId) { _, target in + guard target != nil else { return } + consumePendingRestore(using: scrollProxy) } .scrollIndicators(.hidden) } @@ -408,65 +391,68 @@ struct MaterialReaderView: View { // MARK: - Reading Session Lifecycle private func openReadingSession() { - // Prevent duplicate open - guard !isV2Active else { - print("[READER] V2 session already active, skipping open") - return - } - - let targetType = knowledgeBaseId != nil ? "knowledge_source" : "temporary_file" + let fileName = URL(fileURLWithPath: vm.filePath).lastPathComponent let context = ReadingMaterialContext( readingTargetType: knowledgeBaseId != nil ? .knowledgeSource : .temporaryFile, materialId: vm.materialId, knowledgeBaseId: knowledgeBaseId, - title: title.isEmpty ? nil : title + title: title.isEmpty ? nil : title, + materialType: vm.materialType, + filePath: vm.filePath, + fileName: fileName, + fileType: vm.materialType.fileTypeString ) - // V2 primary - if let _ = try? sessionManager.openMaterial(context) { + // V2 session + do { + try sessionManager.openMaterial(context) print("[READER] V2 session opened — materialId=\(vm.materialId)") - } else { - print("[READER] V2 unavailable, falling back to V1") - collector.open(materialId: vm.materialId) - } - - // Query reading progress from API - Task { - do { - let progress = try await readingAPI.getReadingProgress(materialId: vm.materialId, targetType: targetType) - await MainActor.run { - readingStatus = progress.status - readingProgressSeconds = progress.totalActiveSeconds - if progress.isMarkedRead { isMarkedRead = true; MarkedReadStore.shared.mark(vm.materialId) } - if let lp = progress.lastPosition { - apiRestorePosition = ReadingPositionAdapter.fromValue(lp) - } - } - } catch { - // Fallback to local cache when API unavailable - if MarkedReadStore.shared.contains(vm.materialId) { isMarkedRead = true } - apiRestorePosition = positionStore.load(materialId: vm.materialId) - print("[READER] Progress query failed, using local position: \(error)") - } + } catch { + print("[READER] V2 session open FAILED: \(error) — materialId=\(vm.materialId)") } } + private func bootstrapReader(forceReload: Bool = false) async { + if bootstrapState == .bootstrapping { + return + } + if bootstrapState == .ready && !forceReload { + return + } + + bootstrapState = .bootstrapping + pendingRestoreBlockId = nil + isProgrammaticScrollInFlight = false + isInitialRestoreComplete = false + + await ensureLocalFileIfNeeded() + await vm.load() + + guard vm.loadingState == .loaded else { + if case .error(let message) = vm.loadingState { + bootstrapState = .failed(message) + } + return + } + + openReadingSession() + await fetchReadingProgress() + restoreInitialPositionIfNeeded() + if pendingRestoreBlockId == nil { + isInitialRestoreComplete = true + } + bootstrapState = .ready + } + private func closeReadingSession() { - // V2 primary + // V2 session if sessionManager.state == .active || sessionManager.state == .paused { if let lastPos = sessionManager.lastPosition { positionStore.save(materialId: vm.materialId, position: lastPos) } sessionManager.closeMaterial() print("[READER] V2 session closed") - return } - // V1 fallback - if let lastPos = collector.lastPosition { - positionStore.save(materialId: vm.materialId, position: lastPos) - } - _ = collector.close(materialId: vm.materialId) - print("[READER] V1 session closed") } /// Mark the current material as read — optimistic update + event push. @@ -474,58 +460,165 @@ struct MaterialReaderView: View { guard !isMarkedRead else { return } isMarkedRead = true MarkedReadStore.shared.mark(vm.materialId) - if isV2Active { - sessionManager.markAsRead() - } else { - collector.markAsRead(materialId: vm.materialId) - } + sessionManager.markAsRead() } /// Build a NoteAnchor from the current scroll position (for quick note). private func buildAnchor() -> NoteAnchor? { - let pos = sessionManager.lastPosition ?? collector.lastPosition + let pos = sessionManager.lastPosition guard let pos else { return nil } return createNoteAnchor(materialId: vm.materialId, position: pos) } - /// Restore saved reading position on re-entry. - /// Priority: API lastPosition > local positionStore > session lastPosition - private func restorePosition() { - // If API already fetched a position, use it immediately - if let apiPos = apiRestorePosition { - applyPosition(apiPos) + private func ensureLocalFileIfNeeded() async { + guard vm.filePath.isEmpty || !FileManager.default.fileExists(atPath: vm.filePath) else { + return + } + guard let kbId = knowledgeBaseId else { return } - // Fallback: local store + print("[MaterialReader] No local file, downloading from COS: \(vm.materialId)") + do { + let source = try await KnowledgeSourceService.shared.detail(kbId: kbId, id: vm.materialId) + if let fileId = source.fileId { + let downloadUrl = try await FileUploadService.shared.getDownloadUrl(fileId: fileId) + let url = URL(string: downloadUrl)! + let (data, _) = try await URLSession.shared.data(from: url) + let ext = source.originalFilename?.split(separator: ".").last.map(String.init) ?? "md" + let localURL = FileManager.default.temporaryDirectory + .appendingPathComponent("source_\(vm.materialId).\(ext)") + try data.write(to: localURL) + print("[MaterialReader] Downloaded to: \(localURL.path), size: \(data.count)") + vm.filePath = localURL.path + } + } catch { + print("[MaterialReader] COS download failed: \(error.localizedDescription)") + } + } + + private func fetchReadingProgress() async { + let targetType = knowledgeBaseId != nil ? "knowledge_source" : "temporary_file" + + do { + let progress = try await readingAPI.getReadingProgress(materialId: vm.materialId, targetType: targetType) + readingStatus = progress.status + readingProgressSeconds = progress.totalActiveSeconds ?? 0 + if progress.isMarkedRead == true { + isMarkedRead = true + MarkedReadStore.shared.mark(vm.materialId) + } + if let lp = progress.lastPosition { + apiRestorePosition = ReadingPositionAdapter.fromValue(lp) + } + } catch { + if MarkedReadStore.shared.contains(vm.materialId) { + isMarkedRead = true + } + localRestoreCheckpoint = positionStore.loadCheckpoint(materialId: vm.materialId) + apiRestorePosition = localRestoreCheckpoint?.position ?? positionStore.load(materialId: vm.materialId) + print("[READER] Progress query failed, using local position: \(error)") + } + } + + /// Restore saved reading position on re-entry. + /// Same-device local checkpoints are preferred over API progress because + /// the home "continue learning" flow can reopen the reader before the + /// latest uploaded position has been processed server-side. + private func restoreInitialPositionIfNeeded() { + guard !isInitialRestoreComplete else { return } + + if let checkpoint = localRestoreCheckpoint, + sessionManager.isCompatibleRestoreCheckpoint(checkpoint) { + applyPosition(checkpoint.position) + return + } + + if let checkpoint = positionStore.loadCheckpoint(materialId: vm.materialId), + sessionManager.isCompatibleRestoreCheckpoint(checkpoint) { + localRestoreCheckpoint = checkpoint + applyPosition(checkpoint.position) + return + } + + if localRestoreCheckpoint != nil { + print("[READER] Skip incompatible local checkpoint — materialId=\(vm.materialId)") + localRestoreCheckpoint = nil + } + if let saved = positionStore.load(materialId: vm.materialId) { applyPosition(saved) return } - // Last resort: session/collector - if let pos = sessionManager.lastPosition ?? collector.lastPosition { - applyPosition(pos) + if let apiPos = apiRestorePosition { + applyPosition(apiPos) + return } + + if let pos = sessionManager.lastPosition { + applyPosition(pos) + return + } + + isInitialRestoreComplete = true } private func applyPosition(_ pos: ReadingPosition) { + if let restoreBlockId = ReadingPositionAdapter.blockIdForRestore(from: pos, in: vm.blocks) { + pendingRestoreBlockId = restoreBlockId + return + } + switch pos { - case .markdown(let blockId, _): - restoreBlockId = blockId - case .text(let lineNumber, _): - let idx = Int(lineNumber) - 1 - if idx >= 0, idx < vm.blocks.count { - restoreBlockId = ReadingPositionAdapter.blockId(from: vm.blocks[idx]) + case .block, .markdown, .text: + isInitialRestoreComplete = true + print("[READER] Restore target missing, skipped — materialId=\(vm.materialId)") + case .pdf, .pdfViewport, .image, .imageViewport, .workbook, .slide, .epub, .unknown: + isInitialRestoreComplete = true + } + } + + private func confirmRestoredPosition() { + guard let restoreBlockId = pendingRestoreBlockId else { return } + guard let restoredIndex = ReadingPositionAdapter.blockIndex(for: restoreBlockId, in: vm.blocks) else { return } + let restoredProgress: Float + if vm.blocks.count <= 1 { + restoredProgress = 0 + } else { + restoredProgress = Float(restoredIndex) / Float(vm.blocks.count - 1) + } + guard let position = ReadingPositionAdapter.fromBlockScroll( + materialType: vm.materialType, + blocks: vm.blocks, + scrollProgress: restoredProgress, + blockIndex: restoredIndex + ) else { return } + + if sessionManager.state == .active { + sessionManager.updatePosition(position) + } + } + + private func consumePendingRestore(using scrollProxy: ScrollViewProxy) { + guard let restoreBlockId = pendingRestoreBlockId else { return } + isProgrammaticScrollInFlight = true + DispatchQueue.main.async { + scrollProxy.scrollTo(restoreBlockId, anchor: .top) + DispatchQueue.main.async { + confirmRestoredPosition() + pendingRestoreBlockId = nil + isProgrammaticScrollInFlight = false + isInitialRestoreComplete = true } - case .pdf, .image, .epub, .unknown: - break } } /// Report current scroll position to the active session manager. private func reportScrollPosition() { guard !vm.blocks.isEmpty else { return } + guard isInitialRestoreComplete else { return } + guard !isProgrammaticScrollInFlight else { return } let idx = max(0, min(vm.blocks.count - 1, Int(scrollProgress * CGFloat(vm.blocks.count)))) @@ -541,9 +634,6 @@ struct MaterialReaderView: View { // V2 primary if sessionManager.state == .active { sessionManager.updatePosition(pos) - } else { - // V1 fallback - collector.updatePosition(materialId: vm.materialId, position: pos) } } @@ -612,19 +702,80 @@ struct MaterialReaderView: View { // MARK: - Parse Trigger - private func triggerParse(kbId: String, sourceId: String) async { - guard !isParsing else { return } + private func generateKnowledgePoints(kbId: String, sourceId: String) async { + guard !isParsing, !isKnowledgeTaskActive else { return } isParsing = true - parseMessage = "AI 正在整理知识点..." + showParseOverlay = true + parseMessage = "正在提交知识点生成任务..." do { let result = try await KnowledgeSourceService.shared.generateKnowledge(kbId: kbId, sourceId: sourceId) - parseMessage = result.count > 0 - ? "整理完成,生成了 \(result.count) 个知识点" - : "未识别到可提取的知识点" + parseMessage = result.message + latestKnowledgeTask = result.asTaskStatus + ZXToastManager.shared.success(result.message) + restartKnowledgeTaskPollingIfNeeded() } catch { - parseMessage = "整理失败: \(error.localizedDescription)" + parseMessage = "生成失败: \(error.localizedDescription)" + ZXToastManager.shared.error(parseMessage) } isParsing = false + try? await Task.sleep(nanoseconds: 1_500_000_000) + if !isParsing { showParseOverlay = false } + } + + private var generateKnowledgeMenuTitle: String { + switch latestKnowledgeTask?.status { + case "waiting_for_parse": + return "等待解析中" + case "queued": + return "已入队" + case "running": + return "生成中" + case "completed": + return "重新生成知识点" + case "failed": + return "重新生成知识点" + default: + return "生成知识点" + } + } + + private var isKnowledgeTaskActive: Bool { + guard let status = latestKnowledgeTask?.status else { return false } + return ["waiting_for_parse", "queued", "running"].contains(status) + } + + private func refreshKnowledgeTaskStatus() async { + guard let kbId = knowledgeBaseId else { return } + do { + latestKnowledgeTask = try await KnowledgeSourceService.shared.latestKnowledgeGenerationTask( + kbId: kbId, + sourceId: vm.materialId + ) + if let latestKnowledgeTask { + parseMessage = latestKnowledgeTask.message + } + } catch { + print("[MaterialReader] refreshKnowledgeTaskStatus ERROR: \(error.localizedDescription)") + } + } + + private func restartKnowledgeTaskPollingIfNeeded() { + stopKnowledgeTaskPolling() + guard isKnowledgeTaskActive else { return } + knowledgeTaskPollingTask = Task { + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 3_000_000_000) + if Task.isCancelled { break } + await refreshKnowledgeTaskStatus() + if !isKnowledgeTaskActive { break } + } + knowledgeTaskPollingTask = nil + } + } + + private func stopKnowledgeTaskPolling() { + knowledgeTaskPollingTask?.cancel() + knowledgeTaskPollingTask = nil } // MARK: - Unsupported diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionAdapter.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionAdapter.swift index c6ecb63..2d584b0 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionAdapter.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionAdapter.swift @@ -24,10 +24,21 @@ struct ReadingPositionAdapter { switch materialType { case .markdown: guard let bid = blockId else { return nil } - return .markdown(blockId: bid, scrollProgress: clamp(scrollProgress ?? 0)) + return .block( + blockId: bid, + blockProgress: clamp(scrollProgress ?? 0), + overallProgress: clamp(overallProgress ?? scrollProgress ?? 0) + ) case .text: - return .text(lineNumber: lineNumber ?? 1, scrollProgress: clamp(scrollProgress ?? 0)) + guard let bid = blockId else { + return .text(lineNumber: lineNumber ?? 1, scrollProgress: clamp(scrollProgress ?? 0)) + } + return .block( + blockId: bid, + blockProgress: clamp(scrollProgress ?? 0), + overallProgress: clamp(overallProgress ?? scrollProgress ?? 0) + ) case .pdf: return .pdf( @@ -77,6 +88,13 @@ struct ReadingPositionAdapter { /// Convert an API `ReadingPositionValue` DTO into a `ReadingPosition` enum. static func fromValue(_ v: ReadingPositionValue) -> ReadingPosition? { switch v.type { + case "Block": + guard let bid = v.blockId else { return nil } + return .block( + blockId: bid, + blockProgress: clamp(v.blockProgress ?? 0), + overallProgress: clamp(v.overallProgress ?? 0) + ) case "Markdown": guard let bid = v.blockId else { return nil } return .markdown(blockId: bid, scrollProgress: clamp(v.scrollProgress ?? 0)) @@ -94,6 +112,50 @@ struct ReadingPositionAdapter { } } + static func checkpointPosition(from position: ReadingPosition) -> ReadingPosition { + switch position { + case .markdown(let blockId, let scrollProgress): + return .block( + blockId: blockId, + blockProgress: clamp(scrollProgress), + overallProgress: clamp(scrollProgress) + ) + default: + return position + } + } + + static func blockIndex(for blockId: String, in blocks: [DocumentBlock]) -> Int? { + blocks.firstIndex { candidate in + self.blockId(from: candidate) == blockId + } + } + + static func blockIdForRestore(from position: ReadingPosition, in blocks: [DocumentBlock]) -> String? { + guard !blocks.isEmpty else { return nil } + + switch position { + case .block(let blockId, _, let overallProgress): + if blockIndex(for: blockId, in: blocks) != nil { + return blockId + } + return fallbackBlockId(for: overallProgress, in: blocks) + case .markdown(let blockId, let scrollProgress): + if blockIndex(for: blockId, in: blocks) != nil { + return blockId + } + return fallbackBlockId(for: scrollProgress, in: blocks) + case .text(let lineNumber, let scrollProgress): + let lineIndex = Int(lineNumber) - 1 + if lineIndex >= 0, lineIndex < blocks.count { + return blockId(from: blocks[lineIndex]) + } + return fallbackBlockId(for: scrollProgress, in: blocks) + default: + return nil + } + } + // MARK: - Helpers private static func clamp(_ value: Float) -> Float { @@ -102,6 +164,14 @@ struct ReadingPositionAdapter { return value } + private static func fallbackBlockId(for progress: Float, in blocks: [DocumentBlock]) -> String? { + guard !blocks.isEmpty else { return nil } + let normalized = clamp(progress) + let scaled = normalized * Float(max(blocks.count - 1, 0)) + let index = min(blocks.count - 1, max(0, Int(round(scaled)))) + return blockId(from: blocks[index]) + } + static func blockId(from block: DocumentBlock) -> String { switch block { case .heading(let id, _, _): return id diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionStore.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionStore.swift index a897570..ae5afda 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionStore.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingPositionStore.swift @@ -21,12 +21,24 @@ final class ReadingPositionStore { defaults.set(dict, forKey: key(materialId)) } + func save(materialId: String, checkpoint: ReadingCheckpoint) { + defaults.set(checkpointToDict(checkpoint), forKey: key(materialId)) + } + /// Load the last reading position, if any. func load(materialId: String) -> ReadingPosition? { guard let dict = defaults.dictionary(forKey: key(materialId)) else { return nil } + if let checkpoint = checkpointFromDict(dict) { + return checkpoint.position + } return positionFromDict(dict) } + func loadCheckpoint(materialId: String) -> ReadingCheckpoint? { + guard let dict = defaults.dictionary(forKey: key(materialId)) else { return nil } + return checkpointFromDict(dict) + } + /// Remove saved position (e.g. when material is deleted). func remove(materialId: String) { defaults.removeObject(forKey: key(materialId)) @@ -45,6 +57,8 @@ final class ReadingPositionStore { private func positionToDict(_ pos: ReadingPosition) -> [String: Any] { switch pos { + case .block(let blockId, let blockProgress, let overallProgress): + return ["t": "block", "b": blockId, "bp": blockProgress, "op": overallProgress] case .markdown(let blockId, let scrollProgress): return ["t": "markdown", "b": blockId, "s": scrollProgress] case .text(let lineNumber, let scrollProgress): @@ -53,6 +67,14 @@ final class ReadingPositionStore { return ["t": "pdf", "p": pageNumber, "pp": pageProgress, "op": overallProgress] case .image(let zoomScale, let offsetX, let offsetY): return ["t": "image", "z": zoomScale, "x": offsetX, "y": offsetY] + case .pdfViewport(let pageNumber, let anchorX, let anchorY, let zoomScale, let overallProgress): + return ["t": "pdf_viewport", "p": pageNumber, "ax": anchorX, "ay": anchorY, "z": zoomScale, "op": overallProgress] + case .imageViewport(let zoomScale, let anchorX, let anchorY): + return ["t": "image_viewport", "z": zoomScale, "ax": anchorX, "ay": anchorY] + case .workbook(let sheetId, let topRow, let leftColumn, let selectedCell, let zoomScale, let overallProgress): + return ["t": "workbook", "sid": sheetId, "tr": topRow, "lc": leftColumn, "sc": selectedCell as Any, "z": zoomScale, "op": overallProgress as Any] + case .slide(let slideIndex, let zoomScale, let anchorX, let anchorY, let overallProgress): + return ["t": "slide", "si": slideIndex, "z": zoomScale, "ax": anchorX, "ay": anchorY, "op": overallProgress] case .epub(let chapterId, let chapterProgress, let overallProgress): return ["t": "epub", "c": chapterId, "cp": chapterProgress, "op": overallProgress] case .unknown: @@ -63,6 +85,11 @@ final class ReadingPositionStore { private func positionFromDict(_ dict: [String: Any]) -> ReadingPosition? { guard let type = dict["t"] as? String else { return nil } switch type { + case "block": + guard let b = dict["b"] as? String else { return nil } + let bp = (dict["bp"] as? Float) ?? 0 + let op = (dict["op"] as? Float) ?? 0 + return .block(blockId: b, blockProgress: bp, overallProgress: op) case "markdown": guard let b = dict["b"] as? String else { return nil } let s = (dict["s"] as? Float) ?? 0 @@ -81,6 +108,33 @@ final class ReadingPositionStore { let x = (dict["x"] as? Float) ?? 0 let y = (dict["y"] as? Float) ?? 0 return .image(zoomScale: z, offsetX: x, offsetY: y) + case "pdf_viewport": + let p = (dict["p"] as? UInt32) ?? 1 + let ax = (dict["ax"] as? Float) ?? 0.5 + let ay = (dict["ay"] as? Float) ?? 0 + let z = (dict["z"] as? Float) ?? 1 + let op = (dict["op"] as? Float) ?? 0 + return .pdfViewport(pageNumber: p, anchorX: ax, anchorY: ay, zoomScale: z, overallProgress: op) + case "image_viewport": + let z = (dict["z"] as? Float) ?? 1 + let ax = (dict["ax"] as? Float) ?? 0.5 + let ay = (dict["ay"] as? Float) ?? 0.5 + return .imageViewport(zoomScale: z, anchorX: ax, anchorY: ay) + case "workbook": + guard let sid = dict["sid"] as? String else { return nil } + let tr = (dict["tr"] as? UInt32) ?? 0 + let lc = (dict["lc"] as? UInt32) ?? 0 + let sc = dict["sc"] as? String + let z = (dict["z"] as? Float) ?? 1 + let op = dict["op"] as? Float + return .workbook(sheetId: sid, topRow: tr, leftColumn: lc, selectedCell: sc, zoomScale: z, overallProgress: op) + case "slide": + let si = (dict["si"] as? UInt32) ?? 0 + let z = (dict["z"] as? Float) ?? 1 + let ax = (dict["ax"] as? Float) ?? 0.5 + let ay = (dict["ay"] as? Float) ?? 0.5 + let op = (dict["op"] as? Float) ?? 0 + return .slide(slideIndex: si, zoomScale: z, anchorX: ax, anchorY: ay, overallProgress: op) case "epub": guard let c = dict["c"] as? String else { return nil } let cp = (dict["cp"] as? Float) ?? 0 @@ -90,4 +144,70 @@ final class ReadingPositionStore { return .unknown } } + + private func checkpointToDict(_ checkpoint: ReadingCheckpoint) -> [String: Any] { + [ + "schemaVersion": checkpoint.schemaVersion, + "materialId": checkpoint.materialId, + "representationFingerprint": checkpoint.representationFingerprint, + "rendererKind": rendererKindRaw(checkpoint.rendererKind), + "capturedAtMs": checkpoint.capturedAtMs, + "deviceId": checkpoint.deviceId, + "deviceSequence": checkpoint.deviceSequence, + "position": positionToDict(checkpoint.position) + ] + } + + private func checkpointFromDict(_ dict: [String: Any]) -> ReadingCheckpoint? { + guard + let schemaVersion = dict["schemaVersion"] as? UInt32, + let materialId = dict["materialId"] as? String, + let representationFingerprint = dict["representationFingerprint"] as? String, + let rendererKindRaw = dict["rendererKind"] as? String, + let capturedAtMs = dict["capturedAtMs"] as? UInt64, + let deviceId = dict["deviceId"] as? String, + let deviceSequence = dict["deviceSequence"] as? UInt64, + let positionDict = dict["position"] as? [String: Any], + let position = positionFromDict(positionDict), + let rendererKind = rendererKind(from: rendererKindRaw) + else { + return nil + } + + return ReadingCheckpoint( + schemaVersion: schemaVersion, + materialId: materialId, + representationFingerprint: representationFingerprint, + rendererKind: rendererKind, + capturedAtMs: capturedAtMs, + deviceId: deviceId, + deviceSequence: deviceSequence, + position: position + ) + } + + private func rendererKindRaw(_ kind: RendererKind) -> String { + switch kind { + case .block: return "block" + case .pdf: return "pdf" + case .image: return "image" + case .workbook: return "workbook" + case .slide: return "slide" + case .epub: return "epub" + case .unknown: return "unknown" + } + } + + private func rendererKind(from raw: String) -> RendererKind? { + switch raw { + case "block": return .block + case "pdf": return .pdf + case "image": return .image + case "workbook": return .workbook + case "slide": return .slide + case "epub": return .epub + case "unknown": return .unknown + default: return nil + } + } } diff --git a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift index d18fefd..8ef88e6 100644 --- a/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift +++ b/AIStudyApp/AIStudyApp/Features/MaterialReader/ReadingRuntimeSessionManager.swift @@ -39,6 +39,7 @@ final class ReadingRuntimeSessionManager { private(set) var activeSessionId: String? private(set) var activeContext: ReadingMaterialContext? private(set) var lastPosition: ReadingPosition? + private(set) var lastCheckpoint: ReadingCheckpoint? // MARK: - Dependencies @@ -50,9 +51,16 @@ final class ReadingRuntimeSessionManager { private var positionDebounceTask: Task? private var lastHeartbeatAtMs: Int64 = 0 private var backgroundedAtMs: Int64 = 0 + private var activeRendererKind: RendererKind = .unknown + private var activeRepresentationFingerprint = "" + private let positionStore = ReadingPositionStore.shared + private let defaults = UserDefaults.standard /// Max allowed background time before session is considered stale (5 minutes). private let sessionResumeMaxAgeMs: Int64 = 300_000 + private let checkpointSchemaVersion: UInt32 = 2 + private let deviceIdKey = "reading_checkpoint.device_id" + private let deviceSequenceKey = "reading_checkpoint.device_sequence" private init() {} @@ -66,23 +74,41 @@ final class ReadingRuntimeSessionManager { // MARK: - Session Lifecycle /// Start a reading session for a material. - /// - Throws if a session is already active (must close first). + /// If a session is already active for the same material, reuses it. + /// If active for a different material, closes the old one first. func openMaterial(_ context: ReadingMaterialContext) throws { - guard state == .idle || state == .closed else { - throw SessionError.alreadyActive + if state == .active || state == .paused { + if activeContext?.materialId == context.materialId { + return // same material, reuse existing session + } + closeMaterial() // different material, close old session first } state = .opening - let material = ReadingMaterialRef(materialId: context.materialId) let now = nowMs() + // Build V2 material ref with full file metadata + let materialV2 = ReadingMaterialRefV2( + materialId: context.materialId, + filePath: context.filePath, + fileName: context.fileName, + fileType: context.fileType, + mimeType: context.mimeType, + fileSize: context.fileSize, + contentHash: nil, + createdAtMs: nil + ) + do { - let sessionId = try adapter.startSession(material: material, timestampMs: now) + activeRendererKind = rendererKind(for: context) + activeRepresentationFingerprint = try buildRepresentationFingerprint(for: context, rendererKind: activeRendererKind) + let sessionId = try adapter.startSession(material: materialV2, timestampMs: now) activeSessionId = sessionId activeContext = context state = .active ReadingContextRegistry.shared.register(context) lastPosition = nil + lastCheckpoint = nil lastHeartbeatAtMs = now // Push MaterialOpened @@ -113,10 +139,11 @@ final class ReadingRuntimeSessionManager { let now = nowMs() let sinceLastTick = lastHeartbeatAtMs > 0 ? UInt32((now - lastHeartbeatAtMs) / 1000) : 0 _ = try? adapter.pushClosed(sessionId: sessionId, materialId: ctx.materialId, delta: sinceLastTick, timestampMs: now) - _ = try? adapter.closeSession(sessionId) + _ = try? adapter.closeSession(sessionId, timestampMs: now) activeSessionId = nil lastPosition = nil + lastCheckpoint = nil state = .closed } @@ -158,15 +185,17 @@ final class ReadingRuntimeSessionManager { func updatePosition(_ position: ReadingPosition) { guard state == .active, let sessionId = activeSessionId, let ctx = activeContext else { return } lastPosition = position + lastCheckpoint = makeCheckpoint(position: position, context: ctx) positionDebounceTask?.cancel() positionDebounceTask = Task { [weak self] in try? await Task.sleep(nanoseconds: 2_000_000_000) - guard let self, !Task.isCancelled, let pos = self.lastPosition, + guard let self, !Task.isCancelled, let checkpoint = self.lastCheckpoint, let sid = self.activeSessionId, let c = self.activeContext else { return } - _ = try? self.adapter.pushPositionChanged( + self.positionStore.save(materialId: c.materialId, checkpoint: checkpoint) + _ = try? self.adapter.pushCheckpointChanged( sessionId: sid, materialId: c.materialId, - position: pos, timestampMs: self.nowMs() + checkpoint: checkpoint, timestampMs: self.nowMs() ) } } @@ -176,6 +205,12 @@ final class ReadingRuntimeSessionManager { _ = try? adapter.pushMarkedAsRead(sessionId: sessionId, materialId: ctx.materialId, timestampMs: nowMs()) } + func isCompatibleRestoreCheckpoint(_ checkpoint: ReadingCheckpoint) -> Bool { + checkpoint.materialId == activeContext?.materialId && + checkpoint.rendererKind == activeRendererKind && + checkpoint.representationFingerprint == activeRepresentationFingerprint + } + // MARK: - Heartbeat private func startHeartbeat(interval: TimeInterval = 15) { @@ -187,9 +222,9 @@ final class ReadingRuntimeSessionManager { let sid = self.activeSessionId, let ctx = self.activeContext else { return } let now = self.nowMs() - _ = try? self.adapter.pushHeartbeat( + _ = try? self.adapter.pushHeartbeatCheckpoint( sessionId: sid, materialId: ctx.materialId, - delta: UInt32(interval), position: self.lastPosition, + delta: UInt32(interval), checkpoint: self.lastCheckpoint, timestampMs: now ) self.lastHeartbeatAtMs = now @@ -205,10 +240,11 @@ final class ReadingRuntimeSessionManager { private func flushPosition() { positionDebounceTask?.cancel() // Force-send last position before clearing - if let pos = lastPosition, let sid = activeSessionId, let ctx = activeContext { - _ = try? adapter.pushPositionChanged( + if let checkpoint = lastCheckpoint, let sid = activeSessionId, let ctx = activeContext { + positionStore.save(materialId: ctx.materialId, checkpoint: checkpoint) + _ = try? adapter.pushCheckpointChanged( sessionId: sid, materialId: ctx.materialId, - position: pos, timestampMs: nowMs() + checkpoint: checkpoint, timestampMs: nowMs() ) } positionDebounceTask = nil @@ -219,6 +255,88 @@ final class ReadingRuntimeSessionManager { private func nowMs() -> Int64 { Int64(Date().timeIntervalSince1970 * 1000) } + + private func makeCheckpoint(position: ReadingPosition, context: ReadingMaterialContext) -> ReadingCheckpoint { + let normalizedPosition = ReadingPositionAdapter.checkpointPosition(from: position) + let capturedAtMs = UInt64(max(0, nowMs())) + let sequence = nextDeviceSequence() + return ReadingCheckpoint( + schemaVersion: checkpointSchemaVersion, + materialId: context.materialId, + representationFingerprint: activeRepresentationFingerprint, + rendererKind: activeRendererKind, + capturedAtMs: capturedAtMs, + deviceId: deviceId(), + deviceSequence: sequence, + position: normalizedPosition + ) + } + + private func buildRepresentationFingerprint(for context: ReadingMaterialContext, rendererKind: RendererKind) throws -> String { + guard !context.filePath.isEmpty else { + return "\(context.materialId):\(rendererKind)" + } + let sourceFingerprint = try adapter.runtimeComputeFileFingerprint(filePath: context.filePath) + return adapter.runtimeBuildRepresentationFingerprint( + sourceFingerprint: sourceFingerprint, + rendererKind: rendererKind, + parserVersion: parserVersion(for: rendererKind), + schemaVersion: checkpointSchemaVersion + ) + } + + private func rendererKind(for context: ReadingMaterialContext) -> RendererKind { + switch context.fileType.lowercased() { + case "markdown", "text", "word": + return .block + case "pdf": + return .pdf + case "image": + return .image + case "excel": + return .workbook + case "powerpoint": + return .slide + case "epub": + return .epub + default: + return .unknown + } + } + + private func parserVersion(for rendererKind: RendererKind) -> String { + switch rendererKind { + case .block: + return "block-parser-v1" + case .pdf: + return "pdf-locator-v1" + case .image: + return "image-locator-v1" + case .workbook: + return "workbook-parser-v1" + case .slide: + return "slide-parser-v1" + case .epub: + return "epub-locator-v1" + case .unknown: + return "unknown-v1" + } + } + + private func deviceId() -> String { + if let existing = defaults.string(forKey: deviceIdKey) { + return existing + } + let generated = UUID().uuidString + defaults.set(generated, forKey: deviceIdKey) + return generated + } + + private func nextDeviceSequence() -> UInt64 { + let next = UInt64(defaults.integer(forKey: deviceSequenceKey)) + 1 + defaults.set(Int(next), forKey: deviceSequenceKey) + return next + } } // MARK: - Errors diff --git a/AIStudyApp/AIStudyApp/Features/Quiz/QuizViews.swift b/AIStudyApp/AIStudyApp/Features/Quiz/QuizViews.swift index de126c1..9822dad 100644 --- a/AIStudyApp/AIStudyApp/Features/Quiz/QuizViews.swift +++ b/AIStudyApp/AIStudyApp/Features/Quiz/QuizViews.swift @@ -18,7 +18,7 @@ struct QuizListView: View { VStack(spacing: 16) { Image("icon-question").font(.system(size: 40)).foregroundColor(Color.zxF03) Text("暂无测验").font(.system(size: 14, weight: .semibold)).foregroundColor(Color.zxF04) - Text("基于知识库内容自动生成测验题目").font(.system(size: 12)).foregroundColor(Color.zxF03) + Text("基于资料内容 AI 生成测验题目").font(.system(size: 12)).foregroundColor(Color.zxF03) Button { Task { await generateQuiz() } } label: { @@ -125,8 +125,8 @@ struct QuizTakerView: View { Text(question.stem ?? "").font(.system(size: 16, weight: .semibold)).foregroundColor(Color.zxF0).lineSpacing(4) // M-QUIZ-01-CLEANUP-02: 使用 QuizQuestionType 枚举替换裸字符串 - switch question.questionType { - case .choice: + switch question.questionType ?? "" { + case "choice": if let options = question.options { VStack(spacing: 8) { ForEach(Array(options.enumerated()), id: \.offset) { i, opt in @@ -142,7 +142,7 @@ struct QuizTakerView: View { } } } - case .judge: + case "judge": HStack(spacing: 12) { ForEach(["true", "false"], id: \.self) { v in Button { @@ -152,9 +152,9 @@ struct QuizTakerView: View { }.foregroundColor(.primary) } } - case .fill: + case "fill": TextField("输入你的答案", text: Binding(get: { answers[question.id] ?? "" }, set: { setAnswer(question.id, $0) })).font(.system(size: 14)).tint(Color.zxPurple).padding(.horizontal, 16).frame(height: 48).background(Color.zxFill004).clipShape(RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.zxBorder008, lineWidth: 1)) - case nil: + default: // M-QUIZ-01-CLEANUP-02: 未知题型 fail-closed VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle").font(.system(size: 24)).foregroundColor(Color.zxCoral) @@ -271,36 +271,8 @@ struct QuizResultView: View { VStack(alignment: .leading, spacing: 12) { Text("答题详情").font(.system(size: 16, weight: .bold)).foregroundColor(Color.zxF0) ForEach(r.answers ?? []) { a in - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 12) { - Image(systemName: a.isCorrect == true ? "checkmark.circle" : "xmark.circle").font(.system(size: 18)).foregroundColor(a.isCorrect == true ? Color.zxGreen : Color.zxCoral) - Text(a.question?.stem ?? "").font(.system(size: 14)).foregroundColor(Color.zxF0).lineLimit(3) - Spacer() - } - // M-QUIZ-01-03 / M-QUIZ-01-CLEANUP-02: 使用 QuizQuestionType 展示答案 - let qType = a.question?.questionType - let options = a.question?.options ?? [] - let displayUserAnswer = Self.answerDisplayText(raw: a.userAnswer ?? "", type: qType, options: options) - let displayCorrectAnswer = Self.answerDisplayText(raw: a.question?.answer ?? "", type: qType, options: options) - - HStack(spacing: 16) { - if !displayUserAnswer.isEmpty { - HStack(spacing: 4) { - Text("你的答案").font(.system(size: 11)).foregroundColor(Color.zxF04) - Text(displayUserAnswer).font(.system(size: 12, weight: .medium)).foregroundColor(a.isCorrect == true ? Color.zxGreen : Color.zxCoral) - } - } - if !displayCorrectAnswer.isEmpty { - HStack(spacing: 4) { - Text("正确答案").font(.system(size: 11)).foregroundColor(Color.zxF04) - Text(displayCorrectAnswer).font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxGreen) - } - } - } - if let exp = a.question?.explanation { - Text(exp).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(4) - } - }.padding(12).background(a.isCorrect == true ? Color.zxGreen.opacity(0.05) : Color.zxCoral.opacity(0.05)).clipShape(RoundedRectangle(cornerRadius: 12)) + AnswerDetailRow(a: a) + .padding(12).background(a.isCorrect == true ? Color.zxGreen.opacity(0.05) : Color.zxCoral.opacity(0.05)).clipShape(RoundedRectangle(cornerRadius: 12)) } } }.padding(.horizontal, 20).padding(.top, 8).padding(.bottom, 100) @@ -318,6 +290,47 @@ struct QuizResultView: View { } // M-QUIZ-01-CLEANUP-02: 使用 QuizQuestionType.displayAnswer 统一展示 + fileprivate struct AnswerDetailRow: View { + let a: QuizAttemptAnswer + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 12) { + let correct = a.isCorrect == true + let stem = a.question?.stem ?? "" + Image(systemName: correct ? "checkmark.circle" : "xmark.circle").font(.system(size: 18)).foregroundColor(correct ? Color.zxGreen : Color.zxCoral) + Text(stem).font(.system(size: 14)).foregroundColor(Color.zxF0).lineLimit(3) + Spacer() + } + let qType = a.question?.questionType + let options = a.question?.options ?? [] + let displayUserAnswer = Self.answerDisplayText(raw: a.userAnswer ?? "", type: qType, options: options) + let displayCorrectAnswer = Self.answerDisplayText(raw: a.question?.answer ?? "", type: qType, options: options) + HStack(spacing: 16) { + if !displayUserAnswer.isEmpty { + HStack(spacing: 4) { + Text("你的答案").font(.system(size: 11)).foregroundColor(Color.zxF04) + Text(displayUserAnswer).font(.system(size: 12, weight: .medium)).foregroundColor(a.isCorrect == true ? Color.zxGreen : Color.zxCoral) + } + } + if !displayCorrectAnswer.isEmpty { + HStack(spacing: 4) { + Text("正确答案").font(.system(size: 11)).foregroundColor(Color.zxF04) + Text(displayCorrectAnswer).font(.system(size: 12, weight: .medium)).foregroundColor(Color.zxGreen) + } + } + } + if let exp = a.question?.explanation { + Text(exp).font(.system(size: 12)).foregroundColor(Color.zxF04).lineLimit(4) + } + } + } + + static func answerDisplayText(raw: String, type: String?, options: [String]) -> String { + guard let t = type, let qType = QuizQuestionType(rawValue: t) else { return raw } + return qType.displayAnswer(raw: raw, options: options) + } + } + static func answerDisplayText(raw: String, type: QuizQuestionType?, options: [String]) -> String { guard let qType = type else { return raw } // 未知题型保留原始值 return qType.displayAnswer(raw: raw, options: options) diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift index 97217fe..6db27cd 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeView.swift @@ -18,6 +18,7 @@ struct StudyHomeView: View { } } .task { await vm.loadAll() } + .onAppear { Task { await vm.refresh() } } } // MARK: - Loading / Error @@ -121,14 +122,14 @@ struct StudyHomeView: View { @ViewBuilder private var mainActionCard: some View { switch vm.mainAction { - case .continueSession(let materialId, let filePath, let materialType, let kbTitle, let elapsed, _): + case .continueSession(let materialId, let filePath, let materialType, let kbTitle, let elapsed, let resumePosition): actionCard( title: "继续上次学习", subtitle: "《\(kbTitle)》· \(elapsed)", icon: "arrow.triangle.2.circlepath", color: Color.zxPurple, cta: "继续学习", - route: .materialReader(materialId: materialId, filePath: filePath, materialType: materialType, title: kbTitle) + route: .materialReader(materialId: materialId, filePath: filePath, materialType: materialType, title: kbTitle, resumePosition: resumePosition) ) case .todaysReview(let count, let minutes): actionCard( diff --git a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift index 3db2e0b..de04026 100644 --- a/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/Study/StudyHomeViewModel.swift @@ -43,11 +43,29 @@ extension ReadingAPIService: ContinueLearningServicing {} // MARK: - Action states enum MainAction: Equatable { - case continueSession(materialId: String, filePath: String, materialType: MaterialType, kbTitle: String, elapsed: String, resumePosition: ReadingPosition?) + case continueSession(materialId: String, filePath: String, materialType: MaterialType, kbTitle: String, elapsed: String, resumePosition: ReadingPositionValue?) case todaysReview(count: Int, estimatedMinutes: Int) case selfTest(quizId: String, count: Int) case startLearning(knowledgeBaseId: String, kbCount: Int) case empty + + static func == (lhs: MainAction, rhs: MainAction) -> Bool { + switch (lhs, rhs) { + case (.continueSession(let lmid, let lpath, let lmt, let lkb, let lel, let lpos), + .continueSession(let rmid, let rpath, let rmt, let rkb, let rel, let rpos)): + return lmid == rmid && lpath == rpath && lmt == rmt && lkb == rkb && lel == rel && lpos == rpos + case (.todaysReview(let lc, let lm), .todaysReview(let rc, let rm)): + return lc == rc && lm == rm + case (.selfTest(let lq, let lc), .selfTest(let rq, let rc)): + return lq == rq && lc == rc + case (.startLearning(let lkb, let lc), .startLearning(let rkb, let rc)): + return lkb == rkb && lc == rc + case (.empty, .empty): + return true + default: + return false + } + } } enum HomeLoadingState: Equatable { @@ -180,7 +198,7 @@ final class StudyHomeViewModel: ObservableObject { let resolved = MaterialPathResolver.resolve(materialId: materialId, apiType: cl.type, title: cl.title) { let kbTitle = cl.title ?? knowledgeBases.first?.title ?? "学习" let elapsed = formatElapsedSince(cl.lastReadAt) - return .continueSession(materialId: materialId, filePath: resolved.filePath, materialType: resolved.materialType, kbTitle: kbTitle, elapsed: elapsed, resumePosition: nil) + return .continueSession(materialId: materialId, filePath: resolved.filePath, materialType: resolved.materialType, kbTitle: kbTitle, elapsed: elapsed, resumePosition: cl.lastPosition) } if !dueCards.isEmpty { diff --git a/AIStudyApp/AIStudyAppReaderTests/ReadingPositionAdapterTests.swift b/AIStudyApp/AIStudyAppReaderTests/ReadingPositionAdapterTests.swift new file mode 100644 index 0000000..f47981f --- /dev/null +++ b/AIStudyApp/AIStudyAppReaderTests/ReadingPositionAdapterTests.swift @@ -0,0 +1,135 @@ +import XCTest +@testable import AIStudyApp + +final class ReadingPositionAdapterTests: XCTestCase { + + private func makeValue( + type: String?, + blockId: String? = nil, + blockProgress: Float? = nil, + lineNumber: UInt32? = nil, + pageNumber: UInt32? = nil, + scrollProgress: Float? = nil, + pageProgress: Float? = nil, + overallProgress: Float? = nil, + zoomScale: Float? = nil, + offsetX: Float? = nil, + offsetY: Float? = nil, + chapterId: String? = nil, + chapterProgress: Float? = nil + ) -> ReadingPositionValue { + ReadingPositionValue( + type: type, + pageNumber: pageNumber, + blockId: blockId, + blockProgress: blockProgress, + scrollProgress: scrollProgress, + lineNumber: lineNumber, + pageProgress: pageProgress, + overallProgress: overallProgress, + zoomScale: zoomScale, + offsetX: offsetX, + offsetY: offsetY, + chapterId: chapterId, + chapterProgress: chapterProgress, + offset: nil, + percent: nil, + section: nil + ) + } + + func test_fromValue_markdown_with_blockId() { + let v = makeValue(type: "Markdown", blockId: "b1", scrollProgress: 0.5) + let pos = ReadingPositionAdapter.fromValue(v) + guard case let .markdown(blockId, scrollProgress) = pos else { + return XCTFail("Expected .markdown, got \(String(describing: pos))") + } + XCTAssertEqual(blockId, "b1") + XCTAssertEqual(scrollProgress, 0.5) + } + + func test_fromValue_markdown_nil_blockId_returns_nil() { + let v = makeValue(type: "Markdown") + XCTAssertNil(ReadingPositionAdapter.fromValue(v)) + } + + func test_fromValue_text_with_optional_fields() { + let v = makeValue(type: "Text", lineNumber: 42, scrollProgress: 0.8) + guard case let .text(lineNumber, scrollProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .text") + } + XCTAssertEqual(lineNumber, 42) + XCTAssertEqual(scrollProgress, 0.8) + } + + func test_fromValue_pdf_with_all_fields() { + let v = makeValue(type: "Pdf", pageNumber: 5, pageProgress: 0.3, overallProgress: 0.15) + guard case let .pdf(pageNumber, pageProgress, overallProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .pdf") + } + XCTAssertEqual(pageNumber, 5) + XCTAssertEqual(pageProgress, 0.3) + XCTAssertEqual(overallProgress, 0.15) + } + + func test_fromBlockScroll_markdown_returns_position() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + ] + let pos = ReadingPositionAdapter.fromBlockScroll( + materialType: .markdown, blocks: blocks, scrollProgress: 0.5, blockIndex: 0 + ) + guard case .block(let blockId, let blockProgress, let overallProgress) = pos else { + return XCTFail("Expected .block") + } + XCTAssertEqual(blockId, "h1") + XCTAssertEqual(blockProgress, 0.5) + XCTAssertEqual(overallProgress, 0.5) + } + + func test_blockIdForRestore_prefers_existing_blockId() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + .paragraph(id: "p2", text: "Tail"), + ] + + let restored = ReadingPositionAdapter.blockIdForRestore( + from: .block(blockId: "p1", blockProgress: 0.25, overallProgress: 0.8), + in: blocks + ) + + XCTAssertEqual(restored, "p1") + } + + func test_blockIdForRestore_fallsBack_to_progress_when_blockId_missing() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + .paragraph(id: "p2", text: "Tail"), + ] + + let restored = ReadingPositionAdapter.blockIdForRestore( + from: .block(blockId: "missing", blockProgress: 0.1, overallProgress: 0.9), + in: blocks + ) + + XCTAssertEqual(restored, "p2") + } + + func test_blockIdForRestore_text_out_of_range_fallsBack_to_scroll_progress() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + .paragraph(id: "p2", text: "Tail"), + ] + + let restored = ReadingPositionAdapter.blockIdForRestore( + from: .text(lineNumber: 99, scrollProgress: 0.45), + in: blocks + ) + + XCTAssertEqual(restored, "p1") + } +} diff --git a/AIStudyApp/AIStudyAppReaderTests/SessionManagerTests.swift b/AIStudyApp/AIStudyAppReaderTests/SessionManagerTests.swift new file mode 100644 index 0000000..c9d4914 --- /dev/null +++ b/AIStudyApp/AIStudyAppReaderTests/SessionManagerTests.swift @@ -0,0 +1,189 @@ +import XCTest +@testable import AIStudyApp + +@MainActor +private final class SpyReadingRuntimeAdapter: ReadingRuntimeAdapter { + var fingerprint = "fingerprint-1" + private var nextSequence: UInt64 = 1 + + func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String { + "spy-session" + } + + func pauseSession(_ sessionId: String) throws {} + func resumeSession(_ sessionId: String) throws {} + func closeSession(_ sessionId: String, timestampMs: Int64) throws {} + + func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .materialOpened) + } + + func pushClosed(sessionId: String, materialId: String, delta: UInt32, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .materialClosed) + } + + func pushPositionChanged(sessionId: String, materialId: String, position: ReadingPosition, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .positionChanged, position: position) + } + + func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .positionChanged, checkpoint: checkpoint) + } + + func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .heartbeat, position: position) + } + + func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .heartbeat, checkpoint: checkpoint) + } + + func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .markedAsRead) + } + + func exportEvents(limit: UInt32, timestampMs: Int64) -> [ReadingEventV2] { [] } + func ackEvents(_ eventIds: [String]) -> UInt32 { UInt32(eventIds.count) } + func markFailed(_ eventIds: [String]) -> UInt32 { UInt32(eventIds.count) } + func reloadStaleEvents() -> UInt32 { 0 } + func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { 0 } + + func runtimeComputeFileFingerprint(filePath: String) throws -> String { + fingerprint + } + + func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String { + "\(sourceFingerprint)|\(rendererKind)|\(parserVersion)|\(schemaVersion)" + } + + private func makeEvent( + sessionId: String, + materialId: String, + eventType: ReadingEventTypeV2, + position: ReadingPosition? = nil, + checkpoint: ReadingCheckpoint? = nil + ) -> ReadingEventV2 { + defer { nextSequence += 1 } + return ReadingEventV2( + eventId: "evt-\(nextSequence)", + clientSessionId: sessionId, + materialId: materialId, + eventType: eventType, + position: position, + checkpoint: checkpoint, + activeSecondsDelta: 0, + timestampMs: 1_700_000_000_000, + sequence: nextSequence + ) + } +} + +@MainActor +final class SessionManagerTests: XCTestCase { + + var manager: ReadingRuntimeSessionManager! + var noopAdapter: NoopReadingRuntimeAdapter! + fileprivate var spyAdapter: SpyReadingRuntimeAdapter! + + override func setUp() { + super.setUp() + manager = ReadingRuntimeSessionManager.shared + noopAdapter = NoopReadingRuntimeAdapter() + spyAdapter = SpyReadingRuntimeAdapter() + manager.adapter = noopAdapter + manager.closeMaterial() + } + + override func tearDown() { + manager.closeMaterial() + super.tearDown() + } + + func test_initial_state_is_idle() { + XCTAssertTrue(manager.state == .idle || manager.state == .closed) + } + + func test_markAsRead_guards_against_inactive() { + manager.closeMaterial() + manager.markAsRead() + XCTAssertTrue(true) + } + + func test_isCompatibleRestoreCheckpoint_matches_active_representation() throws { + manager.adapter = spyAdapter + let context = ReadingMaterialContext( + readingTargetType: .knowledgeSource, + materialId: "restore-match", + materialType: .markdown, + filePath: "", + fileName: "doc.md", + fileType: "markdown" + ) + try manager.openMaterial(context) + + let checkpoint = ReadingCheckpoint( + schemaVersion: 2, + materialId: "restore-match", + representationFingerprint: "restore-match:block", + rendererKind: .block, + capturedAtMs: 1, + deviceId: "device-1", + deviceSequence: 1, + position: .block(blockId: "b1", blockProgress: 0.2, overallProgress: 0.2) + ) + + XCTAssertTrue(manager.isCompatibleRestoreCheckpoint(checkpoint)) + } + + func test_isCompatibleRestoreCheckpoint_rejects_mismatched_fingerprint() throws { + manager.adapter = spyAdapter + let context = ReadingMaterialContext( + readingTargetType: .knowledgeSource, + materialId: "restore-mismatch", + materialType: .markdown, + filePath: "", + fileName: "doc.md", + fileType: "markdown" + ) + try manager.openMaterial(context) + + let checkpoint = ReadingCheckpoint( + schemaVersion: 2, + materialId: "restore-mismatch", + representationFingerprint: "stale-fingerprint", + rendererKind: .block, + capturedAtMs: 1, + deviceId: "device-1", + deviceSequence: 1, + position: .block(blockId: "b1", blockProgress: 0.2, overallProgress: 0.2) + ) + + XCTAssertFalse(manager.isCompatibleRestoreCheckpoint(checkpoint)) + } + + func test_isCompatibleRestoreCheckpoint_rejects_mismatched_renderer_kind() throws { + manager.adapter = spyAdapter + let context = ReadingMaterialContext( + readingTargetType: .knowledgeSource, + materialId: "restore-renderer", + materialType: .markdown, + filePath: "", + fileName: "doc.md", + fileType: "markdown" + ) + try manager.openMaterial(context) + + let checkpoint = ReadingCheckpoint( + schemaVersion: 2, + materialId: "restore-renderer", + representationFingerprint: "restore-renderer:block", + rendererKind: .pdf, + capturedAtMs: 1, + deviceId: "device-1", + deviceSequence: 1, + position: .pdf(pageNumber: 1, pageProgress: 0, overallProgress: 0) + ) + + XCTAssertFalse(manager.isCompatibleRestoreCheckpoint(checkpoint)) + } +} diff --git a/AIStudyApp/AIStudyAppTests/PositionAdapterTests.swift b/AIStudyApp/AIStudyAppTests/PositionAdapterTests.swift index 88c6d65..9b77cc1 100644 --- a/AIStudyApp/AIStudyAppTests/PositionAdapterTests.swift +++ b/AIStudyApp/AIStudyAppTests/PositionAdapterTests.swift @@ -11,10 +11,11 @@ final class PositionAdapterTests: XCTestCase { blockId: "b1", scrollProgress: 0.5 ) - if case .markdown(_, let progress) = pos! { + if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 0.5) + XCTAssertEqual(overallProgress, 0.5) } else { - XCTFail("Expected markdown position") + XCTFail("Expected block position") } } @@ -24,10 +25,11 @@ final class PositionAdapterTests: XCTestCase { blockId: "b1", scrollProgress: -0.5 ) - if case .markdown(_, let progress) = pos! { + if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 0.0, "Negative progress should clamp to 0") + XCTAssertEqual(overallProgress, 0.0) } else { - XCTFail("Expected markdown position") + XCTFail("Expected block position") } } @@ -37,10 +39,11 @@ final class PositionAdapterTests: XCTestCase { blockId: "b1", scrollProgress: 1.5 ) - if case .markdown(_, let progress) = pos! { + if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 1.0, "Progress >1 should clamp to 1") + XCTAssertEqual(overallProgress, 1.0) } else { - XCTFail("Expected markdown position") + XCTFail("Expected block position") } } @@ -50,10 +53,11 @@ final class PositionAdapterTests: XCTestCase { blockId: "b1", scrollProgress: Float.nan ) - if case .markdown(_, let progress) = pos! { + if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 0.0, "NaN progress should clamp to 0") + XCTAssertEqual(overallProgress, 0.0) } else { - XCTFail("Expected markdown position") + XCTFail("Expected block position") } } @@ -78,8 +82,8 @@ final class PositionAdapterTests: XCTestCase { func test_text_position() { let pos = ReadingPositionAdapter.buildPosition( materialType: .text, - lineNumber: 42, - scrollProgress: 0.7 + scrollProgress: 0.7, + lineNumber: 42 ) if case .text(let line, let progress) = pos! { XCTAssertEqual(line, 42) @@ -108,9 +112,9 @@ final class PositionAdapterTests: XCTestCase { func test_epub_position() { let pos = ReadingPositionAdapter.buildPosition( materialType: .epub, - chapterId: "ch3", chapterProgress: 0.4, - overallProgress: 0.2 + overallProgress: 0.2, + chapterId: "ch3" ) if case .epub(let ch, let chProg, let overall) = pos! { XCTAssertEqual(ch, "ch3") diff --git a/AIStudyApp/AIStudyAppTests/QuizQuestionTypeTests.swift b/AIStudyApp/AIStudyAppTests/QuizQuestionTypeTests.swift index a765039..70bff80 100644 --- a/AIStudyApp/AIStudyAppTests/QuizQuestionTypeTests.swift +++ b/AIStudyApp/AIStudyAppTests/QuizQuestionTypeTests.swift @@ -80,18 +80,18 @@ final class QuizQuestionTypeTests: XCTestCase { // ═══════════════════════════════════════════════ func testQuizQuestionTypeProperty() { - let q = QuizQuestion(id: "1", quizId: nil, type: "choice", stem: nil, options: nil, answer: nil, explanation: nil, orderIndex: nil) - XCTAssertEqual(q.questionType, .choice) + let q = QuizQuestion(id: "1", quizId: nil, stem: nil, questionText: nil, questionType: "choice", options: nil, answer: nil, correctAnswer: nil, explanation: nil, orderIndex: nil) + XCTAssertEqual(q.questionTypeEnum, .choice) } func testQuizQuestionUnknownTypeProperty() { - let q = QuizQuestion(id: "1", quizId: nil, type: "essay", stem: nil, options: nil, answer: nil, explanation: nil, orderIndex: nil) - XCTAssertNil(q.questionType) + let q = QuizQuestion(id: "1", quizId: nil, stem: nil, questionText: nil, questionType: "essay", options: nil, answer: nil, correctAnswer: nil, explanation: nil, orderIndex: nil) + XCTAssertNil(q.questionTypeEnum) } func testQuizQuestionNilTypeProperty() { - let q = QuizQuestion(id: "1", quizId: nil, type: nil, stem: nil, options: nil, answer: nil, explanation: nil, orderIndex: nil) - XCTAssertNil(q.questionType) + let q = QuizQuestion(id: "1", quizId: nil, stem: nil, questionText: nil, questionType: nil, options: nil, answer: nil, correctAnswer: nil, explanation: nil, orderIndex: nil) + XCTAssertNil(q.questionTypeEnum) } // ═══════════════════════════════════════════════ diff --git a/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift b/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift index a328cc3..2989caf 100644 --- a/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift +++ b/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift @@ -239,4 +239,51 @@ final class ReadingPositionAdapterTests: XCTestCase { ) XCTAssertNil(pos) } + + // MARK: - Restore fallback + + func test_blockIdForRestore_prefers_existing_blockId() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + .paragraph(id: "p2", text: "Tail"), + ] + + let restored = ReadingPositionAdapter.blockIdForRestore( + from: .block(blockId: "p1", blockProgress: 0.25, overallProgress: 0.8), + in: blocks + ) + + XCTAssertEqual(restored, "p1") + } + + func test_blockIdForRestore_fallsBack_to_progress_when_blockId_missing() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + .paragraph(id: "p2", text: "Tail"), + ] + + let restored = ReadingPositionAdapter.blockIdForRestore( + from: .block(blockId: "missing", blockProgress: 0.1, overallProgress: 0.9), + in: blocks + ) + + XCTAssertEqual(restored, "p2") + } + + func test_blockIdForRestore_text_out_of_range_fallsBack_to_scroll_progress() { + let blocks: [DocumentBlock] = [ + .heading(id: "h1", level: 1, text: "Title"), + .paragraph(id: "p1", text: "Content"), + .paragraph(id: "p2", text: "Tail"), + ] + + let restored = ReadingPositionAdapter.blockIdForRestore( + from: .text(lineNumber: 99, scrollProgress: 0.45), + in: blocks + ) + + XCTAssertEqual(restored, "p1") + } } diff --git a/AIStudyApp/AIStudyAppTests/SessionManagerTests.swift b/AIStudyApp/AIStudyAppTests/SessionManagerTests.swift index 556c064..0136ccf 100644 --- a/AIStudyApp/AIStudyAppTests/SessionManagerTests.swift +++ b/AIStudyApp/AIStudyAppTests/SessionManagerTests.swift @@ -1,16 +1,95 @@ import XCTest @testable import AIStudyApp +@MainActor +private final class SpyReadingRuntimeAdapter: ReadingRuntimeAdapter { + var fingerprint = "fingerprint-1" + private var nextSequence: UInt64 = 1 + + func startSession(material: ReadingMaterialRefV2, timestampMs: Int64) throws -> String { + "spy-session" + } + + func pauseSession(_ sessionId: String) throws {} + func resumeSession(_ sessionId: String) throws {} + func closeSession(_ sessionId: String, timestampMs: Int64) throws {} + + func pushOpened(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .materialOpened) + } + + func pushClosed(sessionId: String, materialId: String, delta: UInt32, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .materialClosed) + } + + func pushPositionChanged(sessionId: String, materialId: String, position: ReadingPosition, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .positionChanged, position: position) + } + + func pushCheckpointChanged(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .positionChanged, checkpoint: checkpoint) + } + + func pushHeartbeat(sessionId: String, materialId: String, delta: UInt32, position: ReadingPosition?, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .heartbeat, position: position) + } + + func pushHeartbeatCheckpoint(sessionId: String, materialId: String, delta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .heartbeat, checkpoint: checkpoint) + } + + func pushMarkedAsRead(sessionId: String, materialId: String, timestampMs: Int64) throws -> ReadingEventV2 { + makeEvent(sessionId: sessionId, materialId: materialId, eventType: .markedAsRead) + } + + func exportEvents(limit: UInt32, timestampMs: Int64) -> [ReadingEventV2] { [] } + func ackEvents(_ eventIds: [String]) -> UInt32 { UInt32(eventIds.count) } + func markFailed(_ eventIds: [String]) -> UInt32 { UInt32(eventIds.count) } + func reloadStaleEvents() -> UInt32 { 0 } + func cleanupStaleSessions(nowMs: Int64, maxAgeMs: Int64) -> UInt32 { 0 } + + func runtimeComputeFileFingerprint(filePath: String) throws -> String { + fingerprint + } + + func runtimeBuildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String { + "\(sourceFingerprint)|\(rendererKind)|\(parserVersion)|\(schemaVersion)" + } + + private func makeEvent( + sessionId: String, + materialId: String, + eventType: ReadingEventTypeV2, + position: ReadingPosition? = nil, + checkpoint: ReadingCheckpoint? = nil + ) -> ReadingEventV2 { + defer { nextSequence += 1 } + return ReadingEventV2( + eventId: "evt-\(nextSequence)", + clientSessionId: sessionId, + materialId: materialId, + eventType: eventType, + position: position, + checkpoint: checkpoint, + activeSecondsDelta: 0, + timestampMs: 1_700_000_000_000, + sequence: nextSequence + ) + } +} + @MainActor final class SessionManagerTests: XCTestCase { var manager: ReadingRuntimeSessionManager! var noopAdapter: NoopReadingRuntimeAdapter! + var spyAdapter: SpyReadingRuntimeAdapter! override func setUp() { super.setUp() manager = ReadingRuntimeSessionManager.shared noopAdapter = NoopReadingRuntimeAdapter() + spyAdapter = SpyReadingRuntimeAdapter() manager.adapter = noopAdapter // Ensure clean state manager.closeMaterial() @@ -122,4 +201,84 @@ final class SessionManagerTests: XCTestCase { // Also acceptable: state was reset } } + + // MARK: - Restore checkpoint compatibility + + func test_isCompatibleRestoreCheckpoint_matches_active_representation() throws { + manager.adapter = spyAdapter + let context = ReadingMaterialContext( + readingTargetType: .knowledgeSource, + materialId: "restore-match", + materialType: .markdown, + filePath: "", + fileName: "doc.md", + fileType: "markdown" + ) + try manager.openMaterial(context) + + let checkpoint = ReadingCheckpoint( + schemaVersion: 2, + materialId: "restore-match", + representationFingerprint: "restore-match|block|block-parser-v1|2", + rendererKind: .block, + capturedAtMs: 1, + deviceId: "device-1", + deviceSequence: 1, + position: .block(blockId: "b1", blockProgress: 0.2, overallProgress: 0.2) + ) + + XCTAssertTrue(manager.isCompatibleRestoreCheckpoint(checkpoint)) + } + + func test_isCompatibleRestoreCheckpoint_rejects_mismatched_fingerprint() throws { + manager.adapter = spyAdapter + let context = ReadingMaterialContext( + readingTargetType: .knowledgeSource, + materialId: "restore-mismatch", + materialType: .markdown, + filePath: "", + fileName: "doc.md", + fileType: "markdown" + ) + try manager.openMaterial(context) + + let checkpoint = ReadingCheckpoint( + schemaVersion: 2, + materialId: "restore-mismatch", + representationFingerprint: "stale-fingerprint", + rendererKind: .block, + capturedAtMs: 1, + deviceId: "device-1", + deviceSequence: 1, + position: .block(blockId: "b1", blockProgress: 0.2, overallProgress: 0.2) + ) + + XCTAssertFalse(manager.isCompatibleRestoreCheckpoint(checkpoint)) + } + + func test_isCompatibleRestoreCheckpoint_rejects_mismatched_renderer_kind() throws { + manager.adapter = spyAdapter + let context = ReadingMaterialContext( + readingTargetType: .knowledgeSource, + materialId: "restore-renderer", + materialType: .markdown, + filePath: "", + fileName: "doc.md", + fileType: "markdown" + ) + try manager.openMaterial(context) + + let checkpoint = ReadingCheckpoint( + schemaVersion: 2, + materialId: "restore-renderer", + representationFingerprint: "restore-renderer|block|block-parser-v1|2", + rendererKind: .pdf, + capturedAtMs: 1, + deviceId: "device-1", + deviceSequence: 1, + position: .pdf(pageNumber: 1, pageProgress: 0, overallProgress: 0) + ) + + XCTAssertFalse(manager.isCompatibleRestoreCheckpoint(checkpoint)) + } } diff --git a/AIStudyApp/AIStudyAppTests/V2AdapterSmokeTests.swift b/AIStudyApp/AIStudyAppTests/V2AdapterSmokeTests.swift index 3267453..c5e4454 100644 --- a/AIStudyApp/AIStudyAppTests/V2AdapterSmokeTests.swift +++ b/AIStudyApp/AIStudyAppTests/V2AdapterSmokeTests.swift @@ -24,14 +24,23 @@ final class V2AdapterSmokeTests: XCTestCase { func test_noopAdapter_startSession_returnsSessionId() { let adapter = NoopReadingRuntimeAdapter() - let ref = ReadingMaterialRef(materialId: "test-material") + let ref = ReadingMaterialRefV2( + materialId: "test-material", + filePath: "/tmp/test.md", + fileName: "test.md", + fileType: "markdown", + mimeType: nil, + fileSize: nil, + contentHash: nil, + createdAtMs: nil + ) let sid = try! adapter.startSession(material: ref, timestampMs: 1_700_000_000_000) XCTAssertTrue(sid.hasPrefix("noop-")) } func test_noopAdapter_closeSession_doesNotThrow() { let adapter = NoopReadingRuntimeAdapter() - XCTAssertNoThrow(try adapter.closeSession("noop-session")) + XCTAssertNoThrow(try adapter.closeSession("noop-session", timestampMs: 1_700_000_000_000)) } // MARK: - Buffer (Noop)