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 { $getRoot, $insertNodes, LexicalEditor } from 'lexical'
|
||||||
import { Pencil, Search, Trash2 } from 'lucide-react'
|
import { Pencil, Search, Trash2 } from 'lucide-react'
|
||||||
import { Notice } from 'obsidian'
|
import { Notice } from 'obsidian'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
// import { v4 as uuidv4 } from 'uuid'
|
|
||||||
|
|
||||||
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 { TemplateContent } from '../../database/schema'
|
||||||
|
import { useCommands } from '../../hooks/use-commands'
|
||||||
|
|
||||||
import LexicalContentEditable from './chat-input/LexicalContentEditable'
|
import LexicalContentEditable from './chat-input/LexicalContentEditable'
|
||||||
|
|
||||||
|
|
||||||
export interface QuickCommand {
|
export interface QuickCommand {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
content: TemplateContent
|
content: TemplateContent
|
||||||
|
contentText: string
|
||||||
createdAt: Date | undefined
|
createdAt: Date | undefined
|
||||||
updatedAt: Date | undefined
|
updatedAt: Date | undefined
|
||||||
}
|
}
|
||||||
@ -29,30 +28,12 @@ const CommandsView = (
|
|||||||
selectedSerializedNodes?: BaseSerializedNode[]
|
selectedSerializedNodes?: BaseSerializedNode[]
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const [commands, setCommands] = useState<QuickCommand[]>([])
|
const {
|
||||||
|
createCommand,
|
||||||
const { getDatabaseManager } = useDatabase()
|
deleteCommand,
|
||||||
const getManager = useCallback(async (): Promise<DBManager> => {
|
updateCommand,
|
||||||
return await getDatabaseManager()
|
commandList,
|
||||||
}, [getDatabaseManager])
|
} = useCommands()
|
||||||
|
|
||||||
// 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])
|
|
||||||
|
|
||||||
// new command name
|
// new command name
|
||||||
const [newCommandName, setNewCommandName] = useState('')
|
const [newCommandName, setNewCommandName] = useState('')
|
||||||
@ -66,13 +47,13 @@ const CommandsView = (
|
|||||||
const nameInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
const nameInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
||||||
const contentEditorRefs = useRef<Map<string, LexicalEditor>>(new Map())
|
const contentEditorRefs = useRef<Map<string, LexicalEditor>>(new Map())
|
||||||
|
|
||||||
// 为每个正在编辑的命令创建refs
|
// create refs for each command
|
||||||
const commandEditRefs = useRef<Map<string, {
|
const commandEditRefs = useRef<Map<string, {
|
||||||
editorRef: React.RefObject<LexicalEditor>,
|
editorRef: React.RefObject<LexicalEditor>,
|
||||||
contentEditableRef: React.RefObject<HTMLDivElement>
|
contentEditableRef: React.RefObject<HTMLDivElement>
|
||||||
}>>(new Map());
|
}>>(new Map());
|
||||||
|
|
||||||
// 获取或创建命令编辑refs
|
// get or create command edit refs
|
||||||
const getCommandEditRefs = useCallback((id: string) => {
|
const getCommandEditRefs = useCallback((id: string) => {
|
||||||
if (!commandEditRefs.current.has(id)) {
|
if (!commandEditRefs.current.has(id)) {
|
||||||
commandEditRefs.current.set(id, {
|
commandEditRefs.current.set(id, {
|
||||||
@ -94,7 +75,7 @@ const CommandsView = (
|
|||||||
return refs;
|
return refs;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 当编辑状态改变时更新refs
|
// update command edit refs when editing command id changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingCommandId) {
|
if (editingCommandId) {
|
||||||
const refs = getCommandEditRefs(editingCommandId);
|
const refs = getCommandEditRefs(editingCommandId);
|
||||||
@ -133,25 +114,20 @@ const CommandsView = (
|
|||||||
new Notice('Please enter a name for your template')
|
new Notice('Please enter a name for your template')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dbManager = await getManager()
|
|
||||||
dbManager.getCommandManager().createCommand({
|
await createCommand(newCommandName, { nodes })
|
||||||
name: newCommandName,
|
|
||||||
content: { nodes },
|
|
||||||
})
|
|
||||||
|
|
||||||
// clear editor content
|
// clear editor content
|
||||||
editorRef.current.update(() => {
|
editorRef.current.update(() => {
|
||||||
const root = $getRoot()
|
const root = $getRoot()
|
||||||
root.clear()
|
root.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
setNewCommandName('')
|
setNewCommandName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete command
|
// delete command
|
||||||
const handleDeleteCommand = async (id: string) => {
|
const handleDeleteCommand = async (id: string) => {
|
||||||
const dbManager = await getManager()
|
await deleteCommand(id)
|
||||||
await dbManager.getCommandManager().deleteCommand(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// edit command
|
// edit command
|
||||||
@ -173,11 +149,11 @@ const CommandsView = (
|
|||||||
new Notice('Please enter a content for your template')
|
new Notice('Please enter a content for your template')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dbManager = await getManager()
|
await updateCommand(
|
||||||
await dbManager.getCommandManager().updateCommand(id, {
|
id,
|
||||||
name: nameInput.value,
|
nameInput.value,
|
||||||
content: { nodes },
|
{ nodes },
|
||||||
})
|
)
|
||||||
setEditingCommandId(null)
|
setEditingCommandId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,11 +163,16 @@ const CommandsView = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filter commands list
|
// filter commands list
|
||||||
const filteredCommands = commands.filter(
|
const filteredCommands = useMemo(() => {
|
||||||
command =>
|
if (!searchTerm.trim()) {
|
||||||
command.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
return commandList;
|
||||||
command.content.nodes.map(lexicalNodeToPlainText).join('').toLowerCase().includes(searchTerm.toLowerCase())
|
}
|
||||||
)
|
return commandList.filter(
|
||||||
|
command =>
|
||||||
|
command.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
command.contentText.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [commandList, searchTerm]);
|
||||||
|
|
||||||
const getCommandEditorState = (commandContent: TemplateContent): InitialEditorStateType => {
|
const getCommandEditorState = (commandContent: TemplateContent): InitialEditorStateType => {
|
||||||
return (editor: LexicalEditor) => {
|
return (editor: LexicalEditor) => {
|
||||||
@ -287,7 +268,7 @@ const CommandsView = (
|
|||||||
// view mode
|
// view mode
|
||||||
<div className="infio-commands-view-mode">
|
<div className="infio-commands-view-mode">
|
||||||
<div className="infio-commands-name">{command.name}</div>
|
<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">
|
<div className="infio-commands-actions">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditCommand(command)}
|
onClick={() => handleEditCommand(command)}
|
||||||
|
|||||||
@ -242,7 +242,7 @@ export function ModelSelect() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchTerm(e.target.value)
|
setSearchTerm(e.target.value)
|
||||||
setSelectedIndex(0)
|
setSelectedIndex(0)
|
||||||
// 确保下一个渲染循环中仍然聚焦在输入框
|
// Ensure the input is focused in the next render cycle
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}, 0)
|
}, 0)
|
||||||
@ -292,7 +292,7 @@ export function ModelSelect() {
|
|||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchTerm(e.target.value)
|
setSearchTerm(e.target.value)
|
||||||
// 确保下一个渲染循环中仍然聚焦在输入框
|
// ensure the input is focused in the next render cycle
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}, 0)
|
}, 0)
|
||||||
@ -350,7 +350,7 @@ export function ModelSelect() {
|
|||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
/* 模型项样式 */
|
/* Model item styles */
|
||||||
.infio-llm-setting-model-item {
|
.infio-llm-setting-model-item {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -366,7 +366,7 @@ export function ModelSelect() {
|
|||||||
border-left: 3px solid var(--interactive-accent);
|
border-left: 3px solid var(--interactive-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文本溢出处理 */
|
/* Text overflow handling */
|
||||||
.infio-model-item-text-wrapper {
|
.infio-model-item-text-wrapper {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -379,7 +379,7 @@ export function ModelSelect() {
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 高亮样式 - 使用紫色而不是主题色 */
|
/* Highlighted text style - use purple instead of theme color */
|
||||||
.infio-llm-setting-model-item-highlight {
|
.infio-llm-setting-model-item-highlight {
|
||||||
display: inline;
|
display: inline;
|
||||||
color: #9370DB;
|
color: #9370DB;
|
||||||
@ -389,7 +389,7 @@ export function ModelSelect() {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索容器 */
|
/* Search container */
|
||||||
.infio-llm-setting-search-container {
|
.infio-llm-setting-search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -399,7 +399,7 @@ export function ModelSelect() {
|
|||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 提供商选择器容器 */
|
/* Provider selector container */
|
||||||
.infio-llm-setting-provider-container {
|
.infio-llm-setting-provider-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -408,9 +408,7 @@ export function ModelSelect() {
|
|||||||
width: 26%;
|
width: 26%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除提供商选择箭头 */
|
/* Provider selector */
|
||||||
|
|
||||||
/* 提供商选择器 */
|
|
||||||
.infio-llm-setting-provider-switch {
|
.infio-llm-setting-provider-switch {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -435,7 +433,7 @@ export function ModelSelect() {
|
|||||||
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
|
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索框容器 */
|
/* Search container */
|
||||||
.infio-search-input-container {
|
.infio-search-input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -444,9 +442,7 @@ export function ModelSelect() {
|
|||||||
width: 74%;
|
width: 74%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除搜索图标 */
|
/* Search input */
|
||||||
|
|
||||||
/* 搜索输入框 */
|
|
||||||
.infio-llm-setting-item-search {
|
.infio-llm-setting-item-search {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
@ -469,7 +465,7 @@ export function ModelSelect() {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 下拉菜单容器 */
|
/* Dropdown menu container */
|
||||||
.infio-llm-setting-combobox-dropdown {
|
.infio-llm-setting-combobox-dropdown {
|
||||||
max-height: 350px;
|
max-height: 350px;
|
||||||
overflow-y: auto;
|
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 },
|
'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 {
|
export class RAGEngine {
|
||||||
private app: App
|
private app: App
|
||||||
private settings: InfioSettings
|
private settings: InfioSettings
|
||||||
private vectorManager: VectorManager
|
private vectorManager: VectorManager | null = null
|
||||||
private embeddingModel: EmbeddingModel | null = null
|
private embeddingModel: EmbeddingModel | null = null
|
||||||
private initialized = false
|
private initialized = false
|
||||||
|
|
||||||
@ -28,6 +28,11 @@ export class RAGEngine {
|
|||||||
this.embeddingModel = getEmbeddingModel(settings)
|
this.embeddingModel = getEmbeddingModel(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.embeddingModel = null
|
||||||
|
this.vectorManager = null
|
||||||
|
}
|
||||||
|
|
||||||
setSettings(settings: InfioSettings) {
|
setSettings(settings: InfioSettings) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.embeddingModel = getEmbeddingModel(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(
|
async updateVaultIndex(
|
||||||
options: { reindexAll: boolean },
|
options: { reindexAll: boolean },
|
||||||
onQueryProgressChange?: (queryProgress: QueryProgressState) => void,
|
onQueryProgressChange?: (queryProgress: QueryProgressState) => void,
|
||||||
|
|||||||
@ -2,27 +2,21 @@
|
|||||||
import { type PGliteWithLive } from '@electric-sql/pglite/live'
|
import { type PGliteWithLive } from '@electric-sql/pglite/live'
|
||||||
import { App } from 'obsidian'
|
import { App } from 'obsidian'
|
||||||
|
|
||||||
// import { PGLITE_DB_PATH } from '../constants'
|
|
||||||
import { createAndInitDb } from '../pgworker'
|
import { createAndInitDb } from '../pgworker'
|
||||||
|
|
||||||
|
import { CommandManager } from './modules/command/command-manager'
|
||||||
import { ConversationManager } from './modules/conversation/conversation-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 { VectorManager } from './modules/vector/vector-manager'
|
||||||
// import { pgliteResources } from './pglite-resources'
|
|
||||||
// import { migrations } from './sql'
|
|
||||||
|
|
||||||
export class DBManager {
|
export class DBManager {
|
||||||
private app: App
|
private app: App
|
||||||
// private dbPath: string
|
|
||||||
private db: PGliteWithLive | null = null
|
private db: PGliteWithLive | null = null
|
||||||
// private db: PgliteDatabase | null = null
|
|
||||||
private vectorManager: VectorManager
|
private vectorManager: VectorManager
|
||||||
private CommandManager: CommandManager
|
private CommandManager: CommandManager
|
||||||
private conversationManager: ConversationManager
|
private conversationManager: ConversationManager
|
||||||
|
|
||||||
constructor(app: App) {
|
constructor(app: App) {
|
||||||
this.app = app
|
this.app = app
|
||||||
// this.dbPath = dbPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async create(app: App): Promise<DBManager> {
|
static async create(app: App): Promise<DBManager> {
|
||||||
@ -36,123 +30,24 @@ export class DBManager {
|
|||||||
return dbManager
|
return dbManager
|
||||||
}
|
}
|
||||||
|
|
||||||
getPgClient() {
|
getPgClient(): PGliteWithLive | null {
|
||||||
return this.db
|
return this.db
|
||||||
}
|
}
|
||||||
|
|
||||||
getVectorManager() {
|
getVectorManager(): VectorManager {
|
||||||
return this.vectorManager
|
return this.vectorManager
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommandManager() {
|
getCommandManager(): CommandManager {
|
||||||
return this.CommandManager
|
return this.CommandManager
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversationManager() {
|
getConversationManager(): ConversationManager {
|
||||||
return this.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() {
|
async cleanup() {
|
||||||
this.db?.close()
|
this.db?.close()
|
||||||
this.db = null
|
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()
|
throw new DatabaseNotInitializedException()
|
||||||
}
|
}
|
||||||
const result = await this.db.query<SelectTemplate>(
|
const result = await this.db.query<SelectTemplate>(
|
||||||
`SELECT * FROM "template"`
|
`SELECT * FROM "template" ORDER BY created_at DESC`
|
||||||
)
|
)
|
||||||
return result.rows
|
return result.rows
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { App } from 'obsidian'
|
|
||||||
import { Transaction } from '@electric-sql/pglite'
|
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 { editorStateToPlainText } from '../../../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||||
import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat'
|
import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat'
|
||||||
@ -77,6 +77,10 @@ export class ConversationManager {
|
|||||||
await this.repository.delete(id)
|
await this.repository.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async conversations(): Promise<SelectConversation[]>{
|
||||||
|
return this.repository.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
|
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
|
||||||
const db = this.dbManager.getPgClient()
|
const db = this.dbManager.getPgClient()
|
||||||
db?.live.query('SELECT * FROM conversations ORDER BY updated_at DESC', [], (results: { rows: Array<SelectConversation> }) => {
|
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 { editorStateToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||||
import { DBManager } from '../database/database-manager'
|
import { useApp } from '../contexts/AppContext'
|
||||||
import { ChatConversationMeta, ChatMessage } from '../types/chat'
|
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 = {
|
type UseChatHistory = {
|
||||||
createOrUpdateConversation: (
|
createOrUpdateConversation: (
|
||||||
@ -12,64 +17,100 @@ type UseChatHistory = {
|
|||||||
deleteConversation: (id: string) => Promise<void>
|
deleteConversation: (id: string) => Promise<void>
|
||||||
getChatMessagesById: (id: string) => Promise<ChatMessage[] | null>
|
getChatMessagesById: (id: string) => Promise<ChatMessage[] | null>
|
||||||
updateConversationTitle: (id: string, title: string) => Promise<void>
|
updateConversationTitle: (id: string, title: string) => Promise<void>
|
||||||
chatList: ChatConversationMeta[]
|
chatList: ChatConversationMetadata[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChatHistory(): UseChatHistory {
|
export function useChatHistory(): UseChatHistory {
|
||||||
const { getDatabaseManager } = useDatabase()
|
const app = useApp()
|
||||||
|
const chatManager = useMemo(() => new ChatManager(app), [app])
|
||||||
|
|
||||||
const [chatList, setChatList] = useState<ChatConversationMeta[]>([])
|
const [chatList, setChatList] = useState<ChatConversationMetadata[]>([])
|
||||||
|
|
||||||
const getManager = useCallback(async (): Promise<DBManager> => {
|
|
||||||
return await getDatabaseManager()
|
|
||||||
}, [getDatabaseManager])
|
|
||||||
|
|
||||||
const fetchChatList = useCallback(async () => {
|
const fetchChatList = useCallback(async () => {
|
||||||
const dbManager = await getManager()
|
const conversations = await chatManager.listChats()
|
||||||
dbManager.getConversationManager().getAllConversations((conversations) => {
|
setChatList(conversations)
|
||||||
setChatList(conversations)
|
}, [chatManager])
|
||||||
})
|
|
||||||
}, [getManager])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchChatList()
|
void fetchChatList()
|
||||||
}, [fetchChatList])
|
}, [fetchChatList])
|
||||||
|
|
||||||
const createOrUpdateConversation = useCallback(
|
const createOrUpdateConversation = useMemo(
|
||||||
async (id: string, messages: ChatMessage[]): Promise<void> => {
|
() =>
|
||||||
const dbManager = await getManager()
|
debounce(
|
||||||
const conversationManager = dbManager.getConversationManager()
|
async (id: string, messages: ChatMessage[]): Promise<void> => {
|
||||||
await conversationManager.txCreateOrUpdateConversation(id, messages)
|
const serializedMessages = messages.map(serializeChatMessage)
|
||||||
},
|
const existingConversation = await chatManager.findById(id)
|
||||||
[getManager],
|
|
||||||
)
|
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(
|
const deleteConversation = useCallback(
|
||||||
async (id: string): Promise<void> => {
|
async (id: string): Promise<void> => {
|
||||||
const dbManager = await getManager()
|
await chatManager.deleteChat(id)
|
||||||
const conversationManager = dbManager.getConversationManager()
|
await fetchChatList()
|
||||||
await conversationManager.deleteConversation(id)
|
|
||||||
},
|
},
|
||||||
[getManager],
|
[chatManager, fetchChatList],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getChatMessagesById = useCallback(
|
const getChatMessagesById = useCallback(
|
||||||
async (id: string): Promise<ChatMessage[] | null> => {
|
async (id: string): Promise<ChatMessage[] | null> => {
|
||||||
const dbManager = await getManager()
|
const conversation = await chatManager.findById(id)
|
||||||
const conversationManager = dbManager.getConversationManager()
|
if (!conversation) {
|
||||||
return await conversationManager.findConversation(id)
|
return null
|
||||||
},
|
}
|
||||||
[getManager],
|
return conversation.messages.map((message) =>
|
||||||
)
|
deserializeChatMessage(message, app),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[chatManager, app],
|
||||||
|
)
|
||||||
|
|
||||||
const updateConversationTitle = useCallback(
|
const updateConversationTitle = useCallback(
|
||||||
async (id: string, title: string): Promise<void> => {
|
async (id: string, title: string): Promise<void> => {
|
||||||
const dbManager = await getManager()
|
if (title.length === 0) {
|
||||||
const conversationManager = dbManager.getConversationManager()
|
throw new Error('Chat title cannot be empty')
|
||||||
await conversationManager.updateConversationTitle(id, title)
|
}
|
||||||
},
|
const conversation = await chatManager.findById(id)
|
||||||
[getManager],
|
if (!conversation) {
|
||||||
)
|
throw new Error('Conversation not found')
|
||||||
|
}
|
||||||
|
await chatManager.updateChat(conversation.id, {
|
||||||
|
title,
|
||||||
|
})
|
||||||
|
await fetchChatList()
|
||||||
|
},
|
||||||
|
[chatManager, fetchChatList],
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createOrUpdateConversation,
|
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 { InlineEdit } from './core/edit/inline-edit-processor'
|
||||||
import { RAGEngine } from './core/rag/rag-engine'
|
import { RAGEngine } from './core/rag/rag-engine'
|
||||||
import { DBManager } from './database/database-manager'
|
import { DBManager } from './database/database-manager'
|
||||||
|
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
|
||||||
import EventListener from "./event-listener"
|
import EventListener from "./event-listener"
|
||||||
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
||||||
import DocumentChangesListener, {
|
import DocumentChangesListener, {
|
||||||
@ -30,6 +31,7 @@ import {
|
|||||||
import { getMentionableBlockData } from './utils/obsidian'
|
import { getMentionableBlockData } from './utils/obsidian'
|
||||||
import './utils/path'
|
import './utils/path'
|
||||||
|
|
||||||
|
|
||||||
export default class InfioPlugin extends Plugin {
|
export default class InfioPlugin extends Plugin {
|
||||||
private metadataCacheUnloadFn: (() => void) | null = null
|
private metadataCacheUnloadFn: (() => void) | null = null
|
||||||
private activeLeafChangeUnloadFn: (() => void) | null = null
|
private activeLeafChangeUnloadFn: (() => void) | null = null
|
||||||
@ -351,9 +353,21 @@ export default class InfioPlugin extends Plugin {
|
|||||||
editor.replaceRange(customBlock, insertPos);
|
editor.replaceRange(customBlock, insertPos);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// migrate to json database
|
||||||
|
void this.migrateToJsonStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
// RagEngine cleanup
|
||||||
|
this.ragEngine?.cleanup()
|
||||||
|
this.ragEngine = null
|
||||||
|
|
||||||
|
// Promise cleanup
|
||||||
|
this.dbManagerInitPromise = null
|
||||||
|
this.ragEngineInitPromise = null
|
||||||
|
|
||||||
|
this.dbManager?.cleanup()
|
||||||
this.dbManager = null
|
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
|
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
||||||
return this.ragEngineInitPromise
|
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
|
// @ts-nocheck
|
||||||
import { PGlite } from '@electric-sql/pglite'
|
import { PGlite } from '@electric-sql/pglite'
|
||||||
|
|
||||||
import { PGliteWorkerOptions, worker } from '@electric-sql/pglite/worker'
|
import { PGliteWorkerOptions, worker } from '@electric-sql/pglite/worker'
|
||||||
|
|
||||||
import { pgliteResources } from '../database/pglite-resources'
|
import { pgliteResources } from '../database/pglite-resources'
|
||||||
@ -43,7 +42,7 @@ const loadPGliteResources = async (): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
worker({
|
worker({
|
||||||
async init(options: PGliteWorkerOptions) {
|
async init(options: PGliteWorkerOptions, ) {
|
||||||
let db: PGlite;
|
let db: PGlite;
|
||||||
try {
|
try {
|
||||||
const { fsBundle, wasmModule, vectorExtensionBundlePath } =
|
const { fsBundle, wasmModule, vectorExtensionBundlePath } =
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user