Merge branch 'main' into FEATURE/keys-credentials
# Conflicts: # packages/ui/src/views/apikey/index.js
This commit is contained in:
commit
b1f5f52567
|
|
@ -111,7 +111,7 @@ class OpenAIAssistant_Agents implements INode {
|
||||||
|
|
||||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||||
options.logger.info(`Clearing OpenAI Thread ${sessionId}`)
|
options.logger.info(`Clearing OpenAI Thread ${sessionId}`)
|
||||||
await openai.beta.threads.del(sessionId)
|
if (sessionId) await openai.beta.threads.del(sessionId)
|
||||||
options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`)
|
options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,16 +135,25 @@ class OpenAIAssistant_Agents implements INode {
|
||||||
|
|
||||||
const openai = new OpenAI({ apiKey: openAIApiKey })
|
const openai = new OpenAI({ apiKey: openAIApiKey })
|
||||||
|
|
||||||
// Retrieve assistant
|
|
||||||
try {
|
try {
|
||||||
const assistantDetails = JSON.parse(assistant.details)
|
const assistantDetails = JSON.parse(assistant.details)
|
||||||
const openAIAssistantId = assistantDetails.id
|
const openAIAssistantId = assistantDetails.id
|
||||||
|
|
||||||
|
// Retrieve assistant
|
||||||
const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId)
|
const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId)
|
||||||
|
|
||||||
if (formattedTools.length) {
|
if (formattedTools.length) {
|
||||||
let filteredTools = uniqWith([...retrievedAssistant.tools, ...formattedTools], isEqual)
|
let filteredTools = []
|
||||||
|
for (const tool of retrievedAssistant.tools) {
|
||||||
|
if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool)
|
||||||
|
}
|
||||||
|
filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual)
|
||||||
|
// filter out tool with empty function
|
||||||
filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
|
filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
|
||||||
await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools })
|
await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools })
|
||||||
|
} else {
|
||||||
|
let filteredTools = retrievedAssistant.tools.filter((tool) => tool.type !== 'function')
|
||||||
|
await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools })
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({
|
const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({
|
||||||
|
|
@ -152,14 +161,45 @@ class OpenAIAssistant_Agents implements INode {
|
||||||
})
|
})
|
||||||
|
|
||||||
let threadId = ''
|
let threadId = ''
|
||||||
|
let isNewThread = false
|
||||||
if (!chatmessage) {
|
if (!chatmessage) {
|
||||||
const thread = await openai.beta.threads.create({})
|
const thread = await openai.beta.threads.create({})
|
||||||
threadId = thread.id
|
threadId = thread.id
|
||||||
|
isNewThread = true
|
||||||
} else {
|
} else {
|
||||||
const thread = await openai.beta.threads.retrieve(chatmessage.sessionId)
|
const thread = await openai.beta.threads.retrieve(chatmessage.sessionId)
|
||||||
threadId = thread.id
|
threadId = thread.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List all runs
|
||||||
|
if (!isNewThread) {
|
||||||
|
const promise = (threadId: string) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const timeout = setInterval(async () => {
|
||||||
|
const allRuns = await openai.beta.threads.runs.list(threadId)
|
||||||
|
if (allRuns.data && allRuns.data.length) {
|
||||||
|
const firstRunId = allRuns.data[0].id
|
||||||
|
const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status
|
||||||
|
if (
|
||||||
|
runStatus &&
|
||||||
|
(runStatus === 'cancelled' ||
|
||||||
|
runStatus === 'completed' ||
|
||||||
|
runStatus === 'expired' ||
|
||||||
|
runStatus === 'failed')
|
||||||
|
) {
|
||||||
|
clearInterval(timeout)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearInterval(timeout)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await promise(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
// Add message to thread
|
// Add message to thread
|
||||||
await openai.beta.threads.messages.create(threadId, {
|
await openai.beta.threads.messages.create(threadId, {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
|
@ -217,27 +257,41 @@ class OpenAIAssistant_Agents implements INode {
|
||||||
})
|
})
|
||||||
resolve(state)
|
resolve(state)
|
||||||
} else {
|
} else {
|
||||||
reject(
|
await openai.beta.threads.runs.cancel(threadId, runId)
|
||||||
new Error(
|
resolve('requires_action_retry')
|
||||||
`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}. submit_tool_outputs.tool_calls are empty`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (state === 'cancelled' || state === 'expired' || state === 'failed') {
|
} else if (state === 'cancelled' || state === 'expired' || state === 'failed') {
|
||||||
clearInterval(timeout)
|
clearInterval(timeout)
|
||||||
reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`))
|
reject(
|
||||||
|
new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Polling run status
|
// Polling run status
|
||||||
|
let runThreadId = runThread.id
|
||||||
let state = await promise(threadId, runThread.id)
|
let state = await promise(threadId, runThread.id)
|
||||||
while (state === 'requires_action') {
|
while (state === 'requires_action') {
|
||||||
state = await promise(threadId, runThread.id)
|
state = await promise(threadId, runThread.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let retries = 3
|
||||||
|
while (state === 'requires_action_retry') {
|
||||||
|
if (retries > 0) {
|
||||||
|
retries -= 1
|
||||||
|
const newRunThread = await openai.beta.threads.runs.create(threadId, {
|
||||||
|
assistant_id: retrievedAssistant.id
|
||||||
|
})
|
||||||
|
runThreadId = newRunThread.id
|
||||||
|
state = await promise(threadId, newRunThread.id)
|
||||||
|
} else {
|
||||||
|
throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// List messages
|
// List messages
|
||||||
const messages = await openai.beta.threads.messages.list(threadId)
|
const messages = await openai.beta.threads.messages.list(threadId)
|
||||||
const messageData = messages.data ?? []
|
const messageData = messages.data ?? []
|
||||||
|
|
@ -245,12 +299,58 @@ class OpenAIAssistant_Agents implements INode {
|
||||||
if (!assistantMessages.length) return ''
|
if (!assistantMessages.length) return ''
|
||||||
|
|
||||||
let returnVal = ''
|
let returnVal = ''
|
||||||
|
const fileAnnotations = []
|
||||||
for (let i = 0; i < assistantMessages[0].content.length; i += 1) {
|
for (let i = 0; i < assistantMessages[0].content.length; i += 1) {
|
||||||
if (assistantMessages[0].content[i].type === 'text') {
|
if (assistantMessages[0].content[i].type === 'text') {
|
||||||
const content = assistantMessages[0].content[i] as MessageContentText
|
const content = assistantMessages[0].content[i] as MessageContentText
|
||||||
returnVal += content.text.value
|
|
||||||
|
|
||||||
//TODO: handle annotations
|
if (content.text.annotations) {
|
||||||
|
const message_content = content.text
|
||||||
|
const annotations = message_content.annotations
|
||||||
|
|
||||||
|
const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant')
|
||||||
|
|
||||||
|
// Iterate over the annotations and add footnotes
|
||||||
|
for (let index = 0; index < annotations.length; index++) {
|
||||||
|
const annotation = annotations[index]
|
||||||
|
let filePath = ''
|
||||||
|
|
||||||
|
// Gather citations based on annotation attributes
|
||||||
|
const file_citation = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FileCitation)
|
||||||
|
.file_citation
|
||||||
|
if (file_citation) {
|
||||||
|
const cited_file = await openai.files.retrieve(file_citation.file_id)
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename
|
||||||
|
filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName)
|
||||||
|
await downloadFile(cited_file, filePath, dirPath, openAIApiKey)
|
||||||
|
fileAnnotations.push({
|
||||||
|
filePath,
|
||||||
|
fileName
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const file_path = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FilePath).file_path
|
||||||
|
if (file_path) {
|
||||||
|
const cited_file = await openai.files.retrieve(file_path.file_id)
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename
|
||||||
|
filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName)
|
||||||
|
await downloadFile(cited_file, filePath, dirPath, openAIApiKey)
|
||||||
|
fileAnnotations.push({
|
||||||
|
filePath,
|
||||||
|
fileName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the text with a footnote
|
||||||
|
message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
returnVal += message_content.value
|
||||||
|
} else {
|
||||||
|
returnVal += content.text.value
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const content = assistantMessages[0].content[i] as MessageContentImageFile
|
const content = assistantMessages[0].content[i] as MessageContentImageFile
|
||||||
const fileId = content.image_file.file_id
|
const fileId = content.image_file.file_id
|
||||||
|
|
@ -271,7 +371,8 @@ class OpenAIAssistant_Agents implements INode {
|
||||||
return {
|
return {
|
||||||
text: returnVal,
|
text: returnVal,
|
||||||
usedTools,
|
usedTools,
|
||||||
assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData }
|
fileAnnotations,
|
||||||
|
assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export interface IChatMessage {
|
||||||
chatflowid: string
|
chatflowid: string
|
||||||
sourceDocuments?: string
|
sourceDocuments?: string
|
||||||
usedTools?: string
|
usedTools?: string
|
||||||
|
fileAnnotations?: string
|
||||||
chatType: string
|
chatType: string
|
||||||
chatId: string
|
chatId: string
|
||||||
memoryType?: string
|
memoryType?: string
|
||||||
|
|
|
||||||
|
|
@ -36,4 +36,7 @@ export class ChatFlow implements IChatFlow {
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedDate: Date
|
updatedDate: Date
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text' })
|
||||||
|
category?: string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage {
|
||||||
@Column({ nullable: true, type: 'text' })
|
@Column({ nullable: true, type: 'text' })
|
||||||
usedTools?: string
|
usedTools?: string
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text' })
|
||||||
|
fileAnnotations?: string
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
chatType: string
|
chatType: string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddCategoryToChatFlow1699900910291 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const columnExists = await queryRunner.hasColumn('chat_flow', 'category')
|
||||||
|
if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`category\` TEXT;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`category\`;`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const columnExists = await queryRunner.hasColumn('chat_message', 'fileAnnotations')
|
||||||
|
if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileAnnotations\` TEXT;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileAnnotations\`;`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
|
||||||
import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory'
|
import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory'
|
||||||
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
||||||
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
||||||
|
import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
|
||||||
|
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
Init1693840429259,
|
Init1693840429259,
|
||||||
|
|
@ -19,5 +21,7 @@ export const mysqlMigrations = [
|
||||||
AddAnalytic1694432361423,
|
AddAnalytic1694432361423,
|
||||||
AddChatHistory1694658767766,
|
AddChatHistory1694658767766,
|
||||||
AddAssistantEntity1699325775451,
|
AddAssistantEntity1699325775451,
|
||||||
AddUsedToolsToChatMessage1699481607341
|
AddUsedToolsToChatMessage1699481607341,
|
||||||
|
AddCategoryToChatFlow1699900910291,
|
||||||
|
AddFileAnnotationsToChatMessage1700271021237
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ export class AddUsedToolsToChatMessage1699481607341 implements MigrationInterfac
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "usedTools";`)
|
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "usedTools";`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddCategoryToChatFlow1699900910291 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "category" TEXT;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileAnnotations" TEXT;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
|
||||||
import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory'
|
import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory'
|
||||||
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
||||||
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
||||||
|
import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
|
||||||
|
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
Init1693891895163,
|
Init1693891895163,
|
||||||
|
|
@ -19,5 +21,7 @@ export const postgresMigrations = [
|
||||||
AddAnalytic1694432361423,
|
AddAnalytic1694432361423,
|
||||||
AddChatHistory1694658756136,
|
AddChatHistory1694658756136,
|
||||||
AddAssistantEntity1699325775451,
|
AddAssistantEntity1699325775451,
|
||||||
AddUsedToolsToChatMessage1699481607341
|
AddUsedToolsToChatMessage1699481607341,
|
||||||
|
AddCategoryToChatFlow1699900910291,
|
||||||
|
AddFileAnnotationsToChatMessage1700271021237
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddCategoryToChatFlow1699900910291 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "category" TEXT;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";`
|
||||||
|
)
|
||||||
|
await queryRunner.query(`DROP TABLE "chat_message";`)
|
||||||
|
await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`)
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`)
|
||||||
|
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
|
||||||
import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory'
|
import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory'
|
||||||
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
|
||||||
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
|
||||||
|
import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
|
||||||
|
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
|
||||||
|
|
||||||
export const sqliteMigrations = [
|
export const sqliteMigrations = [
|
||||||
Init1693835579790,
|
Init1693835579790,
|
||||||
|
|
@ -19,5 +21,7 @@ export const sqliteMigrations = [
|
||||||
AddAnalytic1694432361423,
|
AddAnalytic1694432361423,
|
||||||
AddChatHistory1694657778173,
|
AddChatHistory1694657778173,
|
||||||
AddAssistantEntity1699325775451,
|
AddAssistantEntity1699325775451,
|
||||||
AddUsedToolsToChatMessage1699481607341
|
AddUsedToolsToChatMessage1699481607341,
|
||||||
|
AddCategoryToChatFlow1699900910291,
|
||||||
|
AddFileAnnotationsToChatMessage1700271021237
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ export class App {
|
||||||
'/api/v1/node-icon/',
|
'/api/v1/node-icon/',
|
||||||
'/api/v1/components-credentials-icon/',
|
'/api/v1/components-credentials-icon/',
|
||||||
'/api/v1/chatflows-streaming',
|
'/api/v1/chatflows-streaming',
|
||||||
|
'/api/v1/openai-assistants-file',
|
||||||
'/api/v1/ip'
|
'/api/v1/ip'
|
||||||
]
|
]
|
||||||
this.app.use((req, res, next) => {
|
this.app.use((req, res, next) => {
|
||||||
|
|
@ -355,8 +356,12 @@ export class App {
|
||||||
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
|
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
|
||||||
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
|
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
|
||||||
|
|
||||||
// Update chatflowpool inSync to false, to build Langchain again because data has been changed
|
// chatFlowPool is initialized only when a flow is opened
|
||||||
this.chatflowPool.updateInSync(chatflow.id, false)
|
// if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined
|
||||||
|
if (this.chatflowPool) {
|
||||||
|
// Update chatflowpool inSync to false, to build Langchain again because data has been changed
|
||||||
|
this.chatflowPool.updateInSync(chatflow.id, false)
|
||||||
|
}
|
||||||
|
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
})
|
})
|
||||||
|
|
@ -782,8 +787,8 @@ export class App {
|
||||||
|
|
||||||
await openai.beta.assistants.update(assistantDetails.id, {
|
await openai.beta.assistants.update(assistantDetails.id, {
|
||||||
name: assistantDetails.name,
|
name: assistantDetails.name,
|
||||||
description: assistantDetails.description,
|
description: assistantDetails.description ?? '',
|
||||||
instructions: assistantDetails.instructions,
|
instructions: assistantDetails.instructions ?? '',
|
||||||
model: assistantDetails.model,
|
model: assistantDetails.model,
|
||||||
tools: filteredTools,
|
tools: filteredTools,
|
||||||
file_ids: uniqWith(
|
file_ids: uniqWith(
|
||||||
|
|
@ -952,7 +957,7 @@ export class App {
|
||||||
|
|
||||||
const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id })
|
const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id })
|
||||||
|
|
||||||
await openai.beta.assistants.del(assistantDetails.id)
|
if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id)
|
||||||
|
|
||||||
return res.json(results)
|
return res.json(results)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -961,6 +966,14 @@ export class App {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Download file from assistant
|
||||||
|
this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => {
|
||||||
|
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath))
|
||||||
|
const fileStream = fs.createReadStream(filePath)
|
||||||
|
fileStream.pipe(res)
|
||||||
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Configuration
|
// Configuration
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
@ -1522,6 +1535,7 @@ export class App {
|
||||||
}
|
}
|
||||||
if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
|
if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
|
||||||
if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools)
|
if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools)
|
||||||
|
if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations)
|
||||||
await this.addChatMessage(apiMessage)
|
await this.addChatMessage(apiMessage)
|
||||||
|
|
||||||
logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
|
logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
|
||||||
|
|
|
||||||
|
|
@ -985,10 +985,14 @@ export const redactCredentialWithPasswordType = (
|
||||||
* @param {any} instance
|
* @param {any} instance
|
||||||
* @param {string} chatId
|
* @param {string} chatId
|
||||||
*/
|
*/
|
||||||
export const checkMemorySessionId = (instance: any, chatId: string): string => {
|
export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => {
|
||||||
if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) {
|
if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) {
|
||||||
instance.memory.sessionId = chatId
|
instance.memory.sessionId = chatId
|
||||||
instance.memory.chatHistory.sessionId = chatId
|
instance.memory.chatHistory.sessionId = chatId
|
||||||
}
|
}
|
||||||
return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined
|
|
||||||
|
if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId
|
||||||
|
else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId)
|
||||||
|
return instance.memory.chatHistory.sessionId
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body)
|
||||||
|
|
||||||
const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body)
|
const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body)
|
||||||
|
|
||||||
const deleteAssistant = (id) => client.delete(`/assistants/${id}`)
|
const deleteAssistant = (id, isDeleteBoth) =>
|
||||||
|
isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getAllAssistants,
|
getAllAssistants,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import { styled, alpha } from '@mui/material/styles'
|
||||||
|
import Menu from '@mui/material/Menu'
|
||||||
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import FileCopyIcon from '@mui/icons-material/FileCopy'
|
||||||
|
import FileDownloadIcon from '@mui/icons-material/Downloading'
|
||||||
|
import FileDeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
import FileCategoryIcon from '@mui/icons-material/Category'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||||
|
import { IconX } from '@tabler/icons'
|
||||||
|
|
||||||
|
import chatflowsApi from 'api/chatflows'
|
||||||
|
|
||||||
|
import useApi from '../../hooks/useApi'
|
||||||
|
import useConfirm from 'hooks/useConfirm'
|
||||||
|
import { uiBaseURL } from '../../store/constant'
|
||||||
|
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions'
|
||||||
|
|
||||||
|
import ConfirmDialog from '../dialog/ConfirmDialog'
|
||||||
|
import SaveChatflowDialog from '../dialog/SaveChatflowDialog'
|
||||||
|
import TagDialog from '../dialog/TagDialog'
|
||||||
|
|
||||||
|
import { generateExportFlowData } from '../../utils/genericHelper'
|
||||||
|
import useNotifier from '../../utils/useNotifier'
|
||||||
|
|
||||||
|
const StyledMenu = styled((props) => (
|
||||||
|
<Menu
|
||||||
|
elevation={0}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))(({ theme }) => ({
|
||||||
|
'& .MuiPaper-root': {
|
||||||
|
borderRadius: 6,
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
minWidth: 180,
|
||||||
|
boxShadow:
|
||||||
|
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
|
||||||
|
'& .MuiMenu-list': {
|
||||||
|
padding: '4px 0'
|
||||||
|
},
|
||||||
|
'& .MuiMenuItem-root': {
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
fontSize: 18,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
marginRight: theme.spacing(1.5)
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function FlowListMenu({ chatflow, updateFlowsApi }) {
|
||||||
|
const { confirm } = useConfirm()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||||
|
|
||||||
|
useNotifier()
|
||||||
|
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||||
|
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||||
|
|
||||||
|
const [flowDialogOpen, setFlowDialogOpen] = useState(false)
|
||||||
|
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false)
|
||||||
|
const [categoryDialogProps, setCategoryDialogProps] = useState({})
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const open = Boolean(anchorEl)
|
||||||
|
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFlowRename = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setFlowDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFlowRename = async (chatflowName) => {
|
||||||
|
const updateBody = {
|
||||||
|
name: chatflowName,
|
||||||
|
chatflow
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateChatflowApi.request(chatflow.id, updateBody)
|
||||||
|
await updateFlowsApi.request()
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: errorData,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFlowCategory = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
if (chatflow.category) {
|
||||||
|
setCategoryDialogProps({
|
||||||
|
category: chatflow.category.split(';')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setCategoryDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFlowCategory = async (categories) => {
|
||||||
|
setCategoryDialogOpen(false)
|
||||||
|
// save categories as string
|
||||||
|
const categoryTags = categories.join(';')
|
||||||
|
const updateBody = {
|
||||||
|
category: categoryTags,
|
||||||
|
chatflow
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateChatflowApi.request(chatflow.id, updateBody)
|
||||||
|
await updateFlowsApi.request()
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: errorData,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
const confirmPayload = {
|
||||||
|
title: `Delete`,
|
||||||
|
description: `Delete chatflow ${chatflow.name}?`,
|
||||||
|
confirmButtonName: 'Delete',
|
||||||
|
cancelButtonName: 'Cancel'
|
||||||
|
}
|
||||||
|
const isConfirmed = await confirm(confirmPayload)
|
||||||
|
|
||||||
|
if (isConfirmed) {
|
||||||
|
try {
|
||||||
|
await chatflowsApi.deleteChatflow(chatflow.id)
|
||||||
|
await updateFlowsApi.request()
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: errorData,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDuplicate = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
try {
|
||||||
|
localStorage.setItem('duplicatedFlowData', chatflow.flowData)
|
||||||
|
window.open(`${uiBaseURL}/canvas`, '_blank')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
try {
|
||||||
|
const flowData = JSON.parse(chatflow.flowData)
|
||||||
|
let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2)
|
||||||
|
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||||
|
|
||||||
|
let exportFileDefaultName = `${chatflow.name} Chatflow.json`
|
||||||
|
|
||||||
|
let linkElement = document.createElement('a')
|
||||||
|
linkElement.setAttribute('href', dataUri)
|
||||||
|
linkElement.setAttribute('download', exportFileDefaultName)
|
||||||
|
linkElement.click()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
id='demo-customized-button'
|
||||||
|
aria-controls={open ? 'demo-customized-menu' : undefined}
|
||||||
|
aria-haspopup='true'
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
disableElevation
|
||||||
|
onClick={handleClick}
|
||||||
|
endIcon={<KeyboardArrowDownIcon />}
|
||||||
|
>
|
||||||
|
Options
|
||||||
|
</Button>
|
||||||
|
<StyledMenu
|
||||||
|
id='demo-customized-menu'
|
||||||
|
MenuListProps={{
|
||||||
|
'aria-labelledby': 'demo-customized-button'
|
||||||
|
}}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleFlowRename} disableRipple>
|
||||||
|
<EditIcon />
|
||||||
|
Rename
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleDuplicate} disableRipple>
|
||||||
|
<FileCopyIcon />
|
||||||
|
Duplicate
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleExport} disableRipple>
|
||||||
|
<FileDownloadIcon />
|
||||||
|
Export
|
||||||
|
</MenuItem>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<MenuItem onClick={handleFlowCategory} disableRipple>
|
||||||
|
<FileCategoryIcon />
|
||||||
|
Update Category
|
||||||
|
</MenuItem>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<MenuItem onClick={handleDelete} disableRipple>
|
||||||
|
<FileDeleteIcon />
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
</StyledMenu>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<SaveChatflowDialog
|
||||||
|
show={flowDialogOpen}
|
||||||
|
dialogProps={{
|
||||||
|
title: `Rename Chatflow`,
|
||||||
|
confirmButtonName: 'Rename',
|
||||||
|
cancelButtonName: 'Cancel'
|
||||||
|
}}
|
||||||
|
onCancel={() => setFlowDialogOpen(false)}
|
||||||
|
onConfirm={saveFlowRename}
|
||||||
|
/>
|
||||||
|
<TagDialog
|
||||||
|
isOpen={categoryDialogOpen}
|
||||||
|
dialogProps={categoryDialogProps}
|
||||||
|
onClose={() => setCategoryDialogOpen(false)}
|
||||||
|
onSubmit={saveFlowCategory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowListMenu.propTypes = {
|
||||||
|
chatflow: PropTypes.object,
|
||||||
|
updateFlowsApi: PropTypes.object
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
import { Button } from '@mui/material'
|
import { Button } from '@mui/material'
|
||||||
|
import MuiToggleButton from '@mui/material/ToggleButton'
|
||||||
|
|
||||||
export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
|
export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
|
||||||
color: 'white',
|
color: 'white',
|
||||||
|
|
@ -9,3 +10,10 @@ export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
|
||||||
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
|
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const StyledToggleButton = styled(MuiToggleButton)(({ theme, color = 'primary' }) => ({
|
||||||
|
'&.Mui-selected, &.Mui-selected:hover': {
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: theme.palette[color].main
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Dialog from '@mui/material/Dialog'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material'
|
||||||
|
|
||||||
|
const TagDialog = ({ isOpen, dialogProps, onClose, onSubmit }) => {
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [categoryValues, setCategoryValues] = useState([])
|
||||||
|
|
||||||
|
const handleInputChange = (event) => {
|
||||||
|
setInputValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputKeyDown = (event) => {
|
||||||
|
if (event.key === 'Enter' && inputValue.trim()) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!categoryValues.includes(inputValue)) {
|
||||||
|
setCategoryValues([...categoryValues, inputValue])
|
||||||
|
setInputValue('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteTag = (categoryToDelete) => {
|
||||||
|
setCategoryValues(categoryValues.filter((category) => category !== categoryToDelete))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
let newCategories = [...categoryValues]
|
||||||
|
if (inputValue.trim() && !categoryValues.includes(inputValue)) {
|
||||||
|
newCategories = [...newCategories, inputValue]
|
||||||
|
setCategoryValues(newCategories)
|
||||||
|
}
|
||||||
|
onSubmit(newCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dialogProps.category) setCategoryValues(dialogProps.category)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setInputValue('')
|
||||||
|
setCategoryValues([])
|
||||||
|
}
|
||||||
|
}, [dialogProps])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth='xs'
|
||||||
|
open={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
aria-labelledby='category-dialog-title'
|
||||||
|
aria-describedby='category-dialog-description'
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||||
|
Set Chatflow Category Tags
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{categoryValues.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 10 }}>
|
||||||
|
{categoryValues.map((category, index) => (
|
||||||
|
<Chip
|
||||||
|
key={index}
|
||||||
|
label={category}
|
||||||
|
onDelete={() => handleDeleteTag(category)}
|
||||||
|
style={{ marginRight: 5, marginBottom: 5 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
fullWidth
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
label='Add a tag'
|
||||||
|
variant='outlined'
|
||||||
|
/>
|
||||||
|
<Typography variant='body2' sx={{ fontStyle: 'italic', mt: 1 }} color='text.secondary'>
|
||||||
|
Enter a tag and press enter to add it to the list. You can add as many tags as you want.
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button variant='contained' onClick={handleSubmit}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TagDialog.propTypes = {
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
dialogProps: PropTypes.object,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
onSubmit: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagDialog
|
||||||
|
|
@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax'
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import {
|
import {
|
||||||
|
|
@ -28,7 +29,7 @@ import DatePicker from 'react-datepicker'
|
||||||
import robotPNG from 'assets/images/robot.png'
|
import robotPNG from 'assets/images/robot.png'
|
||||||
import userPNG from 'assets/images/account.png'
|
import userPNG from 'assets/images/account.png'
|
||||||
import msgEmptySVG from 'assets/images/message_empty.svg'
|
import msgEmptySVG from 'assets/images/message_empty.svg'
|
||||||
import { IconFileExport, IconEraser, IconX } from '@tabler/icons'
|
import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons'
|
||||||
|
|
||||||
// Project import
|
// Project import
|
||||||
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||||
|
|
@ -48,6 +49,7 @@ import useConfirm from 'hooks/useConfirm'
|
||||||
// Utils
|
// Utils
|
||||||
import { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
|
import { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
|
||||||
import useNotifier from 'utils/useNotifier'
|
import useNotifier from 'utils/useNotifier'
|
||||||
|
import { baseURL } from 'store/constant'
|
||||||
|
|
||||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||||
|
|
||||||
|
|
@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||||
}
|
}
|
||||||
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||||
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
||||||
|
if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
|
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
|
||||||
obj[chatPK] = {
|
obj[chatPK] = {
|
||||||
|
|
@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||||
}
|
}
|
||||||
if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||||
if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools)
|
if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools)
|
||||||
|
if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||||
|
|
||||||
loadedMessages.push(obj)
|
loadedMessages.push(obj)
|
||||||
}
|
}
|
||||||
|
|
@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||||
window.open(data, '_blank')
|
window.open(data, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadFile = async (fileAnnotation) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseURL}/api/v1/openai-assistants-file`,
|
||||||
|
{ fileName: fileAnnotation.fileName },
|
||||||
|
{ responseType: 'blob' }
|
||||||
|
)
|
||||||
|
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = fileAnnotation.fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onSourceDialogClick = (data, title) => {
|
const onSourceDialogClick = (data, title) => {
|
||||||
setSourceDialogProps({ data, title })
|
setSourceDialogProps({ data, title })
|
||||||
setSourceDialogOpen(true)
|
setSourceDialogOpen(true)
|
||||||
|
|
@ -648,6 +672,30 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||||
{message.message}
|
{message.message}
|
||||||
</MemoizedReactMarkdown>
|
</MemoizedReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
{message.fileAnnotations && (
|
||||||
|
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||||
|
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
textTransform: 'none',
|
||||||
|
mb: 1,
|
||||||
|
mr: 1
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
variant='outlined'
|
||||||
|
onClick={() => downloadFile(fileAnnotation)}
|
||||||
|
endIcon={
|
||||||
|
<IconDownload color={theme.palette.primary.main} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{fileAnnotation.fileName}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{message.sourceDocuments && (
|
{message.sourceDocuments && (
|
||||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||||
{removeDuplicateURL(message).map((source, index) => {
|
{removeDuplicateURL(message).map((source, index) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Table from '@mui/material/Table'
|
||||||
|
import TableBody from '@mui/material/TableBody'
|
||||||
|
import TableCell, { tableCellClasses } from '@mui/material/TableCell'
|
||||||
|
import TableContainer from '@mui/material/TableContainer'
|
||||||
|
import TableHead from '@mui/material/TableHead'
|
||||||
|
import TableRow from '@mui/material/TableRow'
|
||||||
|
import Paper from '@mui/material/Paper'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import { Button, Stack, Typography } from '@mui/material'
|
||||||
|
import FlowListMenu from '../button/FlowListMenu'
|
||||||
|
|
||||||
|
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||||
|
[`&.${tableCellClasses.head}`]: {
|
||||||
|
backgroundColor: theme.palette.common.black,
|
||||||
|
color: theme.palette.common.white
|
||||||
|
},
|
||||||
|
[`&.${tableCellClasses.body}`]: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const StyledTableRow = styled(TableRow)(({ theme }) => ({
|
||||||
|
'&:nth-of-type(odd)': {
|
||||||
|
backgroundColor: theme.palette.action.hover
|
||||||
|
},
|
||||||
|
// hide last border
|
||||||
|
'&:last-child td, &:last-child th': {
|
||||||
|
border: 0
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const goToCanvas = (selectedChatflow) => {
|
||||||
|
navigate(`/canvas/${selectedChatflow.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableContainer style={{ marginTop: '30', border: 1 }} component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ marginTop: '10', backgroundColor: 'primary' }}>
|
||||||
|
<StyledTableCell component='th' scope='row' style={{ width: '20%' }} key='0'>
|
||||||
|
Name
|
||||||
|
</StyledTableCell>
|
||||||
|
<StyledTableCell style={{ width: '25%' }} key='1'>
|
||||||
|
Category
|
||||||
|
</StyledTableCell>
|
||||||
|
<StyledTableCell style={{ width: '30%' }} key='2'>
|
||||||
|
Nodes
|
||||||
|
</StyledTableCell>
|
||||||
|
<StyledTableCell style={{ width: '15%' }} key='3'>
|
||||||
|
Last Modified Date
|
||||||
|
</StyledTableCell>
|
||||||
|
<StyledTableCell style={{ width: '10%' }} key='4'>
|
||||||
|
Actions
|
||||||
|
</StyledTableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data.filter(filterFunction).map((row, index) => (
|
||||||
|
<StyledTableRow key={index}>
|
||||||
|
<TableCell key='0'>
|
||||||
|
<Typography
|
||||||
|
sx={{ fontSize: '1.2rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}
|
||||||
|
>
|
||||||
|
<Button onClick={() => goToCanvas(row)}>{row.templateName || row.name}</Button>
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell key='1'>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginTop: 5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{row.category &&
|
||||||
|
row.category
|
||||||
|
.split(';')
|
||||||
|
.map((tag, index) => (
|
||||||
|
<Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell key='2'>
|
||||||
|
{images[row.id] && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginTop: 5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => (
|
||||||
|
<div
|
||||||
|
key={img}
|
||||||
|
style={{
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
marginRight: 5,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
marginTop: 5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
|
||||||
|
alt=''
|
||||||
|
src={img}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{images[row.id].length > 5 && (
|
||||||
|
<Typography
|
||||||
|
sx={{ alignItems: 'center', display: 'flex', fontSize: '.8rem', fontWeight: 200 }}
|
||||||
|
>
|
||||||
|
+ {images[row.id].length - 5} More
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell key='3'>{moment(row.updatedDate).format('MMMM Do, YYYY')}</TableCell>
|
||||||
|
<TableCell key='4'>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} justifyContent='center' alignItems='center'>
|
||||||
|
<FlowListMenu chatflow={row} updateFlowsApi={updateFlowsApi} />
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</StyledTableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowListTable.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
images: PropTypes.array,
|
||||||
|
filterFunction: PropTypes.func,
|
||||||
|
updateFlowsApi: PropTypes.object
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import ViewListIcon from '@mui/icons-material/ViewList'
|
||||||
|
import ViewModuleIcon from '@mui/icons-material/ViewModule'
|
||||||
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
||||||
|
import { StyledToggleButton } from '../button/StyledButton'
|
||||||
|
|
||||||
|
export default function Toolbar() {
|
||||||
|
const [view, setView] = React.useState('list')
|
||||||
|
|
||||||
|
const handleChange = (event, nextView) => {
|
||||||
|
setView(nextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButtonGroup value={view} exclusive onChange={handleChange}>
|
||||||
|
<StyledToggleButton variant='contained' value='list' aria-label='list'>
|
||||||
|
<ViewListIcon />
|
||||||
|
</StyledToggleButton>
|
||||||
|
<StyledToggleButton variant='contained' value='module' aria-label='module'>
|
||||||
|
<ViewModuleIcon />
|
||||||
|
</StyledToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,8 +16,12 @@ import {
|
||||||
Paper,
|
Paper,
|
||||||
IconButton,
|
IconButton,
|
||||||
Popover,
|
Popover,
|
||||||
|
Collapse,
|
||||||
Typography,
|
Typography,
|
||||||
Collapse
|
Toolbar,
|
||||||
|
TextField,
|
||||||
|
InputAdornment,
|
||||||
|
ButtonGroup
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
|
@ -38,7 +42,18 @@ import useConfirm from 'hooks/useConfirm'
|
||||||
import useNotifier from 'utils/useNotifier'
|
import useNotifier from 'utils/useNotifier'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import { IconTrash, IconEdit, IconCopy, IconChevronsUp, IconChevronsDown, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons'
|
import {
|
||||||
|
IconTrash,
|
||||||
|
IconEdit,
|
||||||
|
IconCopy,
|
||||||
|
IconChevronsUp,
|
||||||
|
IconChevronsDown,
|
||||||
|
IconX,
|
||||||
|
IconSearch,
|
||||||
|
IconPlus,
|
||||||
|
IconEye,
|
||||||
|
IconEyeOff
|
||||||
|
} from '@tabler/icons'
|
||||||
import APIEmptySVG from 'assets/images/api_empty.svg'
|
import APIEmptySVG from 'assets/images/api_empty.svg'
|
||||||
import * as PropTypes from 'prop-types'
|
import * as PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
|
@ -167,6 +182,14 @@ const APIKey = () => {
|
||||||
const [showApiKeys, setShowApiKeys] = useState([])
|
const [showApiKeys, setShowApiKeys] = useState([])
|
||||||
const openPopOver = Boolean(anchorEl)
|
const openPopOver = Boolean(anchorEl)
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const onSearchChange = (event) => {
|
||||||
|
setSearch(event.target.value)
|
||||||
|
}
|
||||||
|
function filterKeys(data) {
|
||||||
|
return data.keyName.toLowerCase().indexOf(search.toLowerCase()) > -1
|
||||||
|
}
|
||||||
|
|
||||||
const { confirm } = useConfirm()
|
const { confirm } = useConfirm()
|
||||||
|
|
||||||
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||||
|
|
@ -282,12 +305,53 @@ const APIKey = () => {
|
||||||
<>
|
<>
|
||||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||||
<Stack flexDirection='row'>
|
<Stack flexDirection='row'>
|
||||||
<h1>API Keys </h1>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Toolbar
|
||||||
|
disableGutters={true}
|
||||||
<StyledButton variant='contained' sx={{ color: 'white', mr: 1, height: 37 }} onClick={addNew} startIcon={<IconPlus />}>
|
style={{
|
||||||
Create Key
|
margin: 1,
|
||||||
</StyledButton>
|
padding: 1,
|
||||||
|
paddingBottom: 10,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>API Keys </h1>
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
|
||||||
|
variant='outlined'
|
||||||
|
placeholder='Search key name'
|
||||||
|
onChange={onSearchChange}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position='start'>
|
||||||
|
<IconSearch />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<ButtonGroup
|
||||||
|
sx={{ maxHeight: 40 }}
|
||||||
|
disableElevation
|
||||||
|
variant='contained'
|
||||||
|
aria-label='outlined primary button group'
|
||||||
|
>
|
||||||
|
<ButtonGroup disableElevation aria-label='outlined primary button group'>
|
||||||
|
<StyledButton
|
||||||
|
variant='contained'
|
||||||
|
sx={{ color: 'white', mr: 1, height: 37 }}
|
||||||
|
onClick={addNew}
|
||||||
|
startIcon={<IconPlus />}
|
||||||
|
>
|
||||||
|
Create Key
|
||||||
|
</StyledButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Toolbar>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
{apiKeys.length <= 0 && (
|
{apiKeys.length <= 0 && (
|
||||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogConte
|
||||||
|
|
||||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
|
||||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||||
import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown'
|
import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown'
|
||||||
import CredentialInputHandler from 'views/canvas/CredentialInputHandler'
|
import CredentialInputHandler from 'views/canvas/CredentialInputHandler'
|
||||||
import { File } from 'ui-component/file/File'
|
import { File } from 'ui-component/file/File'
|
||||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||||
|
import DeleteConfirmDialog from './DeleteConfirmDialog'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import { IconX } from '@tabler/icons'
|
import { IconX } from '@tabler/icons'
|
||||||
|
|
@ -23,7 +23,6 @@ import { IconX } from '@tabler/icons'
|
||||||
import assistantsApi from 'api/assistants'
|
import assistantsApi from 'api/assistants'
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import useConfirm from 'hooks/useConfirm'
|
|
||||||
import useApi from 'hooks/useApi'
|
import useApi from 'hooks/useApi'
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
|
|
@ -71,14 +70,8 @@ const assistantAvailableModels = [
|
||||||
|
|
||||||
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
const portalElement = document.getElementById('portal')
|
const portalElement = document.getElementById('portal')
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
// ==============================|| Snackbar ||============================== //
|
|
||||||
|
|
||||||
useNotifier()
|
useNotifier()
|
||||||
const { confirm } = useConfirm()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||||
|
|
||||||
|
|
@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
const [assistantFiles, setAssistantFiles] = useState([])
|
const [assistantFiles, setAssistantFiles] = useState([])
|
||||||
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
|
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
|
const [deleteDialogProps, setDeleteDialogProps] = useState({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||||
|
|
@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getAssistantObjApi.data) {
|
if (getAssistantObjApi.data) {
|
||||||
setOpenAIAssistantId(getAssistantObjApi.data.id)
|
syncData(getAssistantObjApi.data)
|
||||||
setAssistantName(getAssistantObjApi.data.name)
|
|
||||||
setAssistantDesc(getAssistantObjApi.data.description)
|
|
||||||
setAssistantModel(getAssistantObjApi.data.model)
|
|
||||||
setAssistantInstructions(getAssistantObjApi.data.instructions)
|
|
||||||
setAssistantFiles(getAssistantObjApi.data.files ?? [])
|
|
||||||
|
|
||||||
let tools = []
|
|
||||||
if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) {
|
|
||||||
for (const tool of getAssistantObjApi.data.tools) {
|
|
||||||
tools.push(tool.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setAssistantTools(tools)
|
|
||||||
}
|
}
|
||||||
}, [getAssistantObjApi.data])
|
}, [getAssistantObjApi.data])
|
||||||
|
|
||||||
|
|
@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [dialogProps])
|
}, [dialogProps])
|
||||||
|
|
||||||
|
const syncData = (data) => {
|
||||||
|
setOpenAIAssistantId(data.id)
|
||||||
|
setAssistantName(data.name)
|
||||||
|
setAssistantDesc(data.description)
|
||||||
|
setAssistantModel(data.model)
|
||||||
|
setAssistantInstructions(data.instructions)
|
||||||
|
setAssistantFiles(data.files ?? [])
|
||||||
|
|
||||||
|
let tools = []
|
||||||
|
if (data.tools && data.tools.length) {
|
||||||
|
for (const tool of data.tools) {
|
||||||
|
tools.push(tool.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAssistantTools(tools)
|
||||||
|
}
|
||||||
|
|
||||||
const addNewAssistant = async () => {
|
const addNewAssistant = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
|
|
@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAssistant = async () => {
|
const onSyncClick = async () => {
|
||||||
const confirmPayload = {
|
setLoading(true)
|
||||||
title: `Delete Assistant`,
|
try {
|
||||||
description: `Delete Assistant ${assistantName}?`,
|
const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential)
|
||||||
confirmButtonName: 'Delete',
|
if (getResp.data) {
|
||||||
cancelButtonName: 'Cancel'
|
syncData(getResp.data)
|
||||||
}
|
|
||||||
const isConfirmed = await confirm(confirmPayload)
|
|
||||||
|
|
||||||
if (isConfirmed) {
|
|
||||||
try {
|
|
||||||
const delResp = await assistantsApi.deleteAssistant(assistantId)
|
|
||||||
if (delResp.data) {
|
|
||||||
enqueueSnackbar({
|
|
||||||
message: 'Assistant deleted',
|
|
||||||
options: {
|
|
||||||
key: new Date().getTime() + Math.random(),
|
|
||||||
variant: 'success',
|
|
||||||
action: (key) => (
|
|
||||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
|
||||||
<IconX />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onConfirm()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
|
||||||
enqueueSnackbar({
|
enqueueSnackbar({
|
||||||
message: `Failed to delete Assistant: ${errorData}`,
|
message: 'Assistant successfully synced!',
|
||||||
options: {
|
options: {
|
||||||
key: new Date().getTime() + Math.random(),
|
key: new Date().getTime() + Math.random(),
|
||||||
variant: 'error',
|
variant: 'success',
|
||||||
persist: true,
|
|
||||||
action: (key) => (
|
action: (key) => (
|
||||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
<IconX />
|
<IconX />
|
||||||
|
|
@ -351,8 +326,71 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onCancel()
|
|
||||||
}
|
}
|
||||||
|
setLoading(false)
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: `Failed to sync Assistant: ${errorData}`,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeleteClick = () => {
|
||||||
|
setDeleteDialogProps({
|
||||||
|
title: `Delete Assistant`,
|
||||||
|
description: `Delete Assistant ${assistantName}?`,
|
||||||
|
cancelButtonName: 'Cancel'
|
||||||
|
})
|
||||||
|
setDeleteDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAssistant = async (isDeleteBoth) => {
|
||||||
|
setDeleteDialogOpen(false)
|
||||||
|
try {
|
||||||
|
const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth)
|
||||||
|
if (delResp.data) {
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'Assistant deleted',
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'success',
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: `Failed to delete Assistant: ${errorData}`,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onCancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{dialogProps.type === 'EDIT' && (
|
{dialogProps.type === 'EDIT' && (
|
||||||
<StyledButton color='error' variant='contained' onClick={() => deleteAssistant()}>
|
<StyledButton color='secondary' variant='contained' onClick={() => onSyncClick()}>
|
||||||
|
Sync
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
{dialogProps.type === 'EDIT' && (
|
||||||
|
<StyledButton color='error' variant='contained' onClick={() => onDeleteClick()}>
|
||||||
Delete
|
Delete
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
)}
|
)}
|
||||||
|
|
@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||||
{dialogProps.confirmButtonName}
|
{dialogProps.confirmButtonName}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
<ConfirmDialog />
|
<DeleteConfirmDialog
|
||||||
|
show={deleteDialogOpen}
|
||||||
|
dialogProps={deleteDialogProps}
|
||||||
|
onCancel={() => setDeleteDialogOpen(false)}
|
||||||
|
onDelete={() => deleteAssistant()}
|
||||||
|
onDeleteBoth={() => deleteAssistant(true)}
|
||||||
|
/>
|
||||||
{loading && <BackdropLoader open={loading} />}
|
{loading && <BackdropLoader open={loading} />}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
) : null
|
) : null
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||||
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
|
||||||
|
const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => {
|
||||||
|
const portalElement = document.getElementById('portal')
|
||||||
|
|
||||||
|
const component = show ? (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth='xs'
|
||||||
|
open={show}
|
||||||
|
onClose={onCancel}
|
||||||
|
aria-labelledby='alert-dialog-title'
|
||||||
|
aria-describedby='alert-dialog-description'
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||||
|
{dialogProps.title}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<span>{dialogProps.description}</span>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 20 }}>
|
||||||
|
<StyledButton sx={{ mb: 1 }} color='orange' variant='contained' onClick={onDelete}>
|
||||||
|
Delete only from Flowise
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton sx={{ mb: 1 }} color='error' variant='contained' onClick={onDeleteBoth}>
|
||||||
|
Delete from both OpenAI and Flowise
|
||||||
|
</StyledButton>
|
||||||
|
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return createPortal(component, portalElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteConfirmDialog.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
dialogProps: PropTypes.object,
|
||||||
|
onDeleteBoth: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeleteConfirmDialog
|
||||||
|
|
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import { Grid, Box, Stack } from '@mui/material'
|
import { Grid, Box, Stack, Toolbar, ToggleButton, ButtonGroup, InputAdornment, TextField } from '@mui/material'
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
// project imports
|
// project imports
|
||||||
|
|
@ -11,7 +11,6 @@ import MainCard from 'ui-component/cards/MainCard'
|
||||||
import ItemCard from 'ui-component/cards/ItemCard'
|
import ItemCard from 'ui-component/cards/ItemCard'
|
||||||
import { gridSpacing } from 'store/constant'
|
import { gridSpacing } from 'store/constant'
|
||||||
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
|
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
|
||||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
|
||||||
import LoginDialog from 'ui-component/dialog/LoginDialog'
|
import LoginDialog from 'ui-component/dialog/LoginDialog'
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
|
@ -24,7 +23,11 @@ import useApi from 'hooks/useApi'
|
||||||
import { baseURL } from 'store/constant'
|
import { baseURL } from 'store/constant'
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { IconPlus } from '@tabler/icons'
|
import { IconPlus, IconSearch, IconLayoutGrid, IconList } from '@tabler/icons'
|
||||||
|
import * as React from 'react'
|
||||||
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
||||||
|
import { FlowListTable } from '../../ui-component/table/FlowListTable'
|
||||||
|
import { StyledButton } from '../../ui-component/button/StyledButton'
|
||||||
|
|
||||||
// ==============================|| CHATFLOWS ||============================== //
|
// ==============================|| CHATFLOWS ||============================== //
|
||||||
|
|
||||||
|
|
@ -35,10 +38,28 @@ const Chatflows = () => {
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(true)
|
const [isLoading, setLoading] = useState(true)
|
||||||
const [images, setImages] = useState({})
|
const [images, setImages] = useState({})
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
|
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
|
||||||
const [loginDialogProps, setLoginDialogProps] = useState({})
|
const [loginDialogProps, setLoginDialogProps] = useState({})
|
||||||
|
|
||||||
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
|
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
|
||||||
|
const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card')
|
||||||
|
|
||||||
|
const handleChange = (event, nextView) => {
|
||||||
|
localStorage.setItem('flowDisplayStyle', nextView)
|
||||||
|
setView(nextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSearchChange = (event) => {
|
||||||
|
setSearch(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterFlows(data) {
|
||||||
|
return (
|
||||||
|
data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 ||
|
||||||
|
(data.category && data.category.toLowerCase().indexOf(search.toLowerCase()) > -1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const onLoginClick = (username, password) => {
|
const onLoginClick = (username, password) => {
|
||||||
localStorage.setItem('username', username)
|
localStorage.setItem('username', username)
|
||||||
|
|
@ -102,26 +123,86 @@ const Chatflows = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||||
<Stack flexDirection='row'>
|
<Stack flexDirection='column'>
|
||||||
<h1>Chatflows</h1>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Grid sx={{ mb: 1.25 }} container direction='row'>
|
<Toolbar
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
disableGutters={true}
|
||||||
<Grid item>
|
style={{
|
||||||
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
|
margin: 1,
|
||||||
Add New
|
padding: 1,
|
||||||
</StyledButton>
|
paddingBottom: 10,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>Chatflows</h1>
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
|
||||||
|
variant='outlined'
|
||||||
|
placeholder='Search name or category'
|
||||||
|
onChange={onSearchChange}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position='start'>
|
||||||
|
<IconSearch />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<ButtonGroup sx={{ maxHeight: 40 }} disableElevation variant='contained' aria-label='outlined primary button group'>
|
||||||
|
<ButtonGroup disableElevation variant='contained' aria-label='outlined primary button group'>
|
||||||
|
<ToggleButtonGroup sx={{ maxHeight: 40 }} value={view} color='primary' exclusive onChange={handleChange}>
|
||||||
|
<ToggleButton
|
||||||
|
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
|
||||||
|
variant='contained'
|
||||||
|
value='card'
|
||||||
|
title='Card View'
|
||||||
|
selectedColor='#00abc0'
|
||||||
|
>
|
||||||
|
<IconLayoutGrid />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
|
||||||
|
variant='contained'
|
||||||
|
value='list'
|
||||||
|
title='List View'
|
||||||
|
>
|
||||||
|
<IconList />
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Box sx={{ width: 5 }} />
|
||||||
|
<ButtonGroup disableElevation aria-label='outlined primary button group'>
|
||||||
|
<StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />}>
|
||||||
|
Add New
|
||||||
|
</StyledButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Toolbar>
|
||||||
|
</Box>
|
||||||
|
{!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && (
|
||||||
|
<Grid container spacing={gridSpacing}>
|
||||||
|
{getAllChatflowsApi.data.filter(filterFlows).map((data, index) => (
|
||||||
|
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
|
||||||
|
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
)}
|
||||||
|
{!isLoading && view === 'list' && getAllChatflowsApi.data && (
|
||||||
|
<FlowListTable
|
||||||
|
sx={{ mt: 20 }}
|
||||||
|
data={getAllChatflowsApi.data}
|
||||||
|
images={images}
|
||||||
|
filterFunction={filterFlows}
|
||||||
|
updateFlowsApi={getAllChatflowsApi}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container spacing={gridSpacing}>
|
|
||||||
{!isLoading &&
|
|
||||||
getAllChatflowsApi.data &&
|
|
||||||
getAllChatflowsApi.data.map((data, index) => (
|
|
||||||
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
|
|
||||||
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
|
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
|
||||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||||
<Box sx={{ p: 2, height: 'auto' }}>
|
<Box sx={{ p: 2, height: 'auto' }}>
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax'
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from 'rehype-raw'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material'
|
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material'
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
import { IconSend } from '@tabler/icons'
|
import { IconSend, IconDownload } from '@tabler/icons'
|
||||||
|
|
||||||
// project import
|
// project import
|
||||||
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||||
|
|
@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||||
|
|
||||||
setMessages((prevMessages) => [
|
setMessages((prevMessages) => [
|
||||||
...prevMessages,
|
...prevMessages,
|
||||||
{ message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' }
|
{
|
||||||
|
message: text,
|
||||||
|
sourceDocuments: data?.sourceDocuments,
|
||||||
|
usedTools: data?.usedTools,
|
||||||
|
fileAnnotations: data?.fileAnnotations,
|
||||||
|
type: 'apiMessage'
|
||||||
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadFile = async (fileAnnotation) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseURL}/api/v1/openai-assistants-file`,
|
||||||
|
{ fileName: fileAnnotation.fileName },
|
||||||
|
{ responseType: 'blob' }
|
||||||
|
)
|
||||||
|
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = fileAnnotation.fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get chatmessages successful
|
// Get chatmessages successful
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getChatmessageApi.data?.length) {
|
if (getChatmessageApi.data?.length) {
|
||||||
|
|
@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||||
}
|
}
|
||||||
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
||||||
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
|
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
|
||||||
|
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
|
||||||
return obj
|
return obj
|
||||||
})
|
})
|
||||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||||
|
|
@ -331,6 +359,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||||
{message.message}
|
{message.message}
|
||||||
</MemoizedReactMarkdown>
|
</MemoizedReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
{message.fileAnnotations && (
|
||||||
|
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||||
|
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }}
|
||||||
|
key={index}
|
||||||
|
variant='outlined'
|
||||||
|
onClick={() => downloadFile(fileAnnotation)}
|
||||||
|
endIcon={<IconDownload color={theme.palette.primary.main} />}
|
||||||
|
>
|
||||||
|
{fileAnnotation.fileName}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{message.sourceDocuments && (
|
{message.sourceDocuments && (
|
||||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||||
{removeDuplicateURL(message).map((source, index) => {
|
{removeDuplicateURL(message).map((source, index) => {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,23 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import { Button, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material'
|
import {
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
Toolbar,
|
||||||
|
TextField,
|
||||||
|
InputAdornment,
|
||||||
|
ButtonGroup
|
||||||
|
} from '@mui/material'
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
// project imports
|
// project imports
|
||||||
|
|
@ -25,7 +41,7 @@ import useConfirm from 'hooks/useConfirm'
|
||||||
import useNotifier from 'utils/useNotifier'
|
import useNotifier from 'utils/useNotifier'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons'
|
import { IconTrash, IconEdit, IconX, IconPlus, IconSearch } from '@tabler/icons'
|
||||||
import CredentialEmptySVG from 'assets/images/credential_empty.svg'
|
import CredentialEmptySVG from 'assets/images/credential_empty.svg'
|
||||||
|
|
||||||
// const
|
// const
|
||||||
|
|
@ -56,6 +72,14 @@ const Credentials = () => {
|
||||||
const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials)
|
const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials)
|
||||||
const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials)
|
const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials)
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const onSearchChange = (event) => {
|
||||||
|
setSearch(event.target.value)
|
||||||
|
}
|
||||||
|
function filterCredentials(data) {
|
||||||
|
return data.credentialName.toLowerCase().indexOf(search.toLowerCase()) > -1
|
||||||
|
}
|
||||||
|
|
||||||
const listCredential = () => {
|
const listCredential = () => {
|
||||||
const dialogProp = {
|
const dialogProp = {
|
||||||
title: 'Add New Credential',
|
title: 'Add New Credential',
|
||||||
|
|
@ -168,17 +192,53 @@ const Credentials = () => {
|
||||||
<>
|
<>
|
||||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||||
<Stack flexDirection='row'>
|
<Stack flexDirection='row'>
|
||||||
<h1>Credentials </h1>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Toolbar
|
||||||
|
disableGutters={true}
|
||||||
<StyledButton
|
style={{
|
||||||
variant='contained'
|
margin: 1,
|
||||||
sx={{ color: 'white', mr: 1, height: 37 }}
|
padding: 1,
|
||||||
onClick={listCredential}
|
paddingBottom: 10,
|
||||||
startIcon={<IconPlus />}
|
display: 'flex',
|
||||||
>
|
justifyContent: 'space-between',
|
||||||
Add Credential
|
width: '100%'
|
||||||
</StyledButton>
|
}}
|
||||||
|
>
|
||||||
|
<h1>Credentials </h1>
|
||||||
|
<TextField
|
||||||
|
size='small'
|
||||||
|
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
|
||||||
|
variant='outlined'
|
||||||
|
placeholder='Search credential name'
|
||||||
|
onChange={onSearchChange}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position='start'>
|
||||||
|
<IconSearch />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<ButtonGroup
|
||||||
|
sx={{ maxHeight: 40 }}
|
||||||
|
disableElevation
|
||||||
|
variant='contained'
|
||||||
|
aria-label='outlined primary button group'
|
||||||
|
>
|
||||||
|
<ButtonGroup disableElevation aria-label='outlined primary button group'>
|
||||||
|
<StyledButton
|
||||||
|
variant='contained'
|
||||||
|
sx={{ color: 'white', mr: 1, height: 37 }}
|
||||||
|
onClick={listCredential}
|
||||||
|
startIcon={<IconPlus />}
|
||||||
|
>
|
||||||
|
Add Credential
|
||||||
|
</StyledButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Toolbar>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
{credentials.length <= 0 && (
|
{credentials.length <= 0 && (
|
||||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||||
|
|
@ -205,7 +265,7 @@ const Credentials = () => {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{credentials.map((credential, index) => (
|
{credentials.filter(filterCredentials).map((credential, index) => (
|
||||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
<TableCell component='th' scope='row'>
|
<TableCell component='th' scope='row'>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue