Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CodeEditor/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(

dependencies: [
.package(url: "https://github.com/raspu/Highlightr", from: "2.1.2"),
.package(url: "https://github.com/ls1intum/artemis-ios-core-modules", from: "7.0.0"),
.package(url: "https://github.com/ls1intum/artemis-ios-core-modules", from: "8.0.0"),
],

targets: [
Expand Down
12 changes: 12 additions & 0 deletions CodeEditor/Sources/CodeEditor/Extensions/UIColorExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// UIColorExtension.swift
//
//
// Created by Tarlan Ismayilsoy on 02.12.23.
//

import UIKit

extension UIColor {
static let feedbackSuggestionColor = UIColor(netHex: 0xB54EFE)
}
27 changes: 22 additions & 5 deletions CodeEditor/Sources/CodeEditor/LighbulbButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,23 @@ final class LightbulbButton: UIButton {
fatalError("not implemented")
}


private func setup() {
let image = UIImage(systemName: "lightbulb.fill")
setImage(image, for: .normal)
imageView?.contentMode = .scaleAspectFit
imageView?.tintColor = .yellow
self.backgroundColor = .feedbackSuggestionColor
self.layer.cornerRadius = 6

let image = UIImage(named: "SuggestedFeedbackSymbol")?.withRenderingMode(.alwaysTemplate)
let customImgView = UIImageView()
customImgView.image = image
customImgView.tintColor = .white
customImgView.contentMode = .scaleAspectFit
self.addSubview(customImgView)

customImgView.translatesAutoresizingMaskIntoConstraints = false
customImgView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
customImgView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
customImgView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.5) .isActive = true
customImgView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.6) .isActive = true

addTarget(self, action: #selector(self.onLightBulbTap), for: .touchUpInside)
}

Expand All @@ -39,3 +50,9 @@ final class LightbulbButton: UIButton {
toggleShowAddFeedback()
}
}

#Preview {
LightbulbButton(frame: .init(x: 0, y: 0, width: 350, height: 350),
setSelectedFeedbackSuggestionId: {},
toggleShowAddFeedback: {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public protocol FeedbackSuggestion: Equatable, Decodable {
var title: String { get }
var description: String { get }
var credits: Double { get }
var gradingInstruction: GradingInstruction? { get }
var structuredGradingInstructionId: Int? { get }

var associatedAssessmentFeedbackId: UUID? { get set } // not decoded
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,13 @@ public struct ProgrammingFeedbackSuggestion: FeedbackSuggestion, Decodable {

public var credits: Double

public var gradingInstruction: GradingInstruction?
public var structuredGradingInstructionId: Int?

public var associatedAssessmentFeedbackId: UUID?

// TODO: rename/remove the fields below once programming suggestions are integrated into Athena
public let participationId: Int
public let srcFile: String
public let fromLine: Int
public let toLine: Int

enum DecodingKeys: String, CodingKey {
case exercise_id
case participation_id
case src_file
case from_line
case to_line
case text
case credits
}

// TODO: correct the decoding logic below once programming suggestions are integrated into Athena
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: DecodingKeys.self)
id = Int.random(in: 1...999999)
exerciseId = try values.decode(Int.self, forKey: .exercise_id)
submissionId = -1
title = "Suggestion"
participationId = try values.decode(Int.self, forKey: .participation_id)
srcFile = try values.decode(String.self, forKey: .src_file)
fromLine = try values.decode(Int.self, forKey: .from_line)
toLine = try values.decode(Int.self, forKey: .to_line)
description = try values.decode(String.self, forKey: .text)
credits = try values.decode(Double.self, forKey: .credits)
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
public var filePath: String?

public var lineStart: Int?

public var lineEnd: Int?
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,14 @@ public struct TextFeedbackSuggestion: FeedbackSuggestion {

public let credits: Double

public let gradingInstruction: GradingInstruction?
public let structuredGradingInstructionId: Int?

public var associatedAssessmentFeedbackId: UUID?

public let indexStart: Int?

public let indexEnd: Int?

public static func == (lhs: TextFeedbackSuggestion, rhs: TextFeedbackSuggestion) -> Bool {
lhs.id == rhs.id
&& lhs.exerciseId == rhs.exerciseId
&& lhs.submissionId == rhs.submissionId
&& lhs.title == rhs.title
&& lhs.description == rhs.description
&& lhs.credits == rhs.credits
}

public var textBlockContent: String?

public var isReferenced: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,14 @@ class RoundedCornerLayoutManager: NSLayoutManager {
}

private func drawProgrammingFeedbackSuggestions(_ paraNumber: Int, _ rect: CGRect, _ origin: CGPoint) {
let ctx = UIGraphicsGetCurrentContext()
guard let ctx else {
guard let ctx = UIGraphicsGetCurrentContext() else {
return
}
UIGraphicsPushContext(ctx)
ctx.setFillColor(CGColor(red: 0, green: 0.2, blue: 0.8, alpha: 0.8))
ctx.setStrokeColor(CGColor(red: 0, green: 0.2, blue: 0.8, alpha: 0.8))
ctx.setFillColor(UIColor.feedbackSuggestionColor.cgColor)

let programmingSuggestions = feedbackSuggestions.compactMap({ $0 as? ProgrammingFeedbackSuggestion })
if programmingSuggestions.contains(where: { paraNumber + 1 >= $0.fromLine && paraNumber + 1 <= $0.toLine }) {
if programmingSuggestions.contains(where: { paraNumber + 1 >= $0.lineStart ?? 0 && paraNumber + 1 <= $0.lineEnd ?? 0 }) {
let path = CGPath(
rect: CGRect(
x: rect.origin.x,
Expand Down
2 changes: 1 addition & 1 deletion CodeEditor/Sources/CodeEditor/UXCodeTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ final class UXCodeTextView: UXTextView, HighlightDelegate, UIScrollViewDelegate
var lineNumber = 1
layoutManager.enumerateLineFragments(forGlyphRange: layoutManager.glyphRange(for: textContainer)) { rect, _, _, _, _ in
let offset = self.calculateWrapOffsetOf(lineNumber)
if let feedback = self.feedbackSuggestions.first(where: { $0.fromLine == lineNumber - offset }) {
if let feedback = self.feedbackSuggestions.first(where: { $0.lineStart == lineNumber - offset }) {
// TODO: get rid of the string interpolation once programming exercise suggestions are integrated into Athena
if let lightbulb = self.buildLightbulbButton(rect: rect, feedbackId: "\(feedback.id)") {
self.lightBulbs.append(lightbulb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,13 @@ public struct UXCodeTextViewRepresentable: UXViewRepresentable {
textView.string = editorBindings.source.wrappedValue
}

textView.feedbackSuggestions = editorBindings.feedbackSuggestions
textView.updateLightBulbs()
} else if editorBindings.feedbackSuggestions.count != textView.lightBulbs.count {
textView.feedbackSuggestions = editorBindings.feedbackSuggestions
textView.updateLightBulbs()
}

textView.setNeedsDisplay()
textView.pencilOnly = editorBindings.pencilOnly.wrappedValue
textView.dragSelection = self.editorBindings.dragSelection?.wrappedValue
Expand Down
6 changes: 1 addition & 5 deletions Themis.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@
8384C43129210474008CCB4D /* FeedbackListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8384C43029210474008CCB4D /* FeedbackListView.swift */; };
8384C439292A8592008CCB4D /* FeedbackCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8384C438292A8592008CCB4D /* FeedbackCellView.swift */; };
8384C441292AA1FB008CCB4D /* GradingCriteriaCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8384C440292AA1FB008CCB4D /* GradingCriteriaCellView.swift */; };
A90D8C7F29818AB70066DBFD /* ThemisAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90D8C7E29818AB70066DBFD /* ThemisAPI.swift */; };
A9752D792992AA8B004441D1 /* ExamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9752D782992AA8B004441D1 /* ExamSection.swift */; };
A9752D7B2992AAEB004441D1 /* ExamSectionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9752D7A2992AAEB004441D1 /* ExamSectionDetailView.swift */; };
DA0687C2292FB4870091B88A /* SubmissionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0687C1292FB4870091B88A /* SubmissionListView.swift */; };
Expand Down Expand Up @@ -374,7 +373,6 @@
8384C438292A8592008CCB4D /* FeedbackCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FeedbackCellView.swift; path = Themis/Views/Assessment/CorrectionSidebar/FeedbackCellView.swift; sourceTree = SOURCE_ROOT; };
8384C440292AA1FB008CCB4D /* GradingCriteriaCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GradingCriteriaCellView.swift; path = Themis/Views/Assessment/CorrectionSidebar/GradingCriteriaCellView.swift; sourceTree = SOURCE_ROOT; };
83E729E82915786600DB7B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A90D8C7E29818AB70066DBFD /* ThemisAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemisAPI.swift; sourceTree = "<group>"; };
A9752D782992AA8B004441D1 /* ExamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamSection.swift; sourceTree = "<group>"; };
A9752D7A2992AAEB004441D1 /* ExamSectionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamSectionDetailView.swift; sourceTree = "<group>"; };
DA0687C1292FB4870091B88A /* SubmissionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionListView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1102,7 +1100,6 @@
children = (
E2E460DC29292ECF00ECC0A5 /* Stack.swift */,
DAFFA3FA297F066200EA72B8 /* ArtemisDateHelpers.swift */,
A90D8C7E29818AB70066DBFD /* ThemisAPI.swift */,
);
name = API;
path = Themis/API;
Expand Down Expand Up @@ -1544,7 +1541,6 @@
65B49F472A080A9900C9A45F /* ToolbarRedoButton.swift in Sources */,
65F019012A1CCC0300BB1C98 /* UnknownSubmissionServiceImpl.swift in Sources */,
E2192ED3291E47820092CE58 /* RESTController.swift in Sources */,
A90D8C7F29818AB70066DBFD /* ThemisAPI.swift in Sources */,
E1DC9BA3293D241100674F5B /* SubmissionSearchView.swift in Sources */,
DA3F0F01294A2DF800A7B807 /* AddFeedbackView.swift in Sources */,
DA295CC6293A67EB000E04DC /* AuthenticationEnvironmentVariables.swift in Sources */,
Expand Down Expand Up @@ -1921,7 +1917,7 @@
repositoryURL = "https://github.com/ls1intum/artemis-ios-core-modules";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 7.0.0;
minimumVersion = 8.0.0;
};
};
65F007962A86C837000FD641 /* XCRemoteSwiftPackageReference "SwiftUI-Shimmer" */ = {
Expand Down
92 changes: 0 additions & 92 deletions Themis/API/ThemisAPI.swift

This file was deleted.

13 changes: 10 additions & 3 deletions Themis/Models/Feedback/AssessmentFeedback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ public struct AssessmentFeedback: Identifiable {
init(
baseFeedback: Feedback = Feedback(),
scope: ThemisFeedbackScope,
detail: (any FeedbackDetail)? = nil
detail: (any FeedbackDetail)? = nil,
textPrefix: String? = nil
) {
self.baseFeedback = baseFeedback
self.scope = scope
self.detail = detail
self.detail?.buildArtemisFeedback(feedback: &self.baseFeedback)

if let textPrefix {
self.baseFeedback.text = textPrefix + (self.baseFeedback.text ?? "")
}
}

mutating func setBaseFeedback(to feedback: Feedback) {
Expand Down Expand Up @@ -73,8 +78,10 @@ extension AssessmentFeedback {
var newIncompleteFeedbackDetail = incompleteFeedbackDetail

if var incompleteFeedbackDetail = incompleteFeedbackDetail as? ProgrammingFeedbackDetail,
let codeSuggestion = suggestion as? ProgrammingFeedbackSuggestion {
let lines = NSRange(location: codeSuggestion.fromLine, length: codeSuggestion.toLine - codeSuggestion.fromLine)
let codeSuggestion = suggestion as? ProgrammingFeedbackSuggestion,
let lineStart = codeSuggestion.lineStart,
let lineEnd = codeSuggestion.lineEnd {
let lines = NSRange(location: lineStart, length: lineEnd - lineStart)
incompleteFeedbackDetail.lines = lines
newIncompleteFeedbackDetail = incompleteFeedbackDetail
}
Expand Down
26 changes: 20 additions & 6 deletions Themis/Services/Assessment/AthenaService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ struct AthenaService {

let client = APIClient()

// MARK: - Get Feedback Suggestions
private struct GetFeedbackSuggestionsRequest: APIRequest {
typealias Response = [TextFeedbackSuggestion]
private struct GetFeedbackSuggestionsRequest<ResponseType: Decodable>: APIRequest {
typealias Response = [ResponseType]

let exerciseType: String
let exerciseId: Int
let submissionId: Int

Expand All @@ -28,11 +28,25 @@ struct AthenaService {
}

var resourceName: String {
"api/athena/text-exercises/\(exerciseId)/submissions/\(submissionId)/feedback-suggestions"
"api/athena/\(exerciseType)-exercises/\(exerciseId)/submissions/\(submissionId)/feedback-suggestions"
}
}

func getFeedbackSuggestions(exerciseId: Int, submissionId: Int) async throws -> [TextFeedbackSuggestion] {
try await client.sendRequest(GetFeedbackSuggestionsRequest(exerciseId: exerciseId, submissionId: submissionId)).get().0
// MARK: - Get Text Feedback Suggestions
func getTextFeedbackSuggestions(exerciseId: Int, submissionId: Int) async throws -> [TextFeedbackSuggestion] {
try await client
.sendRequest(GetFeedbackSuggestionsRequest<TextFeedbackSuggestion>(exerciseType: TextExercise.type,
exerciseId: exerciseId,
submissionId: submissionId))
.get().0
}

// MARK: - Get Programming Feedback Suggestions
func getProgrammingFeedbackSuggestions(exerciseId: Int, submissionId: Int) async throws -> [ProgrammingFeedbackSuggestion] {
try await client
.sendRequest(GetFeedbackSuggestionsRequest<ProgrammingFeedbackSuggestion>(exerciseType: ProgrammingExercise.type,
exerciseId: exerciseId,
submissionId: submissionId))
.get().0
}
}
Loading