From b175a7c94ad2da4e04f3bc4417b937705765c6f6 Mon Sep 17 00:00:00 2001 From: Alexandr Karachev Date: Thu, 23 Jan 2025 16:45:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BA=D0=B0=D1=80=D1=82=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=B4=D0=B5=20=D0=B4=D0=B5=D1=80=D0=B5=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- asset/mental_map_quiz/MentalMap.js | 8 +- asset/mental_map_quiz/TreeView/TreeView.js | 5 +- .../mental_map_quiz/TreeView/TreeViewBody.js | 193 ++++++++++-------- .../TreeView/TreeVoiceControl.js | 6 +- backend/MentalMap/MentalMap.php | 32 +++ backend/views/mental-map-history/index.php | 4 +- frontend/MentalMap/MentalMap.php | 12 +- frontend/controllers/MentalMapController.php | 85 +++++++- 8 files changed, 240 insertions(+), 105 deletions(-) diff --git a/asset/mental_map_quiz/MentalMap.js b/asset/mental_map_quiz/MentalMap.js index 2cbd375a..c25e1dde 100644 --- a/asset/mental_map_quiz/MentalMap.js +++ b/asset/mental_map_quiz/MentalMap.js @@ -473,7 +473,13 @@ export default function MentalMap(element, deck, params) { this.element.appendChild(TreeView({ name: json.name, tree: json.treeData, - history: [] + history, + params: { + story_id: params?.story_id, + slide_id: params?.slide_id, + mental_map_id: params.mentalMapId, + repetition_mode: repetitionMode, + } }, new VoiceResponse(new MissingWordsRecognition({})))) return } diff --git a/asset/mental_map_quiz/TreeView/TreeView.js b/asset/mental_map_quiz/TreeView/TreeView.js index 25254764..841a0556 100644 --- a/asset/mental_map_quiz/TreeView/TreeView.js +++ b/asset/mental_map_quiz/TreeView/TreeView.js @@ -5,11 +5,12 @@ import TreeViewBody from "./TreeViewBody"; * @param name * @param tree * @param history + * @param {{story_id: number|null, slide_id: number|null, mental_map_id: string}} params * @param {VoiceResponse} voiceResponse * @returns {HTMLDivElement} * @constructor */ -export default function TreeView({name, tree, history}, voiceResponse) { +export default function TreeView({name, tree, history, params}, voiceResponse) { const wrap = document.createElement('div') wrap.style.display = 'flex' @@ -21,7 +22,7 @@ export default function TreeView({name, tree, history}, voiceResponse) { header.innerHTML = `

${name}

` wrap.appendChild(header) - wrap.appendChild(TreeViewBody(tree, voiceResponse, history)) + wrap.appendChild(TreeViewBody(tree, voiceResponse, history, params)) return wrap } diff --git a/asset/mental_map_quiz/TreeView/TreeViewBody.js b/asset/mental_map_quiz/TreeView/TreeViewBody.js index aa634f06..6847f74b 100644 --- a/asset/mental_map_quiz/TreeView/TreeViewBody.js +++ b/asset/mental_map_quiz/TreeView/TreeViewBody.js @@ -86,7 +86,7 @@ function createRow(node, level = 0) { return row; } -function processTreeNodes(list, body, history, voiceResponse) { +function processTreeNodes(list, body, history, voiceResponse, params) { for (const listItem of list) { const rowElement = body.querySelector(`.node-row[data-node-id='${listItem.id}']`) @@ -108,88 +108,105 @@ function processTreeNodes(list, body, history, voiceResponse) { const finalSpan = voiceResponseElem.querySelector('.final_span') const interimSpan = voiceResponseElem.querySelector('.interim_span') - const treeVoiceControlElement = TreeVoiceControl(voiceResponse, (action, targetElement) => { + const startClickHandler = targetElement => { + finalSpan.innerHTML = '' + interimSpan.innerHTML = '' + resultSpan.innerHTML = '' + rowElement.querySelectorAll('.target-text').forEach(el => el.classList.add('selected')) + voiceResponse.onResult(args => { + finalSpan.innerHTML = args.args?.result + interimSpan.innerHTML = args.args?.interim + }) + } - if (action === 'start') { - finalSpan.innerHTML = '' - interimSpan.innerHTML = '' - resultSpan.innerHTML = '' + const stopClickHandler = targetElement => { - rowElement.querySelectorAll('.target-text').forEach(el => el.classList.add('selected')) + rowElement.querySelectorAll('.target-text').forEach(el => el.classList.remove('selected')) - voiceResponse.onResult(args => { - finalSpan.innerHTML = args.args?.result - interimSpan.innerHTML = args.args?.interim - }) + const userResponse = finalSpan.innerHTML + if (!userResponse) { + return } - if (action === 'stop') { - rowElement.querySelectorAll('.target-text').forEach(el => el.classList.remove('selected')) + const rootElement = targetElement.closest('.node-row') + const backdrop = createRewriteContent('Обработка ответа...') + rootElement.appendChild(backdrop.getElement()) + + sendMessage( + `/admin/index.php?r=gpt/stream/retelling-rewrite`, + { + userResponse, + slideTexts: listItem.title + }, + (message) => resultSpan.innerText = message, + (error) => console.log('error', error), + () => { + if (resultSpan.innerText.length === 0) { + backdrop.remove() + return + } + retellingResponseSpan.innerText = '' + sendMessage(`/admin/index.php?r=gpt/stream/retelling`, { + userResponse: resultSpan.innerText, + slideTexts: listItem.title + }, + (message) => retellingResponseSpan.innerText = message, + (error) => console.log('error', error), + () => { - const userResponse = finalSpan.innerHTML - if (!userResponse) { - return - } + const content = rowElement.querySelector('.node-title').innerHTML - const rootElement = targetElement.closest('.node-row') - const backdrop = createRewriteContent('Обработка ответа...') - rootElement.appendChild(backdrop.getElement()) - - sendMessage( - `/admin/index.php?r=gpt/stream/retelling-rewrite`, - { - userResponse, - slideTexts: listItem.title - }, - (message) => resultSpan.innerText = message, - (error) => console.log('error', error), - () => { - if (resultSpan.innerText.length === 0) { backdrop.remove() - return - } - retellingResponseSpan.innerText = '' - sendMessage(`/admin/index.php?r=gpt/stream/retelling`, { - userResponse: resultSpan.innerText, - slideTexts: listItem.title - }, - (message) => retellingResponseSpan.innerText = message, - (error) => console.log('error', error), - () => { - backdrop.remove() - - const json = processOutputAsJson(retellingResponseSpan.innerText) - if (json === null) { - console.log('no json') - return - } - const val = Number(json?.overall_similarity) - const historyItem = history.find(i => i.id === nodeId) - if (val > 50) { - nodeStatusElement.innerHTML = nodeStatusSuccessHtml + const json = processOutputAsJson(retellingResponseSpan.innerText) + if (json === null) { + console.log('no json') + return + } + const val = Number(json?.overall_similarity) - if (historyItem) { - historyItem.done = true - } else { - history.push({id: nodeId, done: true}) - } + const historyItem = history.find(i => i.id === nodeId) + if (val > 50) { + nodeStatusElement.innerHTML = nodeStatusSuccessHtml - processTreeNodes(list, body, history, voiceResponse) + if (historyItem) { + historyItem.done = true } else { - if (historyItem) { - historyItem.done = false - } else { - history.push({id: nodeId, done: false}) - } - nodeStatusElement.innerHTML = nodeStatusFailedHtml + history.push({id: nodeId, done: true}) } + + processTreeNodes(list, body, history, voiceResponse, params) + } else { + if (historyItem) { + historyItem.done = false + } else { + history.push({id: nodeId, done: false}) + } + nodeStatusElement.innerHTML = nodeStatusFailedHtml } - ) - } - ) - } - }) + + saveUserResult({ + ...params, + image_fragment_id: nodeId, + overall_similarity: Number(json?.overall_similarity), + text_hiding_percentage: 0, // textHidingPercentage, + text_target_percentage: 0, // textTargetPercentage, + content, + }).then(response => { + if (response && response?.success) { + historyItem.all = response.history.all + historyItem.hiding = response.history.hiding + historyItem.target = response.history.target + } + }) + } + ) + } + ) + } + + const treeVoiceControlElement = TreeVoiceControl(voiceResponse, startClickHandler, stopClickHandler) + rowElement.querySelector('.node-control').appendChild(treeVoiceControlElement) if (!rowElement.checkVisibility()) { @@ -201,22 +218,6 @@ function processTreeNodes(list, body, history, voiceResponse) { } break - - /*} else { - nodeStatusElement.innerHTML = historyItem.done ? nodeStatusSuccessHtml : nodeStatusFailedHtml - - if (!historyItem.done) { - - const treeVoiceControlElement = TreeVoiceControl(voiceResponse, (action, targetElement) => { - historyItem.done = true - processTreeNodes(list, body, history, voiceResponse) - }) - - rowElement.querySelector('.node-control').appendChild(treeVoiceControlElement) - - break - } - }*/ } } @@ -227,7 +228,7 @@ function flatten(nodes, level = 0) { ]) } -export default function TreeViewBody(tree, voiceResponse, history) { +export default function TreeViewBody(tree, voiceResponse, history, params) { const body = document.createElement('div') body.classList.add('tree-body') @@ -237,8 +238,8 @@ export default function TreeViewBody(tree, voiceResponse, history) { body.appendChild(createRow(node)) }) - const sortedList = [...list].sort((a, b) => a.level - b.level) - processTreeNodes(sortedList, body, history, voiceResponse) + //const sortedList = [...list].sort((a, b) => a.level - b.level) + processTreeNodes(list, body, history, voiceResponse, params) return body } @@ -284,3 +285,19 @@ function resetNodeRow(row) { .map(s => voiceResponseElem.querySelector(`${s}`).innerHTML = '') row.querySelector('.node-control').innerHTML = '' } + +async function saveUserResult(payload) { + const response = await fetch(`/mental-map/save`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-Token': $('meta[name=csrf-token]').attr('content') + }, + body: JSON.stringify(payload), + }) + if (!response.ok) { + throw new Error(response.statusText) + } + return await response.json() +} diff --git a/asset/mental_map_quiz/TreeView/TreeVoiceControl.js b/asset/mental_map_quiz/TreeView/TreeVoiceControl.js index a518d575..3649986f 100644 --- a/asset/mental_map_quiz/TreeView/TreeVoiceControl.js +++ b/asset/mental_map_quiz/TreeView/TreeVoiceControl.js @@ -4,7 +4,7 @@ * @returns {HTMLDivElement} * @constructor */ -export default function TreeVoiceControl(voiceResponse, clickHandler) { +export default function TreeVoiceControl(voiceResponse, startClickHandler, stopClickHandler) { const elem = document.createElement('div') elem.classList.add('question-voice') elem.style.bottom = '0' @@ -30,10 +30,10 @@ export default function TreeVoiceControl(voiceResponse, clickHandler) { voiceResponse.stop((args) => { elem.querySelector('.gn').classList.remove('recording') elem.querySelector('.pulse-ring').remove() - clickHandler('stop', e.target) + stopClickHandler(e.target) }) } else { - clickHandler('start', e.target) + startClickHandler(e.target) setTimeout(() => { voiceResponse.start(new Event('voiceResponseStart'), 'ru-RU', function () { const ring = document.createElement('div') diff --git a/backend/MentalMap/MentalMap.php b/backend/MentalMap/MentalMap.php index 63fc4dfd..724e54c3 100644 --- a/backend/MentalMap/MentalMap.php +++ b/backend/MentalMap/MentalMap.php @@ -53,6 +53,38 @@ public function getImages(): array return $this->payload['map']['images'] ?? []; } + public function isMentalMapAsTree(): bool + { + return $this->payload['treeView'] ?? false; + } + + public function getTreeData(): array + { + return $this->payload['treeData'] ?? []; + } + + private function flatten(array $element): array + { + $flatArray = []; + foreach ($element as $key => $node) { + if (array_key_exists('children', $node)) { + $flatArray = array_merge($flatArray, $this->flatten($node['children'] ?? [])); + unset($node['children']); + } + $flatArray[] = $node; + } + return $flatArray; + } + + public function getItems(): array + { + $items = $this->getImages(); + if ($this->isMentalMapAsTree()) { + return $this->flatten($this->getTreeData()); + } + return $items; + } + public function updateMapText(string $text): void { $payload = $this->payload; diff --git a/backend/views/mental-map-history/index.php b/backend/views/mental-map-history/index.php index 0de0de3a..0a1554e3 100644 --- a/backend/views/mental-map-history/index.php +++ b/backend/views/mental-map-history/index.php @@ -81,7 +81,7 @@ class="h2">title ?> getImages() as $image): ?> + foreach ($mentalMap->getItems() as $image): ?> - + diff --git a/frontend/MentalMap/MentalMap.php b/frontend/MentalMap/MentalMap.php index beb5053e..0059a800 100644 --- a/frontend/MentalMap/MentalMap.php +++ b/frontend/MentalMap/MentalMap.php @@ -29,8 +29,18 @@ public static function isDone(array $history): bool if (count($history) === 0) { return false; } - return array_reduce($history, static function(bool $carry, array $item): bool { + return array_reduce($history, static function (bool $carry, array $item): bool { return $carry && (int) $item['all'] > 0; }, true); } + + public function isMentalMapAsTree(): bool + { + return $this->payload['treeView'] ?? false; + } + + public function getTreeData(): array + { + return $this->payload['treeData'] ?? []; + } } diff --git a/frontend/controllers/MentalMapController.php b/frontend/controllers/MentalMapController.php index deb0d890..4c8a268a 100644 --- a/frontend/controllers/MentalMapController.php +++ b/frontend/controllers/MentalMapController.php @@ -79,17 +79,37 @@ public function actionInit(Request $request, Response $response, WebUser $user): $repetitionMode = $rawBody['repetition_mode'] ?? false; - if ($repetitionMode) { - $history = array_map(static function (array $image): array { + + //$treeHistory = $this->createMentalMapTreeHistory($list, $mentalMap->uuid, $user->getId()); + //die(print_r($treeHistory)); + + if ($mentalMap->isMentalMapAsTree()) { + $list = $this->flatten($mentalMap->getTreeData()); + if (!$repetitionMode) { + $list = $this->createMentalMapTreeHistory($list, $mentalMap->uuid, $user->getId()); + } + $history = array_map(static function (array $item): array { return [ - 'id' => $image['id'], - 'all' => 0, - 'hiding' => 0, - 'target' => 0, + 'id' => $item['id'], + 'done' => $item['done'] ?? false, + //'all' => $item['all'] ?? 0, + //'hiding' => $item['hiding'] ?? 0, + //'target' => $item['target'] ?? 0, ]; - }, $mentalMap->getImages()); + }, $list); } else { - $history = $this->createMentalMapHistory($mentalMap->getImages(), $mentalMap->uuid, $user->getId()); + $items = $mentalMap->getImages(); + if (!$repetitionMode) { + $items = $this->createMentalMapHistory($items, $mentalMap->uuid, $user->getId()); + } + $history = array_map(static function (array $item): array { + return [ + 'id' => $item['id'], + 'all' => $item['all'] ?? 0, + 'hiding' => $item['hiding'] ?? 0, + 'target' => $item['target'] ?? 0, + ]; + }, $items); } $prompt = null; @@ -228,6 +248,42 @@ private function createMentalMapHistory(array $images, string $mentalMapId, int }, $history); } + private function createMentalMapTreeHistory(array $nodeList, string $mentalMapId, int $userId): array + { + $history = array_map(static function (array $node): array { + return [ + 'id' => $node['id'], + 'done' => false, + ]; + }, $nodeList); + + $rows = (new Query()) + ->select([ + 'id' => 'h.image_fragment_id', + 'all' => 'MAX(h.overall_similarity)', + 'hiding' => 'MAX(h.text_hiding_percentage)', + 'target' => 'MAX(h.text_target_percentage)', + ]) + ->from(['h' => 'mental_map_history']) + ->where([ + 'h.mental_map_id' => $mentalMapId, + 'h.user_id' => $userId, + ]) + ->groupBy('h.image_fragment_id') + ->indexBy('id') + ->all(); + + return array_map(static function (array $item) use ($rows): array { + if (isset($rows[$item['id']])) { + $item['done'] = (int) $rows[$item['id']]['all'] > 50; + $item['all'] = (int) $rows[$item['id']]['all']; + $item['hiding'] = (int) $rows[$item['id']]['hiding']; + $item['target'] = (int) $rows[$item['id']]['target']; + } + return $item; + }, $history); + } + public function actionFinish(Request $request, Response $response, WebUser $user): array { $response->format = Response::FORMAT_JSON; @@ -263,4 +319,17 @@ public function actionUpdateRewritePrompt(Request $request, Response $response): return ['success' => true]; } + + private function flatten(array $element): array + { + $flatArray = []; + foreach ($element as $key => $node) { + if (array_key_exists('children', $node)) { + $flatArray = array_merge($flatArray, $this->flatten($node['children'] ?? [])); + unset($node['children']); + } + $flatArray[] = $node; + } + return $flatArray; + } }