update, use json database replace pglite, for sync
This commit is contained in:
parent
10970a8803
commit
96b9fcef3b
@ -4,20 +4,19 @@ import { InitialEditorStateType } from '@lexical/react/LexicalComposer'
|
||||
import { $getRoot, $insertNodes, LexicalEditor } from 'lexical'
|
||||
import { Pencil, Search, Trash2 } from 'lucide-react'
|
||||
import { Notice } from 'obsidian'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
// import { v4 as uuidv4 } from 'uuid'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { lexicalNodeToPlainText } from '../../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||
import { useDatabase } from '../../contexts/DatabaseContext'
|
||||
import { DBManager } from '../../database/database-manager'
|
||||
import { TemplateContent } from '../../database/schema'
|
||||
import { useCommands } from '../../hooks/use-commands'
|
||||
|
||||
import LexicalContentEditable from './chat-input/LexicalContentEditable'
|
||||
|
||||
|
||||
export interface QuickCommand {
|
||||
id: string
|
||||
name: string
|
||||
content: TemplateContent
|
||||
contentText: string
|
||||
createdAt: Date | undefined
|
||||
updatedAt: Date | undefined
|
||||
}
|
||||
@ -29,30 +28,12 @@ const CommandsView = (
|
||||
selectedSerializedNodes?: BaseSerializedNode[]
|
||||
}
|
||||
) => {
|
||||
const [commands, setCommands] = useState<QuickCommand[]>([])
|
||||
|
||||
const { getDatabaseManager } = useDatabase()
|
||||
const getManager = useCallback(async (): Promise<DBManager> => {
|
||||
return await getDatabaseManager()
|
||||
}, [getDatabaseManager])
|
||||
|
||||
// init get all commands
|
||||
const fetchCommands = useCallback(async () => {
|
||||
const dbManager = await getManager()
|
||||
dbManager.getCommandManager().getAllCommands((rows) => {
|
||||
setCommands(rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
content: row.content,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
})))
|
||||
})
|
||||
}, [getManager])
|
||||
|
||||
useEffect(() => {
|
||||
void fetchCommands()
|
||||
}, [fetchCommands])
|
||||
const {
|
||||
createCommand,
|
||||
deleteCommand,
|
||||
updateCommand,
|
||||
commandList,
|
||||
} = useCommands()
|
||||
|
||||
// new command name
|
||||
const [newCommandName, setNewCommandName] = useState('')
|
||||
@ -66,13 +47,13 @@ const CommandsView = (
|
||||
const nameInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
||||
const contentEditorRefs = useRef<Map<string, LexicalEditor>>(new Map())
|
||||
|
||||
// 为每个正在编辑的命令创建refs
|
||||
// create refs for each command
|
||||
const commandEditRefs = useRef<Map<string, {
|
||||
editorRef: React.RefObject<LexicalEditor>,
|
||||
contentEditableRef: React.RefObject<HTMLDivElement>
|
||||
}>>(new Map());
|
||||
|
||||
// 获取或创建命令编辑refs
|
||||
// get or create command edit refs
|
||||
const getCommandEditRefs = useCallback((id: string) => {
|
||||
if (!commandEditRefs.current.has(id)) {
|
||||
commandEditRefs.current.set(id, {
|
||||
@ -94,7 +75,7 @@ const CommandsView = (
|
||||
return refs;
|
||||
}, []);
|
||||
|
||||
// 当编辑状态改变时更新refs
|
||||
// update command edit refs when editing command id changes
|
||||
useEffect(() => {
|
||||
if (editingCommandId) {
|
||||
const refs = getCommandEditRefs(editingCommandId);
|
||||
@ -133,25 +114,20 @@ const CommandsView = (
|
||||
new Notice('Please enter a name for your template')
|
||||
return
|
||||
}
|
||||
const dbManager = await getManager()
|
||||
dbManager.getCommandManager().createCommand({
|
||||
name: newCommandName,
|
||||
content: { nodes },
|
||||
})
|
||||
|
||||
await createCommand(newCommandName, { nodes })
|
||||
|
||||
// clear editor content
|
||||
editorRef.current.update(() => {
|
||||
const root = $getRoot()
|
||||
root.clear()
|
||||
})
|
||||
|
||||
setNewCommandName('')
|
||||
}
|
||||
|
||||
// delete command
|
||||
const handleDeleteCommand = async (id: string) => {
|
||||
const dbManager = await getManager()
|
||||
await dbManager.getCommandManager().deleteCommand(id)
|
||||
await deleteCommand(id)
|
||||
}
|
||||
|
||||
// edit command
|
||||
@ -173,11 +149,11 @@ const CommandsView = (
|
||||
new Notice('Please enter a content for your template')
|
||||
return
|
||||
}
|
||||
const dbManager = await getManager()
|
||||
await dbManager.getCommandManager().updateCommand(id, {
|
||||
name: nameInput.value,
|
||||
content: { nodes },
|
||||
})
|
||||
await updateCommand(
|
||||
id,
|
||||
nameInput.value,
|
||||
{ nodes },
|
||||
)
|
||||
setEditingCommandId(null)
|
||||
}
|
||||
|
||||
@ -187,11 +163,16 @@ const CommandsView = (
|
||||
}
|
||||
|
||||
// filter commands list
|
||||
const filteredCommands = commands.filter(
|
||||
command =>
|
||||
command.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
command.content.nodes.map(lexicalNodeToPlainText).join('').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
const filteredCommands = useMemo(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
return commandList;
|
||||
}
|
||||
return commandList.filter(
|
||||
command =>
|
||||
command.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
command.contentText.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [commandList, searchTerm]);
|
||||
|
||||
const getCommandEditorState = (commandContent: TemplateContent): InitialEditorStateType => {
|
||||
return (editor: LexicalEditor) => {
|
||||
@ -287,7 +268,7 @@ const CommandsView = (
|
||||
// view mode
|
||||
<div className="infio-commands-view-mode">
|
||||
<div className="infio-commands-name">{command.name}</div>
|
||||
<div className="infio-commands-content">{command.content.nodes.map(lexicalNodeToPlainText).join('')}</div>
|
||||
<div className="infio-commands-content">{command.contentText}</div>
|
||||
<div className="infio-commands-actions">
|
||||
<button
|
||||
onClick={() => handleEditCommand(command)}
|
||||
|
||||
@ -242,7 +242,7 @@ export function ModelSelect() {
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
setSelectedIndex(0)
|
||||
// 确保下一个渲染循环中仍然聚焦在输入框
|
||||
// Ensure the input is focused in the next render cycle
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 0)
|
||||
@ -292,7 +292,7 @@ export function ModelSelect() {
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
// 确保下一个渲染循环中仍然聚焦在输入框
|
||||
// ensure the input is focused in the next render cycle
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 0)
|
||||
@ -350,7 +350,7 @@ export function ModelSelect() {
|
||||
</DropdownMenu.Root>
|
||||
<style>
|
||||
{`
|
||||
/* 模型项样式 */
|
||||
/* Model item styles */
|
||||
.infio-llm-setting-model-item {
|
||||
display: block;
|
||||
padding: 0;
|
||||
@ -366,7 +366,7 @@ export function ModelSelect() {
|
||||
border-left: 3px solid var(--interactive-accent);
|
||||
}
|
||||
|
||||
/* 文本溢出处理 */
|
||||
/* Text overflow handling */
|
||||
.infio-model-item-text-wrapper {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -379,7 +379,7 @@ export function ModelSelect() {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* 高亮样式 - 使用紫色而不是主题色 */
|
||||
/* Highlighted text style - use purple instead of theme color */
|
||||
.infio-llm-setting-model-item-highlight {
|
||||
display: inline;
|
||||
color: #9370DB;
|
||||
@ -389,7 +389,7 @@ export function ModelSelect() {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 搜索容器 */
|
||||
/* Search container */
|
||||
.infio-llm-setting-search-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -399,7 +399,7 @@ export function ModelSelect() {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
/* 提供商选择器容器 */
|
||||
/* Provider selector container */
|
||||
.infio-llm-setting-provider-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -408,9 +408,7 @@ export function ModelSelect() {
|
||||
width: 26%;
|
||||
}
|
||||
|
||||
/* 移除提供商选择箭头 */
|
||||
|
||||
/* 提供商选择器 */
|
||||
/* Provider selector */
|
||||
.infio-llm-setting-provider-switch {
|
||||
width: 100% !important;
|
||||
margin: 0;
|
||||
@ -435,7 +433,7 @@ export function ModelSelect() {
|
||||
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* 搜索框容器 */
|
||||
/* Search container */
|
||||
.infio-search-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -443,10 +441,8 @@ export function ModelSelect() {
|
||||
flex: 1 1 auto;
|
||||
width: 74%;
|
||||
}
|
||||
|
||||
/* 移除搜索图标 */
|
||||
|
||||
/* 搜索输入框 */
|
||||
|
||||
/* Search input */
|
||||
.infio-llm-setting-item-search {
|
||||
width: 100% !important;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
@ -469,7 +465,7 @@ export function ModelSelect() {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 下拉菜单容器 */
|
||||
/* Dropdown menu container */
|
||||
.infio-llm-setting-combobox-dropdown {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
|
||||
@ -63,4 +63,4 @@ export const GROQ_PRICES: Record<string, ModelPricing> = {
|
||||
'llama-3.1-8b-instant': { input: 0.05, output: 0.08 },
|
||||
}
|
||||
|
||||
export const PGLITE_DB_PATH = '.infio_vector_db.tar.gz'
|
||||
export const PGLITE_DB_PATH = '.infio_pglite_db.tar.gz'
|
||||
|
||||
@ -13,7 +13,7 @@ import { getEmbeddingModel } from './embedding'
|
||||
export class RAGEngine {
|
||||
private app: App
|
||||
private settings: InfioSettings
|
||||
private vectorManager: VectorManager
|
||||
private vectorManager: VectorManager | null = null
|
||||
private embeddingModel: EmbeddingModel | null = null
|
||||
private initialized = false
|
||||
|
||||
@ -28,6 +28,11 @@ export class RAGEngine {
|
||||
this.embeddingModel = getEmbeddingModel(settings)
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.embeddingModel = null
|
||||
this.vectorManager = null
|
||||
}
|
||||
|
||||
setSettings(settings: InfioSettings) {
|
||||
this.settings = settings
|
||||
this.embeddingModel = getEmbeddingModel(settings)
|
||||
@ -40,8 +45,6 @@ export class RAGEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement automatic vault re-indexing when settings are changed.
|
||||
// Currently, users must manually re-index the vault.
|
||||
async updateVaultIndex(
|
||||
options: { reindexAll: boolean },
|
||||
onQueryProgressChange?: (queryProgress: QueryProgressState) => void,
|
||||
|
||||
@ -2,27 +2,21 @@
|
||||
import { type PGliteWithLive } from '@electric-sql/pglite/live'
|
||||
import { App } from 'obsidian'
|
||||
|
||||
// import { PGLITE_DB_PATH } from '../constants'
|
||||
import { createAndInitDb } from '../pgworker'
|
||||
|
||||
import { CommandManager } from './modules/command/command-manager'
|
||||
import { ConversationManager } from './modules/conversation/conversation-manager'
|
||||
import { CommandManager as CommandManager } from './modules/command/command-manager'
|
||||
import { VectorManager } from './modules/vector/vector-manager'
|
||||
// import { pgliteResources } from './pglite-resources'
|
||||
// import { migrations } from './sql'
|
||||
|
||||
export class DBManager {
|
||||
private app: App
|
||||
// private dbPath: string
|
||||
private db: PGliteWithLive | null = null
|
||||
// private db: PgliteDatabase | null = null
|
||||
private vectorManager: VectorManager
|
||||
private CommandManager: CommandManager
|
||||
private conversationManager: ConversationManager
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app
|
||||
// this.dbPath = dbPath
|
||||
}
|
||||
|
||||
static async create(app: App): Promise<DBManager> {
|
||||
@ -36,123 +30,24 @@ export class DBManager {
|
||||
return dbManager
|
||||
}
|
||||
|
||||
getPgClient() {
|
||||
getPgClient(): PGliteWithLive | null {
|
||||
return this.db
|
||||
}
|
||||
|
||||
getVectorManager() {
|
||||
getVectorManager(): VectorManager {
|
||||
return this.vectorManager
|
||||
}
|
||||
|
||||
getCommandManager() {
|
||||
getCommandManager(): CommandManager {
|
||||
return this.CommandManager
|
||||
}
|
||||
|
||||
getConversationManager() {
|
||||
getConversationManager(): ConversationManager {
|
||||
return this.conversationManager
|
||||
}
|
||||
|
||||
// private async createNewDatabase() {
|
||||
// const { fsBundle, wasmModule, vectorExtensionBundlePath } =
|
||||
// await this.loadPGliteResources()
|
||||
// this.db = await PGlite.create({
|
||||
// fsBundle: fsBundle,
|
||||
// wasmModule: wasmModule,
|
||||
// extensions: {
|
||||
// vector: vectorExtensionBundlePath,
|
||||
// live,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
// private async loadExistingDatabase() {
|
||||
// try {
|
||||
// const databaseFileExists = await this.app.vault.adapter.exists(
|
||||
// this.dbPath,
|
||||
// )
|
||||
// if (!databaseFileExists) {
|
||||
// return null
|
||||
// }
|
||||
// const fileBuffer = await this.app.vault.adapter.readBinary(this.dbPath)
|
||||
// const fileBlob = new Blob([fileBuffer], { type: 'application/x-gzip' })
|
||||
// const { fsBundle, wasmModule, vectorExtensionBundlePath } =
|
||||
// await this.loadPGliteResources()
|
||||
// this.db = await PGlite.create({
|
||||
// loadDataDir: fileBlob,
|
||||
// fsBundle: fsBundle,
|
||||
// wasmModule: wasmModule,
|
||||
// extensions: {
|
||||
// vector: vectorExtensionBundlePath,
|
||||
// live
|
||||
// },
|
||||
// })
|
||||
// // return drizzle(this.pgClient)
|
||||
// } catch (error) {
|
||||
// console.error('Error loading database:', error)
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async migrateDatabase(): Promise<void> {
|
||||
// if (!this.db) {
|
||||
// throw new Error('Database client not initialized');
|
||||
// }
|
||||
|
||||
// try {
|
||||
// // Execute SQL migrations
|
||||
// for (const [_key, migration] of Object.entries(migrations)) {
|
||||
// // Split SQL into individual commands and execute them one by one
|
||||
// const commands = migration.sql.split('\n\n').filter(cmd => cmd.trim());
|
||||
// for (const command of commands) {
|
||||
// await this.db.query(command);
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error executing SQL migrations:', error);
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
|
||||
async save(): Promise<void> {
|
||||
console.warn("need remove")
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
this.db?.close()
|
||||
this.db = null
|
||||
}
|
||||
|
||||
// private async loadPGliteResources(): Promise<{
|
||||
// fsBundle: Blob
|
||||
// wasmModule: WebAssembly.Module
|
||||
// vectorExtensionBundlePath: URL
|
||||
// }> {
|
||||
// try {
|
||||
// // Convert base64 to binary data
|
||||
// const wasmBinary = Buffer.from(pgliteResources.wasmBase64, 'base64')
|
||||
// const dataBinary = Buffer.from(pgliteResources.dataBase64, 'base64')
|
||||
// const vectorBinary = Buffer.from(pgliteResources.vectorBase64, 'base64')
|
||||
|
||||
// // Create blobs from binary data
|
||||
// const fsBundle = new Blob([dataBinary], {
|
||||
// type: 'application/octet-stream',
|
||||
// })
|
||||
// const wasmModule = await WebAssembly.compile(wasmBinary)
|
||||
|
||||
// // Create a blob URL for the vector extension
|
||||
// const vectorBlob = new Blob([vectorBinary], {
|
||||
// type: 'application/gzip',
|
||||
// })
|
||||
// const vectorExtensionBundlePath = URL.createObjectURL(vectorBlob)
|
||||
|
||||
// return {
|
||||
// fsBundle,
|
||||
// wasmModule,
|
||||
// vectorExtensionBundlePath: new URL(vectorExtensionBundlePath),
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error loading PGlite resources:', error)
|
||||
// throw error
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
85
src/database/json/base.ts
Normal file
85
src/database/json/base.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import * as path from 'path'
|
||||
|
||||
import { App, normalizePath } from 'obsidian'
|
||||
|
||||
export abstract class AbstractJsonRepository<T, M> {
|
||||
protected dataDir: string
|
||||
protected app: App
|
||||
|
||||
constructor(app: App, dataDir: string) {
|
||||
this.app = app
|
||||
this.dataDir = normalizePath(dataDir)
|
||||
this.ensureDirectory()
|
||||
}
|
||||
|
||||
private async ensureDirectory(): Promise<void> {
|
||||
if (!(await this.app.vault.adapter.exists(this.dataDir))) {
|
||||
await this.app.vault.adapter.mkdir(this.dataDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Each subclass implements how to generate a file name from a data row.
|
||||
protected abstract generateFileName(row: T): string
|
||||
|
||||
// Each subclass implements how to parse a file name into metadata.
|
||||
protected abstract parseFileName(fileName: string): M | null
|
||||
|
||||
public async create(row: T): Promise<void> {
|
||||
const fileName = this.generateFileName(row)
|
||||
const filePath = normalizePath(path.join(this.dataDir, fileName))
|
||||
const content = JSON.stringify(row, null, 2)
|
||||
|
||||
if (await this.app.vault.adapter.exists(filePath)) {
|
||||
throw new Error(`File already exists: ${filePath}`)
|
||||
}
|
||||
|
||||
await this.app.vault.adapter.write(filePath, content)
|
||||
}
|
||||
|
||||
public async update(oldRow: T, newRow: T): Promise<void> {
|
||||
const oldFileName = this.generateFileName(oldRow)
|
||||
const newFileName = this.generateFileName(newRow)
|
||||
const content = JSON.stringify(newRow, null, 2)
|
||||
|
||||
if (oldFileName === newFileName) {
|
||||
// Simple update - filename hasn't changed
|
||||
const filePath = normalizePath(path.join(this.dataDir, oldFileName))
|
||||
await this.app.vault.adapter.write(filePath, content)
|
||||
} else {
|
||||
// Filename has changed - create new file and delete old one
|
||||
const newFilePath = normalizePath(path.join(this.dataDir, newFileName))
|
||||
await this.app.vault.adapter.write(newFilePath, content)
|
||||
await this.delete(oldFileName)
|
||||
}
|
||||
}
|
||||
|
||||
// List metadata for all records by parsing file names.
|
||||
public async listMetadata(): Promise<(M & { fileName: string })[]> {
|
||||
const files = await this.app.vault.adapter.list(this.dataDir)
|
||||
return files.files
|
||||
.map((filePath) => path.basename(filePath))
|
||||
.filter((fileName) => fileName.endsWith('.json'))
|
||||
.map((fileName) => {
|
||||
const metadata = this.parseFileName(fileName)
|
||||
return metadata ? { ...metadata, fileName } : null
|
||||
})
|
||||
.filter(
|
||||
(metadata): metadata is M & { fileName: string } => metadata !== null,
|
||||
)
|
||||
}
|
||||
|
||||
public async read(fileName: string): Promise<T | null> {
|
||||
const filePath = normalizePath(path.join(this.dataDir, fileName))
|
||||
if (!(await this.app.vault.adapter.exists(filePath))) return null
|
||||
|
||||
const content = await this.app.vault.adapter.read(filePath)
|
||||
return JSON.parse(content) as T
|
||||
}
|
||||
|
||||
public async delete(fileName: string): Promise<void> {
|
||||
const filePath = normalizePath(path.join(this.dataDir, fileName))
|
||||
if (await this.app.vault.adapter.exists(filePath)) {
|
||||
await this.app.vault.adapter.remove(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/database/json/chat/ChatManager.ts
Normal file
115
src/database/json/chat/ChatManager.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { App } from 'obsidian'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { AbstractJsonRepository } from '../base'
|
||||
import { CHAT_DIR, ROOT_DIR } from '../constants'
|
||||
import { EmptyChatTitleException } from '../exception'
|
||||
|
||||
import {
|
||||
CHAT_SCHEMA_VERSION,
|
||||
ChatConversation,
|
||||
ChatConversationMetadata,
|
||||
} from './types'
|
||||
|
||||
export class ChatManager extends AbstractJsonRepository<
|
||||
ChatConversation,
|
||||
ChatConversationMetadata
|
||||
> {
|
||||
constructor(app: App) {
|
||||
super(app, `${ROOT_DIR}/${CHAT_DIR}`)
|
||||
}
|
||||
|
||||
protected generateFileName(chat: ChatConversation): string {
|
||||
// Format: v{schemaVersion}_{title}_{updatedAt}_{id}.json
|
||||
const encodedTitle = encodeURIComponent(chat.title)
|
||||
return `v${chat.schemaVersion}_${encodedTitle}_${chat.updatedAt}_${chat.id}.json`
|
||||
}
|
||||
|
||||
protected parseFileName(fileName: string): ChatConversationMetadata | null {
|
||||
// Parse: v{schemaVersion}_{title}_{updatedAt}_{id}.json
|
||||
const regex = new RegExp(
|
||||
`^v${CHAT_SCHEMA_VERSION}_(.+)_(\\d+)_([0-9a-f-]+)\\.json$`,
|
||||
)
|
||||
const match = fileName.match(regex)
|
||||
if (!match) return null
|
||||
|
||||
const title = decodeURIComponent(match[1])
|
||||
const updatedAt = parseInt(match[2], 10)
|
||||
const id = match[3]
|
||||
|
||||
return {
|
||||
id,
|
||||
schemaVersion: CHAT_SCHEMA_VERSION,
|
||||
title,
|
||||
updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
public async createChat(
|
||||
initialData: Partial<ChatConversation>,
|
||||
): Promise<ChatConversation> {
|
||||
if (initialData.title && initialData.title.length === 0) {
|
||||
throw new EmptyChatTitleException()
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const newChat: ChatConversation = {
|
||||
id: uuidv4(),
|
||||
title: 'New chat',
|
||||
messages: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
schemaVersion: CHAT_SCHEMA_VERSION,
|
||||
...initialData,
|
||||
}
|
||||
|
||||
await this.create(newChat)
|
||||
return newChat
|
||||
}
|
||||
|
||||
public async findById(id: string): Promise<ChatConversation | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.id === id)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async updateChat(
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Omit<ChatConversation, 'id' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
|
||||
>,
|
||||
): Promise<ChatConversation | null> {
|
||||
const chat = await this.findById(id)
|
||||
if (!chat) return null
|
||||
|
||||
if (updates.title !== undefined && updates.title.length === 0) {
|
||||
throw new EmptyChatTitleException()
|
||||
}
|
||||
|
||||
const updatedChat: ChatConversation = {
|
||||
...chat,
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
await this.update(chat, updatedChat)
|
||||
return updatedChat
|
||||
}
|
||||
|
||||
public async deleteChat(id: string): Promise<boolean> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.id === id)
|
||||
if (!targetMetadata) return false
|
||||
|
||||
await this.delete(targetMetadata.fileName)
|
||||
return true
|
||||
}
|
||||
|
||||
public async listChats(): Promise<ChatConversationMetadata[]> {
|
||||
const metadata = await this.listMetadata()
|
||||
return metadata.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
}
|
||||
19
src/database/json/chat/types.ts
Normal file
19
src/database/json/chat/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { SerializedChatMessage } from '../../../types/chat'
|
||||
|
||||
export const CHAT_SCHEMA_VERSION = 1
|
||||
|
||||
export type ChatConversation = {
|
||||
id: string
|
||||
title: string
|
||||
messages: SerializedChatMessage[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
|
||||
export type ChatConversationMetadata = {
|
||||
id: string
|
||||
title: string
|
||||
updatedAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
148
src/database/json/command/TemplateManager.ts
Executable file
148
src/database/json/command/TemplateManager.ts
Executable file
@ -0,0 +1,148 @@
|
||||
import fuzzysort from 'fuzzysort'
|
||||
import { App } from 'obsidian'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { AbstractJsonRepository } from '../base'
|
||||
import { ROOT_DIR, TEMPLATE_DIR } from '../constants'
|
||||
import {
|
||||
DuplicateTemplateException,
|
||||
EmptyTemplateNameException,
|
||||
} from '../exception'
|
||||
|
||||
import { TEMPLATE_SCHEMA_VERSION, Template, TemplateMetadata } from './types'
|
||||
|
||||
export class TemplateManager extends AbstractJsonRepository<
|
||||
Template,
|
||||
TemplateMetadata
|
||||
> {
|
||||
constructor(app: App) {
|
||||
super(app, `${ROOT_DIR}/${TEMPLATE_DIR}`)
|
||||
}
|
||||
|
||||
protected generateFileName(template: Template): string {
|
||||
// Format: v{schemaVersion}_name_id.json (with name encoded)
|
||||
const encodedName = encodeURIComponent(template.name)
|
||||
return `v${TEMPLATE_SCHEMA_VERSION}_${encodedName}_${template.id}.json`
|
||||
}
|
||||
|
||||
protected parseFileName(fileName: string): TemplateMetadata | null {
|
||||
const match = fileName.match(
|
||||
new RegExp(`^v${TEMPLATE_SCHEMA_VERSION}_(.+)_([0-9a-f-]+)\\.json$`),
|
||||
)
|
||||
if (!match) return null
|
||||
|
||||
const encodedName = match[1]
|
||||
const id = match[2]
|
||||
const name = decodeURIComponent(encodedName)
|
||||
|
||||
return { id, name, schemaVersion: TEMPLATE_SCHEMA_VERSION }
|
||||
}
|
||||
|
||||
public async createTemplate(
|
||||
template: Omit<
|
||||
Template,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'schemaVersion'
|
||||
>,
|
||||
): Promise<Template> {
|
||||
if (template.name !== undefined && template.name.length === 0) {
|
||||
throw new EmptyTemplateNameException()
|
||||
}
|
||||
|
||||
const existingTemplate = await this.findByName(template.name)
|
||||
if (existingTemplate) {
|
||||
throw new DuplicateTemplateException(template.name)
|
||||
}
|
||||
|
||||
const newTemplate: Template = {
|
||||
id: uuidv4(),
|
||||
...template,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
schemaVersion: TEMPLATE_SCHEMA_VERSION,
|
||||
}
|
||||
|
||||
await this.create(newTemplate)
|
||||
return newTemplate
|
||||
}
|
||||
|
||||
public async ListTemplates(): Promise<Template[]> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const allTemplates = await Promise.all(allMetadata.map(async (meta) => this.read(meta.fileName)))
|
||||
return allTemplates.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
public async findById(id: string): Promise<Template | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.id === id)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async findByName(name: string): Promise<Template | null> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.name === name)
|
||||
|
||||
if (!targetMetadata) return null
|
||||
|
||||
return this.read(targetMetadata.fileName)
|
||||
}
|
||||
|
||||
public async updateTemplate(
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Omit<Template, 'id' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
|
||||
>,
|
||||
): Promise<Template | null> {
|
||||
if (updates.name !== undefined && updates.name.length === 0) {
|
||||
throw new EmptyTemplateNameException()
|
||||
}
|
||||
|
||||
const template = await this.findById(id)
|
||||
if (!template) return null
|
||||
|
||||
if (updates.name && updates.name !== template.name) {
|
||||
const existingTemplate = await this.findByName(updates.name)
|
||||
if (existingTemplate) {
|
||||
throw new DuplicateTemplateException(updates.name)
|
||||
}
|
||||
}
|
||||
|
||||
const updatedTemplate: Template = {
|
||||
...template,
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
await this.update(template, updatedTemplate)
|
||||
return updatedTemplate
|
||||
}
|
||||
|
||||
public async deleteTemplate(id: string): Promise<boolean> {
|
||||
const template = await this.findById(id)
|
||||
if (!template) return false
|
||||
|
||||
const fileName = this.generateFileName(template)
|
||||
await this.delete(fileName)
|
||||
return true
|
||||
}
|
||||
|
||||
public async searchTemplates(query: string): Promise<Template[]> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const results = fuzzysort.go(query, allMetadata, {
|
||||
keys: ['name'],
|
||||
threshold: 0.2,
|
||||
limit: 20,
|
||||
all: true,
|
||||
})
|
||||
|
||||
const templates = (
|
||||
await Promise.all(
|
||||
results.map(async (result) => this.read(result.obj.fileName)),
|
||||
)
|
||||
).filter((template): template is Template => template !== null)
|
||||
|
||||
return templates
|
||||
}
|
||||
}
|
||||
18
src/database/json/command/types.ts
Normal file
18
src/database/json/command/types.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { SerializedLexicalNode } from 'lexical'
|
||||
|
||||
export const TEMPLATE_SCHEMA_VERSION = 1
|
||||
|
||||
export type Template = {
|
||||
id: string
|
||||
name: string
|
||||
content: { nodes: SerializedLexicalNode[] }
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
|
||||
export type TemplateMetadata = {
|
||||
id: string
|
||||
name: string
|
||||
schemaVersion: number
|
||||
}
|
||||
4
src/database/json/constants.ts
Normal file
4
src/database/json/constants.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const ROOT_DIR = '.infio_json_db'
|
||||
export const TEMPLATE_DIR = 'templates'
|
||||
export const CHAT_DIR = 'chats'
|
||||
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
|
||||
20
src/database/json/exception.ts
Normal file
20
src/database/json/exception.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export class DuplicateTemplateException extends Error {
|
||||
constructor(templateName: string) {
|
||||
super(`Template with name "${templateName}" already exists`)
|
||||
this.name = 'DuplicateTemplateException'
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyTemplateNameException extends Error {
|
||||
constructor() {
|
||||
super('Template name cannot be empty')
|
||||
this.name = 'EmptyTemplateNameException'
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyChatTitleException extends Error {
|
||||
constructor() {
|
||||
super('Chat title cannot be empty')
|
||||
this.name = 'EmptyChatTitleException'
|
||||
}
|
||||
}
|
||||
118
src/database/json/migrateToJsonDatabase.ts
Normal file
118
src/database/json/migrateToJsonDatabase.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { App, normalizePath } from 'obsidian'
|
||||
|
||||
import { DBManager } from '../database-manager'
|
||||
import { DuplicateTemplateException } from '../exception'
|
||||
import { ConversationManager } from '../modules/conversation/conversation-manager'
|
||||
|
||||
import { ChatManager } from './chat/ChatManager'
|
||||
import { INITIAL_MIGRATION_MARKER, ROOT_DIR } from './constants'
|
||||
import { TemplateManager } from './command/TemplateManager'
|
||||
import { serializeChatMessage } from './utils'
|
||||
|
||||
async function hasMigrationCompleted(app: App): Promise<boolean> {
|
||||
const markerPath = normalizePath(`${ROOT_DIR}/${INITIAL_MIGRATION_MARKER}`)
|
||||
return await app.vault.adapter.exists(markerPath)
|
||||
}
|
||||
|
||||
async function markMigrationCompleted(app: App): Promise<void> {
|
||||
const markerPath = normalizePath(`${ROOT_DIR}/${INITIAL_MIGRATION_MARKER}`)
|
||||
await app.vault.adapter.write(
|
||||
markerPath,
|
||||
`Migration completed on ${new Date().toISOString()}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function transferChatHistory(app: App, dbManager: DBManager): Promise<void> {
|
||||
const oldChatManager = new ConversationManager(app, dbManager)
|
||||
const newChatManager = new ChatManager(app)
|
||||
|
||||
const chatList = await oldChatManager.conversations()
|
||||
|
||||
for (const chatMeta of chatList) {
|
||||
try {
|
||||
const existingChat = await newChatManager.findById(chatMeta.id)
|
||||
if (existingChat) {
|
||||
continue
|
||||
}
|
||||
|
||||
const oldChatMessageList = await oldChatManager.findConversation(chatMeta.id)
|
||||
if (!oldChatMessageList) {
|
||||
continue
|
||||
}
|
||||
|
||||
await newChatManager.createChat({
|
||||
id: chatMeta.id,
|
||||
title: chatMeta.title,
|
||||
messages: oldChatMessageList.map(msg => serializeChatMessage(msg)),
|
||||
createdAt: chatMeta.created_at instanceof Date ? chatMeta.created_at.getTime() : chatMeta.created_at,
|
||||
updatedAt: chatMeta.updated_at instanceof Date ? chatMeta.updated_at.getTime() : chatMeta.updated_at,
|
||||
})
|
||||
|
||||
const verifyChat = await newChatManager.findById(chatMeta.id)
|
||||
if (!verifyChat) {
|
||||
throw new Error(`Failed to verify migration of chat ${chatMeta.id}`)
|
||||
}
|
||||
|
||||
await oldChatManager.deleteConversation(chatMeta.id)
|
||||
} catch (error) {
|
||||
console.error(`Error migrating chat ${chatMeta.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Chat history migration to JSON database completed')
|
||||
}
|
||||
|
||||
async function transferTemplates(
|
||||
app: App,
|
||||
dbManager: DBManager,
|
||||
): Promise<void> {
|
||||
const jsonTemplateManager = new TemplateManager(app)
|
||||
const templateManager = dbManager.getCommandManager()
|
||||
|
||||
const templates = await templateManager.findAllCommands()
|
||||
|
||||
for (const template of templates) {
|
||||
try {
|
||||
if (await jsonTemplateManager.findByName(template.name)) {
|
||||
// Template already exists, skip
|
||||
continue
|
||||
}
|
||||
await jsonTemplateManager.createTemplate({
|
||||
name: template.name,
|
||||
content: template.content,
|
||||
})
|
||||
|
||||
const verifyTemplate = await jsonTemplateManager.findByName(template.name)
|
||||
if (!verifyTemplate) {
|
||||
throw new Error(
|
||||
`Failed to verify migration of template ${template.name}`,
|
||||
)
|
||||
}
|
||||
|
||||
await templateManager.deleteCommand(template.id)
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateTemplateException) {
|
||||
console.log(`Duplicate template found: ${template.name}. Skipping...`)
|
||||
} else {
|
||||
console.error(`Error migrating template ${template.name}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Templates migration to JSON database completed')
|
||||
}
|
||||
|
||||
export async function migrateToJsonDatabase(
|
||||
app: App,
|
||||
dbManager: DBManager,
|
||||
onMigrationComplete?: () => void,
|
||||
): Promise<void> {
|
||||
if (await hasMigrationCompleted(app)) {
|
||||
return
|
||||
}
|
||||
|
||||
await transferChatHistory(app, dbManager)
|
||||
await transferTemplates(app, dbManager)
|
||||
await markMigrationCompleted(app)
|
||||
onMigrationComplete?.()
|
||||
}
|
||||
63
src/database/json/utils.ts
Normal file
63
src/database/json/utils.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { App } from 'obsidian'
|
||||
|
||||
import { ChatMessage, SerializedChatMessage } from '../../types/chat'
|
||||
import { Mentionable } from '../../types/mentionable'
|
||||
import {
|
||||
deserializeMentionable,
|
||||
serializeMentionable,
|
||||
} from '../../utils/mentionable'
|
||||
|
||||
|
||||
export const serializeChatMessage = (message: ChatMessage): SerializedChatMessage => {
|
||||
switch (message.role) {
|
||||
case 'user':
|
||||
return {
|
||||
role: 'user',
|
||||
applyStatus: message.applyStatus,
|
||||
content: message.content,
|
||||
promptContent: message.promptContent,
|
||||
id: message.id,
|
||||
mentionables: message.mentionables.map(serializeMentionable),
|
||||
similaritySearchResults: message.similaritySearchResults,
|
||||
}
|
||||
case 'assistant':
|
||||
return {
|
||||
role: 'assistant',
|
||||
applyStatus: message.applyStatus,
|
||||
content: message.content,
|
||||
reasoningContent: message.reasoningContent,
|
||||
id: message.id,
|
||||
metadata: message.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deserializeChatMessage = (
|
||||
message: SerializedChatMessage,
|
||||
app: App,
|
||||
): ChatMessage => {
|
||||
switch (message.role) {
|
||||
case 'user': {
|
||||
return {
|
||||
role: 'user',
|
||||
applyStatus: message.applyStatus,
|
||||
content: message.content,
|
||||
promptContent: message.promptContent,
|
||||
id: message.id,
|
||||
mentionables: message.mentionables
|
||||
.map((m) => deserializeMentionable(m, app))
|
||||
.filter((m): m is Mentionable => m !== null),
|
||||
similaritySearchResults: message.similaritySearchResults,
|
||||
}
|
||||
}
|
||||
case 'assistant':
|
||||
return {
|
||||
role: 'assistant',
|
||||
applyStatus: message.applyStatus,
|
||||
content: message.content,
|
||||
reasoningContent: message.reasoningContent,
|
||||
id: message.id,
|
||||
metadata: message.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,7 @@ export class CommandRepository {
|
||||
throw new DatabaseNotInitializedException()
|
||||
}
|
||||
const result = await this.db.query<SelectTemplate>(
|
||||
`SELECT * FROM "template"`
|
||||
`SELECT * FROM "template" ORDER BY created_at DESC`
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { App } from 'obsidian'
|
||||
import { Transaction } from '@electric-sql/pglite'
|
||||
import { App } from 'obsidian'
|
||||
|
||||
import { editorStateToPlainText } from '../../../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat'
|
||||
@ -77,6 +77,10 @@ export class ConversationManager {
|
||||
await this.repository.delete(id)
|
||||
}
|
||||
|
||||
async conversations(): Promise<SelectConversation[]>{
|
||||
return this.repository.findAll()
|
||||
}
|
||||
|
||||
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
|
||||
const db = this.dbManager.getPgClient()
|
||||
db?.live.query('SELECT * FROM conversations ORDER BY updated_at DESC', [], (results: { rows: Array<SelectConversation> }) => {
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useDatabase } from '../contexts/DatabaseContext'
|
||||
import { DBManager } from '../database/database-manager'
|
||||
import { ChatConversationMeta, ChatMessage } from '../types/chat'
|
||||
import { editorStateToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||
import { useApp } from '../contexts/AppContext'
|
||||
import { ChatManager } from '../database/json/chat/ChatManager'
|
||||
import { ChatConversationMetadata } from '../database/json/chat/types'
|
||||
import { deserializeChatMessage, serializeChatMessage } from '../database/json/utils'
|
||||
import { ChatMessage } from '../types/chat'
|
||||
|
||||
type UseChatHistory = {
|
||||
createOrUpdateConversation: (
|
||||
@ -12,64 +17,100 @@ type UseChatHistory = {
|
||||
deleteConversation: (id: string) => Promise<void>
|
||||
getChatMessagesById: (id: string) => Promise<ChatMessage[] | null>
|
||||
updateConversationTitle: (id: string, title: string) => Promise<void>
|
||||
chatList: ChatConversationMeta[]
|
||||
chatList: ChatConversationMetadata[]
|
||||
}
|
||||
|
||||
export function useChatHistory(): UseChatHistory {
|
||||
const { getDatabaseManager } = useDatabase()
|
||||
const app = useApp()
|
||||
const chatManager = useMemo(() => new ChatManager(app), [app])
|
||||
|
||||
const [chatList, setChatList] = useState<ChatConversationMeta[]>([])
|
||||
|
||||
const getManager = useCallback(async (): Promise<DBManager> => {
|
||||
return await getDatabaseManager()
|
||||
}, [getDatabaseManager])
|
||||
const [chatList, setChatList] = useState<ChatConversationMetadata[]>([])
|
||||
|
||||
const fetchChatList = useCallback(async () => {
|
||||
const dbManager = await getManager()
|
||||
dbManager.getConversationManager().getAllConversations((conversations) => {
|
||||
setChatList(conversations)
|
||||
})
|
||||
}, [getManager])
|
||||
const conversations = await chatManager.listChats()
|
||||
setChatList(conversations)
|
||||
}, [chatManager])
|
||||
|
||||
useEffect(() => {
|
||||
void fetchChatList()
|
||||
}, [fetchChatList])
|
||||
|
||||
const createOrUpdateConversation = useCallback(
|
||||
async (id: string, messages: ChatMessage[]): Promise<void> => {
|
||||
const dbManager = await getManager()
|
||||
const conversationManager = dbManager.getConversationManager()
|
||||
await conversationManager.txCreateOrUpdateConversation(id, messages)
|
||||
},
|
||||
[getManager],
|
||||
)
|
||||
const createOrUpdateConversation = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
async (id: string, messages: ChatMessage[]): Promise<void> => {
|
||||
const serializedMessages = messages.map(serializeChatMessage)
|
||||
const existingConversation = await chatManager.findById(id)
|
||||
|
||||
if (existingConversation) {
|
||||
if (isEqual(existingConversation.messages, serializedMessages)) {
|
||||
return
|
||||
}
|
||||
await chatManager.updateChat(existingConversation.id, {
|
||||
messages: serializedMessages,
|
||||
})
|
||||
} else {
|
||||
const firstUserMessage = messages.find((v) => v.role === 'user')
|
||||
|
||||
await chatManager.createChat({
|
||||
id,
|
||||
title: firstUserMessage?.content
|
||||
? editorStateToPlainText(firstUserMessage.content).substring(
|
||||
0,
|
||||
50,
|
||||
)
|
||||
: 'New chat',
|
||||
messages: serializedMessages,
|
||||
})
|
||||
}
|
||||
|
||||
await fetchChatList()
|
||||
},
|
||||
300,
|
||||
{
|
||||
maxWait: 1000,
|
||||
},
|
||||
),
|
||||
[chatManager, fetchChatList],
|
||||
)
|
||||
|
||||
const deleteConversation = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const dbManager = await getManager()
|
||||
const conversationManager = dbManager.getConversationManager()
|
||||
await conversationManager.deleteConversation(id)
|
||||
await chatManager.deleteChat(id)
|
||||
await fetchChatList()
|
||||
},
|
||||
[getManager],
|
||||
[chatManager, fetchChatList],
|
||||
)
|
||||
|
||||
const getChatMessagesById = useCallback(
|
||||
async (id: string): Promise<ChatMessage[] | null> => {
|
||||
const dbManager = await getManager()
|
||||
const conversationManager = dbManager.getConversationManager()
|
||||
return await conversationManager.findConversation(id)
|
||||
},
|
||||
[getManager],
|
||||
)
|
||||
const getChatMessagesById = useCallback(
|
||||
async (id: string): Promise<ChatMessage[] | null> => {
|
||||
const conversation = await chatManager.findById(id)
|
||||
if (!conversation) {
|
||||
return null
|
||||
}
|
||||
return conversation.messages.map((message) =>
|
||||
deserializeChatMessage(message, app),
|
||||
)
|
||||
},
|
||||
[chatManager, app],
|
||||
)
|
||||
|
||||
const updateConversationTitle = useCallback(
|
||||
async (id: string, title: string): Promise<void> => {
|
||||
const dbManager = await getManager()
|
||||
const conversationManager = dbManager.getConversationManager()
|
||||
await conversationManager.updateConversationTitle(id, title)
|
||||
},
|
||||
[getManager],
|
||||
)
|
||||
const updateConversationTitle = useCallback(
|
||||
async (id: string, title: string): Promise<void> => {
|
||||
if (title.length === 0) {
|
||||
throw new Error('Chat title cannot be empty')
|
||||
}
|
||||
const conversation = await chatManager.findById(id)
|
||||
if (!conversation) {
|
||||
throw new Error('Conversation not found')
|
||||
}
|
||||
await chatManager.updateChat(conversation.id, {
|
||||
title,
|
||||
})
|
||||
await fetchChatList()
|
||||
},
|
||||
[chatManager, fetchChatList],
|
||||
)
|
||||
|
||||
return {
|
||||
createOrUpdateConversation,
|
||||
|
||||
86
src/hooks/use-commands.ts
Normal file
86
src/hooks/use-commands.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { lexicalNodeToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||
import { useApp } from '../contexts/AppContext'
|
||||
import { TemplateManager } from '../database/json/command/TemplateManager'
|
||||
import { TemplateContent } from '../database/schema'
|
||||
|
||||
|
||||
export interface QuickCommand {
|
||||
id: string
|
||||
name: string
|
||||
content: TemplateContent
|
||||
contentText: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
type UseCommands = {
|
||||
createCommand: (name: string, content: TemplateContent) => Promise<void>
|
||||
deleteCommand: (id: string) => Promise<void>
|
||||
updateCommand: (id: string, name: string, content: TemplateContent) => Promise<void>
|
||||
commandList: QuickCommand[]
|
||||
}
|
||||
|
||||
export function useCommands(): UseCommands {
|
||||
|
||||
const [commandList, setCommandList] = useState<QuickCommand[]>([])
|
||||
|
||||
|
||||
const app = useApp()
|
||||
const templateManager = useMemo(() => new TemplateManager(app), [app])
|
||||
|
||||
const fetchCommandList = useCallback(async () => {
|
||||
templateManager.ListTemplates().then((rows) => {
|
||||
setCommandList(rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
content: row.content,
|
||||
contentText: row.content.nodes.map(lexicalNodeToPlainText).join(''),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
})))
|
||||
})
|
||||
}, [templateManager])
|
||||
|
||||
useEffect(() => {
|
||||
void fetchCommandList()
|
||||
}, [fetchCommandList])
|
||||
|
||||
const createCommand = useCallback(
|
||||
async (name: string, content: TemplateContent): Promise<void> => {
|
||||
await templateManager.createTemplate({
|
||||
name,
|
||||
content,
|
||||
})
|
||||
fetchCommandList()
|
||||
},
|
||||
[templateManager, fetchCommandList],
|
||||
)
|
||||
|
||||
const deleteCommand = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
await templateManager.deleteTemplate(id)
|
||||
fetchCommandList()
|
||||
},
|
||||
[templateManager, fetchCommandList],
|
||||
)
|
||||
|
||||
const updateCommand = useCallback(
|
||||
async (id: string, name: string, content: TemplateContent): Promise<void> => {
|
||||
await templateManager.updateTemplate(id, {
|
||||
name,
|
||||
content,
|
||||
})
|
||||
fetchCommandList()
|
||||
},
|
||||
[templateManager, fetchCommandList],
|
||||
)
|
||||
|
||||
return {
|
||||
createCommand,
|
||||
deleteCommand,
|
||||
updateCommand,
|
||||
commandList,
|
||||
}
|
||||
}
|
||||
39
src/main.ts
39
src/main.ts
@ -11,6 +11,7 @@ import { getDiffStrategy } from "./core/diff/DiffStrategy"
|
||||
import { InlineEdit } from './core/edit/inline-edit-processor'
|
||||
import { RAGEngine } from './core/rag/rag-engine'
|
||||
import { DBManager } from './database/database-manager'
|
||||
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
|
||||
import EventListener from "./event-listener"
|
||||
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
||||
import DocumentChangesListener, {
|
||||
@ -30,6 +31,7 @@ import {
|
||||
import { getMentionableBlockData } from './utils/obsidian'
|
||||
import './utils/path'
|
||||
|
||||
|
||||
export default class InfioPlugin extends Plugin {
|
||||
private metadataCacheUnloadFn: (() => void) | null = null
|
||||
private activeLeafChangeUnloadFn: (() => void) | null = null
|
||||
@ -351,9 +353,21 @@ export default class InfioPlugin extends Plugin {
|
||||
editor.replaceRange(customBlock, insertPos);
|
||||
},
|
||||
});
|
||||
|
||||
// migrate to json database
|
||||
void this.migrateToJsonStorage()
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// RagEngine cleanup
|
||||
this.ragEngine?.cleanup()
|
||||
this.ragEngine = null
|
||||
|
||||
// Promise cleanup
|
||||
this.dbManagerInitPromise = null
|
||||
this.ragEngineInitPromise = null
|
||||
|
||||
this.dbManager?.cleanup()
|
||||
this.dbManager = null
|
||||
}
|
||||
|
||||
@ -468,4 +482,29 @@ export default class InfioPlugin extends Plugin {
|
||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
||||
return this.ragEngineInitPromise
|
||||
}
|
||||
|
||||
private async migrateToJsonStorage() {
|
||||
try {
|
||||
const dbManager = await this.getDbManager()
|
||||
await migrateToJsonDatabase(this.app, dbManager, async () => {
|
||||
await this.reloadChatView()
|
||||
console.log('Migration to JSON storage completed successfully')
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to migrate to JSON storage:', error)
|
||||
new Notice(
|
||||
'Failed to migrate to JSON storage. Please check the console for details.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async reloadChatView() {
|
||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
||||
return
|
||||
}
|
||||
new Notice('Reloading "infio" due to migration', 1000)
|
||||
leaves[0].detach()
|
||||
await this.activateChatView()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { PGlite } from '@electric-sql/pglite'
|
||||
|
||||
import { PGliteWorkerOptions, worker } from '@electric-sql/pglite/worker'
|
||||
|
||||
import { pgliteResources } from '../database/pglite-resources'
|
||||
@ -43,7 +42,7 @@ const loadPGliteResources = async (): Promise<{
|
||||
}
|
||||
|
||||
worker({
|
||||
async init(options: PGliteWorkerOptions) {
|
||||
async init(options: PGliteWorkerOptions, ) {
|
||||
let db: PGlite;
|
||||
try {
|
||||
const { fsBundle, wasmModule, vectorExtensionBundlePath } =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user