Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Tiny AI Mark Changed text.
18
 *
19
 * This module marks text that was returned by the AI service
20
 * and that has been changed by a human prior to being inserted.
21
 *
22
 * @module      tiny_aiplacement/textmark
23
 * @copyright   2023 Matt Porritt <matt.porritt@moodle.com>
24
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
export default class TinyAiTextMarker {
28
    /**
29
     * Finds the longest common subsequence of two strings.
30
     *
31
     * @param {string} a The first string.
32
     * @param {string} b The second string.
33
     * @returns {string} The longest common subsequence.
34
     */
35
    static longestCommonSubsequence(a, b) {
36
        const lengths = Array(a.length + 1)
37
            .fill(null)
38
            .map(() => Array(b.length + 1).fill(0));
39
 
40
        for (let i = 0; i < a.length; i++) {
41
            for (let j = 0; j < b.length; j++) {
42
                if (a[i] === b[j]) {
43
                    lengths[i + 1][j + 1] = lengths[i][j] + 1;
44
                } else {
45
                    lengths[i + 1][j + 1] = Math.max(lengths[i + 1][j], lengths[i][j + 1]);
46
                }
47
            }
48
        }
49
 
50
        let i = a.length;
51
        let j = b.length;
52
        let lcs = '';
53
 
54
        while (i > 0 && j > 0) {
55
            if (a[i - 1] === b[j - 1]) {
56
                lcs = a[i - 1] + lcs;
57
                i--;
58
                j--;
59
            } else if (lengths[i - 1][j] > lengths[i][j - 1]) {
60
                i--;
61
            } else {
62
                j--;
63
            }
64
        }
65
 
66
        return lcs;
67
    }
68
 
69
    /**
70
     * Finds the differences between the original and edited text using the LCS algorithm.
71
     *
72
     * @param {string} originalText The original text.
73
     * @param {string} editedText The edited text.
74
     * @returns {Array<Object>} An array of difference objects with start, end, and text properties.
75
     */
76
    static findDifferences(originalText, editedText) {
77
        const lcs = TinyAiTextMarker.longestCommonSubsequence(originalText, editedText);
78
        let differences = [];
79
        let i = 0;
80
        let j = 0;
81
 
82
        for (let k = 0; k < lcs.length; k++) {
83
            let commonChar = lcs[k];
84
 
85
            while (originalText[i] !== commonChar || editedText[j] !== commonChar) {
86
                let start = j;
87
                while (editedText[j] !== commonChar) {
88
                    j++;
89
                }
90
                let editedSection = editedText.slice(start, j);
91
                differences.push({start, end: j, text: editedSection});
92
 
93
                while (originalText[i] !== commonChar) {
94
                    i++;
95
                }
96
            }
97
 
98
            i++;
99
            j++;
100
        }
101
 
102
        if (j < editedText.length) {
103
            differences.push({start: j, end: editedText.length, text: editedText.slice(j)});
104
        }
105
 
106
        return differences;
107
    }
108
 
109
    /**
110
     * Wraps the given edited section in a span tag with a 'user-edited' class.
111
     *
112
     * @param {string} editedSection The edited section of the text.
113
     * @returns {Promise<string>} A promise that resolves with the wrapped edited section.
114
     */
115
    static async wrapInSpan(editedSection) {
116
        return new Promise((resolve, reject) => {
117
            try {
118
                let wrappedText = `<span class="user-edited">${editedSection}</span>`;
119
                resolve(wrappedText);
120
            } catch (error) {
121
                reject(error);
122
            }
123
        });
124
    }
125
 
126
    /**
127
     * Wraps the edited sections of the text in span tags with a 'user-edited' class.
128
     *
129
     * @param {string} originalText The original text.
130
     * @param {string} editedText The edited text.
131
     * @returns {Promise<string>} A promise that resolves with the edited text, where edited sections are wrapped in span tags.
132
     */
133
    static async wrapEditedSections(originalText, editedText) {
134
        let differences = TinyAiTextMarker.findDifferences(originalText, editedText);
135
        let wrappedText = editedText;
136
 
137
        for (let i = differences.length - 1; i >= 0; i--) {
138
            let {start, end, text} = differences[i];
139
            let wrappedSection = await TinyAiTextMarker.wrapInSpan(text);
140
            wrappedText = wrappedText.slice(0, start) + wrappedSection + wrappedText.slice(end);
141
        }
142
 
143
        return wrappedText;
144
    }
145
 
146
}