diff --git a/.dockerignore b/.dockerignore index 8def8deff..6caa290ef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,4 +12,7 @@ docs # exclude aux files .git -.idea \ No newline at end of file +.idea + +# exclude database dumps +db_dumps \ No newline at end of file diff --git a/.gitignore b/.gitignore index 74c59fa64..12d654fdc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ backend/sessions backend/node_modules backend/logs backend/coverage - +logs/ # ignore build files dist/ frontend/node_modules diff --git a/backend/db/.sequelizerc b/backend/db/.sequelizerc index 5d15bd0a3..a75031b2a 100644 --- a/backend/db/.sequelizerc +++ b/backend/db/.sequelizerc @@ -1,4 +1,5 @@ const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') }); module.exports = { 'config': path.resolve('./config', 'config.js'), diff --git a/backend/db/config/config.js b/backend/db/config/config.js index 950a86320..1142a3158 100644 --- a/backend/db/config/config.js +++ b/backend/db/config/config.js @@ -3,6 +3,7 @@ * * @author Nils Dycke */ + module.exports = { development: { username: 'postgres', diff --git a/backend/db/migrations/20260331100037-create-api_key.js b/backend/db/migrations/20260331100037-create-api_key.js new file mode 100644 index 000000000..562dfff5a --- /dev/null +++ b/backend/db/migrations/20260331100037-create-api_key.js @@ -0,0 +1,82 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;'); + + await queryInterface.createTable('ai_api_key', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + userId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'user', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + provider: { + type: Sequelize.STRING, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + apiEndpoint: { + type: Sequelize.STRING, + allowNull: true, + defaultValue: null, + }, + encryptedKey: { + type: Sequelize.BLOB, + allowNull: false, + }, + enabled: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + usageLimitMonthly: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + lastUsedAt: { + type: Sequelize.DATE, + allowNull: true, + defaultValue: null, + }, + deleted: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + defaultValue: null, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('ai_api_key'); + }, +}; diff --git a/backend/db/migrations/20260331101522-create-llm_provider.js b/backend/db/migrations/20260331101522-create-llm_provider.js new file mode 100644 index 000000000..0c31aa88e --- /dev/null +++ b/backend/db/migrations/20260331101522-create-llm_provider.js @@ -0,0 +1,55 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('ai_model', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + modelPath: { + type: Sequelize.STRING, + allowNull: false, + }, + apiBaseUrl: { + type: Sequelize.STRING, + allowNull: false, + }, + enabled: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + deleted: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + defaultValue: null, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('ai_model'); + }, +}; diff --git a/backend/db/migrations/20260331103048-create-llm_log.js b/backend/db/migrations/20260331103048-create-llm_log.js new file mode 100644 index 000000000..08ffde05f --- /dev/null +++ b/backend/db/migrations/20260331103048-create-llm_log.js @@ -0,0 +1,117 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('ai_log', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + userId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'user', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + apiKeyId: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + references: { + model: 'ai_api_key', + key: 'id', + }, + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }, + modelId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'ai_model', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + documentId: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + studySessionId: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + studyStepId: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + input: { + type: Sequelize.JSONB, + allowNull: true, + }, + output: { + type: Sequelize.JSONB, + allowNull: true, + }, + inputTokens: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + outputTokens: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + estimatedCost: { + type: Sequelize.FLOAT, + allowNull: true, + defaultValue: null, + }, + latencyMs: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + status: { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'success', + }, + deleted: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + defaultValue: null, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('ai_log'); + }, +}; diff --git a/backend/db/migrations/20260331104511-create-prompt_template.js b/backend/db/migrations/20260331104511-create-prompt_template.js new file mode 100644 index 000000000..b3c61bab0 --- /dev/null +++ b/backend/db/migrations/20260331104511-create-prompt_template.js @@ -0,0 +1,96 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('prompt_template', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + userId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'user', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + description: { + type: Sequelize.TEXT, + allowNull: true, + defaultValue: null, + }, + provider: { + type: Sequelize.STRING, + allowNull: true, + defaultValue: null, + }, + model: { + type: Sequelize.STRING, + allowNull: true, + defaultValue: null, + }, + promptText: { + type: Sequelize.TEXT, + allowNull: false, + }, + inputMapping: { + type: Sequelize.JSONB, + allowNull: true, + defaultValue: {}, + }, + defaultOutputMapping: { + type: Sequelize.JSONB, + allowNull: true, + defaultValue: {}, + }, + shared: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + sharedScope: { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'none', + }, + sharedTargetId: { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }, + deleted: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + defaultValue: null, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('prompt_template'); + }, +}; diff --git a/backend/db/migrations/20260331111539-basic-setting-llm.js b/backend/db/migrations/20260331111539-basic-setting-llm.js new file mode 100644 index 000000000..64eb5120d --- /dev/null +++ b/backend/db/migrations/20260331111539-basic-setting-llm.js @@ -0,0 +1,66 @@ +'use strict'; + +const settings = [ + { + key: 'service.llm.enabled', + value: 'true', + type: 'boolean', + description: 'Enable or disable the LLM service for direct API calls', + onlyAdmin: false, + }, + { + key: 'service.llm.defaultProvider', + value: 'openai', + type: 'string', + description: 'Default LLM provider slug when no user key is available', + onlyAdmin: true, + }, + { + key: 'service.llm.maxTokensPerRequest', + value: '4096', + type: 'number', + description: 'Maximum output tokens allowed per single LLM request', + onlyAdmin: true, + }, + { + key: 'service.llm.requestTimeout', + value: '120000', + type: 'number', + description: 'Timeout in ms for LLM API requests', + onlyAdmin: true, + }, + { + key: 'service.llm.systemApiKeyProvider', + value: '', + type: 'string', + description: 'System-level fallback API key provider slug (leave empty to disable)', + onlyAdmin: true, + }, + { + key: 'service.llm.systemApiKey', + value: '', + type: 'encrypted', + description: 'System-level fallback API key. Used when users have no personal key.', + onlyAdmin: true, + }, +]; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.bulkInsert( + 'setting', + settings.map((s) => ({ + ...s, + createdAt: new Date(), + updatedAt: new Date(), + })), + {} + ); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete('setting', { + key: settings.map((s) => s.key), + }, {}); + }, +}; diff --git a/backend/db/migrations/20260331113017-basic-llm_nav_and_rights.js b/backend/db/migrations/20260331113017-basic-llm_nav_and_rights.js new file mode 100644 index 000000000..7e107eb66 --- /dev/null +++ b/backend/db/migrations/20260331113017-basic-llm_nav_and_rights.js @@ -0,0 +1,76 @@ +'use strict'; + +const navElements = [ + { + name: 'API Keys', + groupId: 'Default', + icon: 'key', + order: 15, + admin: false, + path: 'api_keys', + component: 'LlmDashboard', + }, + { + name: 'Models', + groupId: 'Default', + icon: 'robot', + order: 16, + admin: false, + path: 'models', + component: 'LlmProviders', + }, +]; + +const userRights = [ + { + name: 'frontend.dashboard.api_keys.view', + description: 'Access to the API Keys dashboard', + }, + { + name: 'frontend.dashboard.models.view', + description: 'Access to the Models dashboard', + }, +]; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.bulkInsert( + 'user_right', + userRights.map((right) => ({ + ...right, + createdAt: new Date(), + updatedAt: new Date(), + })), + {} + ); + + await queryInterface.bulkInsert( + 'nav_element', + await Promise.all( + navElements.map(async (t) => { + const groupId = await queryInterface.rawSelect('nav_group', { + where: { name: t.groupId }, + }, ['id']); + + return { + ...t, + groupId: groupId, + createdAt: new Date(), + updatedAt: new Date(), + }; + }) + ), + {} + ); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete('nav_element', { + name: navElements.map((t) => t.name), + }, {}); + + await queryInterface.bulkDelete('user_right', { + name: userRights.map((r) => r.name), + }, {}); + }, +}; diff --git a/backend/db/models/api_key.js b/backend/db/models/api_key.js new file mode 100644 index 000000000..aacde4671 --- /dev/null +++ b/backend/db/models/api_key.js @@ -0,0 +1,89 @@ +'use strict'; +const MetaModel = require('../MetaModel.js'); + +module.exports = (sequelize, DataTypes) => { + class ApiKey extends MetaModel { + static autoTable = true; + + static associate(models) { + ApiKey.belongsTo(models['user'], { + foreignKey: 'userId', + as: 'owner', + }); + } + + /** + * Find all API keys accessible to a user (own + shared with their studies/projects) + * @param {number} userId + * @param {Object} options + * @returns {Promise} + */ + static async getAccessibleKeys(userId, options = {}) { + const {Op} = require('sequelize'); + return await this.findAll({ + where: { + deleted: false, + enabled: true, + [Op.or]: [ + {userId: userId}, + {shared: true, sharedScope: 'system'}, + ], + }, + raw: true, + ...options, + }); + } + + /** + * Resolve the best API key for a given user and provider. + * Priority: user's own key > shared study/project key > system fallback + * @param {number} userId + * @param {string} provider + * @returns {Promise} + */ + static async resolveKey(userId, provider) { + const {Op} = require('sequelize'); + const keys = await this.findAll({ + where: { + deleted: false, + enabled: true, + provider: provider, + [Op.or]: [ + {userId: userId}, + {shared: true}, + ], + }, + order: [ + [sequelize.literal(`CASE WHEN "userId" = ${parseInt(userId)} THEN 0 ELSE 1 END`), 'ASC'], + ['createdAt', 'ASC'], + ], + raw: true, + }); + return keys.length > 0 ? keys[0] : null; + } + } + + ApiKey.init({ + userId: DataTypes.INTEGER, + provider: DataTypes.STRING, + name: DataTypes.STRING, + apiEndpoint: DataTypes.STRING, + encryptedKey: DataTypes.TEXT, + enabled: DataTypes.BOOLEAN, + shared: DataTypes.BOOLEAN, + sharedScope: DataTypes.STRING, + sharedTargetId: DataTypes.INTEGER, + usageLimitMonthly: DataTypes.INTEGER, + lastUsedAt: DataTypes.DATE, + deleted: DataTypes.BOOLEAN, + deletedAt: DataTypes.DATE, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, { + sequelize, + modelName: 'api_key', + tableName: 'api_key', + }); + + return ApiKey; +}; diff --git a/backend/db/models/llm_log.js b/backend/db/models/llm_log.js new file mode 100644 index 000000000..1669b03d8 --- /dev/null +++ b/backend/db/models/llm_log.js @@ -0,0 +1,37 @@ +'use strict'; +const MetaModel = require('../MetaModel.js'); + +module.exports = (sequelize, DataTypes) => { + class AiLog extends MetaModel { + static autoTable = true; + + static associate(models) { + } + } + + AiLog.init({ + userId: DataTypes.INTEGER, + apiKeyId: DataTypes.INTEGER, + modelId: DataTypes.INTEGER, + documentId: DataTypes.INTEGER, + studySessionId: DataTypes.INTEGER, + studyStepId: DataTypes.INTEGER, + input: DataTypes.JSONB, + output: DataTypes.JSONB, + inputTokens: DataTypes.INTEGER, + outputTokens: DataTypes.INTEGER, + estimatedCost: DataTypes.FLOAT, + latencyMs: DataTypes.INTEGER, + status: DataTypes.STRING, + deleted: DataTypes.BOOLEAN, + deletedAt: DataTypes.DATE, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, { + sequelize, + modelName: 'ai_log', + tableName: 'ai_log', + }); + + return AiLog; +}; diff --git a/backend/db/models/llm_provider.js b/backend/db/models/llm_provider.js new file mode 100644 index 000000000..3c42c0dfa --- /dev/null +++ b/backend/db/models/llm_provider.js @@ -0,0 +1,53 @@ +'use strict'; +const MetaModel = require('../MetaModel.js'); + +module.exports = (sequelize, DataTypes) => { + class LlmProvider extends MetaModel { + static autoTable = true; + static publicTable = true; + + static associate(models) { + } + + /** + * Get provider by slug + * @param {string} slug + * @returns {Promise} + */ + static async getBySlug(slug) { + return await this.findOne({ + where: {slug: slug, deleted: false}, + raw: true, + }); + } + + /** + * Get all enabled providers + * @returns {Promise} + */ + static async getEnabled() { + return await this.findAll({ + where: {enabled: true, deleted: false}, + raw: true, + }); + } + } + + LlmProvider.init({ + name: DataTypes.STRING, + slug: DataTypes.STRING, + apiBaseUrl: DataTypes.STRING, + enabled: DataTypes.BOOLEAN, + models: DataTypes.JSONB, + deleted: DataTypes.BOOLEAN, + deletedAt: DataTypes.DATE, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, { + sequelize, + modelName: 'llm_provider', + tableName: 'llm_provider', + }); + + return LlmProvider; +}; diff --git a/backend/db/models/prompt_template.js b/backend/db/models/prompt_template.js new file mode 100644 index 000000000..9aefc9e88 --- /dev/null +++ b/backend/db/models/prompt_template.js @@ -0,0 +1,58 @@ +'use strict'; +const MetaModel = require('../MetaModel.js'); + +module.exports = (sequelize, DataTypes) => { + class PromptTemplate extends MetaModel { + static autoTable = true; + + static associate(models) { + PromptTemplate.belongsTo(models['user'], { + foreignKey: 'userId', + as: 'creator', + }); + } + + /** + * Get all templates accessible to a user (own + shared) + * @param {number} userId + * @returns {Promise} + */ + static async getAccessible(userId) { + const {Op} = require('sequelize'); + return await this.findAll({ + where: { + deleted: false, + [Op.or]: [ + {userId: userId}, + {shared: true}, + ], + }, + raw: true, + }); + } + } + + PromptTemplate.init({ + userId: DataTypes.INTEGER, + name: DataTypes.STRING, + description: DataTypes.TEXT, + provider: DataTypes.STRING, + model: DataTypes.STRING, + promptText: DataTypes.TEXT, + inputMapping: DataTypes.JSONB, + defaultOutputMapping: DataTypes.JSONB, + shared: DataTypes.BOOLEAN, + sharedScope: DataTypes.STRING, + sharedTargetId: DataTypes.INTEGER, + deleted: DataTypes.BOOLEAN, + deletedAt: DataTypes.DATE, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, { + sequelize, + modelName: 'prompt_template', + tableName: 'prompt_template', + }); + + return PromptTemplate; +}; diff --git a/backend/db/models/setting.js b/backend/db/models/setting.js index 45ec7e6af..04c0e6fcd 100644 --- a/backend/db/models/setting.js +++ b/backend/db/models/setting.js @@ -13,6 +13,14 @@ module.exports = (sequelize, DataTypes) => { // define association here } + static get encryptionKey() { + const key = process.env.ENCRYPTION_KEY; + if (!key) { + throw new Error('ENCRYPTION_KEY env var is required for encrypted settings'); + } + return key; + } + /** * Get all settings and overwrite to be sure no admin settings are returned to user * @param {boolean} includeAdmin include admin settings @@ -40,6 +48,13 @@ module.exports = (sequelize, DataTypes) => { try { let setting = await Setting.findOne({where: {key: key}, raw: true}); if (setting) { + if (setting.type === 'encrypted' && setting.value) { + const [[result]] = await sequelize.query( + `SELECT pgp_sym_decrypt(decode($1, 'base64'), $2) AS val`, + {bind: [setting.value, Setting.encryptionKey]} + ); + return result.val; + } return setting.value; } return null; @@ -56,10 +71,21 @@ module.exports = (sequelize, DataTypes) => { */ static async set(key, value) { try { + let finalValue = value; + if (value) { + const existing = await Setting.findOne({where: {key: key}, attributes: ['type'], raw: true}); + if (existing && existing.type === 'encrypted') { + const [[result]] = await sequelize.query( + `SELECT encode(pgp_sym_encrypt($1, $2), 'base64') AS val`, + {bind: [value, Setting.encryptionKey]} + ); + finalValue = result.val; + } + } const [instance, created] = await Setting.upsert({ key: key, - value: value, + value: finalValue, }, { conflictFields: ['key'] }); diff --git a/frontend/src/components/annotator/pdfViewer/Highlights.vue b/frontend/src/components/annotator/pdfViewer/Highlights.vue index 2b1393912..de063d404 100644 --- a/frontend/src/components/annotator/pdfViewer/Highlights.vue +++ b/frontend/src/components/annotator/pdfViewer/Highlights.vue @@ -120,6 +120,7 @@ export default { commentStates(newVal, oldVal) { newVal.filter(s => !oldVal.includes(s)).forEach(state => { const annotation = this.getAnnotationByCommentState(state); + if (!annotation) {return false}; const annotationPage = annotation?.selectors?.target?.[0]?.selector?.find(s => s.type === 'PagePositionSelector').number; const isCollapsed = this.collapsedAnnotationIds.includes(annotation.id); const isPageLoaded = this.isPdfPageLoaded(annotationPage); @@ -518,9 +519,10 @@ export default { return textNodes; }, getAnnotationByCommentState(state) { - let comment = this.$store.getters['table/comment/get'](state.commentId); - let annotation = this.$store.getters['table/annotation/get'](comment.annotationId); - return annotation; + const comment = this.$store.getters['table/comment/get'](state.commentId); + if (!comment) return null; + const annotation = this.$store.getters['table/annotation/get'](comment.annotationId); + return annotation || null; }, } } diff --git a/frontend/src/components/dashboard/LlmDashboard.vue b/frontend/src/components/dashboard/LlmDashboard.vue new file mode 100644 index 000000000..dcb7878a1 --- /dev/null +++ b/frontend/src/components/dashboard/LlmDashboard.vue @@ -0,0 +1,830 @@ + + + + + diff --git a/frontend/src/components/dashboard/LlmProviders.vue b/frontend/src/components/dashboard/LlmProviders.vue new file mode 100644 index 000000000..a56fa0fe1 --- /dev/null +++ b/frontend/src/components/dashboard/LlmProviders.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/frontend/src/store/modules/service.js b/frontend/src/store/modules/service.js index c9183e1b6..3018f2e5b 100644 --- a/frontend/src/store/modules/service.js +++ b/frontend/src/store/modules/service.js @@ -178,6 +178,29 @@ export default { } } + if (service === "LLMService") { + if (serviceType === "providerUpdate") { + state.services[service][serviceType] = Array.isArray(data.data) ? data.data : []; + } else if (serviceType === "apiKeyUpdate") { + state.services[service][serviceType] = Array.isArray(data.data) ? data.data : []; + } else if (serviceType === "promptTemplateUpdate") { + state.services[service][serviceType] = Array.isArray(data.data) ? data.data : []; + } else if (serviceType === "llmResult") { + let cur; + if (!(serviceType in state.services[service])) { + cur = {}; + } else { + cur = {...state.services[service][serviceType]}; + } + if (!data.data.error) { + cur[data.data.id] = data.data; + } + state.services[service][serviceType] = cur; + } else if (serviceType === "usageStats" || serviceType === "usageLogs") { + state.services[service][serviceType] = data.data; + } + } + if (service === "BackgroundTaskService") { if (!(serviceType in state.services[service])) { state.services[service][serviceType] = {}; @@ -190,7 +213,8 @@ export default { }, /** - * Removes an NLP result by the given requestID from the store. + * Removes a result by the given requestID from the store. + * Works for both NLPService (skillResults) and LLMService (llmResult). * * @param state * @param requestId @@ -199,6 +223,9 @@ export default { if (service in state.services && 'skillResults' in state.services[service]) { delete state.services[service]['skillResults'][requestId]; } + if (service in state.services && 'llmResult' in state.services[service]) { + delete state.services[service]['llmResult'][requestId]; + } }, }, actions: {}