+
@@ -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 @@