mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
add mobile version for pro user
This commit is contained in:
parent
1483b3b8b9
commit
669656e138
@ -1,4 +1,7 @@
|
|||||||
releases:
|
releases:
|
||||||
|
- version: "0.8.5"
|
||||||
|
features:
|
||||||
|
- "add mobile version for pro user, fix update error"
|
||||||
- version: "0.8.4"
|
- version: "0.8.4"
|
||||||
features:
|
features:
|
||||||
- "test mobile version"
|
- "test mobile version"
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"id": "infio-copilot",
|
"id": "infio-copilot",
|
||||||
"name": "Infio Copilot",
|
"name": "Infio Copilot",
|
||||||
"version": "0.8.4",
|
"version": "0.8.5",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
|
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
|
||||||
"author": "Felix.D",
|
"author": "Felix.D",
|
||||||
"authorUrl": "https://github.com/infiolab",
|
"authorUrl": "https://github.com/infiolab",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-infio-copilot",
|
"name": "obsidian-infio-copilot",
|
||||||
"version": "0.8.4",
|
"version": "0.8.5",
|
||||||
"description": "A Cursor-inspired AI assistant that offers smart autocomplete and interactive chat with your selected notes",
|
"description": "A Cursor-inspired AI assistant that offers smart autocomplete and interactive chat with your selected notes",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,29 +1,9 @@
|
|||||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
|
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { machineId } from 'node-machine-id';
|
import { Notice, Plugin, requestUrl } from "obsidian";
|
||||||
import { Notice, Platform, Plugin, requestUrl } from "obsidian";
|
|
||||||
|
|
||||||
import { INFIO_BASE_URL } from "../constants";
|
import { INFIO_BASE_URL } from "../constants";
|
||||||
|
import { getDeviceId, getOperatingSystem } from "../utils/device-id";
|
||||||
function getOperatingSystem(): string {
|
|
||||||
if (Platform.isWin) {
|
|
||||||
return 'windows';
|
|
||||||
}
|
|
||||||
if (Platform.isMacOS) {
|
|
||||||
return 'macos';
|
|
||||||
}
|
|
||||||
if (Platform.isLinux) {
|
|
||||||
return 'linux';
|
|
||||||
}
|
|
||||||
if (Platform.isAndroidApp) {
|
|
||||||
return 'android';
|
|
||||||
}
|
|
||||||
if (Platform.isIosApp) {
|
|
||||||
return 'ios';
|
|
||||||
}
|
|
||||||
// 如果所有已知的平台都不是,则返回未知
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
// API响应类型定义
|
// API响应类型定义
|
||||||
export type UserPlanResponse = {
|
export type UserPlanResponse = {
|
||||||
@ -76,8 +56,8 @@ export const checkGeneral = async (
|
|||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('API密钥不能为空');
|
throw new Error('API密钥不能为空');
|
||||||
}
|
}
|
||||||
const deviceId = await machineId();
|
const deviceId = await getDeviceId();
|
||||||
const deviceName = getOperatingSystem();
|
const deviceName = getOperatingSystem();
|
||||||
if (!deviceId || !deviceName) {
|
if (!deviceId || !deviceName) {
|
||||||
throw new Error('设备ID和设备名称不能为空');
|
throw new Error('设备ID和设备名称不能为空');
|
||||||
}
|
}
|
||||||
@ -336,12 +316,12 @@ export const upgradeToProVersion = async (
|
|||||||
console.log(`重载插件: ${plugin.manifest.id}`);
|
console.log(`重载插件: ${plugin.manifest.id}`);
|
||||||
try {
|
try {
|
||||||
// 禁用插件
|
// 禁用插件
|
||||||
// @ts-ignore
|
// @ts-expect-error obsidian typings do not expose this internal API
|
||||||
await plugin.app.plugins.disablePlugin(plugin.manifest.id);
|
await plugin.app.plugins.disablePlugin(plugin.manifest.id);
|
||||||
console.log(`插件已禁用: ${plugin.manifest.id}`);
|
console.log(`插件已禁用: ${plugin.manifest.id}`);
|
||||||
|
|
||||||
// 启用插件
|
// 启用插件
|
||||||
// @ts-ignore
|
// @ts-expect-error obsidian typings do not expose this internal API
|
||||||
await plugin.app.plugins.enablePlugin(plugin.manifest.id);
|
await plugin.app.plugins.enablePlugin(plugin.manifest.id);
|
||||||
console.log(`插件已重新启用: ${plugin.manifest.id}`);
|
console.log(`插件已重新启用: ${plugin.manifest.id}`);
|
||||||
|
|
||||||
|
|||||||
568
src/main.desktop.ts
Normal file
568
src/main.desktop.ts
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { EditorView } from '@codemirror/view'
|
||||||
|
// import { PGlite } from '@electric-sql/pglite'
|
||||||
|
import { Editor, MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian'
|
||||||
|
|
||||||
|
import { ApplyView } from './ApplyView'
|
||||||
|
import { ChatView } from './ChatView'
|
||||||
|
import { ChatProps } from './components/chat-view/ChatView'
|
||||||
|
import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, JSON_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants'
|
||||||
|
import { getDiffStrategy } from "./core/diff/DiffStrategy"
|
||||||
|
import { InlineEdit } from './core/edit/inline-edit-processor'
|
||||||
|
import { McpHub } from './core/mcp/McpHub'
|
||||||
|
import { RAGEngine } from './core/rag/rag-engine'
|
||||||
|
import { TransEngine } from './core/transformations/trans-engine'
|
||||||
|
import { DBManager } from './database/database-manager'
|
||||||
|
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
|
||||||
|
import { EmbeddingManager } from './embedworker/EmbeddingManager'
|
||||||
|
import EventListener from "./event-listener"
|
||||||
|
import JsonView from './JsonFileView'
|
||||||
|
import { t } from './lang/helpers'
|
||||||
|
import { PreviewView } from './PreviewView'
|
||||||
|
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
||||||
|
import DocumentChangesListener, {
|
||||||
|
DocumentChanges,
|
||||||
|
getPrefix, getSuffix,
|
||||||
|
hasMultipleCursors,
|
||||||
|
hasSelection
|
||||||
|
} from "./render-plugin/document-changes-listener"
|
||||||
|
import RenderSuggestionPlugin from "./render-plugin/render-surgestion-plugin"
|
||||||
|
import { InlineSuggestionState } from "./render-plugin/states"
|
||||||
|
import { InfioSettingTab } from './settings/SettingTab'
|
||||||
|
import StatusBar from "./status-bar"
|
||||||
|
import {
|
||||||
|
InfioSettings,
|
||||||
|
parseInfioSettings,
|
||||||
|
} from './types/settings'
|
||||||
|
import { createDataviewManager, DataviewManager } from './utils/dataview'
|
||||||
|
import { getMentionableBlockData } from './utils/obsidian'
|
||||||
|
import './utils/path'
|
||||||
|
import { onEnt } from './utils/web-search'
|
||||||
|
|
||||||
|
type DesktopAugmented = Plugin & {
|
||||||
|
metadataCacheUnloadFn: (() => void) | null
|
||||||
|
activeLeafChangeUnloadFn: (() => void) | null
|
||||||
|
dbManagerInitPromise: Promise<DBManager> | null
|
||||||
|
ragEngineInitPromise: Promise<RAGEngine> | null
|
||||||
|
transEngineInitPromise: Promise<TransEngine> | null
|
||||||
|
mcpHubInitPromise: Promise<McpHub> | null
|
||||||
|
settings: InfioSettings
|
||||||
|
settingTab: InfioSettingTab
|
||||||
|
settingsListeners: ((newSettings: InfioSettings) => void)[]
|
||||||
|
initChatProps?: ChatProps
|
||||||
|
dbManager: DBManager | null
|
||||||
|
mcpHub: McpHub | null
|
||||||
|
ragEngine: RAGEngine | null
|
||||||
|
transEngine: TransEngine | null
|
||||||
|
embeddingManager: EmbeddingManager | null
|
||||||
|
inlineEdit: InlineEdit | null
|
||||||
|
diffStrategy?: DiffStrategy
|
||||||
|
dataviewManager: DataviewManager | null
|
||||||
|
|
||||||
|
// methods (attached below)
|
||||||
|
loadSettings: () => Promise<void>
|
||||||
|
setSettings: (newSettings: InfioSettings) => Promise<void>
|
||||||
|
addSettingsListener: (listener: (newSettings: InfioSettings) => void) => () => void
|
||||||
|
openChatView: (openNewChat?: boolean) => Promise<void>
|
||||||
|
activateChatView: (chatProps?: ChatProps, openNewChat?: boolean) => Promise<void>
|
||||||
|
addSelectionToChat: (editor: Editor, view: MarkdownView) => Promise<void>
|
||||||
|
getDbManager: () => Promise<DBManager>
|
||||||
|
getMcpHub: () => Promise<McpHub | null>
|
||||||
|
getRAGEngine: () => Promise<RAGEngine>
|
||||||
|
getTransEngine: () => Promise<TransEngine>
|
||||||
|
getEmbeddingManager: () => EmbeddingManager | null
|
||||||
|
migrateToJsonStorage: () => Promise<void>
|
||||||
|
reloadChatView: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadDesktop(base: Plugin) {
|
||||||
|
const plugin = base as DesktopAugmented
|
||||||
|
// initialize fields
|
||||||
|
plugin.metadataCacheUnloadFn = null
|
||||||
|
plugin.activeLeafChangeUnloadFn = null
|
||||||
|
plugin.dbManagerInitPromise = null
|
||||||
|
plugin.ragEngineInitPromise = null
|
||||||
|
plugin.transEngineInitPromise = null
|
||||||
|
plugin.mcpHubInitPromise = null
|
||||||
|
plugin.initChatProps = undefined
|
||||||
|
plugin.dbManager = null
|
||||||
|
plugin.mcpHub = null
|
||||||
|
plugin.ragEngine = null
|
||||||
|
plugin.transEngine = null
|
||||||
|
plugin.embeddingManager = null
|
||||||
|
plugin.inlineEdit = null
|
||||||
|
plugin.diffStrategy = undefined
|
||||||
|
plugin.dataviewManager = null
|
||||||
|
plugin.settingsListeners = []
|
||||||
|
|
||||||
|
// attach methods migrated from original class
|
||||||
|
plugin.loadSettings = async function () {
|
||||||
|
this.settings = parseInfioSettings(await this.loadData())
|
||||||
|
await this.saveData(this.settings)
|
||||||
|
}
|
||||||
|
plugin.setSettings = async function (newSettings: InfioSettings) {
|
||||||
|
this.settings = newSettings
|
||||||
|
await this.saveData(newSettings)
|
||||||
|
this.ragEngine?.setSettings(newSettings)
|
||||||
|
this.transEngine?.setSettings(newSettings)
|
||||||
|
this.settingsListeners.forEach((listener) => listener(newSettings))
|
||||||
|
}
|
||||||
|
plugin.addSettingsListener = function (listener: (ns: InfioSettings) => void) {
|
||||||
|
this.settingsListeners.push(listener)
|
||||||
|
return () => {
|
||||||
|
this.settingsListeners = this.settingsListeners.filter((l) => l !== listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.openChatView = async function (openNewChat = false) {
|
||||||
|
const view = this.app.workspace.getActiveViewOfType(MarkdownView)
|
||||||
|
const editor = view?.editor
|
||||||
|
if (!view || !editor) {
|
||||||
|
await this.activateChatView(undefined, openNewChat)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selectedBlockData = await getMentionableBlockData(editor, view)
|
||||||
|
await this.activateChatView({ selectedBlock: selectedBlockData ?? undefined }, openNewChat)
|
||||||
|
}
|
||||||
|
plugin.activateChatView = async function (chatProps?: ChatProps, openNewChat = false) {
|
||||||
|
this.initChatProps = chatProps
|
||||||
|
const leaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0]
|
||||||
|
await (leaf ?? this.app.workspace.getRightLeaf(false))?.setViewState({ type: CHAT_VIEW_TYPE, active: true })
|
||||||
|
if (openNewChat && leaf && leaf.view instanceof ChatView) {
|
||||||
|
leaf.view.openNewChat(chatProps?.selectedBlock)
|
||||||
|
}
|
||||||
|
this.app.workspace.revealLeaf(this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0])
|
||||||
|
}
|
||||||
|
plugin.addSelectionToChat = async function (editor: Editor, view: MarkdownView) {
|
||||||
|
const data = await getMentionableBlockData(editor, view)
|
||||||
|
if (!data) return
|
||||||
|
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||||
|
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
||||||
|
await this.activateChatView({ selectedBlock: data })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.app.workspace.revealLeaf(leaves[0])
|
||||||
|
const chatView = leaves[0].view
|
||||||
|
chatView.addSelectionToChat(data)
|
||||||
|
chatView.focusMessage()
|
||||||
|
}
|
||||||
|
plugin.getDbManager = async function (): Promise<DBManager> {
|
||||||
|
if (this.dbManager) return this.dbManager
|
||||||
|
if (!this.dbManagerInitPromise) {
|
||||||
|
this.dbManagerInitPromise = (async () => {
|
||||||
|
this.dbManager = await DBManager.create(this.app, this.settings.ragOptions.filesystem)
|
||||||
|
return this.dbManager
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
return this.dbManagerInitPromise
|
||||||
|
}
|
||||||
|
plugin.getMcpHub = async function (): Promise<McpHub | null> {
|
||||||
|
if (!this.settings.mcpEnabled) return null
|
||||||
|
if (this.mcpHub) return this.mcpHub
|
||||||
|
if (!this.mcpHubInitPromise) {
|
||||||
|
this.mcpHubInitPromise = (async () => {
|
||||||
|
this.mcpHub = new McpHub(this.app, this as unknown as Plugin)
|
||||||
|
await this.mcpHub.onload()
|
||||||
|
return this.mcpHub
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
return this.mcpHubInitPromise
|
||||||
|
}
|
||||||
|
plugin.getRAGEngine = async function (): Promise<RAGEngine> {
|
||||||
|
if (this.ragEngine) return this.ragEngine
|
||||||
|
if (!this.ragEngineInitPromise) {
|
||||||
|
this.ragEngineInitPromise = (async () => {
|
||||||
|
const dbManager = await this.getDbManager()
|
||||||
|
this.ragEngine = new RAGEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
||||||
|
return this.ragEngine
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
return this.ragEngineInitPromise
|
||||||
|
}
|
||||||
|
plugin.getTransEngine = async function (): Promise<TransEngine> {
|
||||||
|
if (this.transEngine) return this.transEngine
|
||||||
|
if (!this.transEngineInitPromise) {
|
||||||
|
this.transEngineInitPromise = (async () => {
|
||||||
|
const dbManager = await this.getDbManager()
|
||||||
|
this.transEngine = new TransEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
||||||
|
return this.transEngine
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
return this.transEngineInitPromise
|
||||||
|
}
|
||||||
|
plugin.getEmbeddingManager = function (): EmbeddingManager | null {
|
||||||
|
return this.embeddingManager
|
||||||
|
}
|
||||||
|
plugin.migrateToJsonStorage = async function () {
|
||||||
|
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(t('notifications.migrationFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.reloadChatView = async function () {
|
||||||
|
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
||||||
|
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) return
|
||||||
|
new Notice(t('notifications.reloadingInfio'), 1000)
|
||||||
|
leaves[0].detach()
|
||||||
|
await this.activateChatView()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Original onload body starts here (adapted) ====
|
||||||
|
await plugin.loadSettings()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
void plugin.migrateToJsonStorage().then(() => { })
|
||||||
|
void onEnt('loaded')
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
plugin.settingTab = new InfioSettingTab(plugin.app, plugin as unknown as any)
|
||||||
|
plugin.addSettingTab(plugin.settingTab)
|
||||||
|
|
||||||
|
plugin.dataviewManager = createDataviewManager(plugin.app)
|
||||||
|
|
||||||
|
plugin.embeddingManager = new EmbeddingManager()
|
||||||
|
console.log('EmbeddingManager initialized')
|
||||||
|
|
||||||
|
plugin.addRibbonIcon('wand-sparkles', t('main.openInfioCopilot'), () => plugin.openChatView())
|
||||||
|
|
||||||
|
plugin.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, plugin as unknown as any))
|
||||||
|
plugin.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf))
|
||||||
|
plugin.registerView(PREVIEW_VIEW_TYPE, (leaf) => new PreviewView(leaf))
|
||||||
|
plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, plugin as unknown as any))
|
||||||
|
|
||||||
|
plugin.inlineEdit = new InlineEdit(plugin as unknown as any, plugin.settings);
|
||||||
|
plugin.registerMarkdownCodeBlockProcessor("infioedit", (source, el, ctx) => {
|
||||||
|
plugin.inlineEdit?.Processor(source, el, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusBar = StatusBar.fromApp(plugin as unknown as any);
|
||||||
|
const eventListener = EventListener.fromSettings(
|
||||||
|
plugin.settings,
|
||||||
|
statusBar,
|
||||||
|
plugin.app
|
||||||
|
);
|
||||||
|
|
||||||
|
plugin.diffStrategy = getDiffStrategy(
|
||||||
|
plugin.settings.chatModelId || "",
|
||||||
|
plugin.app,
|
||||||
|
plugin.settings.fuzzyMatchThreshold,
|
||||||
|
plugin.settings.experimentalDiffStrategy,
|
||||||
|
plugin.settings.multiSearchReplaceDiffStrategy,
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin.addSettingsListener((newSettings) => {
|
||||||
|
plugin.inlineEdit = new InlineEdit(plugin as unknown as any, newSettings);
|
||||||
|
eventListener.handleSettingChanged(newSettings)
|
||||||
|
plugin.diffStrategy = getDiffStrategy(
|
||||||
|
plugin.settings.chatModelId || "",
|
||||||
|
plugin.app,
|
||||||
|
plugin.settings.fuzzyMatchThreshold,
|
||||||
|
plugin.settings.experimentalDiffStrategy,
|
||||||
|
plugin.settings.multiSearchReplaceDiffStrategy,
|
||||||
|
)
|
||||||
|
if (plugin.settings.mcpEnabled && !plugin.mcpHub) {
|
||||||
|
void plugin.getMcpHub()
|
||||||
|
} else if (!plugin.settings.mcpEnabled && plugin.mcpHub) {
|
||||||
|
plugin.mcpHub.dispose()
|
||||||
|
plugin.mcpHub = null
|
||||||
|
plugin.mcpHubInitPromise = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.registerEditorExtension([
|
||||||
|
InlineSuggestionState,
|
||||||
|
CompletionKeyWatcher(
|
||||||
|
eventListener.handleAcceptKeyPressed.bind(eventListener) as () => boolean,
|
||||||
|
eventListener.handlePartialAcceptKeyPressed.bind(eventListener) as () => boolean,
|
||||||
|
eventListener.handleCancelKeyPressed.bind(eventListener) as () => boolean,
|
||||||
|
),
|
||||||
|
DocumentChangesListener(
|
||||||
|
eventListener.handleDocumentChange.bind(eventListener) as (documentChange: DocumentChanges) => Promise<void>
|
||||||
|
),
|
||||||
|
RenderSuggestionPlugin(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
plugin.app.workspace.onLayoutReady(() => {
|
||||||
|
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
|
if (view) {
|
||||||
|
// @ts-expect-error, not typed
|
||||||
|
const editorView = view.editor.cm as EditorView;
|
||||||
|
eventListener.onViewUpdate(editorView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.registerEvent(
|
||||||
|
plugin.app.workspace.on("active-leaf-change", (leaf) => {
|
||||||
|
if (leaf?.view instanceof MarkdownView) {
|
||||||
|
// @ts-expect-error, not typed
|
||||||
|
const editorView = leaf.view.editor.cm as EditorView;
|
||||||
|
eventListener.onViewUpdate(editorView);
|
||||||
|
if (leaf.view.file) {
|
||||||
|
eventListener.handleFileChange(leaf.view.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
plugin.registerEvent(
|
||||||
|
plugin.app.metadataCache.on("changed", (file: TFile) => {
|
||||||
|
if (file) {
|
||||||
|
eventListener.handleFileChange(file);
|
||||||
|
// is not worth it to update the file index on every file change
|
||||||
|
// plugin.ragEngine?.updateFileIndex(file);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
plugin.registerEvent(
|
||||||
|
plugin.app.metadataCache.on("deleted", (file: TFile) => {
|
||||||
|
if (file) {
|
||||||
|
plugin.ragEngine?.deleteFileIndex(file);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'open-new-chat',
|
||||||
|
name: t('main.openNewChat'),
|
||||||
|
callback: () => plugin.openChatView(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'add-selection-to-chat',
|
||||||
|
name: t('main.addSelectionToChat'),
|
||||||
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||||
|
plugin.addSelectionToChat(editor, view)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'rebuild-vault-index',
|
||||||
|
name: t('main.rebuildVaultIndex'),
|
||||||
|
callback: async () => {
|
||||||
|
const notice = new Notice(t('notifications.rebuildingIndex'), 0)
|
||||||
|
try {
|
||||||
|
const ragEngine = await plugin.getRAGEngine()
|
||||||
|
await ragEngine.updateVaultIndex(
|
||||||
|
{ reindexAll: true },
|
||||||
|
(queryProgress) => {
|
||||||
|
if (queryProgress.type === 'indexing') {
|
||||||
|
const { completedChunks, totalChunks } =
|
||||||
|
queryProgress.indexProgress
|
||||||
|
notice.setMessage(
|
||||||
|
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
notice.setMessage(t('notifications.rebuildComplete'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notice.setMessage(t('notifications.rebuildFailed'))
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { notice.hide() }, 1000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'update-vault-index',
|
||||||
|
name: t('main.updateVaultIndex'),
|
||||||
|
callback: async () => {
|
||||||
|
const notice = new Notice(t('notifications.updatingIndex'), 0)
|
||||||
|
try {
|
||||||
|
const ragEngine = await plugin.getRAGEngine()
|
||||||
|
await ragEngine.updateVaultIndex(
|
||||||
|
{ reindexAll: false },
|
||||||
|
(queryProgress) => {
|
||||||
|
if (queryProgress.type === 'indexing') {
|
||||||
|
const { completedChunks, totalChunks } =
|
||||||
|
queryProgress.indexProgress
|
||||||
|
notice.setMessage(
|
||||||
|
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
notice.setMessage(t('notifications.updateComplete'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notice.setMessage(t('notifications.updateFailed'))
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { notice.hide() }, 1000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'autocomplete-accept',
|
||||||
|
name: t('main.autocompleteAccept'),
|
||||||
|
editorCheckCallback: (
|
||||||
|
checking: boolean,
|
||||||
|
editor: Editor,
|
||||||
|
view: MarkdownView
|
||||||
|
) => {
|
||||||
|
if (checking) {
|
||||||
|
return (
|
||||||
|
eventListener.isSuggesting()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eventListener.handleAcceptCommand();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'autocomplete-predict',
|
||||||
|
name: t('main.autocompletePredict'),
|
||||||
|
editorCheckCallback: (
|
||||||
|
checking: boolean,
|
||||||
|
editor: Editor,
|
||||||
|
view: MarkdownView
|
||||||
|
) => {
|
||||||
|
// @ts-expect-error, not typed
|
||||||
|
const editorView = editor.cm as EditorView;
|
||||||
|
const state = editorView.state;
|
||||||
|
if (checking) {
|
||||||
|
return eventListener.isIdle() && !hasMultipleCursors(state) && !hasSelection(state);
|
||||||
|
}
|
||||||
|
const prefix = getPrefix(state)
|
||||||
|
const suffix = getSuffix(state)
|
||||||
|
eventListener.handlePredictCommand(prefix, suffix);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: "autocomplete-toggle",
|
||||||
|
name: t('main.autocompleteToggle'),
|
||||||
|
callback: () => {
|
||||||
|
const newValue = !plugin.settings.autocompleteEnabled;
|
||||||
|
plugin.setSettings({
|
||||||
|
...plugin.settings,
|
||||||
|
autocompleteEnabled: newValue,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: "autocomplete-enable",
|
||||||
|
name: t('main.autocompleteEnable'),
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
if (checking) {
|
||||||
|
return !plugin.settings.autocompleteEnabled;
|
||||||
|
}
|
||||||
|
plugin.setSettings({
|
||||||
|
...plugin.settings,
|
||||||
|
autocompleteEnabled: true,
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: "autocomplete-disable",
|
||||||
|
name: t('main.autocompleteDisable'),
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
if (checking) {
|
||||||
|
return plugin.settings.autocompleteEnabled;
|
||||||
|
}
|
||||||
|
plugin.setSettings({
|
||||||
|
...plugin.settings,
|
||||||
|
autocompleteEnabled: false,
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: "ai-inline-edit",
|
||||||
|
name: t('main.inlineEditCommand'),
|
||||||
|
editorCallback: (editor: Editor) => {
|
||||||
|
const selection = editor.getSelection();
|
||||||
|
if (!selection) {
|
||||||
|
new Notice(t('notifications.selectTextFirst'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const from = editor.getCursor("from");
|
||||||
|
const insertPos = { line: from.line, ch: 0 };
|
||||||
|
const customBlock = "```infioedit\n```\n";
|
||||||
|
editor.replaceRange(customBlock, insertPos);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'test-dataview-simple',
|
||||||
|
name: '测试 Dataview(简单查询)',
|
||||||
|
callback: async () => {
|
||||||
|
console.log('开始测试 Dataview...');
|
||||||
|
if (!plugin.dataviewManager) { new Notice('DataviewManager 未初始化'); return; }
|
||||||
|
if (!plugin.dataviewManager.isDataviewAvailable()) {
|
||||||
|
new Notice('Dataview 插件未安装或未启用');
|
||||||
|
console.log('Dataview API 不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Dataview API 可用,执行简单查询...');
|
||||||
|
try {
|
||||||
|
const result = await plugin.dataviewManager.executeQuery('LIST FROM ""');
|
||||||
|
if (result.success) {
|
||||||
|
new Notice('Dataview 查询成功!结果已在控制台输出');
|
||||||
|
} else {
|
||||||
|
new Notice(`查询失败: ${result.error}`);
|
||||||
|
console.error('查询错误:', result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行测试查询失败:', error);
|
||||||
|
new Notice('执行测试查询时发生错误');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin.addCommand({
|
||||||
|
id: 'test-local-embed',
|
||||||
|
name: '测试本地嵌入模型',
|
||||||
|
callback: async () => {
|
||||||
|
try {
|
||||||
|
if (!plugin.embeddingManager) { new Notice('EmbeddingManager 未初始化', 5000); return; }
|
||||||
|
await plugin.embeddingManager.loadModel("Xenova/all-MiniLM-L6-v2", true);
|
||||||
|
const testText = "hello world";
|
||||||
|
const result = await plugin.embeddingManager.embed(testText);
|
||||||
|
const resultMessage = `\n\t嵌入测试完成!\n\t文本: "${testText}"\n\tToken 数量: ${result.tokens}\n\t向量维度: ${result.vec.length}\n\t向量前4个值: [${result.vec.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]\n\t\t\t\t\t\t`.trim();
|
||||||
|
console.log('本地嵌入测试结果:', result);
|
||||||
|
const modal = new Modal(plugin.app);
|
||||||
|
modal.titleEl.setText('本地嵌入测试结果');
|
||||||
|
modal.contentEl.createEl('pre', { text: resultMessage });
|
||||||
|
modal.open();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('嵌入测试失败:', error);
|
||||||
|
new Notice(`嵌入测试失败: ${error.message}`, 5000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unloadDesktop(base: Plugin) {
|
||||||
|
const plugin = base as DesktopAugmented
|
||||||
|
plugin.dbManagerInitPromise = null
|
||||||
|
plugin.ragEngineInitPromise = null
|
||||||
|
plugin.transEngineInitPromise = null
|
||||||
|
plugin.mcpHubInitPromise = null
|
||||||
|
plugin.ragEngine?.cleanup()
|
||||||
|
plugin.ragEngine = null
|
||||||
|
plugin.transEngine?.cleanup()
|
||||||
|
plugin.transEngine = null
|
||||||
|
plugin.dbManager?.cleanup()
|
||||||
|
plugin.dbManager = null
|
||||||
|
plugin.mcpHub?.dispose()
|
||||||
|
plugin.mcpHub = null
|
||||||
|
plugin.embeddingManager?.terminate()
|
||||||
|
plugin.embeddingManager = null
|
||||||
|
plugin.dataviewManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
491
src/main.mobile.ts
Normal file
491
src/main.mobile.ts
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { App, Notice, Platform, Plugin, PluginSettingTab, Setting, requestUrl } from 'obsidian';
|
||||||
|
|
||||||
|
import { ApiKeyModal } from './components/modals/ApiKeyModal';
|
||||||
|
import { ProUpgradeModal } from './components/modals/ProUpgradeModal';
|
||||||
|
// import { checkGeneral, fetchUserPlan, upgradeToProVersion } from './hooks/use-infio';
|
||||||
|
import { InfioSettings, parseInfioSettings } from './types/settings-mobile';
|
||||||
|
import { getDeviceId, getOperatingSystem } from './utils/device-id';
|
||||||
|
|
||||||
|
INFIO_BASE_URL = 'https://api.infio.app'
|
||||||
|
|
||||||
|
// API响应类型定义
|
||||||
|
export type CheckGeneralResponse = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
dl_zip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckGeneralParams = {
|
||||||
|
device_id: string;
|
||||||
|
device_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查设备一般状态
|
||||||
|
* @param apiKey API密钥
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param deviceName 设备名称
|
||||||
|
* @returns Promise<CheckGeneralResponse>
|
||||||
|
*/
|
||||||
|
export const checkGeneral = async (
|
||||||
|
apiKey: string
|
||||||
|
): Promise<CheckGeneralResponse> => {
|
||||||
|
try {
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('API密钥不能为空');
|
||||||
|
}
|
||||||
|
const deviceId = await getDeviceId();
|
||||||
|
const deviceName = getOperatingSystem();
|
||||||
|
if (!deviceId || !deviceName) {
|
||||||
|
throw new Error('设备ID和设备名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `${INFIO_BASE_URL}/subscription/check_general`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
device_id: deviceId,
|
||||||
|
device_name: deviceName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.json.success) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return response.json;
|
||||||
|
} else {
|
||||||
|
console.error('检查 gerenal 会员失败:', response.json.message);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: response.json.message || '检查设备一般状态失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查 gerenal 会员失败:', error);
|
||||||
|
|
||||||
|
// 返回错误响应格式
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : '检查设备状态时出现未知错误'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// API响应类型定义
|
||||||
|
export type UserPlanResponse = {
|
||||||
|
plan: string;
|
||||||
|
status: string;
|
||||||
|
dl_zip?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpgradeResult = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchUserPlan = async (apiKey: string): Promise<UserPlanResponse> => {
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `${INFIO_BASE_URL}/subscription/status`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return response.json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理临时目录
|
||||||
|
*/
|
||||||
|
const cleanupTempDirectory = async (adapter: Plugin['app']['vault']['adapter'], tempDir: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// 检查目录是否存在
|
||||||
|
if (await adapter.exists(tempDir)) {
|
||||||
|
console.log(`清理临时目录: ${tempDir}`);
|
||||||
|
// 删除临时目录及其所有内容
|
||||||
|
await adapter.rmdir(tempDir, true);
|
||||||
|
console.log(`临时目录清理完成: ${tempDir}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("清理临时目录失败:", error);
|
||||||
|
// 不抛出错误,因为这不是关键操作
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并解压ZIP文件到临时目录
|
||||||
|
*/
|
||||||
|
const downloadAndExtractToTemp = async (
|
||||||
|
adapter: Plugin['app']['vault']['adapter'],
|
||||||
|
tempDir: string,
|
||||||
|
downloadUrl: string
|
||||||
|
): Promise<void> => {
|
||||||
|
console.log(`开始下载文件: ${downloadUrl}`);
|
||||||
|
|
||||||
|
// 下载ZIP文件
|
||||||
|
let zipResponse;
|
||||||
|
try {
|
||||||
|
zipResponse = await requestUrl({
|
||||||
|
url: downloadUrl,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
console.log("文件下载完成,状态:", zipResponse.status);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("下载失败:", error);
|
||||||
|
throw new Error("网络连接失败,无法下载Pro版本文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!zipResponse.arrayBuffer) {
|
||||||
|
console.log("响应格式无效,缺少arrayBuffer");
|
||||||
|
throw new Error("下载的文件格式无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("正在解压文件到临时目录...");
|
||||||
|
console.log(`开始解压文件到临时目录: ${tempDir}`);
|
||||||
|
|
||||||
|
// 解压ZIP文件
|
||||||
|
let zipData: JSZip;
|
||||||
|
try {
|
||||||
|
const zip = new JSZip();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
zipData = await zip.loadAsync(zipResponse.arrayBuffer);
|
||||||
|
console.log("ZIP文件解析成功");
|
||||||
|
} catch (error) {
|
||||||
|
console.log("ZIP文件解析失败:", error);
|
||||||
|
throw new Error("文件解压失败,可能文件已损坏");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时目录
|
||||||
|
try {
|
||||||
|
if (!(await adapter.exists(tempDir))) {
|
||||||
|
await adapter.mkdir(tempDir);
|
||||||
|
console.log(`临时目录创建成功: ${tempDir}`);
|
||||||
|
} else {
|
||||||
|
console.log(`临时目录已存在: ${tempDir}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("创建临时目录失败:", error);
|
||||||
|
throw new Error("无法创建临时目录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压所有文件到临时目录
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
const files = Object.keys(zipData.files);
|
||||||
|
console.log(files);
|
||||||
|
console.log(`ZIP文件中包含 ${files.length} 个条目`);
|
||||||
|
|
||||||
|
let extractedCount = 0;
|
||||||
|
for (const filename of files) {
|
||||||
|
const file = zipData.files[filename];
|
||||||
|
|
||||||
|
// 跳过目录
|
||||||
|
if (file?.dir) {
|
||||||
|
console.log(`跳过目录: ${filename}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`正在解压文件: ${filename}`);
|
||||||
|
|
||||||
|
// 获取文件内容
|
||||||
|
const content = await file?.async("text");
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.log(`跳过空文件: ${filename}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取文件名(去掉路径前缀)
|
||||||
|
const pathParts = filename.split('/');
|
||||||
|
const actualFileName = pathParts[pathParts.length - 1];
|
||||||
|
|
||||||
|
// 直接写入到临时目录根目录,不创建子目录
|
||||||
|
const tempFilePath = `${tempDir}/${actualFileName}`;
|
||||||
|
|
||||||
|
// 写入文件到临时目录
|
||||||
|
await adapter.write(tempFilePath, content);
|
||||||
|
extractedCount++;
|
||||||
|
console.log(`文件解压完成: ${actualFileName} (${extractedCount}/${files.filter(f => !zipData.files[f].dir).length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`所有文件解压完成,共解压 ${extractedCount} 个文件`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("文件解压过程中出错:", error);
|
||||||
|
throw new Error("文件解压过程中出现错误");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从临时目录复制文件到插件目录
|
||||||
|
*/
|
||||||
|
const copyFilesFromTemp = async (
|
||||||
|
adapter: Plugin['app']['vault']['adapter'],
|
||||||
|
tempDir: string,
|
||||||
|
pluginDir: string
|
||||||
|
): Promise<void> => {
|
||||||
|
console.log("正在更新插件文件...");
|
||||||
|
console.log(`开始从临时目录复制文件到插件目录: ${tempDir} -> ${pluginDir}`);
|
||||||
|
|
||||||
|
// 需要复制的关键文件
|
||||||
|
const filesToCopy = ['main.js', 'styles.css', 'manifest.json'];
|
||||||
|
|
||||||
|
// 检查必需文件是否存在
|
||||||
|
const mainJsPath = `${tempDir}/main.js`;
|
||||||
|
if (!(await adapter.exists(mainJsPath))) {
|
||||||
|
console.log("关键文件缺失: main.js");
|
||||||
|
throw new Error("升级文件不完整,缺少关键组件");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制文件
|
||||||
|
let copiedCount = 0;
|
||||||
|
for (const filename of filesToCopy) {
|
||||||
|
const tempFilePath = `${tempDir}/${filename}`;
|
||||||
|
const pluginFilePath = `${pluginDir}/${filename}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await adapter.exists(tempFilePath)) {
|
||||||
|
const content = await adapter.read(tempFilePath);
|
||||||
|
await adapter.write(pluginFilePath, content);
|
||||||
|
copiedCount++;
|
||||||
|
} else if (filename !== 'main.js') {
|
||||||
|
console.log(`可选文件不存在,跳过: ${filename}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`文件更新失败: ${filename}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`文件复制完成,共复制 ${copiedCount} 个文件`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并安装Pro版本
|
||||||
|
*/
|
||||||
|
export const upgradeToProVersion = async (
|
||||||
|
plugin: Plugin,
|
||||||
|
dl_zip: string
|
||||||
|
): Promise<UpgradeResult> => {
|
||||||
|
const tempDir = '.infio_download_cache';
|
||||||
|
const adapter = plugin.app.vault.adapter;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取插件目录
|
||||||
|
const pluginDir = plugin.manifest.dir;
|
||||||
|
if (!pluginDir) {
|
||||||
|
console.log("插件目录未找到");
|
||||||
|
throw new Error("无法找到插件目录");
|
||||||
|
}
|
||||||
|
new Notice("正在加载...");
|
||||||
|
|
||||||
|
await cleanupTempDirectory(adapter, tempDir);
|
||||||
|
|
||||||
|
await downloadAndExtractToTemp(
|
||||||
|
adapter,
|
||||||
|
tempDir,
|
||||||
|
dl_zip
|
||||||
|
);
|
||||||
|
|
||||||
|
await copyFilesFromTemp(adapter, tempDir, pluginDir);
|
||||||
|
|
||||||
|
new Notice("加载完成,成功升级");
|
||||||
|
|
||||||
|
await cleanupTempDirectory(adapter, tempDir);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
console.log(`重载插件: ${plugin.manifest.id}`);
|
||||||
|
try {
|
||||||
|
// 禁用插件
|
||||||
|
// @ts-expect-error obsidian typings do not expose this internal API
|
||||||
|
await plugin.app.plugins.disablePlugin(plugin.manifest.id);
|
||||||
|
console.log(`插件已禁用: ${plugin.manifest.id}`);
|
||||||
|
|
||||||
|
// 启用插件
|
||||||
|
// @ts-expect-error obsidian typings do not expose this internal API
|
||||||
|
await plugin.app.plugins.enablePlugin(plugin.manifest.id);
|
||||||
|
console.log(`插件已重新启用: ${plugin.manifest.id}`);
|
||||||
|
|
||||||
|
new Notice("插件重载完成");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("插件重载失败:", error);
|
||||||
|
new Notice("插件重载失败,请手动重启插件");
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "加载完成"
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("错误详情:", error);
|
||||||
|
|
||||||
|
// 发生错误时也要清理临时目录
|
||||||
|
await cleanupTempDirectory(adapter, tempDir);
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "升级过程中出现未知错误";
|
||||||
|
console.log(`最终错误信息: ${errorMessage}`);
|
||||||
|
new Notice(`加载失败: ${errorMessage}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: errorMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class MobileSettingTab extends PluginSettingTab {
|
||||||
|
plugin: Plugin & { settings: InfioSettings; setSettings: (s: InfioSettings) => Promise<void> }
|
||||||
|
|
||||||
|
constructor(app: App, plugin: Plugin & { settings: InfioSettings; setSettings: (s: InfioSet·tings) => Promise<void> }) {
|
||||||
|
super(app, plugin)
|
||||||
|
this.plugin = plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this
|
||||||
|
containerEl.empty()
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const title = containerEl.createEl('h2', { text: 'Infio Mobile' })
|
||||||
|
title.style.marginBottom = '8px'
|
||||||
|
|
||||||
|
// Description
|
||||||
|
containerEl.createEl('div', { text: '仅用于在移动端填写 API Key 以下载正式移动版本。' })
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Infio API Key')
|
||||||
|
.setDesc('用于验证并下载移动端正式版本')
|
||||||
|
.addText((text) => {
|
||||||
|
text
|
||||||
|
.setPlaceholder('sk-...')
|
||||||
|
.setValue(this.plugin.settings?.infioProvider?.apiKey || '')
|
||||||
|
.onChange(async (value) => {
|
||||||
|
await this.plugin.setSettings({
|
||||||
|
...this.plugin.settings,
|
||||||
|
infioProvider: {
|
||||||
|
...(this.plugin.settings?.infioProvider || { name: 'Infio', apiKey: '', baseUrl: '', useCustomUrl: false, models: [] }),
|
||||||
|
apiKey: value,
|
||||||
|
},
|
||||||
|
// 兼容字段
|
||||||
|
infioApiKey: value,
|
||||||
|
})
|
||||||
|
new Notice('已保存 API Key')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 升级到 Pro 按钮
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('升级到Pro')
|
||||||
|
.setDesc('填写 API Key 后点击下载并升级到正式移动版')
|
||||||
|
.addButton((button) => {
|
||||||
|
button.setButtonText('升级到Pro').onClick(async () => {
|
||||||
|
const originalText = button.buttonEl.textContent || '升级到Pro'
|
||||||
|
button.setDisabled(true)
|
||||||
|
button.setButtonText('加载中...')
|
||||||
|
try {
|
||||||
|
const apiKey = this.plugin.settings?.infioProvider?.apiKey || this.plugin.settings?.infioApiKey || ''
|
||||||
|
if (!apiKey) {
|
||||||
|
if (this.app) {
|
||||||
|
new ApiKeyModal(this.app).open()
|
||||||
|
} else {
|
||||||
|
new Notice('请先在Infio Provider设置中配置 Infio API Key')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPlan = await fetchUserPlan(apiKey)
|
||||||
|
const plan = (userPlan.plan || '').toLowerCase()
|
||||||
|
const isProUser = plan.startsWith('pro')
|
||||||
|
const isGeneralUser = plan.startsWith('general')
|
||||||
|
let dl_zip = userPlan.dl_zip || ''
|
||||||
|
|
||||||
|
if (!isProUser && !isGeneralUser) {
|
||||||
|
if (this.app) {
|
||||||
|
new ProUpgradeModal(this.app).open()
|
||||||
|
} else {
|
||||||
|
new Notice('您的账户不是会员用户, 请先购买会员')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGeneralUser) {
|
||||||
|
const result = await checkGeneral(apiKey)
|
||||||
|
if (!result.success) {
|
||||||
|
if (this.app) {
|
||||||
|
new ProUpgradeModal(this.app).open()
|
||||||
|
} else {
|
||||||
|
new Notice('您的账户不是会员用户, 请先购买会员')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dl_zip = result.dl_zip || dl_zip
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.isMobile) {
|
||||||
|
dl_zip = dl_zip.replace('.zip', '.mobile.zip')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dl_zip) {
|
||||||
|
new Notice('无法获取下载地址,请稍后再试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await upgradeToProVersion(this.plugin, dl_zip)
|
||||||
|
if (!result.success) {
|
||||||
|
new Notice(`加载失败: ${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
new Notice('升级过程中发生错误')
|
||||||
|
console.error(_error)
|
||||||
|
} finally {
|
||||||
|
button.setDisabled(false)
|
||||||
|
button.setButtonText(originalText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hint for users
|
||||||
|
const hint = containerEl.createEl('div', { text: Platform.isMobile ? '已检测到移动端环境。填写 API Key 后,将在插件中验证并引导下载正式版本。' : '非移动端环境。' })
|
||||||
|
hint.style.marginTop = '8px'
|
||||||
|
; // keep style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMobile(base: Plugin) {
|
||||||
|
const plugin = base as Plugin & {
|
||||||
|
settings: InfioSettings
|
||||||
|
loadSettings: () => Promise<void>
|
||||||
|
setSettings: (s: InfioSettings) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.loadSettings = async function () {
|
||||||
|
this.settings = parseInfioSettings(await this.loadData())
|
||||||
|
await this.saveData(this.settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.setSettings = async function (newSettings: InfioSettings) {
|
||||||
|
this.settings = newSettings
|
||||||
|
await this.saveData(newSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
await plugin.loadSettings()
|
||||||
|
|
||||||
|
// Only settings tab
|
||||||
|
plugin.addSettingTab(new MobileSettingTab(plugin.app, plugin))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unloadMobile(_base: Plugin) {
|
||||||
|
// nothing to cleanup in lite mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
691
src/main.ts
691
src/main.ts
@ -1,685 +1,26 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { EditorView } from '@codemirror/view'
|
import { Platform, Plugin } from 'obsidian'
|
||||||
// import { PGlite } from '@electric-sql/pglite'
|
|
||||||
import { Editor, MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian'
|
|
||||||
|
|
||||||
import { ApplyView } from './ApplyView'
|
|
||||||
import { ChatView } from './ChatView'
|
|
||||||
import { ChatProps } from './components/chat-view/ChatView'
|
|
||||||
import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, JSON_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants'
|
|
||||||
import { getDiffStrategy } from "./core/diff/DiffStrategy"
|
|
||||||
import { InlineEdit } from './core/edit/inline-edit-processor'
|
|
||||||
import { McpHub } from './core/mcp/McpHub'
|
|
||||||
import { RAGEngine } from './core/rag/rag-engine'
|
|
||||||
import { TransEngine } from './core/transformations/trans-engine'
|
|
||||||
import { DBManager } from './database/database-manager'
|
|
||||||
import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase'
|
|
||||||
import { EmbeddingManager } from './embedworker/EmbeddingManager'
|
|
||||||
import EventListener from "./event-listener"
|
|
||||||
import JsonView from './JsonFileView'
|
|
||||||
import { t } from './lang/helpers'
|
|
||||||
import { PreviewView } from './PreviewView'
|
|
||||||
import CompletionKeyWatcher from "./render-plugin/completion-key-watcher"
|
|
||||||
import DocumentChangesListener, {
|
|
||||||
DocumentChanges,
|
|
||||||
getPrefix, getSuffix,
|
|
||||||
hasMultipleCursors,
|
|
||||||
hasSelection
|
|
||||||
} from "./render-plugin/document-changes-listener"
|
|
||||||
import RenderSuggestionPlugin from "./render-plugin/render-surgestion-plugin"
|
|
||||||
import { InlineSuggestionState } from "./render-plugin/states"
|
|
||||||
import { InfioSettingTab } from './settings/SettingTab'
|
|
||||||
import StatusBar from "./status-bar"
|
|
||||||
import {
|
|
||||||
InfioSettings,
|
|
||||||
parseInfioSettings,
|
|
||||||
} from './types/settings'
|
|
||||||
import { createDataviewManager, DataviewManager } from './utils/dataview'
|
|
||||||
import { getMentionableBlockData } from './utils/obsidian'
|
|
||||||
import './utils/path'
|
|
||||||
import { onEnt } from './utils/web-search'
|
|
||||||
|
|
||||||
export default class InfioPlugin extends Plugin {
|
export default class InfioPlugin extends Plugin {
|
||||||
private metadataCacheUnloadFn: (() => void) | null = null
|
|
||||||
private activeLeafChangeUnloadFn: (() => void) | null = null
|
|
||||||
private dbManagerInitPromise: Promise<DBManager> | null = null
|
|
||||||
private ragEngineInitPromise: Promise<RAGEngine> | null = null
|
|
||||||
private transEngineInitPromise: Promise<TransEngine> | null = null
|
|
||||||
private mcpHubInitPromise: Promise<McpHub> | null = null
|
|
||||||
settings: InfioSettings
|
|
||||||
settingTab: InfioSettingTab
|
|
||||||
settingsListeners: ((newSettings: InfioSettings) => void)[] = []
|
|
||||||
initChatProps?: ChatProps
|
|
||||||
dbManager: DBManager | null = null
|
|
||||||
mcpHub: McpHub | null = null
|
|
||||||
ragEngine: RAGEngine | null = null
|
|
||||||
transEngine: TransEngine | null = null
|
|
||||||
embeddingManager: EmbeddingManager | null = null
|
|
||||||
inlineEdit: InlineEdit | null = null
|
|
||||||
diffStrategy?: DiffStrategy
|
|
||||||
dataviewManager: DataviewManager | null = null
|
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
// load settings
|
if (Platform.isMobile) {
|
||||||
await this.loadSettings()
|
console.log('Infio Copilot: Mobile platform detected, skipping desktop-only features.')
|
||||||
|
const mod = await import('./main.mobile')
|
||||||
// migrate to json database
|
await mod.loadMobile(this)
|
||||||
setTimeout(() => {
|
} else {
|
||||||
void this.migrateToJsonStorage().then(() => { })
|
console.log('Infio Copilot: Desktop platform detected, loading desktop features.')
|
||||||
void onEnt('loaded')
|
const mod = await import('./main.desktop')
|
||||||
}, 100)
|
await mod.loadDesktop(this)
|
||||||
|
}
|
||||||
// add settings tab
|
|
||||||
this.settingTab = new InfioSettingTab(this.app, this)
|
|
||||||
this.addSettingTab(this.settingTab)
|
|
||||||
|
|
||||||
// initialize dataview manager
|
|
||||||
this.dataviewManager = createDataviewManager(this.app)
|
|
||||||
|
|
||||||
// initialize embedding manager
|
|
||||||
this.embeddingManager = new EmbeddingManager()
|
|
||||||
console.log('EmbeddingManager initialized')
|
|
||||||
|
|
||||||
// add icon to ribbon
|
|
||||||
this.addRibbonIcon('wand-sparkles', t('main.openInfioCopilot'), () =>
|
|
||||||
this.openChatView(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// register views
|
|
||||||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this))
|
|
||||||
this.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf))
|
|
||||||
this.registerView(PREVIEW_VIEW_TYPE, (leaf) => new PreviewView(leaf))
|
|
||||||
this.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, this))
|
|
||||||
|
|
||||||
// register markdown processor for Inline Edit
|
|
||||||
this.inlineEdit = new InlineEdit(this, this.settings);
|
|
||||||
this.registerMarkdownCodeBlockProcessor("infioedit", (source, el, ctx) => {
|
|
||||||
this.inlineEdit?.Processor(source, el, ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// setup autocomplete event listener
|
|
||||||
const statusBar = StatusBar.fromApp(this);
|
|
||||||
const eventListener = EventListener.fromSettings(
|
|
||||||
this.settings,
|
|
||||||
statusBar,
|
|
||||||
this.app
|
|
||||||
);
|
|
||||||
|
|
||||||
// initialize diff strategy
|
|
||||||
this.diffStrategy = getDiffStrategy(
|
|
||||||
this.settings.chatModelId || "",
|
|
||||||
this.app,
|
|
||||||
this.settings.fuzzyMatchThreshold,
|
|
||||||
this.settings.experimentalDiffStrategy,
|
|
||||||
this.settings.multiSearchReplaceDiffStrategy,
|
|
||||||
)
|
|
||||||
|
|
||||||
// add settings change listener
|
|
||||||
this.addSettingsListener((newSettings) => {
|
|
||||||
// Update inlineEdit when settings change
|
|
||||||
this.inlineEdit = new InlineEdit(this, newSettings);
|
|
||||||
// Update autocomplete event listener when settings change
|
|
||||||
eventListener.handleSettingChanged(newSettings)
|
|
||||||
// Update diff strategy when settings change
|
|
||||||
this.diffStrategy = getDiffStrategy(
|
|
||||||
this.settings.chatModelId || "",
|
|
||||||
this.app,
|
|
||||||
this.settings.fuzzyMatchThreshold,
|
|
||||||
this.settings.experimentalDiffStrategy,
|
|
||||||
this.settings.multiSearchReplaceDiffStrategy,
|
|
||||||
)
|
|
||||||
// Update MCP Hub when settings change
|
|
||||||
if (this.settings.mcpEnabled && !this.mcpHub) {
|
|
||||||
void this.getMcpHub()
|
|
||||||
} else if (!this.settings.mcpEnabled && this.mcpHub) {
|
|
||||||
this.mcpHub.dispose()
|
|
||||||
this.mcpHub = null
|
|
||||||
this.mcpHubInitPromise = null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// setup autocomplete render plugin
|
|
||||||
this.registerEditorExtension([
|
|
||||||
InlineSuggestionState,
|
|
||||||
CompletionKeyWatcher(
|
|
||||||
eventListener.handleAcceptKeyPressed.bind(eventListener) as () => boolean,
|
|
||||||
eventListener.handlePartialAcceptKeyPressed.bind(eventListener) as () => boolean,
|
|
||||||
eventListener.handleCancelKeyPressed.bind(eventListener) as () => boolean,
|
|
||||||
),
|
|
||||||
DocumentChangesListener(
|
|
||||||
eventListener.handleDocumentChange.bind(eventListener) as (documentChange: DocumentChanges) => Promise<void>
|
|
||||||
),
|
|
||||||
RenderSuggestionPlugin(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.app.workspace.onLayoutReady(() => {
|
|
||||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
||||||
|
|
||||||
if (view) {
|
|
||||||
// @ts-expect-error, not typed
|
|
||||||
const editorView = view.editor.cm as EditorView;
|
|
||||||
eventListener.onViewUpdate(editorView);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/// *** Event Listeners ***
|
|
||||||
this.registerEvent(
|
|
||||||
this.app.workspace.on("active-leaf-change", (leaf) => {
|
|
||||||
if (leaf?.view instanceof MarkdownView) {
|
|
||||||
// @ts-expect-error, not typed
|
|
||||||
const editorView = leaf.view.editor.cm as EditorView;
|
|
||||||
eventListener.onViewUpdate(editorView);
|
|
||||||
if (leaf.view.file) {
|
|
||||||
eventListener.handleFileChange(leaf.view.file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.registerEvent(
|
|
||||||
this.app.metadataCache.on("changed", (file: TFile) => {
|
|
||||||
if (file) {
|
|
||||||
eventListener.handleFileChange(file);
|
|
||||||
// is not worth it to update the file index on every file change
|
|
||||||
// this.ragEngine?.updateFileIndex(file);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.registerEvent(
|
|
||||||
this.app.metadataCache.on("deleted", (file: TFile) => {
|
|
||||||
if (file) {
|
|
||||||
this.ragEngine?.deleteFileIndex(file);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/// *** Commands ***
|
|
||||||
this.addCommand({
|
|
||||||
id: 'open-new-chat',
|
|
||||||
name: t('main.openNewChat'),
|
|
||||||
callback: () => this.openChatView(true),
|
|
||||||
})
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: 'add-selection-to-chat',
|
|
||||||
name: t('main.addSelectionToChat'),
|
|
||||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
||||||
this.addSelectionToChat(editor, view)
|
|
||||||
},
|
|
||||||
// hotkeys: [
|
|
||||||
// {
|
|
||||||
// modifiers: ['Mod', 'Shift'],
|
|
||||||
// key: 'l',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
})
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: 'rebuild-vault-index',
|
|
||||||
name: t('main.rebuildVaultIndex'),
|
|
||||||
callback: async () => {
|
|
||||||
const notice = new Notice(t('notifications.rebuildingIndex'), 0)
|
|
||||||
try {
|
|
||||||
const ragEngine = await this.getRAGEngine()
|
|
||||||
await ragEngine.updateVaultIndex(
|
|
||||||
{ reindexAll: true },
|
|
||||||
(queryProgress) => {
|
|
||||||
if (queryProgress.type === 'indexing') {
|
|
||||||
const { completedChunks, totalChunks } =
|
|
||||||
queryProgress.indexProgress
|
|
||||||
notice.setMessage(
|
|
||||||
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
notice.setMessage(t('notifications.rebuildComplete'))
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
notice.setMessage(t('notifications.rebuildFailed'))
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => {
|
|
||||||
notice.hide()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: 'update-vault-index',
|
|
||||||
name: t('main.updateVaultIndex'),
|
|
||||||
callback: async () => {
|
|
||||||
const notice = new Notice(t('notifications.updatingIndex'), 0)
|
|
||||||
try {
|
|
||||||
const ragEngine = await this.getRAGEngine()
|
|
||||||
await ragEngine.updateVaultIndex(
|
|
||||||
{ reindexAll: false },
|
|
||||||
(queryProgress) => {
|
|
||||||
if (queryProgress.type === 'indexing') {
|
|
||||||
const { completedChunks, totalChunks } =
|
|
||||||
queryProgress.indexProgress
|
|
||||||
notice.setMessage(
|
|
||||||
t('notifications.indexingChunks', { completedChunks, totalChunks }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
notice.setMessage(t('notifications.updateComplete'))
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
notice.setMessage(t('notifications.updateFailed'))
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => {
|
|
||||||
notice.hide()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: 'autocomplete-accept',
|
|
||||||
name: t('main.autocompleteAccept'),
|
|
||||||
editorCheckCallback: (
|
|
||||||
checking: boolean,
|
|
||||||
editor: Editor,
|
|
||||||
view: MarkdownView
|
|
||||||
) => {
|
|
||||||
if (checking) {
|
|
||||||
return (
|
|
||||||
eventListener.isSuggesting()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
eventListener.handleAcceptCommand();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: 'autocomplete-predict',
|
|
||||||
name: t('main.autocompletePredict'),
|
|
||||||
editorCheckCallback: (
|
|
||||||
checking: boolean,
|
|
||||||
editor: Editor,
|
|
||||||
view: MarkdownView
|
|
||||||
) => {
|
|
||||||
// @ts-expect-error, not typed
|
|
||||||
const editorView = editor.cm as EditorView;
|
|
||||||
const state = editorView.state;
|
|
||||||
if (checking) {
|
|
||||||
return eventListener.isIdle() && !hasMultipleCursors(state) && !hasSelection(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = getPrefix(state)
|
|
||||||
const suffix = getSuffix(state)
|
|
||||||
|
|
||||||
eventListener.handlePredictCommand(prefix, suffix);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: "autocomplete-toggle",
|
|
||||||
name: t('main.autocompleteToggle'),
|
|
||||||
callback: () => {
|
|
||||||
const newValue = !this.settings.autocompleteEnabled;
|
|
||||||
this.setSettings({
|
|
||||||
...this.settings,
|
|
||||||
autocompleteEnabled: newValue,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: "autocomplete-enable",
|
|
||||||
name: t('main.autocompleteEnable'),
|
|
||||||
checkCallback: (checking) => {
|
|
||||||
if (checking) {
|
|
||||||
return !this.settings.autocompleteEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setSettings({
|
|
||||||
...this.settings,
|
|
||||||
autocompleteEnabled: true,
|
|
||||||
})
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: "autocomplete-disable",
|
|
||||||
name: t('main.autocompleteDisable'),
|
|
||||||
checkCallback: (checking) => {
|
|
||||||
if (checking) {
|
|
||||||
return this.settings.autocompleteEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setSettings({
|
|
||||||
...this.settings,
|
|
||||||
autocompleteEnabled: false,
|
|
||||||
})
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: "ai-inline-edit",
|
|
||||||
name: t('main.inlineEditCommand'),
|
|
||||||
// hotkeys: [
|
|
||||||
// {
|
|
||||||
// modifiers: ['Mod', 'Shift'],
|
|
||||||
// key: "k",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
editorCallback: (editor: Editor) => {
|
|
||||||
const selection = editor.getSelection();
|
|
||||||
if (!selection) {
|
|
||||||
new Notice(t('notifications.selectTextFirst'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get the selection start position
|
|
||||||
const from = editor.getCursor("from");
|
|
||||||
// Create the position for inserting the block
|
|
||||||
const insertPos = { line: from.line, ch: 0 };
|
|
||||||
// Create the AI block with the selected text
|
|
||||||
const customBlock = "```infioedit\n```\n";
|
|
||||||
// Insert the block above the selection
|
|
||||||
editor.replaceRange(customBlock, insertPos);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加简单测试命令
|
|
||||||
this.addCommand({
|
|
||||||
id: 'test-dataview-simple',
|
|
||||||
name: '测试 Dataview(简单查询)',
|
|
||||||
callback: async () => {
|
|
||||||
console.log('开始测试 Dataview...');
|
|
||||||
|
|
||||||
if (!this.dataviewManager) {
|
|
||||||
new Notice('DataviewManager 未初始化');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dataviewManager.isDataviewAvailable()) {
|
|
||||||
new Notice('Dataview 插件未安装或未启用');
|
|
||||||
console.log('Dataview API 不可用');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Dataview API 可用,执行简单查询...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 执行一个最简单的查询
|
|
||||||
const result = await this.dataviewManager.executeQuery('LIST FROM ""');
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
new Notice('Dataview 查询成功!结果已在控制台输出');
|
|
||||||
// console.log('查询结果:', result.data);
|
|
||||||
} else {
|
|
||||||
new Notice(`查询失败: ${result.error}`);
|
|
||||||
console.error('查询错误:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('执行测试查询失败:', error);
|
|
||||||
new Notice('执行测试查询时发生错误');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加本地嵌入测试命令
|
|
||||||
this.addCommand({
|
|
||||||
id: 'test-local-embed',
|
|
||||||
name: '测试本地嵌入模型',
|
|
||||||
callback: async () => {
|
|
||||||
try {
|
|
||||||
if (!this.embeddingManager) {
|
|
||||||
new Notice('EmbeddingManager 未初始化', 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载模型
|
|
||||||
await this.embeddingManager.loadModel("Xenova/all-MiniLM-L6-v2", true);
|
|
||||||
|
|
||||||
// 测试嵌入 "hello world"
|
|
||||||
const testText = "hello world";
|
|
||||||
|
|
||||||
const result = await this.embeddingManager.embed(testText);
|
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
const resultMessage = `
|
|
||||||
嵌入测试完成!
|
|
||||||
文本: "${testText}"
|
|
||||||
Token 数量: ${result.tokens}
|
|
||||||
向量维度: ${result.vec.length}
|
|
||||||
向量前4个值: [${result.vec.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
console.log('本地嵌入测试结果:', result);
|
|
||||||
|
|
||||||
// 创建模态框显示结果
|
|
||||||
const modal = new Modal(this.app);
|
|
||||||
modal.titleEl.setText('本地嵌入测试结果');
|
|
||||||
modal.contentEl.createEl('pre', { text: resultMessage });
|
|
||||||
modal.open();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('嵌入测试失败:', error);
|
|
||||||
new Notice(`嵌入测试失败: ${error.message}`, 5000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
// Promise cleanup
|
if (Platform.isMobile) {
|
||||||
this.dbManagerInitPromise = null
|
void import('./main.mobile').then((m) => m.unloadMobile?.(this)).catch(() => {})
|
||||||
this.ragEngineInitPromise = null
|
} else {
|
||||||
this.transEngineInitPromise = null
|
void import('./main.desktop').then((m) => m.unloadDesktop?.(this)).catch(() => {})
|
||||||
this.mcpHubInitPromise = null
|
|
||||||
// RagEngine cleanup
|
|
||||||
this.ragEngine?.cleanup()
|
|
||||||
this.ragEngine = null
|
|
||||||
// TransEngine cleanup
|
|
||||||
this.transEngine?.cleanup()
|
|
||||||
this.transEngine = null
|
|
||||||
// Database cleanup
|
|
||||||
this.dbManager?.cleanup()
|
|
||||||
this.dbManager = null
|
|
||||||
// MCP Hub cleanup
|
|
||||||
this.mcpHub?.dispose()
|
|
||||||
this.mcpHub = null
|
|
||||||
// EmbeddingManager cleanup
|
|
||||||
this.embeddingManager?.terminate()
|
|
||||||
this.embeddingManager = null
|
|
||||||
// Dataview cleanup
|
|
||||||
this.dataviewManager = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadSettings() {
|
|
||||||
this.settings = parseInfioSettings(await this.loadData())
|
|
||||||
await this.saveData(this.settings) // Save updated settings
|
|
||||||
}
|
|
||||||
|
|
||||||
async setSettings(newSettings: InfioSettings) {
|
|
||||||
this.settings = newSettings
|
|
||||||
await this.saveData(newSettings)
|
|
||||||
this.ragEngine?.setSettings(newSettings)
|
|
||||||
this.transEngine?.setSettings(newSettings)
|
|
||||||
this.settingsListeners.forEach((listener) => listener(newSettings))
|
|
||||||
}
|
|
||||||
|
|
||||||
addSettingsListener(
|
|
||||||
listener: (newSettings: InfioSettings) => void,
|
|
||||||
) {
|
|
||||||
this.settingsListeners.push(listener)
|
|
||||||
return () => {
|
|
||||||
this.settingsListeners = this.settingsListeners.filter(
|
|
||||||
(l) => l !== listener,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openChatView(openNewChat = false) {
|
|
||||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView)
|
|
||||||
const editor = view?.editor
|
|
||||||
if (!view || !editor) {
|
|
||||||
this.activateChatView(undefined, openNewChat)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const selectedBlockData = await getMentionableBlockData(editor, view)
|
|
||||||
this.activateChatView(
|
|
||||||
{
|
|
||||||
selectedBlock: selectedBlockData ?? undefined,
|
|
||||||
},
|
|
||||||
openNewChat,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async activateChatView(chatProps?: ChatProps, openNewChat = false) {
|
|
||||||
// chatProps is consumed in ChatView.tsx
|
|
||||||
this.initChatProps = chatProps
|
|
||||||
|
|
||||||
const leaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0]
|
|
||||||
|
|
||||||
await (leaf ?? this.app.workspace.getRightLeaf(false))?.setViewState({
|
|
||||||
type: CHAT_VIEW_TYPE,
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (openNewChat && leaf && leaf.view instanceof ChatView) {
|
|
||||||
leaf.view.openNewChat(chatProps?.selectedBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.app.workspace.revealLeaf(
|
|
||||||
this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async addSelectionToChat(editor: Editor, view: MarkdownView) {
|
|
||||||
const data = await getMentionableBlockData(editor, view)
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
|
||||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
|
||||||
await this.activateChatView({
|
|
||||||
selectedBlock: data,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// bring leaf to foreground (uncollapse sidebar if it's collapsed)
|
|
||||||
await this.app.workspace.revealLeaf(leaves[0])
|
|
||||||
|
|
||||||
const chatView = leaves[0].view
|
|
||||||
chatView.addSelectionToChat(data)
|
|
||||||
chatView.focusMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDbManager(): Promise<DBManager> {
|
|
||||||
if (this.dbManager) {
|
|
||||||
return this.dbManager
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dbManagerInitPromise) {
|
|
||||||
this.dbManagerInitPromise = (async () => {
|
|
||||||
this.dbManager = await DBManager.create(
|
|
||||||
this.app,
|
|
||||||
this.settings.ragOptions.filesystem,
|
|
||||||
)
|
|
||||||
return this.dbManager
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
|
||||||
return this.dbManagerInitPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMcpHub(): Promise<McpHub | null> {
|
|
||||||
// MCP is not enabled
|
|
||||||
if (!this.settings.mcpEnabled) {
|
|
||||||
// new Notice('MCP is not enabled')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we already have an instance, return it
|
|
||||||
if (this.mcpHub) {
|
|
||||||
return this.mcpHub
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.mcpHubInitPromise) {
|
|
||||||
this.mcpHubInitPromise = (async () => {
|
|
||||||
this.mcpHub = new McpHub(this.app, this)
|
|
||||||
await this.mcpHub.onload()
|
|
||||||
return this.mcpHub
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
|
||||||
return this.mcpHubInitPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRAGEngine(): Promise<RAGEngine> {
|
|
||||||
if (this.ragEngine) {
|
|
||||||
return this.ragEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.ragEngineInitPromise) {
|
|
||||||
this.ragEngineInitPromise = (async () => {
|
|
||||||
const dbManager = await this.getDbManager()
|
|
||||||
this.ragEngine = new RAGEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
|
||||||
return this.ragEngine
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
|
||||||
return this.ragEngineInitPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTransEngine(): Promise<TransEngine> {
|
|
||||||
if (this.transEngine) {
|
|
||||||
return this.transEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.transEngineInitPromise) {
|
|
||||||
this.transEngineInitPromise = (async () => {
|
|
||||||
const dbManager = await this.getDbManager()
|
|
||||||
this.transEngine = new TransEngine(this.app, this.settings, dbManager, this.embeddingManager)
|
|
||||||
return this.transEngine
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if initialization is running, wait for it to complete instead of creating a new initialization promise
|
|
||||||
return this.transEngineInitPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmbeddingManager(): EmbeddingManager | null {
|
|
||||||
return this.embeddingManager
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
t('notifications.migrationFailed'),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async reloadChatView() {
|
|
||||||
const leaves = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)
|
|
||||||
if (leaves.length === 0 || !(leaves[0].view instanceof ChatView)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
new Notice(t('notifications.reloadingInfio'), 1000)
|
|
||||||
leaves[0].detach()
|
|
||||||
await this.activateChatView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import * as React from "react";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import { t } from '../lang/helpers';
|
import { t } from '../lang/helpers';
|
||||||
import InfioPlugin from '../main';
|
|
||||||
import { InfioSettings } from '../types/settings';
|
import { InfioSettings } from '../types/settings';
|
||||||
import { findFilesMatchingPatterns } from '../utils/glob-utils';
|
import { findFilesMatchingPatterns } from '../utils/glob-utils';
|
||||||
|
|
||||||
@ -24,13 +23,18 @@ import PreprocessingSettings from './components/PreprocessingSettings';
|
|||||||
import PrivacySettings from './components/PrivacySettings';
|
import PrivacySettings from './components/PrivacySettings';
|
||||||
import TriggerSettingsSection from './components/TriggerSettingsSection';
|
import TriggerSettingsSection from './components/TriggerSettingsSection';
|
||||||
|
|
||||||
|
type InfioPluginLike = Plugin & {
|
||||||
|
settings: InfioSettings;
|
||||||
|
setSettings: (s: InfioSettings) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export class InfioSettingTab extends PluginSettingTab {
|
export class InfioSettingTab extends PluginSettingTab {
|
||||||
plugin: InfioPlugin;
|
plugin: InfioPluginLike;
|
||||||
private autoCompleteContainer: HTMLElement | null = null;
|
private autoCompleteContainer: HTMLElement | null = null;
|
||||||
private modelsContainer: HTMLElement | null = null;
|
private modelsContainer: HTMLElement | null = null;
|
||||||
private pluginInfoContainer: HTMLElement | null = null;
|
private pluginInfoContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(app: App, plugin: InfioPlugin) {
|
constructor(app: App, plugin: InfioPluginLike) {
|
||||||
super(app, plugin)
|
super(app, plugin)
|
||||||
this.plugin = plugin
|
this.plugin = plugin
|
||||||
}
|
}
|
||||||
|
|||||||
659
src/types/settings-mobile.ts
Normal file
659
src/types/settings-mobile.ts
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { DEFAULT_MODELS } from '../constants';
|
||||||
|
import {
|
||||||
|
MAX_DELAY,
|
||||||
|
MAX_MAX_CHAR_LIMIT,
|
||||||
|
MIN_DELAY,
|
||||||
|
MIN_MAX_CHAR_LIMIT,
|
||||||
|
MIN_MAX_TOKENS,
|
||||||
|
fewShotExampleSchema,
|
||||||
|
modelOptionsSchema
|
||||||
|
} from '../settings/versions/shared';
|
||||||
|
export const DEFAULT_SETTINGS = {
|
||||||
|
// version: "1",
|
||||||
|
|
||||||
|
// General settings
|
||||||
|
autocompleteEnabled: true,
|
||||||
|
advancedMode: false,
|
||||||
|
apiProvider: "openai",
|
||||||
|
// API settings
|
||||||
|
azureOAIApiSettings: {
|
||||||
|
key: "",
|
||||||
|
url: "https://YOUR_AOI_SERVICE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions",
|
||||||
|
},
|
||||||
|
openAIApiSettings: {
|
||||||
|
key: "",
|
||||||
|
url: "https://api.openai.com/v1/chat/completions",
|
||||||
|
model: "gpt-3.5-turbo",
|
||||||
|
},
|
||||||
|
ollamaApiSettings: {
|
||||||
|
url: "http://localhost:11434/api/chat",
|
||||||
|
model: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Trigger settings
|
||||||
|
triggers: [
|
||||||
|
{ type: "string", value: "# " },
|
||||||
|
{ type: "string", value: ". " },
|
||||||
|
{ type: "string", value: ": " },
|
||||||
|
{ type: "string", value: ", " },
|
||||||
|
{ type: "string", value: "! " },
|
||||||
|
{ type: "string", value: "? " },
|
||||||
|
{ type: "string", value: "`" },
|
||||||
|
{ type: "string", value: "' " },
|
||||||
|
{ type: "string", value: "= " },
|
||||||
|
{ type: "string", value: "$ " },
|
||||||
|
{ type: "string", value: "> " },
|
||||||
|
{ type: "string", value: "\n" },
|
||||||
|
|
||||||
|
// bullet list
|
||||||
|
{ type: "regex", value: "[\\t ]*(\\-|\\*)[\\t ]+$" },
|
||||||
|
// numbered list
|
||||||
|
{ type: "regex", value: "[\\t ]*[0-9A-Za-z]+\\.[\\t ]+$" },
|
||||||
|
// new line with spaces
|
||||||
|
{ type: "regex", value: "\\$\\$\\n[\\t ]*$" },
|
||||||
|
// markdown multiline code block
|
||||||
|
{ type: "regex", value: "```[a-zA-Z0-9]*(\\n\\s*)?$" },
|
||||||
|
// task list normal, sub or numbered.
|
||||||
|
{ type: "regex", value: "\\s*(-|[0-9]+\\.) \\[.\\]\\s+$" },
|
||||||
|
],
|
||||||
|
|
||||||
|
delay: 500,
|
||||||
|
// Request settings
|
||||||
|
modelOptions: {
|
||||||
|
temperature: 1,
|
||||||
|
top_p: 0.1,
|
||||||
|
frequency_penalty: 0.25,
|
||||||
|
presence_penalty: 0,
|
||||||
|
max_tokens: MIN_MAX_TOKENS,
|
||||||
|
},
|
||||||
|
// Prompt settings
|
||||||
|
systemMessage: `Your job is to predict the most logical text that should be written at the location of the <mask/>.
|
||||||
|
Your answer can be either code, a single word, or multiple sentences.
|
||||||
|
If the <mask/> is in the middle of a partial sentence, your answer should only be the 1 or 2 words fixes the sentence and not the entire sentence.
|
||||||
|
You are not allowed to have any overlapping text directly surrounding the <mask/>.
|
||||||
|
Your answer must be in the same language as the text directly surrounding the <mask/>.
|
||||||
|
Your response must have the following format:
|
||||||
|
THOUGHT: here, you reason about the answer; use the 80/20 principle to be brief.
|
||||||
|
LANGUAGE: here, you write the language of your answer, e.g. English, Python, Dutch, etc.
|
||||||
|
ANSWER: here, you write the text that should be at the location of <mask/>
|
||||||
|
`,
|
||||||
|
fewShotExamples: [
|
||||||
|
],
|
||||||
|
userMessageTemplate: "{{prefix}}<mask/>{{suffix}}",
|
||||||
|
chainOfThoughRemovalRegex: `(.|\\n)*ANSWER:`,
|
||||||
|
// Preprocessing settings
|
||||||
|
dontIncludeDataviews: true,
|
||||||
|
maxPrefixCharLimit: 4000,
|
||||||
|
maxSuffixCharLimit: 4000,
|
||||||
|
// Postprocessing settings
|
||||||
|
removeDuplicateMathBlockIndicator: true,
|
||||||
|
removeDuplicateCodeBlockIndicator: true,
|
||||||
|
ignoredFilePatterns: "**/secret/**\n",
|
||||||
|
ignoredTags: "",
|
||||||
|
cacheSuggestions: true,
|
||||||
|
debugMode: false,
|
||||||
|
};
|
||||||
|
import { ApiProvider } from './llm/model';
|
||||||
|
|
||||||
|
export function isRegexValid(value: string): boolean {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(value);
|
||||||
|
regex.test("");
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidIgnorePattern(value: string): boolean {
|
||||||
|
if (typeof value !== "string" || value.length === 0) return false;
|
||||||
|
// 不允许以单个反斜杠结尾
|
||||||
|
if (/\\$/.test(value)) return false;
|
||||||
|
|
||||||
|
const openerToCloser: Record<string, string> = { "[": "]", "{": "}", "(": ")" };
|
||||||
|
const validExtglobLeaders = new Set(["!", "?", "+", "*", "@"]);
|
||||||
|
const stack: string[] = [];
|
||||||
|
|
||||||
|
const isEscaped = (s: string, i: number): boolean => {
|
||||||
|
let backslashes = 0;
|
||||||
|
for (let k = i - 1; k >= 0 && s[k] === "\\"; k--) backslashes++;
|
||||||
|
return backslashes % 2 === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const ch = value[i];
|
||||||
|
if (isEscaped(value, i)) continue;
|
||||||
|
|
||||||
|
if (ch === "[" || ch === "{" || ch === "(") {
|
||||||
|
// 括号需作为 extglob 的一部分,如 !(...), ?(...), +(...), *(...), @(...)
|
||||||
|
if (ch === "(") {
|
||||||
|
const prev = value[i - 1];
|
||||||
|
if (!validExtglobLeaders.has(prev ?? "")) return false;
|
||||||
|
}
|
||||||
|
stack.push(openerToCloser[ch]);
|
||||||
|
} else if (ch === "]" || ch === "}" || ch === ")") {
|
||||||
|
const expected = stack.pop();
|
||||||
|
if (expected !== ch) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.length === 0;
|
||||||
|
}
|
||||||
|
export const SETTINGS_SCHEMA_VERSION = 0.5
|
||||||
|
|
||||||
|
const InfioProviderSchema = z.object({
|
||||||
|
name: z.literal('Infio'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Infio',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const OpenRouterProviderSchema = z.object({
|
||||||
|
name: z.literal('OpenRouter'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'OpenRouter',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const SiliconFlowProviderSchema = z.object({
|
||||||
|
name: z.literal('SiliconFlow'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'SiliconFlow',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const AlibabaQwenProviderSchema = z.object({
|
||||||
|
name: z.literal('AlibabaQwen'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'AlibabaQwen',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const AnthropicProviderSchema = z.object({
|
||||||
|
name: z.literal('Anthropic'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Anthropic',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const DeepSeekProviderSchema = z.object({
|
||||||
|
name: z.literal('DeepSeek'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'DeepSeek',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const GoogleProviderSchema = z.object({
|
||||||
|
name: z.literal('Google'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Google',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const OpenAIProviderSchema = z.object({
|
||||||
|
name: z.literal('OpenAI'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'OpenAI',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const OpenAICompatibleProviderSchema = z.object({
|
||||||
|
name: z.literal('OpenAICompatible'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
|
useCustomUrl: z.boolean().catch(true),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'OpenAICompatible',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: true,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const OllamaProviderSchema = z.object({
|
||||||
|
name: z.literal('Ollama'),
|
||||||
|
apiKey: z.string().catch('ollama'),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Ollama',
|
||||||
|
apiKey: 'ollama',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: true,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const GroqProviderSchema = z.object({
|
||||||
|
name: z.literal('Groq'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Groq',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const GrokProviderSchema = z.object({
|
||||||
|
name: z.literal('Grok'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Grok',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const MoonshotProviderSchema = z.object({
|
||||||
|
name: z.literal('Moonshot'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'Moonshot',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const LocalProviderSchema = z.object({
|
||||||
|
name: z.literal('LocalProvider'),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
useCustomUrl: z.boolean().catch(false),
|
||||||
|
models: z.array(z.string()).catch([])
|
||||||
|
}).catch({
|
||||||
|
name: 'LocalProvider',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
useCustomUrl: false,
|
||||||
|
models: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const ollamaModelSchema = z.object({
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
model: z.string().catch(''),
|
||||||
|
})
|
||||||
|
|
||||||
|
const openAICompatibleModelSchema = z.object({
|
||||||
|
baseUrl: z.string().catch(''),
|
||||||
|
apiKey: z.string().catch(''),
|
||||||
|
model: z.string().catch(''),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ragOptionsSchema = z.object({
|
||||||
|
filesystem: z.enum(['idb', 'opfs']).catch('opfs'),
|
||||||
|
chunkSize: z.number().catch(500),
|
||||||
|
batchSize: z.number().catch(32),
|
||||||
|
thresholdTokens: z.number().catch(8192),
|
||||||
|
minSimilarity: z.number().catch(0.0),
|
||||||
|
limit: z.number().catch(10),
|
||||||
|
excludePatterns: z.array(z.string()).catch([]),
|
||||||
|
includePatterns: z.array(z.string()).catch([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const triggerSchema = z.object({
|
||||||
|
type: z.enum(['string', 'regex']),
|
||||||
|
value: z.string().min(1, { message: "Trigger value must be at least 1 character long" })
|
||||||
|
}).strict().superRefine((trigger, ctx) => {
|
||||||
|
if (trigger.type === "regex") {
|
||||||
|
if (!trigger.value.endsWith("$")) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Regex triggers must end with a $.",
|
||||||
|
path: ["value"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isRegexValid(trigger.value)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid regex: "${trigger.value}"`,
|
||||||
|
path: ["value"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const FilesSearchSettingsSchema = z.object({
|
||||||
|
method: z.enum(['match', 'regex', 'semantic', 'auto']).catch('auto'),
|
||||||
|
regexBackend: z.enum(['coreplugin', 'ripgrep']).catch('coreplugin'),
|
||||||
|
matchBackend: z.enum(['omnisearch', 'coreplugin']).catch('coreplugin'),
|
||||||
|
ripgrepPath: z.string().catch(''),
|
||||||
|
}).catch({
|
||||||
|
method: 'auto',
|
||||||
|
regexBackend: 'coreplugin',
|
||||||
|
matchBackend: 'coreplugin',
|
||||||
|
ripgrepPath: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const InfioSettingsSchema = z.object({
|
||||||
|
// Version
|
||||||
|
version: z.literal(SETTINGS_SCHEMA_VERSION).catch(SETTINGS_SCHEMA_VERSION),
|
||||||
|
|
||||||
|
// Provider
|
||||||
|
defaultProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||||
|
infioProvider: InfioProviderSchema,
|
||||||
|
openrouterProvider: OpenRouterProviderSchema,
|
||||||
|
siliconflowProvider: SiliconFlowProviderSchema,
|
||||||
|
alibabaQwenProvider: AlibabaQwenProviderSchema,
|
||||||
|
anthropicProvider: AnthropicProviderSchema,
|
||||||
|
deepseekProvider: DeepSeekProviderSchema,
|
||||||
|
openaiProvider: OpenAIProviderSchema,
|
||||||
|
googleProvider: GoogleProviderSchema,
|
||||||
|
ollamaProvider: OllamaProviderSchema,
|
||||||
|
groqProvider: GroqProviderSchema,
|
||||||
|
grokProvider: GrokProviderSchema,
|
||||||
|
moonshotProvider: MoonshotProviderSchema,
|
||||||
|
openaicompatibleProvider: OpenAICompatibleProviderSchema,
|
||||||
|
localproviderProvider: LocalProviderSchema,
|
||||||
|
|
||||||
|
// MCP Servers
|
||||||
|
mcpEnabled: z.boolean().catch(false),
|
||||||
|
|
||||||
|
// Chat Model start list
|
||||||
|
collectedChatModels: z.array(z.object({
|
||||||
|
provider: z.nativeEnum(ApiProvider),
|
||||||
|
modelId: z.string(),
|
||||||
|
})).catch([]),
|
||||||
|
|
||||||
|
// Insight Model start list
|
||||||
|
collectedInsightModels: z.array(z.object({
|
||||||
|
provider: z.nativeEnum(ApiProvider),
|
||||||
|
modelId: z.string(),
|
||||||
|
})).catch([]),
|
||||||
|
|
||||||
|
// Apply Model start list
|
||||||
|
collectedApplyModels: z.array(z.object({
|
||||||
|
provider: z.nativeEnum(ApiProvider),
|
||||||
|
modelId: z.string(),
|
||||||
|
})).catch([]),
|
||||||
|
|
||||||
|
// Embedding Model start list
|
||||||
|
collectedEmbeddingModels: z.array(z.object({
|
||||||
|
provider: z.nativeEnum(ApiProvider),
|
||||||
|
modelId: z.string(),
|
||||||
|
})).catch([]),
|
||||||
|
|
||||||
|
// Active Provider Tab (for UI state)
|
||||||
|
activeProviderTab: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||||
|
|
||||||
|
// Chat Model
|
||||||
|
chatModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||||
|
chatModelId: z.string().catch(''),
|
||||||
|
|
||||||
|
// Insight Model
|
||||||
|
insightModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||||
|
insightModelId: z.string().catch(''),
|
||||||
|
|
||||||
|
// Apply Model
|
||||||
|
applyModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||||
|
applyModelId: z.string().catch(''),
|
||||||
|
|
||||||
|
// Embedding Model
|
||||||
|
embeddingModelProvider: z.nativeEnum(ApiProvider).catch(ApiProvider.Infio),
|
||||||
|
embeddingModelId: z.string().catch(''),
|
||||||
|
|
||||||
|
// fuzzyMatchThreshold
|
||||||
|
fuzzyMatchThreshold: z.number().catch(0.85),
|
||||||
|
|
||||||
|
// experimentalDiffStrategy
|
||||||
|
experimentalDiffStrategy: z.boolean().catch(false),
|
||||||
|
|
||||||
|
// multiSearchReplaceDiffStrategy
|
||||||
|
multiSearchReplaceDiffStrategy: z.boolean().catch(true),
|
||||||
|
|
||||||
|
// Workspace
|
||||||
|
workspace: z.string().catch(''),
|
||||||
|
// Mode
|
||||||
|
mode: z.string().catch('ask'),
|
||||||
|
defaultMention: z.enum(['none', 'current-file', 'vault']).catch('none'),
|
||||||
|
|
||||||
|
// web search
|
||||||
|
serperApiKey: z.string().catch(''),
|
||||||
|
serperSearchEngine: z.enum(['google', 'duckduckgo', 'bing']).catch('google'),
|
||||||
|
jinaApiKey: z.string().catch(''),
|
||||||
|
|
||||||
|
// Files Search
|
||||||
|
filesSearchSettings: FilesSearchSettingsSchema,
|
||||||
|
|
||||||
|
/// [compatible]
|
||||||
|
// activeModels [compatible]
|
||||||
|
activeModels: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
provider: z.string(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
isEmbeddingModel: z.boolean(),
|
||||||
|
isBuiltIn: z.boolean(),
|
||||||
|
apiKey: z.string().optional(),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
|
dimension: z.number().optional(),
|
||||||
|
})
|
||||||
|
).catch(DEFAULT_MODELS),
|
||||||
|
// API Keys [compatible]
|
||||||
|
infioApiKey: z.string().catch(''),
|
||||||
|
openAIApiKey: z.string().catch(''),
|
||||||
|
anthropicApiKey: z.string().catch(''),
|
||||||
|
geminiApiKey: z.string().catch(''),
|
||||||
|
groqApiKey: z.string().catch(''),
|
||||||
|
deepseekApiKey: z.string().catch(''),
|
||||||
|
ollamaEmbeddingModel: ollamaModelSchema.catch({
|
||||||
|
baseUrl: '',
|
||||||
|
model: '',
|
||||||
|
}),
|
||||||
|
ollamaChatModel: ollamaModelSchema.catch({
|
||||||
|
baseUrl: '',
|
||||||
|
model: '',
|
||||||
|
}),
|
||||||
|
openAICompatibleChatModel: openAICompatibleModelSchema.catch({
|
||||||
|
baseUrl: '',
|
||||||
|
apiKey: '',
|
||||||
|
model: '',
|
||||||
|
}),
|
||||||
|
ollamaApplyModel: ollamaModelSchema.catch({
|
||||||
|
baseUrl: '',
|
||||||
|
model: '',
|
||||||
|
}),
|
||||||
|
openAICompatibleApplyModel: openAICompatibleModelSchema.catch({
|
||||||
|
baseUrl: '',
|
||||||
|
apiKey: '',
|
||||||
|
model: '',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// System Prompt
|
||||||
|
systemPrompt: z.string().catch(''),
|
||||||
|
|
||||||
|
// RAG Options
|
||||||
|
ragOptions: ragOptionsSchema.catch({
|
||||||
|
filesystem: 'opfs',
|
||||||
|
batchSize: 32,
|
||||||
|
chunkSize: 500,
|
||||||
|
thresholdTokens: 8192,
|
||||||
|
minSimilarity: 0.0,
|
||||||
|
limit: 10,
|
||||||
|
excludePatterns: [],
|
||||||
|
includePatterns: [],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// autocomplete options
|
||||||
|
autocompleteEnabled: z.boolean(),
|
||||||
|
advancedMode: z.boolean(),
|
||||||
|
|
||||||
|
// [compatible]
|
||||||
|
apiProvider: z.enum(['azure', 'openai', "ollama"]),
|
||||||
|
azureOAIApiSettings: z.string().catch(''),
|
||||||
|
openAIApiSettings: z.string().catch(''),
|
||||||
|
ollamaApiSettings: z.string().catch(''),
|
||||||
|
|
||||||
|
triggers: z.array(triggerSchema),
|
||||||
|
delay: z.number().int().min(MIN_DELAY, { message: "Delay must be between 0ms and 2000ms" }).max(MAX_DELAY, { message: "Delay must be between 0ms and 2000ms" }),
|
||||||
|
modelOptions: modelOptionsSchema,
|
||||||
|
systemMessage: z.string().min(3, { message: "System message must be at least 3 characters long" }),
|
||||||
|
fewShotExamples: z.array(fewShotExampleSchema),
|
||||||
|
userMessageTemplate: z.string().min(3, { message: "User message template must be at least 3 characters long" }),
|
||||||
|
chainOfThoughRemovalRegex: z.string().refine((regex) => isRegexValid(regex), { message: "Invalid regex" }),
|
||||||
|
dontIncludeDataviews: z.boolean(),
|
||||||
|
maxPrefixCharLimit: z.number().int().min(MIN_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at least ${MIN_MAX_CHAR_LIMIT}` }).max(MAX_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at most ${MAX_MAX_CHAR_LIMIT}` }),
|
||||||
|
maxSuffixCharLimit: z.number().int().min(MIN_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at least ${MIN_MAX_CHAR_LIMIT}` }).max(MAX_MAX_CHAR_LIMIT, { message: `Max prefix char limit must be at most ${MAX_MAX_CHAR_LIMIT}` }),
|
||||||
|
removeDuplicateMathBlockIndicator: z.boolean(),
|
||||||
|
removeDuplicateCodeBlockIndicator: z.boolean(),
|
||||||
|
ignoredFilePatterns: z.string().refine((value) => value
|
||||||
|
.split("\n")
|
||||||
|
.filter(s => s.trim().length > 0)
|
||||||
|
.filter(s => !isValidIgnorePattern(s)).length === 0,
|
||||||
|
{ message: "Invalid ignore pattern" }
|
||||||
|
),
|
||||||
|
ignoredTags: z.string().refine((value) => value
|
||||||
|
.split("\n")
|
||||||
|
.filter(s => s.includes(" ")).length === 0, { message: "Tags cannot contain spaces" }
|
||||||
|
).refine((value) => value
|
||||||
|
.split("\n")
|
||||||
|
.filter(s => s.includes("#")).length === 0, { message: "Enter tags without the # symbol" }
|
||||||
|
).refine((value) => value
|
||||||
|
.split("\n")
|
||||||
|
.filter(s => s.includes(",")).length === 0, { message: "Enter each tag on a new line without commas" }
|
||||||
|
),
|
||||||
|
cacheSuggestions: z.boolean(),
|
||||||
|
debugMode: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InfioSettings = z.infer<typeof InfioSettingsSchema>
|
||||||
|
export type FilesSearchSettings = z.infer<typeof FilesSearchSettingsSchema>
|
||||||
|
|
||||||
|
type Migration = {
|
||||||
|
fromVersion: number
|
||||||
|
toVersion: number
|
||||||
|
migrate: (data: Record<string, unknown>) => Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIGRATIONS: Migration[] = [
|
||||||
|
{
|
||||||
|
fromVersion: 0.1,
|
||||||
|
toVersion: 0.4,
|
||||||
|
migrate: (data) => {
|
||||||
|
const newData = { ...data }
|
||||||
|
newData.version = 0.4
|
||||||
|
return newData
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fromVersion: 0.4,
|
||||||
|
toVersion: 0.5,
|
||||||
|
migrate: (data) => {
|
||||||
|
const newData = { ...data }
|
||||||
|
newData.version = SETTINGS_SCHEMA_VERSION
|
||||||
|
|
||||||
|
// Handle max_tokens minimum value increase from 800 to 4096
|
||||||
|
if (newData.modelOptions && typeof newData.modelOptions === 'object') {
|
||||||
|
const modelOptions = newData.modelOptions as Record<string, any>
|
||||||
|
if (typeof modelOptions.max_tokens === 'number' && modelOptions.max_tokens < MIN_MAX_TOKENS) {
|
||||||
|
console.log(`Updating max_tokens from ${modelOptions.max_tokens} to ${MIN_MAX_TOKENS} due to minimum value change`)
|
||||||
|
modelOptions.max_tokens = MIN_MAX_TOKENS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newData
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function migrateSettings(
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
let currentData = { ...data }
|
||||||
|
const currentVersion = (currentData.version as number) ?? 0
|
||||||
|
|
||||||
|
for (const migration of MIGRATIONS) {
|
||||||
|
if (
|
||||||
|
currentVersion >= migration.fromVersion &&
|
||||||
|
currentVersion < migration.toVersion &&
|
||||||
|
migration.toVersion <= SETTINGS_SCHEMA_VERSION
|
||||||
|
) {
|
||||||
|
console.debug(
|
||||||
|
`Migrating settings from ${migration.fromVersion} to ${migration.toVersion}`,
|
||||||
|
)
|
||||||
|
currentData = migration.migrate(currentData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseInfioSettings(data: unknown): InfioSettings {
|
||||||
|
try {
|
||||||
|
const migratedData = migrateSettings(data as Record<string, unknown>)
|
||||||
|
return InfioSettingsSchema.parse(migratedData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse settings with migrated data, using default settings instead: ", error);
|
||||||
|
return InfioSettingsSchema.parse({ ...DEFAULT_SETTINGS })
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/utils/device-id.ts
Normal file
84
src/utils/device-id.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Platform } from 'obsidian'
|
||||||
|
|
||||||
|
const DEVICE_ID_STORAGE_KEY = 'infio_device_id'
|
||||||
|
|
||||||
|
function generatePseudoId(): string {
|
||||||
|
// RFC4122-ish v4 UUID (non-crypto), sufficient for stable device identifier when persisted
|
||||||
|
let timeSeed = Date.now()
|
||||||
|
let perfSeed = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
||||||
|
? Math.floor(performance.now() * 1000)
|
||||||
|
: 0
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {
|
||||||
|
let rand = Math.random() * 16
|
||||||
|
if (timeSeed > 0) {
|
||||||
|
rand = (timeSeed + rand) % 16
|
||||||
|
timeSeed = Math.floor(timeSeed / 16)
|
||||||
|
} else {
|
||||||
|
rand = (perfSeed + rand) % 16
|
||||||
|
perfSeed = Math.floor(perfSeed / 16)
|
||||||
|
}
|
||||||
|
const value = ch === 'x' ? rand : (rand & 0x3) | 0x8
|
||||||
|
return Math.floor(value).toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeGetLocalStorage(key: string): string | null {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && window.localStorage) {
|
||||||
|
return window.localStorage.getItem(key)
|
||||||
|
}
|
||||||
|
} catch { /* noop */ }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeSetLocalStorage(key: string, value: string): void {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && window.localStorage) {
|
||||||
|
window.localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
} catch { /* noop */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDeviceId(): Promise<string> {
|
||||||
|
// On mobile, generate and persist a stable pseudo ID
|
||||||
|
if (Platform.isMobile) {
|
||||||
|
const existing = safeGetLocalStorage(DEVICE_ID_STORAGE_KEY)
|
||||||
|
if (existing) return existing
|
||||||
|
const generated = generatePseudoId()
|
||||||
|
safeSetLocalStorage(DEVICE_ID_STORAGE_KEY, generated)
|
||||||
|
return generated
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop: try node-machine-id; fall back to persisted pseudo ID
|
||||||
|
try {
|
||||||
|
const moduleName = 'node-machine-id'
|
||||||
|
// Use dynamic import via variable to avoid bundlers pulling it into mobile builds
|
||||||
|
const mod: unknown = await import(/* @vite-ignore */ moduleName)
|
||||||
|
if (
|
||||||
|
typeof mod === 'object' && mod !== null &&
|
||||||
|
'machineId' in mod && typeof (mod as Record<string, unknown>).machineId === 'function'
|
||||||
|
) {
|
||||||
|
const id = await (mod as { machineId: () => Promise<string> }).machineId()
|
||||||
|
if (id) return id
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore and fall back
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = safeGetLocalStorage(DEVICE_ID_STORAGE_KEY)
|
||||||
|
if (existing) return existing
|
||||||
|
const generated = generatePseudoId()
|
||||||
|
safeSetLocalStorage(DEVICE_ID_STORAGE_KEY, generated)
|
||||||
|
return generated
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOperatingSystem(): string {
|
||||||
|
if (Platform.isWin) return 'windows'
|
||||||
|
if (Platform.isMacOS) return 'macos'
|
||||||
|
if (Platform.isLinux) return 'linux'
|
||||||
|
if (Platform.isAndroidApp) return 'android'
|
||||||
|
if (Platform.isIosApp) return 'ios'
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
481
styles.css
481
styles.css
@ -73,7 +73,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: calc(var(--size-4-1) * -1);
|
margin-top: calc(var(--size-2-1) * -1);
|
||||||
|
padding: 0 var(--size-2-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-header-title {
|
.infio-chat-header-title {
|
||||||
@ -89,8 +90,8 @@
|
|||||||
.infio-chat-current-workspace {
|
.infio-chat-current-workspace {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: var(--size-4-2);
|
margin-left: var(--size-2-2);
|
||||||
padding: var(--size-2-1) var(--size-4-1);
|
padding: var(--size-2-1) var(--size-2-2);
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
@ -102,11 +103,12 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-header-buttons {
|
.infio-chat-header-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--size-2-1);
|
gap: var(--size-1-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-container {
|
.infio-chat-container {
|
||||||
@ -118,12 +120,21 @@
|
|||||||
.infio-stop-gen-btn {
|
.infio-stop-gen-btn {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 160px;
|
bottom: 120px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--size-4-1);
|
gap: var(--size-2-2);
|
||||||
|
padding: var(--size-2-2) var(--size-4-3);
|
||||||
|
background-color: var(--background-modifier-error);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-l);
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
min-height: 44px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +150,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--size-2-1);
|
gap: var(--size-2-1);
|
||||||
padding: 0 var(--size-4-3) var(--size-4-5) var(--size-4-3);
|
padding: 0 var(--size-2-2) var(--size-4-3) var(--size-2-2);
|
||||||
margin: var(--size-4-2) calc(var(--size-4-3) * -1) 0;
|
margin: var(--size-2-2) calc(var(--size-2-2) * -1) 0;
|
||||||
/* 确保内容不会超出容器 */
|
/* 确保内容不会超出容器 */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
@ -158,7 +169,7 @@
|
|||||||
.infio-chat-messages-assistant {
|
.infio-chat-messages-assistant {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: var(--size-2-2);
|
/* 防止助手消息超出容器 */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@ -170,7 +181,8 @@
|
|||||||
*/
|
*/
|
||||||
.infio-starred-commands {
|
.infio-starred-commands {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
margin-bottom: var(--size-4-2);
|
margin-bottom: var(--size-2-2);
|
||||||
|
padding: 0 var(--size-2-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-starred-commands-title {
|
.infio-starred-commands-title {
|
||||||
@ -193,17 +205,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: var(--size-2-1);
|
||||||
padding: 1px 8px;
|
padding: var(--size-2-1) var(--size-2-3);
|
||||||
background-color: rgba(86, 72, 29, 0.3) !important;
|
background-color: rgba(86, 72, 29, 0.3) !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: var(--radius-l);
|
border-radius: var(--radius-l);
|
||||||
font-size: var(--font-ui-small);
|
font-size: var(--font-ui-medium);
|
||||||
color: #f1c43f;
|
color: #f1c43f;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
max-width: 200px;
|
min-height: 40px;
|
||||||
|
max-width: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -581,16 +594,20 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 26px;
|
width: 36px;
|
||||||
height: 26px;
|
height: 36px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:active {
|
||||||
background-color: var(--background-modifier-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,10 +635,11 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
border: var(--input-border-width) solid var(--background-modifier-border);
|
border: var(--input-border-width) solid var(--background-modifier-border);
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
padding: calc(var(--size-2-2) + 1px);
|
padding: var(--size-2-1);
|
||||||
font-size: var(--font-ui-medium);
|
font-size: var(--font-ui-medium);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
margin: 0 var(--size-2-2) var(--size-2-2) var(--size-2-2);
|
||||||
|
|
||||||
&:focus-within,
|
&:focus-within,
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -635,18 +653,18 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
.infio-chat-user-input-files {
|
.infio-chat-user-input-files {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--size-4-1);
|
gap: var(--size-2-1);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding-bottom: var(--size-4-1);
|
padding-bottom: var(--size-2-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-user-input-controls {
|
.infio-chat-user-input-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--size-4-1);
|
gap: var(--size-2-1);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--size-4-4);
|
height: 36px;
|
||||||
|
|
||||||
.infio-chat-user-input-controls__model-select-container {
|
.infio-chat-user-input-controls__model-select-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -658,7 +676,7 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
.infio-chat-user-input-controls__buttons {
|
.infio-chat-user-input-controls__buttons {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--size-4-3);
|
gap: var(--size-2-1);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,20 +684,23 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
.infio-chat-user-input-controls .infio-chat-user-input-submit-button {
|
.infio-chat-user-input-controls .infio-chat-user-input-submit-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--size-4-1);
|
gap: var(--size-2-1);
|
||||||
font-size: var(--font-smallest);
|
font-size: var(--font-ui-smaller);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 0 var(--size-2-1);
|
padding: var(--size-2-1) var(--size-2-2);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
height: var(--size-4-4);
|
height: 32px;
|
||||||
|
min-width: 36px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s ease-in-out;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:active {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-user-input-submit-button-icons {
|
.infio-chat-user-input-submit-button-icons {
|
||||||
@ -691,20 +712,23 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
.infio-chat-user-input-controls .infio-chat-user-input-vault-button {
|
.infio-chat-user-input-controls .infio-chat-user-input-vault-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--size-4-1);
|
gap: var(--size-2-1);
|
||||||
font-size: var(--font-smallest);
|
font-size: var(--font-ui-smaller);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background-color: var(--background-secondary-alt) !important;
|
background-color: var(--background-secondary-alt) !important;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: 0 var(--size-2-1);
|
padding: var(--size-2-1) var(--size-2-2);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
height: var(--size-4-4);
|
height: 32px;
|
||||||
|
min-width: 36px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s ease-in-out;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:active {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
|
background-color: var(--background-modifier-hover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-user-input-vault-button-icons {
|
.infio-chat-user-input-vault-button-icons {
|
||||||
@ -719,12 +743,13 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
font-size: var(--font-smallest);
|
font-size: var(--font-ui-smaller);
|
||||||
padding: var(--size-2-1) var(--size-4-1);
|
padding: var(--size-2-1) var(--size-2-2);
|
||||||
gap: var(--size-2-1);
|
gap: var(--size-2-1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-height: 28px;
|
||||||
|
|
||||||
&.infio-chat-user-input-file-badge-focused {
|
&.infio-chat-user-input-file-badge-focused {
|
||||||
border: 1px solid var(--interactive-accent);
|
border: 1px solid var(--interactive-accent);
|
||||||
@ -791,23 +816,27 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
|
|||||||
|
|
||||||
.infio-chat-edit-cancel-button {
|
.infio-chat-edit-cancel-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: var(--size-2-1);
|
||||||
right: 8px;
|
right: var(--size-2-1);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--text-on-accent-hover);
|
background-color: transparent !important;
|
||||||
background-color: var(--interactive-accent-hover);
|
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
color: var(--text-muted);
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
width: 24px !important;
|
width: 44px !important;
|
||||||
height: 24px !important;
|
height: 44px !important;
|
||||||
|
border-radius: var(--radius-m) !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
background-color: var(--interactive-accent-hover) !important;
|
&:active {
|
||||||
|
background-color: var(--background-modifier-hover) !important;
|
||||||
|
color: var(--text-normal) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -991,15 +1020,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
gap: var(--size-4-1);
|
gap: var(--size-4-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
代码块样式 - 统一管理所有代码块相关样式
|
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
/* 基础代码块容器 */
|
|
||||||
.infio-chat-code-block {
|
.infio-chat-code-block {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
|
/* 防止代码块内容溢出 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@ -1008,43 +1033,70 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RawMarkdownBlock 专用代码块容器 */
|
/* 删除冲突的样式定义 */
|
||||||
|
|
||||||
|
/* RawMarkdownBlock 代码块容器样式 */
|
||||||
.infio-raw-markdown-code-block {
|
.infio-raw-markdown-code-block {
|
||||||
position: relative;
|
position: relative !important;
|
||||||
margin: 0;
|
z-index: 100 !important; /* 提高z-index优先级 */
|
||||||
padding: 0;
|
margin: 0 !important;
|
||||||
width: 100%;
|
padding: 0 !important;
|
||||||
overflow: visible;
|
/* 确保代码块不会被其他元素遮挡 */
|
||||||
clear: both;
|
clear: both;
|
||||||
box-sizing: border-box;
|
|
||||||
/* 创建新的层叠上下文,避免z-index冲突 */
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RawMarkdownBlock 内的 pre 元素 */
|
|
||||||
.infio-raw-markdown-code-block pre {
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
/* 添加背景色以确保可见性 */
|
||||||
|
background: transparent;
|
||||||
/* RawMarkdownBlock 内的 code 元素 */
|
/* 确保不被截断 */
|
||||||
.infio-raw-markdown-code-block code {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保代码块内容正确显示 */
|
||||||
|
.infio-raw-markdown-code-block pre {
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 101 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
/* 确保完整显示 */
|
||||||
|
width: 100% !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-raw-markdown-code-block code {
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 102 !important;
|
||||||
|
/* 确保代码内容完整显示 */
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-reasoning-content-wrapper {
|
||||||
|
/* Height restrictions removed to show full content */
|
||||||
|
/* 防止推理内容溢出 */
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠/展开功能相关样式 */
|
||||||
|
.infio-reasoning-content-wrapper.collapsed {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: max-height 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-reasoning-content-wrapper.expanded {
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
transition: max-height 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 代码块头部样式 */
|
|
||||||
.infio-chat-code-block-header {
|
.infio-chat-code-block-header {
|
||||||
display: none;
|
display: none;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: var(--font-smallest);
|
font-size: var(--font-smallest);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-code-block:hover .infio-chat-code-block-header {
|
.infio-chat-code-block:hover .infio-chat-code-block-header {
|
||||||
@ -1068,7 +1120,6 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 代码块头部文件名样式 */
|
|
||||||
.infio-chat-code-block-header-filename {
|
.infio-chat-code-block-header-filename {
|
||||||
padding-left: var(--size-4-2);
|
padding-left: var(--size-4-2);
|
||||||
font-size: var(--font-medium);
|
font-size: var(--font-medium);
|
||||||
@ -1085,7 +1136,6 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 代码块头部按钮样式 */
|
|
||||||
.infio-chat-code-block-header-button {
|
.infio-chat-code-block-header-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--size-4-1);
|
gap: var(--size-4-1);
|
||||||
@ -1105,7 +1155,9 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
padding-right: var(--size-4-1);
|
padding-right: var(--size-4-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-code-block.has-filename .infio-chat-code-block-header-button button {
|
.infio-chat-code-block.has-filename
|
||||||
|
.infio-chat-code-block-header-button
|
||||||
|
button {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0 var(--size-4-2);
|
padding: 0 var(--size-4-2);
|
||||||
@ -1114,10 +1166,10 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
font-size: var(--font-medium);
|
font-size: var(--font-medium);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.infio-chat-code-block.has-filename .infio-chat-code-block-header-button button:hover {
|
&:hover {
|
||||||
background-color: var(--background-modifier-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-code-block-header-button button {
|
.infio-chat-code-block-header-button button {
|
||||||
@ -1126,36 +1178,12 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
font-size: var(--font-ui-smaller);
|
font-size: var(--font-ui-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 代码块内容样式 */
|
|
||||||
.infio-chat-code-block-content {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 特殊按钮样式 */
|
|
||||||
.infio-dataview-query-button {
|
.infio-dataview-query-button {
|
||||||
color: #008000;
|
color: #008000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
.infio-chat-code-block-content {
|
||||||
推理内容包装器样式
|
margin: 0;
|
||||||
=========================================== */
|
|
||||||
|
|
||||||
.infio-reasoning-content-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 折叠/展开功能相关样式 */
|
|
||||||
.infio-reasoning-content-wrapper.collapsed {
|
|
||||||
max-height: 150px;
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: max-height 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-reasoning-content-wrapper.expanded {
|
|
||||||
max-height: none;
|
|
||||||
overflow-y: visible;
|
|
||||||
transition: max-height 0.3s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Read File Block - Minimal styling for better integration */
|
/* Read File Block - Minimal styling for better integration */
|
||||||
@ -1536,11 +1564,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.infio-chat-lexical-content-editable-root {
|
.infio-chat-lexical-content-editable-root {
|
||||||
min-height: 62px;
|
min-height: 36px;
|
||||||
max-height: 800px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding: var(--size-2-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-lexical-content-editable-root .mention {
|
.infio-chat-lexical-content-editable-root .mention {
|
||||||
@ -1556,10 +1584,11 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.infio-search-lexical-content-editable-root {
|
.infio-search-lexical-content-editable-root {
|
||||||
min-height: 36px;
|
min-height: 40px;
|
||||||
max-height: 120px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding: var(--size-2-1);
|
||||||
/* 确保编辑器不会影响外部滚动 */
|
/* 确保编辑器不会影响外部滚动 */
|
||||||
contain: size style;
|
contain: size style;
|
||||||
}
|
}
|
||||||
@ -1713,22 +1742,27 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
gap: var(--size-2-1);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 20px;
|
height: 44px;
|
||||||
width: 20px;
|
width: 44px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:active {
|
||||||
background-color: var(--background-modifier-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2281,6 +2315,49 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
|||||||
width: 70%;
|
width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端专用优化 */
|
||||||
|
|
||||||
|
/* 增加触摸目标的最小尺寸 */
|
||||||
|
button, .clickable-icon, [role="button"] {
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框按钮使用较小的尺寸以节省空间 */
|
||||||
|
.infio-chat-user-input-controls button,
|
||||||
|
.infio-chat-user-input-controls .clickable-icon {
|
||||||
|
min-height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天头部按钮保持36px尺寸 */
|
||||||
|
.infio-chat-header-buttons button {
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化滚动性能 */
|
||||||
|
.infio-chat-messages,
|
||||||
|
.infio-chat-lexical-content-editable-root,
|
||||||
|
.infio-search-lexical-content-editable-root {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化文本选择 */
|
||||||
|
.infio-chat-messages {
|
||||||
|
-webkit-user-select: text;
|
||||||
|
-moz-user-select: text;
|
||||||
|
-ms-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 防止双击缩放 */
|
||||||
|
button, .clickable-icon {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
/* control pc and mobile view */
|
/* control pc and mobile view */
|
||||||
|
|
||||||
.desktop-only {
|
.desktop-only {
|
||||||
@ -2686,20 +2763,21 @@ button.infio-chat-input-model-select {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 1;
|
border: 1;
|
||||||
padding: var(--size-2-1) var(--size-2-2);
|
padding: var(--size-2-1) var(--size-2-2);
|
||||||
font-size: var(--font-smallest);
|
font-size: var(--font-ui-smaller);
|
||||||
font-weight: var(--font-medium);
|
font-weight: var(--font-medium);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: var(--size-4-4);
|
height: 32px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
gap: var(--size-2-2);
|
gap: var(--size-2-1);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
transition: all 0.15s ease-in-out;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:active {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-modifier-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
}
|
}
|
||||||
@ -2799,6 +2877,7 @@ button.infio-chat-input-model-select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: var(--size-4-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-loading-container {
|
.infio-chat-loading-container {
|
||||||
@ -2998,8 +3077,10 @@ button.infio-chat-input-model-select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.infio-commands-header-title {
|
.infio-commands-header-title {
|
||||||
margin: 0;
|
color: var(--text-normal);
|
||||||
font-size: 24px;
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 var(--size-4-3) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-commands-label {
|
.infio-commands-label {
|
||||||
@ -3655,18 +3736,18 @@ button.infio-chat-input-model-select {
|
|||||||
|
|
||||||
.infio-markdown-actions {
|
.infio-markdown-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1px;
|
top: 8px;
|
||||||
right: 1px;
|
right: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
z-index: 10; /* 降低z-index,确保不会遮挡代码块 */
|
z-index: 50; /* 降低z-index,确保不会遮挡代码块 */
|
||||||
/* 确保不会遮挡代码块 */
|
/* 确保不会遮挡代码块 */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-markdown-actions button {
|
.infio-markdown-actions button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -3677,19 +3758,18 @@ button.infio-chat-input-model-select {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-faint);
|
||||||
border-radius: var(--radius-s);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* 恢复按钮的点击事件 */
|
/* 恢复按钮的点击事件 */
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.infio-markdown-actions button:hover {
|
||||||
background-color: var(--interactive-accent-hover);
|
background-color: var(--background-modifier-hover);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.infio-chat-message-actions-icon--copied {
|
.infio-markdown-actions .infio-chat-message-actions-icon--copied {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-markdown-container-with-actions:hover .infio-markdown-actions {
|
.infio-markdown-container-with-actions:hover .infio-markdown-actions {
|
||||||
@ -3957,136 +4037,3 @@ button.infio-chat-input-model-select {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Structured Progress Styles */
|
|
||||||
.infio-structured-progress {
|
|
||||||
padding: 16px;
|
|
||||||
background-color: var(--background-secondary);
|
|
||||||
border: 1px solid var(--background-modifier-border);
|
|
||||||
border-radius: var(--radius-m);
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-steps {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
position: relative;
|
|
||||||
min-height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-indicator {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 16px;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-line {
|
|
||||||
width: 2px;
|
|
||||||
height: 40px;
|
|
||||||
background-color: var(--background-modifier-border);
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-line.completed {
|
|
||||||
background-color: var(--color-green, #28a745);
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-title {
|
|
||||||
font-size: var(--font-ui-medium);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-normal);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step.running .infio-progress-step-title {
|
|
||||||
color: var(--interactive-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-counter {
|
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
background-color: var(--background-modifier-border);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-description {
|
|
||||||
font-size: var(--font-ui-small);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 4px 0 0 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-details {
|
|
||||||
font-size: var(--font-ui-smaller);
|
|
||||||
color: var(--text-faint);
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
line-height: 1.3;
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 4px;
|
|
||||||
background-color: var(--background-modifier-border);
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-top: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infio-progress-step-bar-fill {
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--interactive-accent);
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tool result content display */
|
|
||||||
.infio-tool-result-content {
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 0;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: var(--background-primary);
|
|
||||||
border: 1px solid var(--background-modifier-border);
|
|
||||||
border-radius: var(--radius-s);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user