} the async promise\n */\n async renderModalContent(bodyComponentTemplate, footerComponentTemplate, templateContext) {\n templateContext.tinyinstanceuniqid = this.uniqid;\n const modal = getEditorUtils(this.uniqid).getModal();\n // Remove all eventually remaining tooltips before rendering a new view.\n document.querySelectorAll('button[data-action]').forEach(button => {\n $(button).tooltip('hide');\n });\n const result = await Promise.all([\n Templates.renderForPromise('tiny_ai/components/moodle-modal-header-title', templateContext),\n Templates.renderForPromise('tiny_ai/components/' + bodyComponentTemplate, templateContext),\n Templates.renderForPromise('tiny_ai/components/' + footerComponentTemplate, templateContext)\n ]);\n if (templateContext.hasOwnProperty('modal_headline')) {\n // If there is no headline specified, we keep the old one.\n modal.setTitle(result[0].html);\n }\n // Hide all eventually still existing tooltips first, because they show on 'hover' and\n // 'focus'. So we need to remove them before removing the corresponding buttons from the DOM.\n // Boostrap 4 still using jQuery for tooltips, so we need jQuery here.\n document.querySelectorAll('button[data-action]').forEach(button => {\n $(button).tooltip('hide');\n });\n modal.setBody(result[1].html);\n modal.setFooter(result[2].html);\n result.forEach((item) => {\n Templates.runTemplateJS(item.js);\n });\n modal.getRoot().attr('data-tiny_ai_uniqid', this.uniqid);\n await this.insertInfoBox();\n await this.insertUserQuotaBox();\n document.querySelectorAll('button[data-action]').forEach(button => {\n button.addEventListener('click', event => {\n $(event.target).closest('button[data-action]').tooltip('hide');\n });\n });\n }\n\n async insertInfoBox() {\n const infoBoxSelector = '[data-rendertarget=\"infobox\"]';\n if (document.querySelector(infoBoxSelector)) {\n await renderInfoBox('tiny_ai', getEditorUtils(this.uniqid).getUserId(), infoBoxSelector,\n ['singleprompt', 'translate', 'tts', 'imggen']);\n }\n }\n\n async insertUserQuotaBox() {\n const usageBoxSelector = '[data-rendertarget=\"usageinfo\"]';\n if (document.querySelector(usageBoxSelector)) {\n await renderUserQuota(usageBoxSelector, ['singleprompt', 'translate', 'tts', 'imggen']);\n }\n }\n}\n"],"names":["constructor","uniqid","datamanager","editorUtils","reset","templateContext","this","getTemplateContext","renderModalContent","modal_headline","BasedataHandler","getTinyAiString","result_text","renderAiResultForEditor","Object","assign","getReplaceButtonsContext","getMode","centered_headline","showIcon","buttons","hasText","button_text","icon_left","icon_right","primary","secondary","tertiary","action","html","getCurrentTool","audioPlayer","document","createElement","controls","src","getCurrentAiResult","type","outerHTML","img","classList","add","bodyComponentTemplate","footerComponentTemplate","tinyinstanceuniqid","modal","getModal","querySelectorAll","forEach","button","tooltip","result","Promise","all","Templates","renderForPromise","hasOwnProperty","setTitle","setBody","setFooter","item","runTemplateJS","js","getRoot","attr","insertInfoBox","insertUserQuotaBox","addEventListener","event","target","closest","querySelector","getUserId"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/constants.js b/amd/src/constants.js
index 361b43d..8d91b63 100644
--- a/amd/src/constants.js
+++ b/amd/src/constants.js
@@ -36,6 +36,7 @@ export const constants = {
imggen: 'imggen',
tts: 'tts',
freeprompt: 'singleprompt',
- describeimg: 'itt'
+ describeimg: 'itt',
+ imagetotext: 'itt'
}
};
diff --git a/amd/src/controllers/base.js b/amd/src/controllers/base.js
index 4dc2ab9..eaa6898 100644
--- a/amd/src/controllers/base.js
+++ b/amd/src/controllers/base.js
@@ -56,6 +56,11 @@ export default class {
await errorAlert(BasedataHandler.getTinyAiString('error_nopromptgiven'));
return null;
}
+ if (['describeimg', 'imagetotext'].includes(this.datamanager.getCurrentTool())
+ && this.datamanager.getCurrentFile() === null) {
+ await errorAlert(BasedataHandler.getTinyAiString('error_nofile'));
+ return null;
+ }
await this.renderer.renderLoading();
let result = null;
try {
diff --git a/amd/src/controllers/file.js b/amd/src/controllers/file.js
new file mode 100644
index 0000000..b05cbdb
--- /dev/null
+++ b/amd/src/controllers/file.js
@@ -0,0 +1,121 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Controller for handling the show/hide prompt button and the associated textarea.
+ *
+ * @module tiny_ai/controllers/dnd
+ * @copyright 2024, ISB Bayern
+ * @author Philipp Memmel
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {getDatamanager, getCurrentModalUniqId} from 'tiny_ai/utils';
+import Templates from 'core/templates';
+import SELECTORS from 'tiny_ai/selectors';
+import {errorAlert} from 'tiny_ai/utils';
+import * as BasedataHandler from 'tiny_ai/datahandler/basedata';
+
+
+export default class {
+
+ dropzone = null;
+
+ constructor(baseSelector) {
+ this.baseElement = document.querySelector(baseSelector);
+ }
+
+ async init() {
+ this.dropzone = this.baseElement.querySelector('[data-type="dropzone"]');
+ const dropzone = this.dropzone;
+ // Setting contentEditable to true makes the browser show a "paste" option in the context menu when
+ // right-clicking the drop zone.
+ dropzone.contentEditable = true;
+
+ dropzone.addEventListener('drop', async(event) => {
+ event.preventDefault();
+ dropzone.classList.remove('tiny_ai_dragover');
+ dropzone.classList.add('tiny_ai_dropzone_filled');
+
+ if (event.dataTransfer.items) {
+ // Use DataTransferItemList interface to access the file(s)
+ const item = [...event.dataTransfer.items].shift();
+ // If dropped item is no file, reject it.
+ if (item.kind === 'file') {
+ await this.handleFile(item.getAsFile());
+ }
+ } else {
+ // Use DataTransfer interface to access the file(s)
+ await this.handleFile([...event.dataTransfer.files].shift());
+ }
+ });
+ document.querySelector(SELECTORS.modalDialog).addEventListener('paste', async(event) => {
+ event.preventDefault();
+ const clipboardData = (event.clipboardData || window.clipboardData);
+ console.log(clipboardData)
+ if (clipboardData.files.length === 0) {
+ await errorAlert(BasedataHandler.getTinyAiString('error_nofileinclipboard_text'),
+ BasedataHandler.getTinyAiString('error_nofileinclipboard_title'));
+ return;
+ }
+ // There should be no tiny_ai_dragover class, just to be safe.
+ dropzone.classList.remove('tiny_ai_dragover');
+ dropzone.classList.add('tiny_ai_dropzone_filled');
+ const file = clipboardData.files[0];
+ this.handleFile(file);
+ });
+ dropzone.addEventListener('dragover', (event) => {
+ event.preventDefault();
+ console.log(event)
+ dropzone.classList.remove('tiny_ai_dropzone_filled');
+ dropzone.classList.add('tiny_ai_dragover');
+ });
+ dropzone.addEventListener('dragleave', (event) => {
+ event.preventDefault();
+ console.log(event)
+ dropzone.classList.remove('tiny_ai_dragover');
+ });
+ }
+
+ async handleFile(file) {
+ const reader = new FileReader();
+ console.log(file)
+ reader.addEventListener(
+ 'load',
+ async() => {
+ const datamanager = getDatamanager(getCurrentModalUniqId(this.baseElement));
+ const fileUploadedEvent = new CustomEvent('fileUploaded', {
+ detail: {
+ newFile: reader.result
+ }
+ });
+ datamanager.getEventEmitterElement().dispatchEvent(fileUploadedEvent);
+ const fileEntryTemplateContext = {
+ icon: file.type === 'application/pdf' ? 'fa-file-pdf' : 'fa-image',
+ filename: file.name,
+ };
+ if (file.type.startsWith('image')) {
+ fileEntryTemplateContext.isImage = true;
+ fileEntryTemplateContext.dataurl = reader.result;
+ }
+ const {html, js} = await Templates.renderForPromise('tiny_ai/components/ai-file-list-entry',
+ fileEntryTemplateContext);
+ Templates.replaceNodeContents(this.dropzone, html, js);
+ },
+ false,
+ );
+ reader.readAsDataURL(file);
+ }
+}
diff --git a/amd/src/controllers/filednd.js b/amd/src/controllers/filednd.js
deleted file mode 100644
index 28b64df..0000000
--- a/amd/src/controllers/filednd.js
+++ /dev/null
@@ -1,73 +0,0 @@
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle. If not, see .
-
-/**
- * Controller for handling the show/hide prompt button and the associated textarea.
- *
- * @module tiny_ai/controllers/dnd
- * @copyright 2024, ISB Bayern
- * @author Philipp Memmel
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-import {getStrings} from 'core/str';
-import {getDatamanager, getCurrentModalUniqId, getIttHandler} from 'tiny_ai/utils';
-
-export default class {
-
- constructor(baseSelector) {
- this.baseElement = document.querySelector(baseSelector);
- }
-
- async init() {
- const dropzone = this.baseElement.querySelector('[data-type="dropzone"]');
- dropzone.addEventListener('drop', (event) => {
- event.preventDefault();
- if (event.dataTransfer.items) {
- // Use DataTransferItemList interface to access the file(s)
- const item = [...event.dataTransfer.items].shift();
- // If dropped item is no file, reject it.
- if (item.kind === 'file') {
- this.handleFile(item);
- }
- } else {
- // Use DataTransfer interface to access the file(s)
- this.handleFile([...event.dataTransfer.files].shift());
- }
- });
- dropzone.addEventListener('dragover', (event) => {
- event.preventDefault();
- });
- }
-
- handleFile(file) {
- file = file.getAsFile();
- const reader = new FileReader();
- reader.addEventListener(
- 'load',
- () => {
- const datamanager = getDatamanager(getCurrentModalUniqId(this.baseElement));
- const fileUploadedEvent = new CustomEvent('fileUploaded', {
- detail: {
- newFile: reader.result
- }
- });
- datamanager.getEventEmitterElement().dispatchEvent(fileUploadedEvent);
- },
- false,
- );
- reader.readAsDataURL(file);
- }
-}
diff --git a/amd/src/controllers/preferences.js b/amd/src/controllers/preferences.js
index d1ba332..5f06de4 100644
--- a/amd/src/controllers/preferences.js
+++ b/amd/src/controllers/preferences.js
@@ -121,7 +121,8 @@ export default class extends BaseController {
this.datamanager.setCurrentOptions(imggenHandler.getOptions());
break;
}
- case 'describeimg': {
+ case 'describeimg':
+ case 'imagetotext': {
const fileUploadArea = this.baseElement.querySelector('[data-preference="fileupload"]');
if (fileUploadArea) {
this.datamanager.getEventEmitterElement().addEventListener('fileUploaded', event => {
@@ -129,7 +130,7 @@ export default class extends BaseController {
this.datamanager.setCurrentOptions(ittHandler.getOptions());
});
}
- this.datamanager.setCurrentPrompt('');
+ this.datamanager.setCurrentPrompt(ittHandler.getPrompt(this.datamanager.getCurrentTool()));
this.datamanager.setCurrentFile(null);
break;
}
diff --git a/amd/src/controllers/start.js b/amd/src/controllers/start.js
index 71f6233..22819e1 100644
--- a/amd/src/controllers/start.js
+++ b/amd/src/controllers/start.js
@@ -43,6 +43,7 @@ export default class extends BaseController {
const imggenButton = this.baseElement.querySelector('[data-action="loadimggen"]');
const freePromptButton = this.baseElement.querySelector('[data-action="loadfreeprompt"]');
const describeimgButton = this.baseElement.querySelector('[data-action="loaddescribeimg"]');
+ const imagetotextButton = this.baseElement.querySelector('[data-action="loadimagetotext"]');
const startHandler = getStartHandler(this.uniqid);
@@ -100,6 +101,12 @@ export default class extends BaseController {
await this.renderer.renderDescribeimg();
});
}
+ if (imagetotextButton) {
+ imagetotextButton.addEventListener('click', async() => {
+ this.datamanager.setCurrentTool('imagetotext');
+ await this.renderer.renderImagetotext();
+ });
+ }
if (freePromptButton) {
if (!freePromptButton.classList.contains('disabled')) {
freePromptButton.addEventListener('click', async () => {
diff --git a/amd/src/datahandler/basedata.js b/amd/src/datahandler/basedata.js
index d15ba9f..243ba7e 100644
--- a/amd/src/datahandler/basedata.js
+++ b/amd/src/datahandler/basedata.js
@@ -35,10 +35,15 @@ const stringKeys = [
'backbutton_tooltip',
'cancel',
'deletebutton_tooltip',
+ 'describeimg_baseprompt',
+ 'describeimg_headline',
'describe_baseprompt',
'describe_headline',
'dismiss',
'dismisssuggestion',
+ 'error_nofile',
+ 'error_nofileinclipboard_text',
+ 'error_nofileinclipboard_title',
'error_nopromptgiven',
'freeprompt_placeholder',
'freepromptbutton_tooltip',
@@ -49,6 +54,9 @@ const stringKeys = [
'hideprompt',
'imggen_headline',
'imggen_placeholder',
+ 'imagetotext_baseprompt',
+ 'imagetotext_headline',
+ 'imagetotext_insertimage',
'insertatcaret',
'insertatcaretbutton_tooltip',
'insertbelow',
@@ -75,8 +83,10 @@ const stringKeys = [
'texttouse',
'toolname_audiogen',
'toolname_describe',
+ 'toolname_describeimg',
'toolname_describe_extension',
'toolname_imggen',
+ 'toolname_imagetotext',
'toolname_summarize',
'toolname_summarize_extension',
'toolname_translate',
@@ -110,10 +120,15 @@ export const init = async() => {
strings.backbutton_tooltip,
strings.cancel,
strings.deletebutton_tooltip,
+ strings.describeimg_baseprompt,
+ strings.describeimg_headline,
strings.describe_baseprompt,
strings.describe_headline,
strings.dismiss,
strings.dismisssuggestion,
+ strings.error_nofile,
+ strings.error_nofileinclipboard_text,
+ strings.error_nofileinclipboard_title,
strings.error_nopromptgiven,
strings.freeprompt_placeholder,
strings.freepromptbutton_tooltip,
@@ -124,6 +139,9 @@ export const init = async() => {
strings.hideprompt,
strings.imggen_headline,
strings.imggen_placeholder,
+ strings.imagetotext_baseprompt,
+ strings.imagetotext_headline,
+ strings.imagetotext_insertimage,
strings.insertatcaret,
strings.insertatcaretbutton_tooltip,
strings.insertbelow,
@@ -150,8 +168,10 @@ export const init = async() => {
strings.texttouse,
strings.toolname_audiogen,
strings.toolname_describe,
+ strings.toolname_describeimg,
strings.toolname_describe_extension,
strings.toolname_imggen,
+ strings.toolname_imagetotext,
strings.toolname_summarize,
strings.toolname_summarize_extension,
strings.toolname_translate,
diff --git a/amd/src/datahandler/itt.js b/amd/src/datahandler/itt.js
index e10d2a8..f05e4cd 100644
--- a/amd/src/datahandler/itt.js
+++ b/amd/src/datahandler/itt.js
@@ -37,10 +37,19 @@ export default class extends BaseHandler {
return options;
}
+ /**
+ * Get the prompt.
+ *
+ * @param {string} tool the tool to generate the prompt for, can be 'describeimage' and 'imagetotext'
+ */
+ getPrompt(tool) {
+ return BasedataHandler.getTinyAiString(tool + '_baseprompt');
+ }
+
/**
* Get the rendering context.
*
- * @param {string} tool the tool to generate the context for, can be 'describeimage' and 'analyzehandwriting'
+ * @param {string} tool the tool to generate the context for, can be 'describeimage' and 'imagetotext'
*/
async getTemplateContext(tool) {
const context = {
@@ -49,6 +58,7 @@ export default class extends BaseHandler {
tool: tool,
textareatype: 'prompt',
placeholder: BasedataHandler.getTinyAiString(tool + '_placeholder'),
+ insertimagedescription: BasedataHandler.getTinyAiString('imagetotext_insertimage')
};
Object.assign(context, BasedataHandler.getShowPromptButtonContext());
diff --git a/amd/src/datahandler/start.js b/amd/src/datahandler/start.js
index ef20514..0e92373 100644
--- a/amd/src/datahandler/start.js
+++ b/amd/src/datahandler/start.js
@@ -213,12 +213,23 @@ export default class extends BaseHandler {
toolname: 'describeimg',
tool: BasedataHandler.getTinyAiString('toolname_describeimg'),
iconstyle: 'solid',
- iconname: 'image',
+ iconname: 'file-image',
disabled: this.isToolDisabled('describeimg', mode).length > 0,
tooltip: stripHtmlTags(this.isToolDisabled('describeimg', mode)),
action: 'loaddescribeimg'
});
}
+ if (!this.isToolHidden('imagetotext')) {
+ toolButtons.push({
+ toolname: 'imagetotext',
+ tool: BasedataHandler.getTinyAiString('toolname_imagetotext'),
+ iconstyle: 'solid',
+ iconname: 'signature',
+ disabled: this.isToolDisabled('imagetotext', mode).length > 0,
+ tooltip: stripHtmlTags(this.isToolDisabled('imagetotext', mode)),
+ action: 'loadimagetotext'
+ });
+ }
// We sort the not disabled tools to the top while keeping the groups "disabled tools" and "not disabled tools"
// in the same order inside the groups.
toolButtons.sort((a, b) => {
diff --git a/amd/src/renderer.js b/amd/src/renderer.js
index 61ad645..33fa09b 100644
--- a/amd/src/renderer.js
+++ b/amd/src/renderer.js
@@ -91,7 +91,12 @@ export default class {
}
async renderDescribeimg() {
- const templateContext = await getIttHandler(this.uniqid).getTemplateContext();
+ const templateContext = await getIttHandler(this.uniqid).getTemplateContext('describeimg');
+ await this.renderModalContent('moodle-modal-body-itt', 'moodle-modal-footer-generate', templateContext);
+ }
+
+ async renderImagetotext() {
+ const templateContext = await getIttHandler(this.uniqid).getTemplateContext('imagetotext');
await this.renderModalContent('moodle-modal-body-itt', 'moodle-modal-footer-generate', templateContext);
}
diff --git a/lang/de/tiny_ai.php b/lang/de/tiny_ai.php
index 7df0ef5..aea4ccd 100644
--- a/lang/de/tiny_ai.php
+++ b/lang/de/tiny_ai.php
@@ -38,9 +38,14 @@
$string['deletebutton_tooltip'] = 'Aktuelles Ergebnis verwerfen und zurück zur Einstellungsseite';
$string['describe_headline'] = 'Ausführliche Beschreibung des markierten Texts';
$string['describe_baseprompt'] = 'Beschreibe den nachfolgenden Text';
+$string['describeimg_headline'] = 'Bildanalyse';
+$string['describeimg_baseprompt'] = 'Beschreibe, was auf dem Bild zu sehen ist';
$string['dismiss'] = 'Verwerfen';
$string['dismisssuggestion'] = 'Möchten Sie den KI-Vorschlag verwerfen?';
$string['errorwithcode'] = 'Ein Fehler ist aufgetreten, Fehlercode: {$a}';
+$string['error_nofile'] = 'Keine Datei eingefügt. Bitte Datei hinzufügen.';
+$string['error_nofileinclipboard_text'] = 'Die Zwischenablage enthält keine Datei. Bitte eine Datei/einen Screenshot in die Zwischenablage einfügen.';
+$string['error_nofileinclipboard_title'] = 'Keine Datei';
$string['error_nopromptgiven'] = 'Kein Prompt angegeben. Bitte einen Prompt eintippen oder einfügen.';
$string['error_tiny_ai_notavailable'] = 'Die KI-Funktionen stehen Ihnen nicht zur Verfügung.';
$string['freepromptbutton_tooltip'] = 'Generiere KI-Antwort';
@@ -53,6 +58,9 @@
$string['hideprompt'] = 'Prompt ausblenden';
$string['imggen_headline'] = 'Bildgenerierung';
$string['imggen_placeholder'] = 'Beschreibung des Bilds hier eingeben oder einfügen, z. B. "Generiere ein fotorealistisches Bild eines Affen mit einem Bleistift in der Hand und einem Hut auf dem Kopf"';
+$string['imagetotext_headline'] = 'Handschrifterkennung';
+$string['imagetotext_baseprompt'] = 'Gib den Text auf dem Bild zurück';
+$string['imagetotext_insertimage'] = 'Ziehen Sie eine Datei per Drag&Drop in diesen Bereich oder fügen Sie sie aus der Zwischenablage ein';
$string['insertatcaret'] = 'An aktueller Position einfügen';
$string['insertatcaret_tooltip'] = 'Aktuelles Ergebnis an der aktuellen Position des Cursors einfügen';
$string['insertbelow'] = 'Unten einfügen';
@@ -88,7 +96,9 @@
$string['toolname_audiogen'] = 'Audiogenerierung';
$string['toolname_describe'] = 'Ausführliche Beschreibung';
$string['toolname_describe_extension'] = 'des markierten Textes';
+$string['toolname_describeimg'] = 'Bildanalyse';
$string['toolname_imggen'] = 'Bildgenerierung';
+$string['toolname_imagetotext'] = 'Handschrifterkennung';
$string['toolname_summarize'] = 'Zusammenfassen';
$string['toolname_summarize_extension'] = 'des markierten Textes';
$string['toolname_translate'] = 'Übersetzen';
diff --git a/lang/en/tiny_ai.php b/lang/en/tiny_ai.php
index d357e99..c1c9e09 100644
--- a/lang/en/tiny_ai.php
+++ b/lang/en/tiny_ai.php
@@ -37,10 +37,15 @@
$string['cancel'] = 'Cancel';
$string['deletebutton_tooltip'] = 'Dismiss current result and go back to the preferences page';
$string['describe_headline'] = 'Detailed description of the selected text';
+$string['describe_baseprompt'] = 'Describe the following text';
+$string['describeimg_headline'] = 'Image analysis';
+$string['describeimg_baseprompt'] = 'Describe what is being shown on the image';
$string['dismiss'] = 'Dismiss';
$string['dismisssuggestion'] = 'Do you want to dismiss the AI suggestion?';
-$string['describe_baseprompt'] = 'Describe the following text';
$string['errorwithcode'] = 'An error occured with error code {$a}';
+$string['error_nofile'] = 'No file added. Please add a file.';
+$string['error_nofileinclipboard_text'] = 'Clipboard does not contain file data. Please copy a file into the clipboard before pasting.';
+$string['error_nofileinclipboard_title'] = 'No file';
$string['error_nopromptgiven'] = 'No prompt given. Please insert a prompt.';
$string['error_tiny_ai_notavailable'] = 'The AI features are not availeble for you.';
$string['freepromptbutton_tooltip'] = 'Generate AI answer';
@@ -53,6 +58,9 @@
$string['hideprompt'] = 'Hide prompt';
$string['imggen_headline'] = 'Image generation';
$string['imggen_placeholder'] = 'Insert or paste description of the image here, for example "Generate a photorealistic image of a monkey that holds a pen in one hand and wears a hat on the head"';
+$string['imagetotext_headline'] = 'Handwriting recognition';
+$string['imagetotext_baseprompt'] = 'Parse the text in the image';
+$string['imagetotext_insertimage'] = 'Drag&Drop a file into this area or paste a file from clipboard';
$string['insertatcaret'] = 'Insert at current position';
$string['insertatcaret_tooltip'] = 'Insert current result at the cursor\'s current position';
$string['insertbelow'] = 'Insert below';
@@ -88,7 +96,9 @@
$string['toolname_audiogen'] = 'Audio generation';
$string['toolname_describe'] = 'Detailed description';
$string['toolname_describe_extension'] = 'of the selected text';
+$string['toolname_describeimg'] = 'Image analysis';
$string['toolname_imggen'] = 'Image generation';
+$string['toolname_imagetotext'] = 'Handwriting recognition';
$string['toolname_summarize'] = 'Summarize';
$string['toolname_summarize_extension'] = 'the selected text';
$string['toolname_translate'] = 'Translate';
diff --git a/scss/partials/ai-dropzone.scss b/scss/partials/ai-dropzone.scss
index ba0adb4..9f9a851 100644
--- a/scss/partials/ai-dropzone.scss
+++ b/scss/partials/ai-dropzone.scss
@@ -1,3 +1,30 @@
.tiny_ai_dropzone {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+ border-width: 2px;
+ border-radius: 2px;
+ border-color: #eeeeee;
+ border-style: dashed;
+ background-color: #fafafa;
+ color: #bdbdbd;
+ outline: none;
+ transition: border .24s ease-in-out;
height: 15rem;
}
+
+.tiny_ai_dragover {
+ background-color: #63676c;
+}
+
+.tiny_ai_dropzone_filled {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+.tiny_ai_dropzone_preview_container {
+ max-width: 80%;
+ max-height: 80%;
+}
diff --git a/styles.css b/styles.css
index 96bb27c..9c0cef8 100644
--- a/styles.css
+++ b/styles.css
@@ -568,9 +568,36 @@
}
.tiny_ai_dropzone {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+ border-width: 2px;
+ border-radius: 2px;
+ border-color: #eeeeee;
+ border-style: dashed;
+ background-color: #fafafa;
+ color: #bdbdbd;
+ outline: none;
+ transition: border 0.24s ease-in-out;
height: 15rem;
}
+.tiny_ai_dragover {
+ background-color: #63676c;
+}
+
+.tiny_ai_dropzone_filled {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+.tiny_ai_dropzone_preview_container {
+ max-width: 80%;
+ max-height: 80%;
+}
+
.tiny_ai-alert {
display: flex;
align-items: center;
diff --git a/templates/components/ai-file-list-entry.mustache b/templates/components/ai-file-list-entry.mustache
new file mode 100644
index 0000000..3bd4862
--- /dev/null
+++ b/templates/components/ai-file-list-entry.mustache
@@ -0,0 +1,6 @@
+
+
{{filename}}
+ {{#isImage}}
+
+ {{/isImage}}
+
diff --git a/templates/components/moodle-modal-body-itt.mustache b/templates/components/moodle-modal-body-itt.mustache
index 2bc21d3..92968a1 100644
--- a/templates/components/moodle-modal-body-itt.mustache
+++ b/templates/components/moodle-modal-body-itt.mustache
@@ -1,7 +1,7 @@