feat: remove knowledge queue
This commit is contained in:
parent
c2462fd51c
commit
ca6027dd83
@ -55,6 +55,8 @@ class FileStorage {
|
|||||||
const storedFilePath = path.join(this.storageDir, file)
|
const storedFilePath = path.join(this.storageDir, file)
|
||||||
const storedStats = fs.statSync(storedFilePath)
|
const storedStats = fs.statSync(storedFilePath)
|
||||||
|
|
||||||
|
console.debug('storedFilePath', storedFilePath)
|
||||||
|
|
||||||
if (storedStats.size === fileSize) {
|
if (storedStats.size === fileSize) {
|
||||||
const [originalHash, storedHash] = await Promise.all([
|
const [originalHash, storedHash] = await Promise.all([
|
||||||
this.getFileHash(filePath),
|
this.getFileHash(filePath),
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
|||||||
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||||
import { FileType, RagAppRequestParams } from '@types'
|
import { FileType, RagAppRequestParams } from '@types'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Logger from 'electron-log'
|
|
||||||
|
|
||||||
class KnowledgeService {
|
class KnowledgeService {
|
||||||
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
||||||
@ -27,7 +26,6 @@ class KnowledgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getRagApplication = async ({ id, model, apiKey, baseURL }: RagAppRequestParams): Promise<RAGApplication> => {
|
private getRagApplication = async ({ id, model, apiKey, baseURL }: RagAppRequestParams): Promise<RAGApplication> => {
|
||||||
Logger.log('getRagApplication', { id, model, apiKey, baseURL })
|
|
||||||
return new RAGApplicationBuilder()
|
return new RAGApplicationBuilder()
|
||||||
.setModel('NO_MODEL')
|
.setModel('NO_MODEL')
|
||||||
.setEmbeddingModel(
|
.setEmbeddingModel(
|
||||||
@ -82,7 +80,7 @@ class KnowledgeService {
|
|||||||
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: data.path }) as any)
|
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: data.path }) as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.ext === '.md') {
|
if (data.ext === '.md' || data.ext === '.mdx') {
|
||||||
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: data.path }) as any)
|
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: data.path }) as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -123,6 +123,10 @@ export class WindowService {
|
|||||||
|
|
||||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||||
|
if (url.includes('localhost:5173')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
shell.openExternal(url)
|
shell.openExternal(url)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BookOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
@ -91,7 +91,7 @@ const Sidebar: FC = () => {
|
|||||||
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
|
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
|
||||||
<StyledLink onClick={() => to('/knowledge')}>
|
<StyledLink onClick={() => to('/knowledge')}>
|
||||||
<Icon className={isRoute('/knowledge')}>
|
<Icon className={isRoute('/knowledge')}>
|
||||||
<BookOutlined />
|
<FileSearchOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { db } from '@renderer/databases/index'
|
import { db } from '@renderer/databases/index'
|
||||||
import { RootState, useAppSelector } from '@renderer/store'
|
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
|
||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
|
||||||
|
import { RootState } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
|
addBase,
|
||||||
addItem,
|
addItem,
|
||||||
addProcessingItem,
|
clearAllProcessing,
|
||||||
clearAllItems,
|
clearCompletedProcessing,
|
||||||
clearCompletedItems,
|
deleteBase,
|
||||||
removeItem as removeItemAction,
|
removeItem as removeItemAction,
|
||||||
removeProcessingItem,
|
|
||||||
renameBase,
|
renameBase,
|
||||||
selectProcessingItemBySource,
|
|
||||||
selectProcessingItemsByType,
|
|
||||||
updateBase,
|
updateBase,
|
||||||
updateFiles as updateFilesAction,
|
updateFiles as updateFilesAction,
|
||||||
updateNotes,
|
updateItemProcessingStatus,
|
||||||
updateProcessingStatus
|
updateNotes
|
||||||
} from '@renderer/store/knowledge'
|
} from '@renderer/store/knowledge'
|
||||||
import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types'
|
import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types'
|
||||||
import { KnowledgeItem } from '@renderer/types'
|
import { KnowledgeItem } from '@renderer/types'
|
||||||
@ -26,7 +27,6 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
export const useKnowledge = (baseId: string) => {
|
export const useKnowledge = (baseId: string) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId))
|
const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId))
|
||||||
const knowledgeState = useAppSelector((state: RootState) => state.knowledge)
|
|
||||||
|
|
||||||
// 重命名知识库
|
// 重命名知识库
|
||||||
const renameKnowledgeBase = (name: string) => {
|
const renameKnowledgeBase = (name: string) => {
|
||||||
@ -41,27 +41,37 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
// 添加文件列表
|
// 添加文件列表
|
||||||
const addFiles = (files: FileType[]) => {
|
const addFiles = (files: FileType[]) => {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const newItem = {
|
const newItem: KnowledgeItem = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: 'file' as const,
|
type: 'file' as const,
|
||||||
content: file,
|
content: file,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
updated_at: Date.now()
|
updated_at: Date.now(),
|
||||||
|
processingStatus: 'pending',
|
||||||
|
processingProgress: 0,
|
||||||
|
processingError: '',
|
||||||
|
retryCount: 0
|
||||||
}
|
}
|
||||||
dispatch(addItem({ baseId, item: newItem }))
|
dispatch(addItem({ baseId, item: newItem }))
|
||||||
}
|
}
|
||||||
|
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加URL
|
// 添加URL
|
||||||
const addUrl = (url: string) => {
|
const addUrl = (url: string) => {
|
||||||
const newUrlItem = {
|
const newUrlItem: KnowledgeItem = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: 'url' as const,
|
type: 'url' as const,
|
||||||
content: url,
|
content: url,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
updated_at: Date.now()
|
updated_at: Date.now(),
|
||||||
|
processingStatus: 'pending',
|
||||||
|
processingProgress: 0,
|
||||||
|
processingError: '',
|
||||||
|
retryCount: 0
|
||||||
}
|
}
|
||||||
dispatch(addItem({ baseId, item: newUrlItem }))
|
dispatch(addItem({ baseId, item: newUrlItem }))
|
||||||
|
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加笔记
|
// 添加笔记
|
||||||
@ -85,10 +95,15 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
type: 'note',
|
type: 'note',
|
||||||
content: '', // store中不需要存储实际内容
|
content: '', // store中不需要存储实际内容
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
updated_at: Date.now()
|
updated_at: Date.now(),
|
||||||
|
processingStatus: 'pending',
|
||||||
|
processingProgress: 0,
|
||||||
|
processingError: '',
|
||||||
|
retryCount: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateNotes({ baseId, item: noteRef }))
|
dispatch(updateNotes({ baseId, item: noteRef }))
|
||||||
|
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新文件列表
|
// 更新文件列表
|
||||||
@ -101,6 +116,7 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
updated_at: Date.now()
|
updated_at: Date.now()
|
||||||
}))
|
}))
|
||||||
dispatch(updateFilesAction({ baseId, items: newItems }))
|
dispatch(updateFilesAction({ baseId, items: newItems }))
|
||||||
|
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新笔记内容
|
// 更新笔记内容
|
||||||
@ -115,6 +131,7 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
await db.knowledge_notes.put(updatedNote)
|
await db.knowledge_notes.put(updatedNote)
|
||||||
dispatch(updateNotes({ baseId, item: updatedNote }))
|
dispatch(updateNotes({ baseId, item: updatedNote }))
|
||||||
}
|
}
|
||||||
|
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取笔记内容
|
// 获取笔记内容
|
||||||
@ -123,47 +140,23 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 移除项目
|
// 移除项目
|
||||||
const removeItem = (item: KnowledgeItem) => {
|
const removeItem = async (item: KnowledgeItem) => {
|
||||||
dispatch(removeItemAction({ baseId, item }))
|
dispatch(removeItemAction({ baseId, item }))
|
||||||
}
|
if (base) {
|
||||||
|
const config = getRagAppRequestParams(base)
|
||||||
// 添加文件到处理队列
|
if (item?.uniqueId) {
|
||||||
const addFileToQueue = (itemId: string) => {
|
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, config })
|
||||||
dispatch(
|
}
|
||||||
addProcessingItem({
|
if (item.type === 'file' && typeof item.content === 'object') {
|
||||||
baseId,
|
await FileManager.deleteFile(item.content.id)
|
||||||
type: 'file',
|
}
|
||||||
sourceId: itemId
|
}
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加URL到处理队列
|
|
||||||
const addUrlToQueue = (itemId: string) => {
|
|
||||||
dispatch(
|
|
||||||
addProcessingItem({
|
|
||||||
baseId,
|
|
||||||
type: 'url',
|
|
||||||
sourceId: itemId
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加笔记到处理队列
|
|
||||||
const addNoteToQueue = (itemId: string) => {
|
|
||||||
dispatch(
|
|
||||||
addProcessingItem({
|
|
||||||
baseId,
|
|
||||||
type: 'note',
|
|
||||||
sourceId: itemId
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新处理状态
|
// 更新处理状态
|
||||||
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
|
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateProcessingStatus({
|
updateItemProcessingStatus({
|
||||||
baseId,
|
baseId,
|
||||||
itemId,
|
itemId,
|
||||||
status,
|
status,
|
||||||
@ -173,38 +166,46 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取特定源的处理状态
|
// 获取特定项目的处理状态
|
||||||
const getProcessingStatus = (sourceId: string) => {
|
const getProcessingStatus = (itemId: string) => {
|
||||||
return selectProcessingItemBySource(knowledgeState, baseId, sourceId)
|
return base?.items.find((item) => item.id === itemId)?.processingStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取特定类型的所有处理项
|
// 获取特定类型的所有处理项
|
||||||
const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => {
|
const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => {
|
||||||
return selectProcessingItemsByType(knowledgeState, baseId, type)
|
return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || []
|
||||||
}
|
|
||||||
|
|
||||||
// 从队列中移除项目
|
|
||||||
const removeFromQueue = (itemId: string) => {
|
|
||||||
dispatch(
|
|
||||||
removeProcessingItem({
|
|
||||||
baseId,
|
|
||||||
itemId
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除已完成的项目
|
// 清除已完成的项目
|
||||||
const clearCompleted = () => {
|
const clearCompleted = () => {
|
||||||
dispatch(clearCompletedItems({ baseId }))
|
dispatch(clearCompletedProcessing({ baseId }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除所有队列项目
|
// 清除所有处理状态
|
||||||
const clearAll = () => {
|
const clearAll = () => {
|
||||||
dispatch(clearAllItems({ baseId }))
|
dispatch(clearAllProcessing({ baseId }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 Sitemap
|
||||||
|
const addSitemap = (url: string) => {
|
||||||
|
const newSitemapItem: KnowledgeItem = {
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'sitemap' as const,
|
||||||
|
content: url,
|
||||||
|
created_at: Date.now(),
|
||||||
|
updated_at: Date.now(),
|
||||||
|
processingStatus: 'pending',
|
||||||
|
processingProgress: 0,
|
||||||
|
processingError: '',
|
||||||
|
retryCount: 0
|
||||||
|
}
|
||||||
|
dispatch(addItem({ baseId, item: newSitemapItem }))
|
||||||
|
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileItems = base?.items.filter((item) => item.type === 'file') || []
|
const fileItems = base?.items.filter((item) => item.type === 'file') || []
|
||||||
const urlItems = base?.items.filter((item) => item.type === 'url') || []
|
const urlItems = base?.items.filter((item) => item.type === 'url') || []
|
||||||
|
const sitemapItems = base?.items.filter((item) => item.type === 'sitemap') || []
|
||||||
const [noteItems, setNoteItems] = useState<KnowledgeItem[]>([])
|
const [noteItems, setNoteItems] = useState<KnowledgeItem[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -224,24 +225,46 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
base,
|
base,
|
||||||
fileItems,
|
fileItems,
|
||||||
urlItems,
|
urlItems,
|
||||||
|
sitemapItems,
|
||||||
noteItems,
|
noteItems,
|
||||||
renameKnowledgeBase,
|
renameKnowledgeBase,
|
||||||
updateKnowledgeBase,
|
updateKnowledgeBase,
|
||||||
addFiles,
|
addFiles,
|
||||||
addUrl,
|
addUrl,
|
||||||
|
addSitemap,
|
||||||
addNote,
|
addNote,
|
||||||
updateFiles,
|
updateFiles,
|
||||||
updateNoteContent,
|
updateNoteContent,
|
||||||
getNoteContent,
|
getNoteContent,
|
||||||
addFileToQueue,
|
|
||||||
addUrlToQueue,
|
|
||||||
addNoteToQueue,
|
|
||||||
updateItemStatus,
|
updateItemStatus,
|
||||||
getProcessingStatus,
|
getProcessingStatus,
|
||||||
getProcessingItemsByType,
|
getProcessingItemsByType,
|
||||||
removeFromQueue,
|
|
||||||
clearCompleted,
|
clearCompleted,
|
||||||
clearAll,
|
clearAll,
|
||||||
removeItem
|
removeItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useKnowledgeBases = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const bases = useSelector((state: RootState) => state.knowledge.bases)
|
||||||
|
|
||||||
|
const addKnowledgeBase = (base: KnowledgeBase) => {
|
||||||
|
dispatch(addBase(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameKnowledgeBase = (baseId: string, name: string) => {
|
||||||
|
dispatch(renameBase({ baseId, name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteKnowledgeBase = (baseId: string) => {
|
||||||
|
dispatch(deleteBase({ baseId }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bases,
|
||||||
|
addKnowledgeBase,
|
||||||
|
renameKnowledgeBase,
|
||||||
|
deleteKnowledgeBase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -555,11 +555,13 @@
|
|||||||
"status_completed": "Completed",
|
"status_completed": "Completed",
|
||||||
"status_failed": "Failed",
|
"status_failed": "Failed",
|
||||||
"url_added": "URL added",
|
"url_added": "URL added",
|
||||||
"query": "Query",
|
|
||||||
"search_placeholder": "Enter text to search",
|
"search_placeholder": "Enter text to search",
|
||||||
"add_note": "Add Note",
|
"add_note": "Add Note",
|
||||||
"no_bases": "No knowledge bases available",
|
"no_bases": "No knowledge bases available",
|
||||||
"clear_selection": "Clear selection"
|
"clear_selection": "Clear selection",
|
||||||
|
"delete_confirm": "Are you sure you want to delete this knowledge base?",
|
||||||
|
"sitemaps": "Site Maps",
|
||||||
|
"add_sitemap": "Add Site Map"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -555,11 +555,13 @@
|
|||||||
"status_completed": "Завершено",
|
"status_completed": "Завершено",
|
||||||
"status_failed": "Ошибка",
|
"status_failed": "Ошибка",
|
||||||
"url_added": "URL добавлен",
|
"url_added": "URL добавлен",
|
||||||
"query": "Поиск",
|
|
||||||
"search_placeholder": "Введите текст для поиска",
|
"search_placeholder": "Введите текст для поиска",
|
||||||
"add_note": "Добавить запись",
|
"add_note": "Добавить запись",
|
||||||
"no_bases": "База знаний не найдена",
|
"no_bases": "База знаний не найдена",
|
||||||
"clear_selection": "Очистить выбор"
|
"clear_selection": "Очистить выбор",
|
||||||
|
"delete_confirm": "Вы уверены, что хотите удалить эту базу знаний?",
|
||||||
|
"sitemaps": "Карта сайта",
|
||||||
|
"add_sitemap": "Добавить карту сайта"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -529,7 +529,7 @@
|
|||||||
"notes_placeholder": "输入此知识库的附加信息或上下文...",
|
"notes_placeholder": "输入此知识库的附加信息或上下文...",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"urls": "网站",
|
"urls": "网址",
|
||||||
"add_url": "添加网址",
|
"add_url": "添加网址",
|
||||||
"url_placeholder": "请输入网址",
|
"url_placeholder": "请输入网址",
|
||||||
"invalid_url": "无效的网址",
|
"invalid_url": "无效的网址",
|
||||||
@ -544,11 +544,13 @@
|
|||||||
"status_completed": "已完成",
|
"status_completed": "已完成",
|
||||||
"status_failed": "失败",
|
"status_failed": "失败",
|
||||||
"url_added": "网址已添加",
|
"url_added": "网址已添加",
|
||||||
"query": "查询",
|
|
||||||
"search_placeholder": "输入查询内容",
|
"search_placeholder": "输入查询内容",
|
||||||
"add_note": "添加笔记",
|
"add_note": "添加笔记",
|
||||||
"no_bases": "暂无知识库",
|
"no_bases": "暂无知识库",
|
||||||
"clear_selection": "清除选择"
|
"clear_selection": "清除选择",
|
||||||
|
"delete_confirm": "确定要删除此知识库吗?",
|
||||||
|
"sitemaps": "站点地图",
|
||||||
|
"add_sitemap": "添加站点地图"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -543,11 +543,13 @@
|
|||||||
"status_completed": "已完成",
|
"status_completed": "已完成",
|
||||||
"status_failed": "失敗",
|
"status_failed": "失敗",
|
||||||
"url_added": "網址已添加",
|
"url_added": "網址已添加",
|
||||||
"query": "查詢",
|
|
||||||
"search_placeholder": "輸入查詢內容",
|
"search_placeholder": "輸入查詢內容",
|
||||||
"add_note": "添加筆記",
|
"add_note": "添加筆記",
|
||||||
"no_bases": "暫無知識庫",
|
"no_bases": "暫無知識庫",
|
||||||
"clear_selection": "清除選擇"
|
"clear_selection": "清除選擇",
|
||||||
|
"delete_confirm": "確定要刪除此知識庫嗎?",
|
||||||
|
"sitemaps": "站點地圖",
|
||||||
|
"add_sitemap": "添加站點地圖"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,10 @@ interface Props {
|
|||||||
files: FileType[]
|
files: FileType[]
|
||||||
setFiles: (files: FileType[]) => void
|
setFiles: (files: FileType[]) => void
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton }) => {
|
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton, disabled }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const extensions = isVisionModel(model)
|
const extensions = isVisionModel(model)
|
||||||
? [...imageExts, ...documentExts, ...textExts]
|
? [...imageExts, ...documentExts, ...textExts]
|
||||||
@ -37,7 +38,7 @@ const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
|
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
|
||||||
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile}>
|
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile} disabled={disabled}>
|
||||||
<PaperClipOutlined style={{ rotate: '135deg' }} />
|
<PaperClipOutlined style={{ rotate: '135deg' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -49,7 +49,7 @@ interface Props {
|
|||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
let _files: FileType[] = []
|
let _files: FileType[] = []
|
||||||
let _base: KnowledgeBase
|
let _base: KnowledgeBase | undefined
|
||||||
|
|
||||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
@ -80,7 +80,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase>(_base)
|
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
||||||
|
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
@ -450,8 +450,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
<ControlOutlined />
|
<ControlOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<KnowledgeBaseButton selectedBase={selectedKnowledgeBase} onSelect={handleKnowledgeBaseSelect} />
|
<KnowledgeBaseButton
|
||||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
selectedBase={selectedKnowledgeBase}
|
||||||
|
onSelect={handleKnowledgeBaseSelect}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
disabled={files.length > 0}
|
||||||
|
/>
|
||||||
|
<AttachmentButton
|
||||||
|
model={model}
|
||||||
|
files={files}
|
||||||
|
setFiles={setFiles}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
disabled={!!selectedKnowledgeBase}
|
||||||
|
/>
|
||||||
<ToolbarButton type="text" onClick={onNewContext}>
|
<ToolbarButton type="text" onClick={onNewContext}>
|
||||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||||
<PicCenterOutlined />
|
<PicCenterOutlined />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BookOutlined } from '@ant-design/icons'
|
import { FileSearchOutlined } from '@ant-design/icons'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
import { KnowledgeBase } from '@renderer/types'
|
import { KnowledgeBase } from '@renderer/types'
|
||||||
import { Button, Popover, Tooltip } from 'antd'
|
import { Button, Popover, Tooltip } from 'antd'
|
||||||
@ -9,6 +9,8 @@ import styled from 'styled-components'
|
|||||||
interface Props {
|
interface Props {
|
||||||
selectedBase?: KnowledgeBase
|
selectedBase?: KnowledgeBase
|
||||||
onSelect: (base?: KnowledgeBase) => void
|
onSelect: (base?: KnowledgeBase) => void
|
||||||
|
disabled?: boolean
|
||||||
|
ToolbarButton?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
|
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
|
||||||
@ -42,14 +44,14 @@ const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect }) => {
|
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, ToolbarButton }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (selectedBase) {
|
if (selectedBase) {
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
|
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
|
||||||
<ToolbarButton type="text" onClick={() => onSelect(undefined)}>
|
<ToolbarButton type="text" onClick={() => onSelect(undefined)}>
|
||||||
<BookOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@ -61,8 +63,8 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect }) => {
|
|||||||
placement="top"
|
placement="top"
|
||||||
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
|
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
|
||||||
trigger="click">
|
trigger="click">
|
||||||
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)}>
|
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
|
||||||
<BookOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -78,40 +80,4 @@ const EmptyMessage = styled.div`
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ToolbarButton = styled(Button)`
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
font-size: 17px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
color: var(--color-icon);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
&.anticon,
|
|
||||||
&.iconfont {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
color: var(--color-icon);
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
.anticon,
|
|
||||||
.iconfont {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background-color: var(--color-primary) !important;
|
|
||||||
.anticon,
|
|
||||||
.iconfont {
|
|
||||||
color: var(--color-white-soft);
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default KnowledgeBaseButton
|
export default KnowledgeBaseButton
|
||||||
|
|||||||
@ -29,18 +29,20 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
|
|||||||
|
|
||||||
if (message.role === 'assistant') {
|
if (message.role === 'assistant') {
|
||||||
let metrixs = ''
|
let metrixs = ''
|
||||||
|
let hasMetrics = false
|
||||||
|
|
||||||
if (message?.metrics?.completion_tokens && message?.metrics?.time_completion_millsec) {
|
if (message?.metrics?.completion_tokens && message?.metrics?.time_completion_millsec) {
|
||||||
|
hasMetrics = true
|
||||||
metrixs = t('settings.messages.metrics', {
|
metrixs = t('settings.messages.metrics', {
|
||||||
time_first_token_millsec: message?.metrics?.time_first_token_millsec,
|
time_first_token_millsec: message?.metrics?.time_first_token_millsec,
|
||||||
token_speed: (message?.metrics?.completion_tokens / (message?.metrics?.time_completion_millsec / 1000)).toFixed(
|
token_speed: (message?.metrics?.completion_tokens / (message?.metrics?.time_completion_millsec / 1000)).toFixed(
|
||||||
2
|
0
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
<MessageMetadata className={`message-tokens ${hasMetrics ? 'has-metrics' : ''}`} onClick={locateMessage}>
|
||||||
<span className="metrics">{metrixs}</span>
|
<span className="metrics">{metrixs}</span>
|
||||||
<span className="tokens">
|
<span className="tokens">
|
||||||
Tokens: {message?.usage?.total_tokens} ↑{message?.usage?.prompt_tokens} ↓{message?.usage?.completion_tokens}
|
Tokens: {message?.usage?.total_tokens} ↑{message?.usage?.prompt_tokens} ↓{message?.usage?.completion_tokens}
|
||||||
@ -68,7 +70,7 @@ const MessageMetadata = styled.div`
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&.has-metrics:hover {
|
||||||
.metrics {
|
.metrics {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined
|
||||||
StopOutlined
|
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||||
@ -18,8 +18,8 @@ import { FC } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import KnowledgeSearchPopup from './KnowledgeSearchPopup'
|
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
||||||
import StatusIcon from './StatusIcon'
|
import StatusIcon from './components/StatusIcon'
|
||||||
|
|
||||||
const { Dragger } = Upload
|
const { Dragger } = Upload
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
@ -35,15 +35,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
noteItems,
|
noteItems,
|
||||||
fileItems,
|
fileItems,
|
||||||
urlItems,
|
urlItems,
|
||||||
|
sitemapItems,
|
||||||
addFiles,
|
addFiles,
|
||||||
updateNoteContent,
|
updateNoteContent,
|
||||||
addUrl,
|
addUrl,
|
||||||
|
addSitemap,
|
||||||
removeItem,
|
removeItem,
|
||||||
getProcessingStatus,
|
getProcessingStatus,
|
||||||
addFileToQueue,
|
|
||||||
addNoteToQueue,
|
|
||||||
addUrlToQueue,
|
|
||||||
clearAll,
|
|
||||||
addNote
|
addNote
|
||||||
} = useKnowledge(selectedBase.id || '')
|
} = useKnowledge(selectedBase.id || '')
|
||||||
|
|
||||||
@ -109,23 +107,36 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddSitemap = async () => {
|
||||||
|
const url = await PromptPopup.show({
|
||||||
|
title: t('knowledge_base.add_sitemap'),
|
||||||
|
message: '',
|
||||||
|
inputPlaceholder: t('knowledge_base.sitemap_placeholder'),
|
||||||
|
inputProps: {
|
||||||
|
maxLength: 1000,
|
||||||
|
rows: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
if (sitemapItems.find((item) => item.content === url)) {
|
||||||
|
message.success(t('knowledge_base.sitemap_added'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addSitemap(url)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid Sitemap URL:', url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddNote = async () => {
|
const handleAddNote = async () => {
|
||||||
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
|
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
|
||||||
note && addNote(note)
|
note && addNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIndexAll = async () => {
|
|
||||||
fileItems.forEach((item) => !item.uniqueId && addFileToQueue(item.id))
|
|
||||||
urlItems.forEach((item) => !item.uniqueId && addUrlToQueue(item.id))
|
|
||||||
noteItems.forEach((note) => !note.uniqueId && addNoteToQueue(note.id))
|
|
||||||
message.success(t('knowledge_base.index_started'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelIndex = () => {
|
|
||||||
clearAll()
|
|
||||||
message.success(t('knowledge_base.index_cancelled'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEditNote = async (note: any) => {
|
const handleEditNote = async (note: any) => {
|
||||||
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
|
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
|
||||||
editedText && updateNoteContent(note.id, editedText)
|
editedText && updateNoteContent(note.id, editedText)
|
||||||
@ -198,6 +209,33 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
</div>
|
</div>
|
||||||
</ContentSection>
|
</ContentSection>
|
||||||
|
|
||||||
|
<ContentSection>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Title level={5}>{t('knowledge_base.sitemaps')}</Title>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddSitemap}>
|
||||||
|
{t('knowledge_base.add_sitemap')}
|
||||||
|
</Button>
|
||||||
|
</TitleWrapper>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
{sitemapItems.map((item) => (
|
||||||
|
<ItemCard key={item.id}>
|
||||||
|
<ItemContent>
|
||||||
|
<ItemInfo>
|
||||||
|
<GlobalOutlined style={{ fontSize: '16px' }} />
|
||||||
|
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||||
|
{item.content as string}
|
||||||
|
</a>
|
||||||
|
</ItemInfo>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
||||||
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
|
</div>
|
||||||
|
</ItemContent>
|
||||||
|
</ItemCard>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ContentSection>
|
||||||
|
|
||||||
<ContentSection>
|
<ContentSection>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Title level={5}>{t('knowledge_base.notes')}</Title>
|
<Title level={5}>{t('knowledge_base.notes')}</Title>
|
||||||
@ -222,19 +260,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ContentSection>
|
</ContentSection>
|
||||||
|
|
||||||
<IndexSection>
|
<IndexSection>
|
||||||
<Button type="primary" onClick={handleIndexAll} icon={<FileTextOutlined />}>
|
<Button type="primary" onClick={() => KnowledgeSearchPopup.show({ base })} icon={<SearchOutlined />}>
|
||||||
{t('knowledge_base.index_all')}
|
{t('knowledge_base.search')}
|
||||||
</Button>
|
|
||||||
<Button onClick={handleCancelIndex} icon={<StopOutlined />} style={{ marginLeft: '10px' }}>
|
|
||||||
{t('knowledge_base.cancel_index')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => KnowledgeSearchPopup.show({ base })}
|
|
||||||
icon={<SearchOutlined />}
|
|
||||||
style={{ marginLeft: '10px' }}>
|
|
||||||
{t('knowledge_base.query')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</IndexSection>
|
</IndexSection>
|
||||||
<div style={{ minHeight: '20px' }} />
|
<div style={{ minHeight: '20px' }} />
|
||||||
@ -321,7 +349,7 @@ const ItemInfo = styled.div`
|
|||||||
const IndexSection = styled.div`
|
const IndexSection = styled.div`
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default KnowledgeContent
|
export default KnowledgeContent
|
||||||
|
|||||||
@ -3,35 +3,35 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
|||||||
import ListItem from '@renderer/components/ListItem'
|
import ListItem from '@renderer/components/ListItem'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { RootState } from '@renderer/store'
|
import { useKnowledgeBases } from '@renderer/hooks/useknowledge'
|
||||||
import { deleteBase, renameBase } from '@renderer/store/knowledge'
|
|
||||||
import { KnowledgeBase } from '@renderer/types'
|
import { KnowledgeBase } from '@renderer/types'
|
||||||
import { Dropdown, Empty, MenuProps } from 'antd'
|
import { Dropdown, Empty, MenuProps } from 'antd'
|
||||||
import { FC, useCallback, useEffect, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import AddKnowledgePopup from './AddKnowledgePopup'
|
import AddKnowledgePopup from './components/AddKnowledgePopup'
|
||||||
import KnowledgeContent from './KnowledgeContent'
|
import KnowledgeContent from './KnowledgeContent'
|
||||||
|
|
||||||
const KnowledgePage: FC = () => {
|
const KnowledgePage: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { bases } = useSelector((state: RootState) => state.knowledge)
|
const { bases, renameKnowledgeBase, deleteKnowledgeBase } = useKnowledgeBases()
|
||||||
const [selectedBase, setSelectedBase] = useState<KnowledgeBase>()
|
const [selectedBase, setSelectedBase] = useState<KnowledgeBase>()
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
const handleAddKnowledge = async () => {
|
const handleAddKnowledge = async () => {
|
||||||
await AddKnowledgePopup.show({
|
await AddKnowledgePopup.show({ title: t('knowledge_base.add.title') })
|
||||||
title: t('knowledge_base.add.title')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bases.length > 0) {
|
if (bases.length > 0) {
|
||||||
setSelectedBase(bases[0])
|
if (!selectedBase) {
|
||||||
|
return setSelectedBase(bases[0])
|
||||||
|
}
|
||||||
|
if (selectedBase && !bases.includes(selectedBase)) {
|
||||||
|
return setSelectedBase(bases[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [bases])
|
}, [bases, selectedBase])
|
||||||
|
|
||||||
const getMenuItems = useCallback(
|
const getMenuItems = useCallback(
|
||||||
(base: KnowledgeBase) => {
|
(base: KnowledgeBase) => {
|
||||||
@ -47,7 +47,7 @@ const KnowledgePage: FC = () => {
|
|||||||
defaultValue: base.name || ''
|
defaultValue: base.name || ''
|
||||||
})
|
})
|
||||||
if (name && base.name !== name) {
|
if (name && base.name !== name) {
|
||||||
dispatch(renameBase({ baseId: base.id, name }))
|
renameKnowledgeBase(base.id, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -58,14 +58,20 @@ const KnowledgePage: FC = () => {
|
|||||||
key: 'delete',
|
key: 'delete',
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
dispatch(deleteBase({ baseId: base.id }))
|
window.modal.confirm({
|
||||||
|
title: t('knowledge_base.delete_confirm'),
|
||||||
|
centered: true,
|
||||||
|
onOk: () => {
|
||||||
|
deleteKnowledgeBase(base.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
return menus
|
return menus
|
||||||
},
|
},
|
||||||
[dispatch, t]
|
[deleteKnowledgeBase, renameKnowledgeBase, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { isEmbeddingModel } from '@renderer/config/models'
|
import { isEmbeddingModel } from '@renderer/config/models'
|
||||||
|
import { useKnowledgeBases } from '@renderer/hooks/useknowledge'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
|
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
|
||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { addBase } from '@renderer/store/knowledge'
|
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { Form, Input, Modal, Select } from 'antd'
|
import { Form, Input, Modal, Select } from 'antd'
|
||||||
import { find, sortBy } from 'lodash'
|
import { find, sortBy } from 'lodash'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
|
|
||||||
interface ShowParams {
|
interface ShowParams {
|
||||||
title: string
|
title: string
|
||||||
@ -29,8 +28,8 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
|||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const [form] = Form.useForm<FormData>()
|
const [form] = Form.useForm<FormData>()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dispatch = useDispatch()
|
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
|
const { addKnowledgeBase } = useKnowledgeBases()
|
||||||
const allModels = providers
|
const allModels = providers
|
||||||
.map((p) => p.models)
|
.map((p) => p.models)
|
||||||
.flat()
|
.flat()
|
||||||
@ -61,14 +60,13 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
items: [],
|
items: [],
|
||||||
processingQueue: [],
|
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
updated_at: Date.now()
|
updated_at: Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.api.knowledgeBase.create(getRagAppRequestParams(newBase))
|
await window.api.knowledgeBase.create(getRagAppRequestParams(newBase))
|
||||||
|
|
||||||
dispatch(addBase(newBase as any))
|
addKnowledgeBase(newBase as any)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
resolve(newBase)
|
resolve(newBase)
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
||||||
import { Center } from '@renderer/components/Layout'
|
import { Center } from '@renderer/components/Layout'
|
||||||
import { KnowledgeBase, ProcessingItem } from '@renderer/types'
|
import { KnowledgeBase, ProcessingStatus } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -9,13 +9,14 @@ import styled from 'styled-components'
|
|||||||
interface StatusIconProps {
|
interface StatusIconProps {
|
||||||
sourceId: string
|
sourceId: string
|
||||||
base: KnowledgeBase
|
base: KnowledgeBase
|
||||||
getProcessingStatus: (sourceId: string) => ProcessingItem | undefined
|
getProcessingStatus: (sourceId: string) => ProcessingStatus | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }) => {
|
const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const status = getProcessingStatus(sourceId)
|
const status = getProcessingStatus(sourceId)
|
||||||
const item = base.items.find((item) => item.id === sourceId)
|
const item = base.items.find((item) => item.id === sourceId)
|
||||||
|
const errorText = item?.processingError
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
if (item?.uniqueId) {
|
if (item?.uniqueId) {
|
||||||
@ -34,7 +35,7 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status.status) {
|
switch (status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('knowledge_base.status_pending')} placement="left">
|
<Tooltip title={t('knowledge_base.status_pending')} placement="left">
|
||||||
@ -55,7 +56,7 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
|
|||||||
)
|
)
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('knowledge_base.status_failed')} placement="left">
|
<Tooltip title={errorText || t('knowledge_base.status_failed')} placement="left">
|
||||||
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
|
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { GithubOutlined, TwitterOutlined } from '@ant-design/icons'
|
import { GithubOutlined, XOutlined } from '@ant-design/icons'
|
||||||
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SendOutlined, SoundOutlined } from '@ant-design/icons'
|
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SendOutlined, SoundOutlined } from '@ant-design/icons'
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
@ -208,7 +208,7 @@ const AboutSettings: FC = () => {
|
|||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>
|
<SettingRowTitle>
|
||||||
<TwitterOutlined />X
|
<XOutlined />X
|
||||||
</SettingRowTitle>
|
</SettingRowTitle>
|
||||||
<Button onClick={() => onOpenWebsite('https://x.com/kangfenmao')}>
|
<Button onClick={() => onOpenWebsite('https://x.com/kangfenmao')}>
|
||||||
{t('settings.about.website.button')}
|
{t('settings.about.website.button')}
|
||||||
|
|||||||
@ -25,7 +25,12 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getMessageParam(message: Message): Promise<MessageParam> {
|
private async getMessageParam(message: Message): Promise<MessageParam> {
|
||||||
const parts: MessageParam['content'] = [{ type: 'text', text: message.content }]
|
const parts: MessageParam['content'] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: await this.getMessageContent(message)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
for (const file of message.files || []) {
|
for (const file of message.files || []) {
|
||||||
if (file.type === FileTypes.IMAGE) {
|
if (file.type === FileTypes.IMAGE) {
|
||||||
@ -83,11 +88,20 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
system: assistant.prompt
|
system: assistant.prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let time_first_token_millsec = 0
|
||||||
|
const start_time_millsec = new Date().getTime()
|
||||||
|
|
||||||
if (!streamOutput) {
|
if (!streamOutput) {
|
||||||
const message = await this.sdk.messages.create({ ...body, stream: false })
|
const message = await this.sdk.messages.create({ ...body, stream: false })
|
||||||
|
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||||
return onChunk({
|
return onChunk({
|
||||||
text: message.content[0].type === 'text' ? message.content[0].text : '',
|
text: message.content[0].type === 'text' ? message.content[0].text : '',
|
||||||
usage: message.usage
|
usage: message.usage,
|
||||||
|
metrics: {
|
||||||
|
completion_tokens: message.usage.output_tokens,
|
||||||
|
time_completion_millsec,
|
||||||
|
time_first_token_millsec: 0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +113,18 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
stream.controller.abort()
|
stream.controller.abort()
|
||||||
return resolve()
|
return resolve()
|
||||||
}
|
}
|
||||||
onChunk({ text })
|
if (time_first_token_millsec == 0) {
|
||||||
|
time_first_token_millsec = new Date().getTime() - start_time_millsec
|
||||||
|
}
|
||||||
|
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||||
|
onChunk({
|
||||||
|
text,
|
||||||
|
metrics: {
|
||||||
|
completion_tokens: undefined,
|
||||||
|
time_completion_millsec,
|
||||||
|
time_first_token_millsec
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on('finalMessage', (message) => {
|
.on('finalMessage', (message) => {
|
||||||
onChunk({
|
onChunk({
|
||||||
@ -108,6 +133,11 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
prompt_tokens: message.usage.input_tokens,
|
prompt_tokens: message.usage.input_tokens,
|
||||||
completion_tokens: message.usage.output_tokens,
|
completion_tokens: message.usage.output_tokens,
|
||||||
total_tokens: sum(Object.values(message.usage))
|
total_tokens: sum(Object.values(message.usage))
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
completion_tokens: message.usage.output_tokens,
|
||||||
|
time_completion_millsec: new Date().getTime() - start_time_millsec,
|
||||||
|
time_first_token_millsec
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
resolve()
|
resolve()
|
||||||
|
|||||||
@ -19,6 +19,24 @@ export default abstract class BaseProvider {
|
|||||||
this.apiKey = this.getApiKey()
|
this.apiKey = this.getApiKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
|
||||||
|
abstract translate(message: Message, assistant: Assistant): Promise<string>
|
||||||
|
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
|
||||||
|
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
|
||||||
|
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
|
||||||
|
abstract check(): Promise<{ valid: boolean; error: Error | null }>
|
||||||
|
abstract models(): Promise<OpenAI.Models.Model[]>
|
||||||
|
abstract generateImage(_params: {
|
||||||
|
prompt: string
|
||||||
|
negativePrompt: string
|
||||||
|
imageSize: string
|
||||||
|
batchSize: number
|
||||||
|
seed?: string
|
||||||
|
numInferenceSteps: number
|
||||||
|
guidanceScale: number
|
||||||
|
signal?: AbortSignal
|
||||||
|
}): Promise<string[]>
|
||||||
|
|
||||||
public getBaseURL(): string {
|
public getBaseURL(): string {
|
||||||
const host = this.provider.apiHost
|
const host = this.provider.apiHost
|
||||||
return host.endsWith('/') ? host : `${host}/v1/`
|
return host.endsWith('/') ? host : `${host}/v1/`
|
||||||
@ -63,7 +81,7 @@ export default abstract class BaseProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMessageContentWithKnowledgeBase(message: Message) {
|
public async getMessageContent(message: Message) {
|
||||||
if (!message.knowledgeBaseIds) {
|
if (!message.knowledgeBaseIds) {
|
||||||
return message.content
|
return message.content
|
||||||
}
|
}
|
||||||
@ -81,8 +99,6 @@ export default abstract class BaseProvider {
|
|||||||
config: getRagAppRequestParams(base)
|
config: getRagAppRequestParams(base)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug('searchResults', searchResults)
|
|
||||||
|
|
||||||
const references = take(searchResults, 5)
|
const references = take(searchResults, 5)
|
||||||
.map((item, index) => {
|
.map((item, index) => {
|
||||||
let sourceUrl = ''
|
let sourceUrl = ''
|
||||||
@ -93,16 +109,15 @@ export default abstract class BaseProvider {
|
|||||||
if (baseItem) {
|
if (baseItem) {
|
||||||
switch (baseItem.type) {
|
switch (baseItem.type) {
|
||||||
case 'file':
|
case 'file':
|
||||||
sourceUrl = `file://${(baseItem?.content as FileType).path}`
|
// sourceUrl = `file://${encodeURIComponent((baseItem?.content as FileType).path)}`
|
||||||
sourceName = (baseItem?.content as FileType).origin_name
|
sourceName = (baseItem?.content as FileType).origin_name
|
||||||
break
|
break
|
||||||
case 'url':
|
case 'url':
|
||||||
sourceUrl = baseItem.content as string
|
sourceUrl = baseItem.content as string
|
||||||
sourceName = ''
|
sourceName = baseItem.content as string
|
||||||
break
|
break
|
||||||
case 'note':
|
case 'note':
|
||||||
sourceUrl = ''
|
sourceName = baseItem.content as string
|
||||||
sourceName = ''
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,26 +133,9 @@ source_url: ${sourceUrl}
|
|||||||
})
|
})
|
||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
|
|
||||||
const prompt = `回答问题请参考以下内容,并使用类似 [^1] content [source_name](source_url) 的脚注格式引用数据来源,脚注内容可以点击跳转。当 source_name 为空的时候可以使用 content 作为脚注内容。`
|
const prompt =
|
||||||
|
'回答问题请参考以下内容,并使用类似 [^1]: source 的脚注格式引用数据来源, source 根据 source_type 决定'
|
||||||
|
|
||||||
return [message.content, prompt, references].join('\n\n')
|
return [message.content, prompt, references].join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
|
|
||||||
abstract translate(message: Message, assistant: Assistant): Promise<string>
|
|
||||||
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
|
|
||||||
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
|
|
||||||
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
|
|
||||||
abstract check(): Promise<{ valid: boolean; error: Error | null }>
|
|
||||||
abstract models(): Promise<OpenAI.Models.Model[]>
|
|
||||||
abstract generateImage(_params: {
|
|
||||||
prompt: string
|
|
||||||
negativePrompt: string
|
|
||||||
imageSize: string
|
|
||||||
batchSize: number
|
|
||||||
seed?: string
|
|
||||||
numInferenceSteps: number
|
|
||||||
guidanceScale: number
|
|
||||||
signal?: AbortSignal
|
|
||||||
}): Promise<string[]>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import axios from 'axios'
|
|||||||
import { first, isEmpty, takeRight } from 'lodash'
|
import { first, isEmpty, takeRight } from 'lodash'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
|
import { CompletionsParams } from '.'
|
||||||
import BaseProvider from './BaseProvider'
|
import BaseProvider from './BaseProvider'
|
||||||
|
|
||||||
export default class GeminiProvider extends BaseProvider {
|
export default class GeminiProvider extends BaseProvider {
|
||||||
@ -34,7 +35,7 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
private async getMessageContents(message: Message): Promise<Content> {
|
private async getMessageContents(message: Message): Promise<Content> {
|
||||||
const role = message.role === 'user' ? 'user' : 'model'
|
const role = message.role === 'user' ? 'user' : 'model'
|
||||||
|
|
||||||
const parts: Part[] = [{ text: message.content }]
|
const parts: Part[] = [{ text: await this.getMessageContent(message) }]
|
||||||
|
|
||||||
for (const file of message.files || []) {
|
for (const file of message.files || []) {
|
||||||
if (file.type === FileTypes.IMAGE) {
|
if (file.type === FileTypes.IMAGE) {
|
||||||
@ -107,29 +108,47 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
const chat = geminiModel.startChat({ history })
|
const chat = geminiModel.startChat({ history })
|
||||||
const messageContents = await this.getMessageContents(userLastMessage!)
|
const messageContents = await this.getMessageContents(userLastMessage!)
|
||||||
|
|
||||||
|
const start_time_millsec = new Date().getTime()
|
||||||
|
|
||||||
if (!streamOutput) {
|
if (!streamOutput) {
|
||||||
const { response } = await chat.sendMessage(messageContents.parts)
|
const { response } = await chat.sendMessage(messageContents.parts)
|
||||||
|
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||||
onChunk({
|
onChunk({
|
||||||
text: response.candidates?.[0].content.parts[0].text,
|
text: response.candidates?.[0].content.parts[0].text,
|
||||||
usage: {
|
usage: {
|
||||||
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
|
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
|
||||||
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
|
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
|
||||||
total_tokens: response.usageMetadata?.totalTokenCount || 0
|
total_tokens: response.usageMetadata?.totalTokenCount || 0
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
completion_tokens: response.usageMetadata?.candidatesTokenCount,
|
||||||
|
time_completion_millsec,
|
||||||
|
time_first_token_millsec: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessagesStream = await chat.sendMessageStream(messageContents.parts)
|
const userMessagesStream = await chat.sendMessageStream(messageContents.parts)
|
||||||
|
let time_first_token_millsec = 0
|
||||||
|
|
||||||
for await (const chunk of userMessagesStream.stream) {
|
for await (const chunk of userMessagesStream.stream) {
|
||||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
||||||
|
if (time_first_token_millsec == 0) {
|
||||||
|
time_first_token_millsec = new Date().getTime() - start_time_millsec
|
||||||
|
}
|
||||||
|
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||||
onChunk({
|
onChunk({
|
||||||
text: chunk.text(),
|
text: chunk.text(),
|
||||||
usage: {
|
usage: {
|
||||||
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
|
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
|
||||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
|
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
|
||||||
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
|
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
|
||||||
|
time_completion_millsec,
|
||||||
|
time_first_token_millsec
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,11 +50,12 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
model: Model
|
model: Model
|
||||||
): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam> {
|
): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam> {
|
||||||
const isVision = isVisionModel(model)
|
const isVision = isVisionModel(model)
|
||||||
|
const content = await this.getMessageContent(message)
|
||||||
|
|
||||||
if (!message.files) {
|
if (!message.files) {
|
||||||
return {
|
return {
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: await this.getMessageContentWithKnowledgeBase(message)
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,21 +75,21 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: message.content + divider + text
|
content: content + divider + text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: message.content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts: ChatCompletionContentPart[] = [
|
const parts: ChatCompletionContentPart[] = [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: message.content
|
text: content
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
|
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { removeProcessingItem, updateBaseItemUniqueId, updateProcessingStatus } from '@renderer/store/knowledge'
|
import { clearCompletedProcessing, updateBaseItemUniqueId, updateItemProcessingStatus } from '@renderer/store/knowledge'
|
||||||
import { ProcessingItem } from '@renderer/types'
|
import { KnowledgeItem } from '@renderer/types'
|
||||||
|
|
||||||
class KnowledgeQueue {
|
class KnowledgeQueue {
|
||||||
private processing: Map<string, boolean> = new Map()
|
private processing: Map<string, boolean> = new Map()
|
||||||
private pollingInterval: NodeJS.Timeout | null = null
|
private pollingInterval: NodeJS.Timeout | null = null
|
||||||
private readonly POLLING_INTERVAL = 5000
|
// private readonly POLLING_INTERVAL = 5000
|
||||||
private readonly MAX_RETRIES = 3
|
private readonly MAX_RETRIES = 3
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -21,10 +21,10 @@ class KnowledgeQueue {
|
|||||||
|
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
state.knowledge.bases.forEach((base) => {
|
state.knowledge.bases.forEach((base) => {
|
||||||
base.processingQueue.forEach((item) => {
|
base.items.forEach((item) => {
|
||||||
if (item.status === 'processing') {
|
if (item.processingStatus === 'processing') {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
updateProcessingStatus({
|
updateItemProcessingStatus({
|
||||||
baseId: base.id,
|
baseId: base.id,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
@ -35,9 +35,9 @@ class KnowledgeQueue {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pollingInterval = setInterval(() => {
|
// this.pollingInterval = setInterval(() => {
|
||||||
this.checkAllBases()
|
// this.checkAllBases()
|
||||||
}, this.POLLING_INTERVAL)
|
// }, this.POLLING_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopPolling(): void {
|
private stopPolling(): void {
|
||||||
@ -47,27 +47,21 @@ class KnowledgeQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkAllBases(): Promise<void> {
|
public async checkAllBases(): Promise<void> {
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
const bases = state.knowledge.bases
|
const bases = state.knowledge.bases
|
||||||
|
|
||||||
console.log('[KnowledgeQueue] Checking all bases for pending items...')
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
bases.map(async (base) => {
|
bases.map(async (base) => {
|
||||||
const processableItems = base.processingQueue.filter((item) => {
|
const processableItems = base.items.filter((item) => {
|
||||||
if (item.status === 'failed') {
|
if (item.processingStatus === 'failed') {
|
||||||
return !item.retryCount || item.retryCount < this.MAX_RETRIES
|
return !item.retryCount || item.retryCount < this.MAX_RETRIES
|
||||||
}
|
}
|
||||||
return item.status === 'pending'
|
return item.processingStatus === 'pending'
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasProcessableItems = processableItems.length > 0
|
const hasProcessableItems = processableItems.length > 0
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[KnowledgeQueue] Base ${base.id}: ${hasProcessableItems ? 'has processable items' : 'no processable items'}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasProcessableItems && !this.processing.get(base.id)) {
|
if (hasProcessableItems && !this.processing.get(base.id)) {
|
||||||
await this.processQueue(base.id)
|
await this.processQueue(base.id)
|
||||||
}
|
}
|
||||||
@ -91,11 +85,11 @@ class KnowledgeQueue {
|
|||||||
throw new Error('Knowledge base not found')
|
throw new Error('Knowledge base not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const processableItems = base.processingQueue.filter((item) => {
|
const processableItems = base.items.filter((item) => {
|
||||||
if (item.status === 'failed') {
|
if (item.processingStatus === 'failed') {
|
||||||
return !item.retryCount || item.retryCount < this.MAX_RETRIES
|
return !item.retryCount || item.retryCount < this.MAX_RETRIES
|
||||||
}
|
}
|
||||||
return item.status === 'pending'
|
return item.processingStatus === 'pending'
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const item of processableItems) {
|
for (const item of processableItems) {
|
||||||
@ -124,7 +118,7 @@ class KnowledgeQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processItem(baseId: string, item: ProcessingItem): Promise<void> {
|
private async processItem(baseId: string, item: KnowledgeItem): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
|
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
|
||||||
console.log(`[KnowledgeQueue] Item ${item.id} has reached max retries, skipping`)
|
console.log(`[KnowledgeQueue] Item ${item.id} has reached max retries, skipping`)
|
||||||
@ -132,9 +126,9 @@ class KnowledgeQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[KnowledgeQueue] Starting to process item ${item.id} (${item.type})`)
|
console.log(`[KnowledgeQueue] Starting to process item ${item.id} (${item.type})`)
|
||||||
// Update status to processing
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
updateProcessingStatus({
|
updateItemProcessingStatus({
|
||||||
baseId,
|
baseId,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
status: 'processing',
|
status: 'processing',
|
||||||
@ -149,10 +143,10 @@ class KnowledgeQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestParams = getRagAppRequestParams(base)
|
const requestParams = getRagAppRequestParams(base)
|
||||||
const sourceItem = base.items.find((i) => i.id === item.sourceId)
|
const sourceItem = base.items.find((i) => i.id === item.id)
|
||||||
|
|
||||||
if (!sourceItem) {
|
if (!sourceItem) {
|
||||||
throw new Error(`[KnowledgeQueue] Source item ${item.sourceId} not found in base ${baseId}`)
|
throw new Error(`[KnowledgeQueue] Source item ${item.id} not found in base ${baseId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: AddLoaderReturn | null = null
|
let result: AddLoaderReturn | null = null
|
||||||
@ -169,10 +163,15 @@ class KnowledgeQueue {
|
|||||||
result = await window.api.knowledgeBase.add({ data: sourceItem.content, config: requestParams })
|
result = await window.api.knowledgeBase.add({ data: sourceItem.content, config: requestParams })
|
||||||
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
|
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
|
||||||
break
|
break
|
||||||
|
case 'sitemap':
|
||||||
|
console.log(`[KnowledgeQueue] Processing Sitemap: ${sourceItem.content}`)
|
||||||
|
result = await window.api.knowledgeBase.add({ data: sourceItem.content, config: requestParams })
|
||||||
|
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
|
||||||
|
break
|
||||||
case 'note':
|
case 'note':
|
||||||
console.log(`[KnowledgeQueue] Processing note: ${sourceItem.content}`)
|
console.log(`[KnowledgeQueue] Processing note: ${sourceItem.content}`)
|
||||||
note = await db.knowledge_notes.get(item.sourceId)
|
note = await db.knowledge_notes.get(item.id)
|
||||||
if (!note) throw new Error(`Source note ${item.sourceId} not found`)
|
if (!note) throw new Error(`Source note ${item.id} not found`)
|
||||||
content = note.content as string
|
content = note.content as string
|
||||||
result = await window.api.knowledgeBase.add({ data: content, config: requestParams })
|
result = await window.api.knowledgeBase.add({ data: content, config: requestParams })
|
||||||
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
|
console.log(`[KnowledgeQueue] Result: ${JSON.stringify(result)}`)
|
||||||
@ -181,36 +180,31 @@ class KnowledgeQueue {
|
|||||||
|
|
||||||
console.log(`[KnowledgeQueue] Successfully completed processing item ${item.id}`)
|
console.log(`[KnowledgeQueue] Successfully completed processing item ${item.id}`)
|
||||||
|
|
||||||
// Mark as completed
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
updateProcessingStatus({
|
updateItemProcessingStatus({
|
||||||
baseId,
|
baseId,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
status: 'completed'
|
status: 'completed'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update uniqueId
|
|
||||||
if (result) {
|
if (result) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
updateBaseItemUniqueId({
|
updateBaseItemUniqueId({
|
||||||
baseId,
|
baseId,
|
||||||
itemId: item.sourceId,
|
itemId: item.id,
|
||||||
uniqueId: result.uniqueId
|
uniqueId: result.uniqueId
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`[KnowledgeQueue] Updated uniqueId for item ${item.sourceId} in base ${baseId}`)
|
console.debug(`[KnowledgeQueue] Updated uniqueId for item ${item.id} in base ${baseId}`)
|
||||||
|
|
||||||
// Remove from queue after successful processing
|
setTimeout(() => store.dispatch(clearCompletedProcessing({ baseId })), 1000)
|
||||||
setTimeout(() => {
|
|
||||||
store.dispatch(removeProcessingItem({ baseId, itemId: item.id }))
|
|
||||||
}, 1000)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[KnowledgeQueue] Error processing item ${item.id}:`, error)
|
console.error(`[KnowledgeQueue] Error processing item ${item.id}:`, error)
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
updateProcessingStatus({
|
updateItemProcessingStatus({
|
||||||
baseId,
|
baseId,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { getRagAppRequestParams } from '@renderer/services/KnowledgeService'
|
import { FileType, KnowledgeBase, KnowledgeItem, ProcessingStatus } from '@renderer/types'
|
||||||
import { FileType, KnowledgeBase, KnowledgeItem, ProcessingItem, ProcessingStatus } from '@renderer/types'
|
|
||||||
|
|
||||||
export interface KnowledgeState {
|
export interface KnowledgeState {
|
||||||
bases: KnowledgeBase[]
|
bases: KnowledgeBase[]
|
||||||
@ -15,12 +14,10 @@ const knowledgeSlice = createSlice({
|
|||||||
name: 'knowledge',
|
name: 'knowledge',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
// 添加知识库
|
|
||||||
addBase(state, action: PayloadAction<KnowledgeBase>) {
|
addBase(state, action: PayloadAction<KnowledgeBase>) {
|
||||||
state.bases.push(action.payload)
|
state.bases.push(action.payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除知识库
|
|
||||||
deleteBase(state, action: PayloadAction<{ baseId: string }>) {
|
deleteBase(state, action: PayloadAction<{ baseId: string }>) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
@ -31,7 +28,6 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重命名知识库
|
|
||||||
renameBase(state, action: PayloadAction<{ baseId: string; name: string }>) {
|
renameBase(state, action: PayloadAction<{ baseId: string; name: string }>) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
@ -40,7 +36,6 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新知识库
|
|
||||||
updateBase(state, action: PayloadAction<KnowledgeBase>) {
|
updateBase(state, action: PayloadAction<KnowledgeBase>) {
|
||||||
const index = state.bases.findIndex((b) => b.id === action.payload.id)
|
const index = state.bases.findIndex((b) => b.id === action.payload.id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@ -48,7 +43,6 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 添加条目
|
|
||||||
addItem(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
|
addItem(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
@ -68,26 +62,15 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除条目
|
|
||||||
removeItem(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
|
removeItem(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
|
||||||
const { baseId, item } = action.payload
|
const { baseId } = action.payload
|
||||||
const base = state.bases.find((b) => b.id === baseId)
|
const base = state.bases.find((b) => b.id === baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
base.items = base.items.filter((item) => item.id !== action.payload.item.id)
|
base.items = base.items.filter((item) => item.id !== action.payload.item.id)
|
||||||
base.updated_at = Date.now()
|
base.updated_at = Date.now()
|
||||||
if (item?.uniqueId) {
|
|
||||||
window.api.knowledgeBase.remove({
|
|
||||||
uniqueId: item.uniqueId,
|
|
||||||
config: getRagAppRequestParams(base)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (item.type === 'file' && typeof item.content === 'object') {
|
|
||||||
FileManager.deleteFile(item.content.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新文件
|
|
||||||
updateFiles(state, action: PayloadAction<{ baseId: string; items: KnowledgeItem[] }>) {
|
updateFiles(state, action: PayloadAction<{ baseId: string; items: KnowledgeItem[] }>) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
@ -98,7 +81,6 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新笔记
|
|
||||||
updateNotes(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
|
updateNotes(state, action: PayloadAction<{ baseId: string; item: KnowledgeItem }>) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
@ -114,33 +96,7 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 添加处理队列项
|
updateItemProcessingStatus(
|
||||||
addProcessingItem(
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ baseId: string; type: 'file' | 'url' | 'note'; sourceId: string }>
|
|
||||||
) {
|
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
|
||||||
if (base) {
|
|
||||||
const newItem: ProcessingItem = {
|
|
||||||
id: `${action.payload.type}-${action.payload.sourceId}`,
|
|
||||||
type: action.payload.type,
|
|
||||||
sourceId: action.payload.sourceId,
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
baseId: action.payload.baseId
|
|
||||||
}
|
|
||||||
|
|
||||||
// 避免重复添加
|
|
||||||
const exists = base.processingQueue.some((item) => item.sourceId === action.payload.sourceId)
|
|
||||||
if (!exists) {
|
|
||||||
base.processingQueue.push(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 更新处理状态
|
|
||||||
updateProcessingStatus(
|
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
baseId: string
|
baseId: string
|
||||||
@ -153,44 +109,42 @@ const knowledgeSlice = createSlice({
|
|||||||
) {
|
) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
const item = base.processingQueue.find((item) => item.id === action.payload.itemId)
|
const item = base.items.find((item) => item.id === action.payload.itemId)
|
||||||
if (item) {
|
if (item) {
|
||||||
item.status = action.payload.status
|
item.processingStatus = action.payload.status
|
||||||
item.progress = action.payload.progress
|
item.processingProgress = action.payload.progress
|
||||||
item.error = action.payload.error
|
item.processingError = action.payload.error
|
||||||
item.retryCount = action.payload.retryCount
|
item.retryCount = action.payload.retryCount
|
||||||
item.updatedAt = Date.now()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 移除处理队列项
|
clearCompletedProcessing(state, action: PayloadAction<{ baseId: string }>) {
|
||||||
removeProcessingItem(state, action: PayloadAction<{ baseId: string; itemId: string }>) {
|
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
base.processingQueue = base.processingQueue.filter((item) => item.id !== action.payload.itemId)
|
base.items.forEach((item) => {
|
||||||
|
if (item.processingStatus === 'completed' || item.processingStatus === 'failed') {
|
||||||
|
item.processingStatus = undefined
|
||||||
|
item.processingProgress = undefined
|
||||||
|
item.processingError = undefined
|
||||||
|
item.retryCount = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清除已完成的项目
|
clearAllProcessing(state, action: PayloadAction<{ baseId: string }>) {
|
||||||
clearCompletedItems(state, action: PayloadAction<{ baseId: string }>) {
|
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
base.processingQueue = base.processingQueue.filter(
|
base.items.forEach((item) => {
|
||||||
(item) => item.status !== 'completed' && item.status !== 'failed'
|
item.processingStatus = undefined
|
||||||
)
|
item.processingProgress = undefined
|
||||||
|
item.processingError = undefined
|
||||||
|
item.retryCount = undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清除所有队列项目
|
|
||||||
clearAllItems(state, action: PayloadAction<{ baseId: string }>) {
|
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
|
||||||
if (base) {
|
|
||||||
base.processingQueue = []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 更新知识库单个条目下面的 uniqueId
|
|
||||||
updateBaseItemUniqueId(state, action: PayloadAction<{ baseId: string; itemId: string; uniqueId: string }>) {
|
updateBaseItemUniqueId(state, action: PayloadAction<{ baseId: string; itemId: string; uniqueId: string }>) {
|
||||||
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
@ -203,25 +157,6 @@ const knowledgeSlice = createSlice({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Selectors
|
|
||||||
export const selectProcessingItemBySource = (
|
|
||||||
state: KnowledgeState,
|
|
||||||
baseId: string,
|
|
||||||
sourceId: string
|
|
||||||
): ProcessingItem | undefined => {
|
|
||||||
const base = state.bases.find((b) => b.id === baseId)
|
|
||||||
return base?.processingQueue.find((item) => item.sourceId === sourceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const selectProcessingItemsByType = (
|
|
||||||
state: KnowledgeState,
|
|
||||||
baseId: string,
|
|
||||||
type: 'file' | 'url' | 'note'
|
|
||||||
): ProcessingItem[] => {
|
|
||||||
const base = state.bases.find((b) => b.id === baseId)
|
|
||||||
return base?.processingQueue.filter((item) => item.type === type) || []
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
addBase,
|
addBase,
|
||||||
deleteBase,
|
deleteBase,
|
||||||
@ -231,11 +166,9 @@ export const {
|
|||||||
updateFiles,
|
updateFiles,
|
||||||
updateNotes,
|
updateNotes,
|
||||||
removeItem,
|
removeItem,
|
||||||
addProcessingItem,
|
updateItemProcessingStatus,
|
||||||
updateProcessingStatus,
|
clearCompletedProcessing,
|
||||||
removeProcessingItem,
|
clearAllProcessing,
|
||||||
clearCompletedItems,
|
|
||||||
clearAllItems,
|
|
||||||
updateBaseItemUniqueId
|
updateBaseItemUniqueId
|
||||||
} = knowledgeSlice.actions
|
} = knowledgeSlice.actions
|
||||||
|
|
||||||
|
|||||||
@ -183,27 +183,18 @@ export interface Shortcut {
|
|||||||
|
|
||||||
export type ProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed'
|
export type ProcessingStatus = 'pending' | 'processing' | 'completed' | 'failed'
|
||||||
|
|
||||||
export type ProcessingItem = {
|
|
||||||
id: string
|
|
||||||
type: 'file' | 'url' | 'note'
|
|
||||||
status: ProcessingStatus
|
|
||||||
progress?: number
|
|
||||||
error?: string
|
|
||||||
createdAt: number
|
|
||||||
updatedAt: number
|
|
||||||
sourceId: string // file id, url, or note id
|
|
||||||
baseId: string
|
|
||||||
retryCount?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KnowledgeItem = {
|
export type KnowledgeItem = {
|
||||||
id: string
|
id: string
|
||||||
baseId?: string
|
baseId?: string
|
||||||
uniqueId?: string
|
uniqueId?: string
|
||||||
type: 'file' | 'url' | 'note'
|
type: 'file' | 'url' | 'note' | 'sitemap'
|
||||||
content: string | FileType // for files: FileType, for urls: string, for notes: string
|
content: string | FileType
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
|
processingStatus?: ProcessingStatus
|
||||||
|
processingProgress?: number
|
||||||
|
processingError?: string
|
||||||
|
retryCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeBase {
|
export interface KnowledgeBase {
|
||||||
@ -214,7 +205,6 @@ export interface KnowledgeBase {
|
|||||||
items: KnowledgeItem[]
|
items: KnowledgeItem[]
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
processingQueue: ProcessingItem[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RagAppRequestParams = {
|
export type RagAppRequestParams = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user