From d9273953d525b499515b6a889485d1f610ffc802 Mon Sep 17 00:00:00 2001 From: lminervino18 Date: Thu, 5 Jun 2025 19:10:07 -0300 Subject: [PATCH 01/39] feat: add markdown and math view support in ChatScreen --- package.json | 2 + screens/ChatScreen.tsx | 83 +++++++++-------- yarn.lock | 198 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 242 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 6685a94..66446e3 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,8 @@ "react-native-draggable-flatlist": "^4.0.3", "react-native-gesture-handler": "~2.24.0", "react-native-maps": "1.20.1", + "react-native-markdown-display": "^7.0.2", + "react-native-math-view": "^3.9.5", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.10.0", diff --git a/screens/ChatScreen.tsx b/screens/ChatScreen.tsx index 6b4abf0..72f2278 100644 --- a/screens/ChatScreen.tsx +++ b/screens/ChatScreen.tsx @@ -1,9 +1,7 @@ -// screens/ChatScreen.tsx import React, { useState, useRef, useEffect } from 'react'; import { SafeAreaView, View, - Text, TextInput, FlatList, TouchableOpacity, @@ -11,13 +9,14 @@ import { Platform, StyleSheet, Image, - ActivityIndicator, + Text, } from 'react-native'; import { useAuth } from '../context/AuthContext'; import { useTheme } from '../hooks/useTheme'; -import { sendToAI, addFeedback } from '../services/chatIA'; +import { sendToAI } from '../services/chatIA'; import { spacing } from '../constants/spacing'; import { fonts } from '../constants/fonts'; +import Markdown from 'react-native-markdown-display'; export default function ChatScreen() { const [messages, setMessages] = useState([]); @@ -66,7 +65,7 @@ export default function ChatScreen() { }, [messages]); return ( - + - + Classy - Your AI assistant to answer questions about using the ClassConnect app. + + Your AI assistant to answer questions about using the ClassConnect app. + powered by - + @@ -111,16 +105,10 @@ export default function ChatScreen() { > {/* Avatar IA o Usuario */} {!item.fromUser ? ( - + ) : ( user?.urlProfilePhoto && ( - + ) )} @@ -128,27 +116,26 @@ export default function ChatScreen() { - {item.text} + {item.text} )} - - ListHeaderComponent={isTyping ? ( - - - - Classy is typing... + ListHeaderComponent={ + isTyping ? ( + + + + Classy is typing... + - - ) : null} + ) : null + } /> {/* Input */} @@ -207,6 +194,11 @@ const styles = StyleSheet.create({ marginVertical: spacing.xs, marginHorizontal: spacing.md, }, + avatar: { + width: 24, + height: 24, + marginHorizontal: 8, + }, inputContainer: { flexDirection: 'row', paddingHorizontal: spacing.md, @@ -227,4 +219,19 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingHorizontal: spacing.md, }, -}); \ No newline at end of file +}); + +const markdownStyles = { + body: { + color: 'white', + fontSize: 15, + flexWrap: 'wrap' as const, + margin: 0, + padding: 0, + lineHeight: 20, + }, + paragraph: { + marginTop: 0, + marginBottom: 0, + }, +}; diff --git a/yarn.lock b/yarn.lock index cb017a2..155e0e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2327,6 +2327,13 @@ resolved "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz" integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw== +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + "@types/http-cache-semantics@*": version "4.0.4" resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz" @@ -2443,6 +2450,11 @@ resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== +"@types/unist@^2": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@types/uuid@^10.0.0": version "10.0.0" resolved "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz" @@ -2476,6 +2488,11 @@ "@urql/core" "^5.1.1" wonka "^6.3.2" +"@xmldom/xmldom@0.9.8": + version "0.9.8" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.9.8.tgz#1471e82bdff9e8f20ee8bbe60d4ffa8a516e78d8" + integrity sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A== + "@xmldom/xmldom@^0.8.8": version "0.8.10" resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz" @@ -3018,6 +3035,11 @@ camelcase@^6.2.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + caniuse-lite@^1.0.30001716: version "1.0.30001717" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz" @@ -3204,6 +3226,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +commander@13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46" + integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== + commander@^12.0.0: version "12.1.0" resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" @@ -3315,6 +3347,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + css-in-js-utils@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz" @@ -3322,6 +3359,20 @@ css-in-js-utils@^3.1.0: dependencies: hyphenate-style-name "^1.0.3" +css-selector-parser@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759" + integrity sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g== + +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + cssom@^0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz" @@ -3556,6 +3607,11 @@ entities@^6.0.0: resolved "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz" integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw== +entities@~2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + env-editor@^0.4.1: version "0.4.2" resolved "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz" @@ -3638,6 +3694,11 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" @@ -4352,6 +4413,33 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hast-util-from-selector@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-from-selector/-/hast-util-from-selector-2.0.1.tgz#acb6c87e7de3698bf1c69b0fb88ccd00be007765" + integrity sha512-oKhgO8/r9HMeNXJspY3gFD6VqCWNN9jpIHPkcPl/qQpelbigKB+5jC9k36M+w1kmEPvd386+PU5LEsxJzCRZMw== + dependencies: + "@types/hast" "^2.0.0" + css-selector-parser "^1.0.0" + hastscript "^7.0.0" + +hast-util-parse-selector@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2" + integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA== + dependencies: + "@types/hast" "^2.0.0" + +hastscript@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b" + integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^3.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + hermes-estree@0.25.1: version "0.25.1" resolved "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz" @@ -5348,6 +5436,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" + integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== + dependencies: + uc.micro "^1.0.1" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" @@ -5377,7 +5472,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== -lodash@^4.17.19: +lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5437,6 +5532,17 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +markdown-it@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" + integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== + dependencies: + argparse "^1.0.7" + entities "~2.0.0" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + marky@^1.2.2: version "1.3.0" resolved "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz" @@ -5447,6 +5553,21 @@ math-intrinsics@^1.1.0: resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +mathjax-full@^3.1.4: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mathjax-full/-/mathjax-full-3.2.2.tgz#43f02e55219db393030985d2b6537ceae82f1fa7" + integrity sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w== + dependencies: + esm "^3.2.25" + mhchemparser "^4.1.0" + mj-context-menu "^0.6.1" + speech-rule-engine "^4.0.6" + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" @@ -5855,6 +5976,11 @@ metro@0.82.3, metro@^0.82.0: ws "^7.5.10" yargs "^17.6.2" +mhchemparser@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.2.1.tgz#d73982e66bc06170a85b1985600ee9dabe157cb0" + integrity sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ== + micromatch@^4.0.4: version "4.0.8" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" @@ -5936,6 +6062,11 @@ minizlib@^3.0.1: dependencies: minipass "^7.1.2" +mj-context-menu@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz#a043c5282bf7e1cf3821de07b13525ca6f85aa69" + integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA== + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" @@ -6345,7 +6476,7 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz" integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== -postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -6405,7 +6536,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.8.1: +prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6414,6 +6545,11 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + protobufjs@^6.11.3: version "6.11.4" resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz" @@ -6587,6 +6723,13 @@ react-native-edge-to-edge@1.6.0: resolved "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz" integrity sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og== +react-native-fit-image@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz#c660d1ad74b9dcaa1cba27a0d9c23837e000226c" + integrity sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg== + dependencies: + prop-types "^15.5.10" + react-native-gesture-handler@~2.24.0: version "2.24.0" resolved "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.24.0.tgz" @@ -6608,6 +6751,26 @@ react-native-maps@1.20.1: dependencies: "@types/geojson" "^7946.0.13" +react-native-markdown-display@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz#b6584cec8d6670c0141fb8780bc2f0710188a4c2" + integrity sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ== + dependencies: + css-to-react-native "^3.0.0" + markdown-it "^10.0.0" + prop-types "^15.7.2" + react-native-fit-image "^1.5.5" + +react-native-math-view@^3.9.5: + version "3.9.5" + resolved "https://registry.yarnpkg.com/react-native-math-view/-/react-native-math-view-3.9.5.tgz#59c6fb0bef8eb460bf16bbdf8169e57ceb12eb4c" + integrity sha512-UJxrisNafszfqIW+utoSDylb72SkZ92cKz1IfE5Dm0s+uIaHxOxepF2DdRbktAV8c0FEFllnXfErcGdh8sfIBw== + dependencies: + hast-util-from-selector "^2.0.0" + lodash "^4.17.21" + mathjax-full "^3.1.4" + transformation-matrix "^2.8.0" + react-native-reanimated@~3.17.4: version "3.17.5" resolved "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz" @@ -7115,6 +7278,20 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +speech-rule-engine@^4.0.6: + version "4.1.2" + resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-4.1.2.tgz#3b31b5813a2fc2eaecdfda26ad29c32599e9a537" + integrity sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw== + dependencies: + "@xmldom/xmldom" "0.9.8" + commander "13.1.0" + wicked-good-xpath "1.3.0" + split-on-first@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" @@ -7451,6 +7628,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +transformation-matrix@^2.8.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/transformation-matrix/-/transformation-matrix-2.16.1.tgz#4a2de06331b94ae953193d1b9a5ba002ec5f658a" + integrity sha512-tdtC3wxVEuzU7X/ydL131Q3JU5cPMEn37oqVLITjRDSDsnSHVFzW2JiCLfZLIQEgWzZHdSy3J6bZzvKEN24jGA== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" @@ -7486,6 +7668,11 @@ ua-parser-js@^1.0.35: resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz" integrity sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + undici-types@~6.21.0: version "6.21.0" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" @@ -7746,6 +7933,11 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wicked-good-xpath@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c" + integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw== + wonka@^6.3.2: version "6.3.5" resolved "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz" From 411b410cc4227ff7c189912c0d4ca50eb7995709 Mon Sep 17 00:00:00 2001 From: lminervino18 Date: Thu, 5 Jun 2025 19:49:03 -0300 Subject: [PATCH 02/39] feat: implement mock API for assessments with CRUD operations --- services/AssestmentsMockApi.ts | 148 +++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 services/AssestmentsMockApi.ts diff --git a/services/AssestmentsMockApi.ts b/services/AssestmentsMockApi.ts new file mode 100644 index 0000000..35b6054 --- /dev/null +++ b/services/AssestmentsMockApi.ts @@ -0,0 +1,148 @@ +import { v4 as uuidv4 } from 'uuid'; + +export type AssessmentType = 'exam' | 'assignment'; + +export type ExerciseType = 'multiple_choice' | 'open'; + +export interface AssessmentExercise { + type: ExerciseType; + enunciate: string; + link?: string; + choices?: string[]; + answer?: string; // sólo para multiple_choice +} + +export interface AssessmentSubmission { + note?: number; + feedback?: string; + AI_feedback?: string; + [exerciseId: string]: any; // respuestas por ejercicio, p. ej. { answer: "..." } +} + +export interface Assessment { + id: string; + courseId: number; + moduleId: number; + title: string; + description: string; + type: AssessmentType; + tolerance_time: number; // en horas + start_time: string; + deadline: string; + created_time: string; + teacher_id: string; + exercises: Record; + submissions: Record; +} + +const mockAssessments: Record = {}; // clave: courseId-moduleId-id + +function generateKey(courseId: number, moduleId: number, id: string): string { + return `${courseId}-${moduleId}-${id}`; +} + +// Crear una evaluación +export async function createAssessment( + data: Omit +): Promise { + const id = uuidv4(); + const created_time = new Date().toISOString(); + const full: Assessment = { + ...data, + id, + created_time, + submissions: {}, + }; + mockAssessments[generateKey(data.courseId, data.moduleId, id)] = full; + return full; +} + +// Editar evaluación +export async function updateAssessment( + courseId: number, + moduleId: number, + id: string, + data: Partial> +): Promise { + const key = generateKey(courseId, moduleId, id); + if (!mockAssessments[key]) throw new Error('Assessment not found'); + mockAssessments[key] = { ...mockAssessments[key], ...data }; + return mockAssessments[key]; +} + +// Eliminar evaluación +export async function deleteAssessment( + courseId: number, + moduleId: number, + id: string +): Promise { + const key = generateKey(courseId, moduleId, id); + delete mockAssessments[key]; +} + +// Obtener todas las evaluaciones de un curso + módulo +export async function getAssessmentsByCourseModule( + courseId: number, + moduleId: number +): Promise { + return Object.values(mockAssessments).filter( + (a) => a.courseId === courseId && a.moduleId === moduleId + ); +} + +// Enviar respuesta como alumno +export async function submitAssessment( + courseId: number, + moduleId: number, + id: string, + userId: string, + responses: Record +): Promise { + const key = generateKey(courseId, moduleId, id); + const assessment = mockAssessments[key]; + if (!assessment) throw new Error('Assessment not found'); + + assessment.submissions[userId] = { + ...assessment.submissions[userId], + ...responses, + }; + + return assessment.submissions[userId]; +} + +// Corrección manual por docente +export async function correctAssessmentManually( + courseId: number, + moduleId: number, + id: string, + userId: string, + correction: Partial +): Promise { + const key = generateKey(courseId, moduleId, id); + const assessment = mockAssessments[key]; + if (!assessment || !assessment.submissions[userId]) throw new Error('Submission not found'); + + assessment.submissions[userId] = { + ...assessment.submissions[userId], + ...correction, + }; + + return assessment.submissions[userId]; +} + +// Corrección automática por IA de un ejercicio +export async function getAIAssessmentCorrection( + courseId: number, + moduleId: number, + id: string, + userId: string, + exerciseId: string +): Promise { + const key = generateKey(courseId, moduleId, id); + const answer = mockAssessments[key]?.submissions[userId]?.[exerciseId]?.answer; + + if (!answer) return 'No answer provided.'; + + // IA simulada + return `AI Score: 8.5. Your explanation is correct, but could benefit from real-world examples.`; +} From aa7b9cb9a85c405f14d49f8ded6c7949da0927a9 Mon Sep 17 00:00:00 2001 From: lminervino18 Date: Thu, 5 Jun 2025 21:26:03 -0300 Subject: [PATCH 03/39] feat(assessments): enable creation of exams, tasks and exercises with choice selection --- app/(courses)/assessments.tsx | 1 + components/ui/forms/AssessmentForm.tsx | 261 ++++++++++++++++++ components/ui/forms/ExerciseForm.tsx | 222 +++++++++++++++ .../ui/lists/ReorderableModuleList.tsx.tsx | 2 +- screens/AssessmentScreen.tsx | 227 +++++++++++++++ screens/CoursesDetailScreen.tsx | 74 +++-- ...tmentsMockApi.ts => assessmentsMockApi.ts} | 54 ++-- 7 files changed, 789 insertions(+), 52 deletions(-) create mode 100644 app/(courses)/assessments.tsx create mode 100644 components/ui/forms/AssessmentForm.tsx create mode 100644 components/ui/forms/ExerciseForm.tsx create mode 100644 screens/AssessmentScreen.tsx rename services/{AssestmentsMockApi.ts => assessmentsMockApi.ts} (74%) diff --git a/app/(courses)/assessments.tsx b/app/(courses)/assessments.tsx new file mode 100644 index 0000000..8b90df4 --- /dev/null +++ b/app/(courses)/assessments.tsx @@ -0,0 +1 @@ +export { default } from '../../screens/AssessmentScreen'; diff --git a/components/ui/forms/AssessmentForm.tsx b/components/ui/forms/AssessmentForm.tsx new file mode 100644 index 0000000..57f28ec --- /dev/null +++ b/components/ui/forms/AssessmentForm.tsx @@ -0,0 +1,261 @@ +import React, { useState } from 'react'; +import { + View, + StyleSheet, + Text, + TouchableOpacity, + ScrollView, + Modal, + Alert, + Platform, +} from 'react-native'; +import TextField from '../fields/TextField'; +import Button from '../buttons/Button'; +import Dialog from '../alerts/Dialog'; +import { spacing } from '../../../constants/spacing'; +import { fonts } from '../../../constants/fonts'; +import { useTheme } from '../../../context/ThemeContext'; +import { useAuth } from '../../../context/AuthContext'; +import DateTimePicker from '@react-native-community/datetimepicker'; +import ExerciseForm from './ExerciseForm'; +import { AssessmentType, AssessmentExercise, createAssessment } from '../../../services/assessmentsMockApi'; + + +type AssessmentFormProps = { + courseId: number; + onClose: () => void; +}; + +export default function AssessmentForm({ courseId, onClose }: AssessmentFormProps) { + const theme = useTheme(); + const { user, authToken } = useAuth(); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [type, setType] = useState('exam'); + const [startDate, setStartDate] = useState(new Date()); + const [deadline, setDeadline] = useState(new Date()); + const [tolerance, setTolerance] = useState('2'); + const [errorVisible, setErrorVisible] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [exercises, setExercises] = useState>({}); + const [showPicker, setShowPicker] = useState<{ field: 'start' | 'deadline' | null; visible: boolean }>({ field: null, visible: false }); + const [exerciseModalVisible, setExerciseModalVisible] = useState(false); + + const handleSubmit = async () => { + if (!title.trim() || !user?.uuid) { + setErrorMessage('Title is required and user must be authenticated.'); + setErrorVisible(true); + return; + } + + try { + await createAssessment( + { + courseId, + title, + description, + type, + tolerance_time: parseInt(tolerance), + start_time: startDate.toISOString(), + deadline: deadline.toISOString(), + exercises, + }, + user.uuid + ); + + Alert.alert('✅ Assessment created'); + onClose(); + } catch (e) { + console.error(e); + setErrorMessage('Something went wrong. Please try again.'); + setErrorVisible(true); + } + }; + + const renderDateField = (label: string, date: Date, field: 'start' | 'deadline') => ( + setShowPicker({ field, visible: true })}> + {label}: {date.toDateString()} + + ); + + const handleAddExercise = (exercise: AssessmentExercise) => { + const key = `exercise_${Object.keys(exercises).length + 1}`; + setExercises(prev => ({ ...prev, [key]: exercise })); + setExerciseModalVisible(false); + }; + + return ( + + Create Assessment + + + {(['exam', 'assignment'] as AssessmentType[]).map((option) => ( + setType(option)} + style={[styles.typeButton, { + backgroundColor: type === option ? theme.primary : theme.card, + borderColor: theme.primary, + }]} + > + + {option === 'exam' ? 'Exam' : 'Task'} + + + ))} + + + + + + + {renderDateField('Start Time', startDate, 'start')} + {renderDateField('Deadline', deadline, 'deadline')} + + + Exercises + setExerciseModalVisible(true)}> + + + + + + {Object.entries(exercises).map(([key, ex]) => ( + + • {ex.enunciate} ({ex.type}) + + ))} + + + +