diff --git a/admin/src/components/AiChat.vue b/admin/src/components/AiChat.vue index e167d685d..f7cf6b2dd 100644 --- a/admin/src/components/AiChat.vue +++ b/admin/src/components/AiChat.vue @@ -15,7 +15,11 @@
-
+
- +
@@ -170,7 +174,18 @@ const sendMessage = () => { onMounted(async () => { if (messages.value.length === 0) { loading.value = true - await resumeChatRefetch() + try { + await resumeChatRefetch() + } catch (error) { + if (error.graphQLErrors && error.graphQLErrors.length > 0) { + toastError(`Error loading chat: ${error.graphQLErrors[0].message}`) + return + } else { + // eslint-disable-next-line no-console + console.log(JSON.stringify(error, null, 2)) + toastError(`Error loading chat: ${error}`) + } + } const messagesFromServer = resumeChatResult.value.resumeChat if (messagesFromServer && messagesFromServer.length > 0) { threadId.value = messagesFromServer[0].threadId @@ -279,6 +294,17 @@ onMounted(async () => { margin-right: auto; } +.message.error { + text-align: center; +} + +.message.error .message-content { + background-color: #f1e5e5; + color: rgb(194 12 12); + margin-left: auto; + margin-right: auto; +} + .input-area { display: flex; padding: 10px; diff --git a/admin/src/graphql/fragments.graphql b/admin/src/graphql/fragments.graphql index 113e15606..c5cf21eba 100644 --- a/admin/src/graphql/fragments.graphql +++ b/admin/src/graphql/fragments.graphql @@ -26,6 +26,7 @@ fragment AiChatMessageFields on ChatGptMessage { content role threadId + isError } fragment UserCommonFields on User { diff --git a/backend/src/apis/openai/OpenaiClient.ts b/backend/src/apis/openai/OpenaiClient.ts index 35744a7dd..eb37a04f8 100644 --- a/backend/src/apis/openai/OpenaiClient.ts +++ b/backend/src/apis/openai/OpenaiClient.ts @@ -11,6 +11,8 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getLogger } from 'log4js' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.openai.OpenaiClient`) +// this is the time after when openai is deleting an inactive thread +const OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS = 60 /** * The `OpenaiClient` class is a singleton that provides an interface to interact with the OpenAI API. @@ -87,21 +89,35 @@ export class OpenaiClient { logger.warn(`No openai thread found for user: ${user.id}`) return [] } - const threadMessages = ( - await this.openai.beta.threads.messages.list(openaiThreadEntity.id, { order: 'desc' }) - ).getPaginatedItems() + if (openaiThreadEntity.updatedAt < new Date(Date.now() - OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS * 24 * 60 * 60 * 1000)) { + logger.info(`Openai thread for user: ${user.id} is older than ${OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS} days, deleting...`) + // let run async, because it could need some time, but we don't need to wait, because we create a new one nevertheless + void this.deleteThread(openaiThreadEntity.id) + return [] + } + try { + const threadMessages = ( + await this.openai.beta.threads.messages.list(openaiThreadEntity.id, { order: 'desc' }) + ).getPaginatedItems() - logger.info(`Resumed thread: ${openaiThreadEntity.id}`) - return threadMessages - .map( - (message) => - new MessageModel( - this.messageContentToString(message), - message.role, - openaiThreadEntity.id, - ), - ) - .reverse() + logger.info(`Resumed thread: ${openaiThreadEntity.id}`) + return threadMessages + .map( + (message) => + new MessageModel( + this.messageContentToString(message), + message.role, + openaiThreadEntity.id, + ), + ) + .reverse() + } catch (e) { + if(e instanceof Error && e.toString().includes('No thread found with id')) { + logger.info(`Thread not found: ${openaiThreadEntity.id}`) + return [] + } + throw e + } } public async deleteThread(threadId: string): Promise { @@ -124,6 +140,7 @@ export class OpenaiClient { } public async runAndGetLastNewMessage(threadId: string): Promise { + const updateOpenAiThreadResolver = OpenaiThreads.update({ id: threadId }, { updatedAt: new Date() }) const run = await this.openai.beta.threads.runs.createAndPoll(threadId, { assistant_id: CONFIG.OPENAI_ASSISTANT_ID, }) @@ -138,6 +155,7 @@ export class OpenaiClient { logger.warn(`No message in thread: ${threadId}, run: ${run.id}`, messagesPage.data) return new MessageModel('No Answer', 'assistant') } + await updateOpenAiThreadResolver return new MessageModel(this.messageContentToString(message), 'assistant') } diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 30027086f..d26bdc702 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -35,7 +35,6 @@ export enum RIGHTS { UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', - COMMUNITY_STATUS = 'COMMUNITY_STATUS', SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE', LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', diff --git a/backend/src/auth/USER_RIGHTS.ts b/backend/src/auth/USER_RIGHTS.ts index 83ce9d871..3f08d1160 100644 --- a/backend/src/auth/USER_RIGHTS.ts +++ b/backend/src/auth/USER_RIGHTS.ts @@ -26,7 +26,6 @@ export const USER_RIGHTS = [ RIGHTS.SEARCH_ADMIN_USERS, RIGHTS.LIST_CONTRIBUTION_LINKS, RIGHTS.COMMUNITY_STATISTICS, - RIGHTS.COMMUNITY_STATUS, RIGHTS.CREATE_CONTRIBUTION_MESSAGE, RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES, RIGHTS.OPEN_CREATIONS, diff --git a/backend/src/federation/client/FederationClientFactory.ts b/backend/src/federation/client/FederationClientFactory.ts index 87794882d..926c3c180 100644 --- a/backend/src/federation/client/FederationClientFactory.ts +++ b/backend/src/federation/client/FederationClientFactory.ts @@ -4,7 +4,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1 import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient' import { ApiVersionType, ensureUrlEndsWithSlash } from 'core' -export type FederationClient = V1_0_FederationClient | V1_1_FederationClient +type FederationClient = V1_0_FederationClient | V1_1_FederationClient interface FederationClientInstance { id: number diff --git a/backend/src/graphql/model/ChatGptMessage.ts b/backend/src/graphql/model/ChatGptMessage.ts index 3a5f08a59..3eaaeb7c7 100644 --- a/backend/src/graphql/model/ChatGptMessage.ts +++ b/backend/src/graphql/model/ChatGptMessage.ts @@ -10,10 +10,14 @@ export class ChatGptMessage { @Field() role: string - @Field() - threadId: string + @Field({ nullable: true }) + threadId?: string - public constructor(data: Partial) { + @Field() + isError: boolean + + public constructor(data: Partial, isError: boolean = false) { Object.assign(this, data) + this.isError = isError } } diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index 3163b886d..e5ccad59b 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -12,7 +12,6 @@ export class Community { this.creationDate = dbCom.creationDate this.uuid = dbCom.communityUuid this.authenticatedAt = dbCom.authenticatedAt - // this.gmsApiKey = dbCom.gmsApiKey // this.hieroTopicId = dbCom.hieroTopicId } @@ -40,10 +39,6 @@ export class Community { @Field(() => Date, { nullable: true }) authenticatedAt: Date | null - // gms api key should only seen by admins, they can use AdminCommunityView - // @Field(() => String, { nullable: true }) - // gmsApiKey: string | null - @Field(() => String, { nullable: true }) hieroTopicId: string | null } diff --git a/backend/src/graphql/resolver/AiChatResolver.ts b/backend/src/graphql/resolver/AiChatResolver.ts index 3fd01826f..c8f6bb4f9 100644 --- a/backend/src/graphql/resolver/AiChatResolver.ts +++ b/backend/src/graphql/resolver/AiChatResolver.ts @@ -15,10 +15,10 @@ export class AiChatResolver { async resumeChat(@Ctx() context: Context): Promise { const openaiClient = OpenaiClient.getInstance() if (!openaiClient) { - return Promise.resolve([new ChatGptMessage({ content: 'OpenAI API is not enabled' })]) + return Promise.resolve([new ChatGptMessage({ content: 'OpenAI API is not enabled', role: 'assistant' }, true)]) } if (!context.user) { - return Promise.resolve([new ChatGptMessage({ content: 'User not found' })]) + return Promise.resolve([new ChatGptMessage({ content: 'User not found', role: 'assistant' }, true)]) } const messages = await openaiClient.resumeThread(context.user) return messages.map((message) => new ChatGptMessage(message)) @@ -42,10 +42,10 @@ export class AiChatResolver { ): Promise { const openaiClient = OpenaiClient.getInstance() if (!openaiClient) { - return Promise.resolve(new ChatGptMessage({ content: 'OpenAI API is not enabled' })) + return Promise.resolve(new ChatGptMessage({ content: 'OpenAI API is not enabled', role: 'assistant' }, true)) } if (!context.user) { - return Promise.resolve(new ChatGptMessage({ content: 'User not found' })) + return Promise.resolve(new ChatGptMessage({ content: 'User not found', role: 'assistant' }, true)) } const messageObj = new Message(message) if (!threadId || threadId.length === 0) { diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index c3604812e..d75541929 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -4,7 +4,6 @@ import { getHomeCommunity } from 'database' import { Arg, Args, Authorized, Mutation, Query, Resolver } from 'type-graphql' - import { Paginated } from '@arg/Paginated' import { EditCommunityInput } from '@input/EditCommunityInput' import { AdminCommunityView } from '@model/AdminCommunityView' @@ -19,6 +18,7 @@ import { getCommunityByIdentifier, getCommunityByUuid, } from './util/communities' +import { updateAllDefinedAndChanged } from 'shared' import { CONFIG } from '@/config' @@ -78,24 +78,22 @@ export class CommunityResolver { if (homeCom.foreign) { throw new LogError('Error: Only the HomeCommunity could be modified!') } - - if ( - homeCom.gmsApiKey !== gmsApiKey || - homeCom.location !== location || - homeCom.hieroTopicId !== hieroTopicId - ) { - // TODO: think about this, it is really expected to delete gmsApiKey if no new one is given? - homeCom.gmsApiKey = gmsApiKey ?? null - if (location) { - homeCom.location = Location2Point(location) - } - // update only with new value, don't overwrite existing value with null or undefined! - if (hieroTopicId) { - homeCom.hieroTopicId = hieroTopicId + let updated = false + // if location is undefined, it should not be changed + // if location is null, it should be set to null + if (typeof location !== 'undefined') { + const newLocation = location ? Location2Point(location) : null + if (newLocation !== homeCom.location) { + homeCom.location = newLocation + updated = true } + } + if (updateAllDefinedAndChanged(homeCom, { gmsApiKey, hieroTopicId })) { + updated = true + } + if (updated) { await DbCommunity.save(homeCom) } - return new AdminCommunityView(homeCom) } } diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 4521ce4ae..3756d3bd9 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -15,7 +15,8 @@ import { QueryLinkResult } from '@union/QueryLinkResult' import { Decay, interpretEncryptedTransferArgs, TransactionTypeId } from 'core' import { AppDatabase, Contribution as DbContribution, - ContributionLink as DbContributionLink, FederatedCommunity as DbFederatedCommunity, + ContributionLink as DbContributionLink, + FederatedCommunity as DbFederatedCommunity, DltTransaction as DbDltTransaction, Transaction as DbTransaction, TransactionLink as DbTransactionLink, @@ -39,8 +40,16 @@ import { Context, getClientTimezoneOffset, getUser } from '@/server/context' import { calculateBalance } from '@/util/validate' import { fullName } from 'core' import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database' -import { calculateDecay, decode, DisburseJwtPayloadType, encode, encryptAndSign, RedeemJwtPayloadType, verify } from 'shared' - +import { + calculateDecay, + compoundInterest, + decode, + DisburseJwtPayloadType, + encode, + encryptAndSign, + RedeemJwtPayloadType, + verify +} from 'shared' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { DisbursementClient as V1_0_DisbursementClient } from '@/federation/client/1_0/DisbursementClient' import { DisbursementClientFactory } from '@/federation/client/DisbursementClientFactory' @@ -93,7 +102,7 @@ export class TransactionLinkResolver { const createdDate = new Date() const validUntil = transactionLinkExpireDate(createdDate) - const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) + const holdAvailableAmount = compoundInterest(amount, CODE_VALID_DAYS_DURATION * 24 * 60 * 60) // validate amount const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 080bec429..505438f6a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -464,7 +464,7 @@ export class TransactionResolver { recipientCommunityIdentifier, ) if (!recipientUser) { - throw new LogError('The recipient user was not found', { recipientIdentifier, recipientCommunityIdentifier }) + throw new LogError('The recipient user was not found', recipientUser) } logger.addContext('to', recipientUser?.id) if (recipientUser.foreign) { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 4aa8f5167..56e3b039b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -25,7 +25,7 @@ import { Root, } from 'type-graphql' import { IRestResponse } from 'typed-rest-client' -import { EntityNotFoundError, In, Point } from 'typeorm' +import { EntityManager, EntityNotFoundError, In, Point } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { UserArgs } from '@arg//UserArgs' @@ -104,7 +104,11 @@ import { deleteUserRole, setUserRole } from './util/modifyUserRole' import { sendUserToGms } from './util/sendUserToGms' import { syncHumhub } from './util/syncHumhub' import { validateAlias } from 'core' +<<<<<<< HEAD import { registerAddressTransaction } from '@/apis/dltConnector' +======= +import { updateAllDefinedAndChanged } from 'shared' +>>>>>>> master const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' @@ -739,18 +743,22 @@ export class UserResolver { user.humhubPublishName as PublishNameType, ) - // try { - if (firstName) { - user.firstName = firstName - } - - if (lastName) { - user.lastName = lastName - } + let updated = updateAllDefinedAndChanged(user, { + firstName, + lastName, + hideAmountGDD, + hideAmountGDT, + humhubAllowed, + gmsAllowed, + gmsPublishName: gmsPublishName?.valueOf(), + humhubPublishName: humhubPublishName?.valueOf(), + gmsPublishLocation: gmsPublishLocation?.valueOf(), + }) // currently alias can only be set, not updated if (alias && !user.alias && (await validateAlias(alias))) { user.alias = alias + updated = true } if (language) { @@ -760,6 +768,7 @@ export class UserResolver { } user.language = language i18n.setLocale(language) + updated = true } if (password && passwordNew) { @@ -780,55 +789,28 @@ export class UserResolver { // Save new password hash and newly encrypted private key user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID user.password = await encryptPassword(user, passwordNew) + updated = true } - // Save hideAmountGDD value - if (hideAmountGDD !== undefined) { - user.hideAmountGDD = hideAmountGDD - } - // Save hideAmountGDT value - if (hideAmountGDT !== undefined) { - user.hideAmountGDT = hideAmountGDT - } - if (humhubAllowed !== undefined) { - user.humhubAllowed = humhubAllowed - } - if (gmsAllowed !== undefined) { - user.gmsAllowed = gmsAllowed - } - if (gmsPublishName !== null && gmsPublishName !== undefined) { - user.gmsPublishName = gmsPublishName - } - if (humhubPublishName !== null && humhubPublishName !== undefined) { - user.humhubPublishName = humhubPublishName - } if (gmsLocation) { user.location = Location2Point(gmsLocation) + updated = true } - if (gmsPublishLocation !== null && gmsPublishLocation !== undefined) { - user.gmsPublishLocation = gmsPublishLocation + + // early exit if no update was made + if (!updated) { + return true } - // } catch (err) { - // console.log('error:', err) - // } - const queryRunner = db.getDataSource().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction('REPEATABLE READ') try { - await queryRunner.manager.save(user).catch((error) => { - throw new LogError('Error saving user', error) - }) - - await queryRunner.commitTransaction() - logger.debug('writing User data successful...', new UserLoggingView(user)) - } catch (e) { - await queryRunner.rollbackTransaction() - throw new LogError('Error on writing updated user data', e) - } finally { - await queryRunner.release() + await DbUser.save(user) + } catch (error) { + const errorMessage = 'Error saving user' + logger.error(errorMessage, error) + throw new Error(errorMessage) } logger.info('updateUserInfos() successfully finished...') + logger.debug('writing User data successful...', new UserLoggingView(user)) await EVENT_USER_INFO_UPDATE(user) // validate if user settings are changed with relevance to update gms-user diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index ff0c513e7..c7f533931 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -39,13 +39,11 @@ export const testEnvironment = async (testLogger = getLogger('apollo'), testI18n } export const resetEntity = async (entity: any) => { - // delete data and reset autoincrement! - await entity.clear() - /*const items = await entity.find({ withDeleted: true }) + const items = await entity.find({ withDeleted: true }) if (items.length > 0) { const ids = items.map((e: any) => e.id) await entity.delete(ids) - }*/ + } } export const resetToken = () => { diff --git a/database/migration/migrations/0094-openai_threads_add_updated_at.ts b/database/migration/migrations/0094-openai_threads_add_updated_at.ts new file mode 100644 index 000000000..8505bf21c --- /dev/null +++ b/database/migration/migrations/0094-openai_threads_add_updated_at.ts @@ -0,0 +1,10 @@ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `openai_threads` ADD COLUMN `updatedAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP AFTER `createdAt`;' + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `openai_threads` DROP COLUMN `updatedAt`;') +} diff --git a/database/src/entity/OpenaiThreads.ts b/database/src/entity/OpenaiThreads.ts index 38e4b6c33..03c216921 100644 --- a/database/src/entity/OpenaiThreads.ts +++ b/database/src/entity/OpenaiThreads.ts @@ -1,13 +1,16 @@ -import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm' +import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm' @Entity('openai_threads') export class OpenaiThreads extends BaseEntity { @PrimaryColumn({ type: 'char', length: 30 }) id: string - @CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + @CreateDateColumn({ type: 'timestamp' }) createdAt: Date + @UpdateDateColumn({ type: 'timestamp' }) + updatedAt: Date + @Column({ name: 'user_id', type: 'int', unsigned: true }) userId: number } diff --git a/frontend/src/components/Breadcrumb/breadcrumb.vue b/frontend/src/components/Breadcrumb/breadcrumb.vue index 305b34fe7..7372ecc6e 100644 --- a/frontend/src/components/Breadcrumb/breadcrumb.vue +++ b/frontend/src/components/Breadcrumb/breadcrumb.vue @@ -23,4 +23,10 @@ export default { margin-bottom: 1rem; padding: 0.75rem 1rem; } + +@media screen and (width <= 450px) { + .page-breadcrumb { + margin-top: 3rem; + } +} diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.vue b/frontend/src/components/GddSend/TransactionConfirmationLink.vue index f31d043a0..d964f3e80 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.vue @@ -20,20 +20,29 @@ {{ $t('advanced-calculation') }} - {{ $t('form.current_balance') }} + {{ $t('form.current_available') }} {{ $filters.GDD(balance) }} - {{ $t('form.your_amount') }} + {{ $t('form.link_amount') }} - + {{ $filters.GDD(amount * -1) }} - {{ $t('form.new_balance') }} - {{ $filters.GDD(balance - amount) }} + {{ $t('decay.decay') }} + {{ $filters.GDD(amount - blockedAmount) }} + + + {{ $t('form.available_after') }} + {{ $filters.GDD(balance - blockedAmount) }} + + + + {{ $t('form.link_decay_description') }} + @@ -57,6 +66,7 @@