Move to ts from js because of problems with import, and make this based on sample plugin
This commit is contained in:
parent
7b8657a56d
commit
bcd152e81f
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
26
.prettierrc
Normal file
26
.prettierrc
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.js", "*.ts"],
|
||||
"options": {
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"bracketSameLine": false,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "always",
|
||||
"printWidth": 140
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.css", "*.scss"],
|
||||
"options": {
|
||||
"tabWidth": 1,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
251
AGENTS.md
Normal file
251
AGENTS.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# Obsidian community plugin
|
||||
|
||||
## Project overview
|
||||
|
||||
- Target: Obsidian Community Plugin (TypeScript → bundled JavaScript).
|
||||
- Entry point: `main.ts` compiled to `main.js` and loaded by Obsidian.
|
||||
- Required release artifacts: `main.js`, `manifest.json`, and optional `styles.css`.
|
||||
|
||||
## Environment & tooling
|
||||
|
||||
- Node.js: use current LTS (Node 18+ recommended).
|
||||
- **Package manager: npm** (required for this sample - `package.json` defines npm scripts and dependencies).
|
||||
- **Bundler: esbuild** (required for this sample - `esbuild.config.mjs` and build scripts depend on it). Alternative bundlers like Rollup or webpack are acceptable for other projects if they bundle all external dependencies into `main.js`.
|
||||
- Types: `obsidian` type definitions.
|
||||
|
||||
**Note**: This sample project has specific technical dependencies on npm and esbuild. If you're creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Dev (watch)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
- To use eslint install eslint from terminal: `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command: `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: `eslint ./src/`
|
||||
|
||||
## File & folder conventions
|
||||
|
||||
- **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts`.
|
||||
- Source lives in `src/`. Keep `main.ts` small and focused on plugin lifecycle (loading, unloading, registering commands).
|
||||
- **Example file structure**:
|
||||
```
|
||||
src/
|
||||
main.ts # Plugin entry point, lifecycle management
|
||||
settings.ts # Settings interface and defaults
|
||||
commands/ # Command implementations
|
||||
command1.ts
|
||||
command2.ts
|
||||
ui/ # UI components, modals, views
|
||||
modal.ts
|
||||
view.ts
|
||||
utils/ # Utility functions, helpers
|
||||
helpers.ts
|
||||
constants.ts
|
||||
types.ts # TypeScript interfaces and types
|
||||
```
|
||||
- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files to version control.
|
||||
- Keep the plugin small. Avoid large dependencies. Prefer browser-compatible packages.
|
||||
- Generated output should be placed at the plugin root or `dist/` depending on your build setup. Release artifacts must end up at the top level of the plugin folder in the vault (`main.js`, `manifest.json`, `styles.css`).
|
||||
|
||||
## Manifest rules (`manifest.json`)
|
||||
|
||||
- Must include (non-exhaustive):
|
||||
- `id` (plugin ID; for local dev it should match the folder name)
|
||||
- `name`
|
||||
- `version` (Semantic Versioning `x.y.z`)
|
||||
- `minAppVersion`
|
||||
- `description`
|
||||
- `isDesktopOnly` (boolean)
|
||||
- Optional: `author`, `authorUrl`, `fundingUrl` (string or map)
|
||||
- Never change `id` after release. Treat it as stable API.
|
||||
- Keep `minAppVersion` accurate when using newer APIs.
|
||||
- Canonical requirements are coded here: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
|
||||
|
||||
## Testing
|
||||
|
||||
- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` (if any) to:
|
||||
```
|
||||
<Vault>/.obsidian/plugins/<plugin-id>/
|
||||
```
|
||||
- Reload Obsidian and enable the plugin in **Settings → Community plugins**.
|
||||
|
||||
## Commands & settings
|
||||
|
||||
- Any user-facing commands should be added via `this.addCommand(...)`.
|
||||
- If the plugin has configuration, provide a settings tab and sensible defaults.
|
||||
- Persist settings using `this.loadData()` / `this.saveData()`.
|
||||
- Use stable command IDs; avoid renaming once released.
|
||||
|
||||
## Versioning & releases
|
||||
|
||||
- Bump `version` in `manifest.json` (SemVer) and update `versions.json` to map plugin version → minimum app version.
|
||||
- Create a GitHub release whose tag exactly matches `manifest.json`'s `version`. Do not use a leading `v`.
|
||||
- Attach `manifest.json`, `main.js`, and `styles.css` (if present) to the release as individual assets.
|
||||
- After the initial release, follow the process to add/update your plugin in the community catalog as required.
|
||||
|
||||
## Security, privacy, and compliance
|
||||
|
||||
Follow Obsidian's **Developer Policies** and **Plugin Guidelines**. In particular:
|
||||
|
||||
- Default to local/offline operation. Only make network requests when essential to the feature.
|
||||
- No hidden telemetry. If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings.
|
||||
- Never execute remote code, fetch and eval scripts, or auto-update plugin code outside of normal releases.
|
||||
- Minimize scope: read/write only what's necessary inside the vault. Do not access files outside the vault.
|
||||
- Clearly disclose any external services used, data sent, and risks.
|
||||
- Respect user privacy. Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented.
|
||||
- Avoid deceptive patterns, ads, or spammy notifications.
|
||||
- Register and clean up all DOM, app, and interval listeners using the provided `register*` helpers so the plugin unloads safely.
|
||||
|
||||
## UX & copy guidelines (for UI text, commands, settings)
|
||||
|
||||
- Prefer sentence case for headings, buttons, and titles.
|
||||
- Use clear, action-oriented imperatives in step-by-step copy.
|
||||
- Use **bold** to indicate literal UI labels. Prefer "select" for interactions.
|
||||
- Use arrow notation for navigation: **Settings → Community plugins**.
|
||||
- Keep in-app strings short, consistent, and free of jargon.
|
||||
|
||||
## Performance
|
||||
|
||||
- Keep startup light. Defer heavy work until needed.
|
||||
- Avoid long-running tasks during `onload`; use lazy initialization.
|
||||
- Batch disk access and avoid excessive vault scans.
|
||||
- Debounce/throttle expensive operations in response to file system events.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
- TypeScript with `"strict": true` preferred.
|
||||
- **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls). Delegate all feature logic to separate modules.
|
||||
- **Split large files**: If any file exceeds ~200-300 lines, consider breaking it into smaller, focused modules.
|
||||
- **Use clear module boundaries**: Each file should have a single, well-defined responsibility.
|
||||
- Bundle everything into `main.js` (no unbundled runtime deps).
|
||||
- Avoid Node/Electron APIs if you want mobile compatibility; set `isDesktopOnly` accordingly.
|
||||
- Prefer `async/await` over promise chains; handle errors gracefully.
|
||||
|
||||
## Mobile
|
||||
|
||||
- Where feasible, test on iOS and Android.
|
||||
- Don't assume desktop-only behavior unless `isDesktopOnly` is `true`.
|
||||
- Avoid large in-memory structures; be mindful of memory and storage constraints.
|
||||
|
||||
## Agent do/don't
|
||||
|
||||
**Do**
|
||||
- Add commands with stable IDs (don't rename once released).
|
||||
- Provide defaults and validation in settings.
|
||||
- Write idempotent code paths so reload/unload doesn't leak listeners or intervals.
|
||||
- Use `this.register*` helpers for everything that needs cleanup.
|
||||
|
||||
**Don't**
|
||||
- Introduce network calls without an obvious user-facing reason and documentation.
|
||||
- Ship features that require cloud services without clear disclosure and explicit opt-in.
|
||||
- Store or transmit vault contents unless essential and consented.
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Organize code across multiple files
|
||||
|
||||
**main.ts** (minimal, lifecycle only):
|
||||
```ts
|
||||
import { Plugin } from "obsidian";
|
||||
import { MySettings, DEFAULT_SETTINGS } from "./settings";
|
||||
import { registerCommands } from "./commands";
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MySettings;
|
||||
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
registerCommands(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**settings.ts**:
|
||||
```ts
|
||||
export interface MySettings {
|
||||
enabled: boolean;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MySettings = {
|
||||
enabled: true,
|
||||
apiKey: "",
|
||||
};
|
||||
```
|
||||
|
||||
**commands/index.ts**:
|
||||
```ts
|
||||
import { Plugin } from "obsidian";
|
||||
import { doSomething } from "./my-command";
|
||||
|
||||
export function registerCommands(plugin: Plugin) {
|
||||
plugin.addCommand({
|
||||
id: "do-something",
|
||||
name: "Do something",
|
||||
callback: () => doSomething(plugin),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Add a command
|
||||
|
||||
```ts
|
||||
this.addCommand({
|
||||
id: "your-command-id",
|
||||
name: "Do the thing",
|
||||
callback: () => this.doTheThing(),
|
||||
});
|
||||
```
|
||||
|
||||
### Persist settings
|
||||
|
||||
```ts
|
||||
interface MySettings { enabled: boolean }
|
||||
const DEFAULT_SETTINGS: MySettings = { enabled: true };
|
||||
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
```
|
||||
|
||||
### Register listeners safely
|
||||
|
||||
```ts
|
||||
this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
|
||||
this.registerDomEvent(window, "resize", () => { /* ... */ });
|
||||
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Plugin doesn't load after build: ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `<Vault>/.obsidian/plugins/<plugin-id>/`.
|
||||
- Build issues: if `main.js` is missing, run `npm run build` or `npm run dev` to compile your TypeScript source code.
|
||||
- Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique.
|
||||
- Settings not persisting: ensure `loadData`/`saveData` are awaited and you re-render the UI after changes.
|
||||
- Mobile-only issues: confirm you're not using desktop-only APIs; check `isDesktopOnly` and adjust.
|
||||
|
||||
## References
|
||||
|
||||
- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
|
||||
- API documentation: https://docs.obsidian.md
|
||||
- Developer policies: https://docs.obsidian.md/Developer+policies
|
||||
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
|
||||
- Style guide: https://help.obsidian.md/style-guide
|
||||
5
LICENSE
Normal file
5
LICENSE
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Copyright (C) 2020-2025 by Dynalist Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
94
README-Basic.md
Normal file
94
README-Basic.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Obsidian Sample Plugin
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
|
||||
## First time developing plugins?
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
|
||||
## Releasing new releases
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
|
||||
## How to use
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
|
||||
## Manually installing the plugin
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
|
||||
## Improve code quality with eslint (optional)
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- To use eslint with this project, make sure to install eslint from terminal:
|
||||
- `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command:
|
||||
- `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||
- `eslint ./src/`
|
||||
|
||||
## Funding URL
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
140
main.ts
Normal file
140
main.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { EventBus } from './src/utils/EventBus';
|
||||
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
|
||||
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
|
||||
|
||||
// ----------------------------------------------- Settings ---------------------------------------------------------------
|
||||
|
||||
interface LLMAgentSettings {
|
||||
apiBaseUrl: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||
apiBaseUrl: 'http://localhost:5000/api'
|
||||
};
|
||||
|
||||
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
|
||||
|
||||
export default class LLMAgentPlugin extends Plugin {
|
||||
settings: LLMAgentSettings;
|
||||
eventBus: EventBus;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
this.eventBus = new EventBus();
|
||||
|
||||
// Регистрируем кастомные views
|
||||
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
|
||||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
|
||||
|
||||
// Добавляем команды
|
||||
this.addCommand({
|
||||
id: 'open-llm-agent-graph',
|
||||
name: 'Открыть граф диалогов LLM Agent',
|
||||
callback: () => this.activateGraphView()
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'open-llm-agent-chat',
|
||||
name: 'Открыть чат LLM Agent',
|
||||
callback: () => this.activateChatView()
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'new-llm-agent-chat',
|
||||
name: 'Новый чат LLM Agent',
|
||||
callback: () => this.handleNewChat()
|
||||
});
|
||||
|
||||
// Добавляем иконку в ribbon
|
||||
this.addRibbonIcon('brain-circuit', 'LLM Agent', () => {
|
||||
this.activateGraphView();
|
||||
this.activateChatView();
|
||||
});
|
||||
|
||||
console.log('LLM Agent plugin загружен');
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log('LLM Agent plugin выгружен');
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Settings Management ---------------------------------------------------------------
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
// ----------------------------------------------- View Management ---------------------------------------------------------------
|
||||
|
||||
async activateGraphView() {
|
||||
const existingLeaf = this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE)[0];
|
||||
if (existingLeaf) {
|
||||
this.app.workspace.revealLeaf(existingLeaf);
|
||||
} else {
|
||||
await this.app.workspace.getLeaf('tab').setViewState({
|
||||
type: GRAPH_VIEW_TYPE,
|
||||
active: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async activateChatView() {
|
||||
const existingLeaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0];
|
||||
if (existingLeaf) {
|
||||
this.app.workspace.revealLeaf(existingLeaf);
|
||||
} else {
|
||||
const rightLeaf = this.app.workspace.getRightLeaf(false);
|
||||
if (rightLeaf) {
|
||||
await rightLeaf.setViewState({
|
||||
type: CHAT_VIEW_TYPE,
|
||||
active: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||
|
||||
handleNewChat() {
|
||||
this.eventBus.emit('new-chat');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Sample Setting Tab ---------------------------------------------------------------
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: LLMAgentPlugin;
|
||||
|
||||
constructor(app: App, plugin: LLMAgentPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('API Base URL')
|
||||
.setDesc('Базовый URL для API LLM Agent')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('Введите URL')
|
||||
.setValue(this.plugin.settings.apiBaseUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.apiBaseUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
134
mainBasic.ts
Normal file
134
mainBasic.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
|
||||
// Remember to rename these classes and interfaces!
|
||||
|
||||
interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default'
|
||||
}
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('This is a notice!');
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-complex',
|
||||
name: 'Open sample modal (complex)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply "checking" if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.setText('Woah!');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc('It\'s a secret')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"id": "llm-agent-plugin",
|
||||
"name": "LLM Agent",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "LLM Agent with graph visualization and chat interface",
|
||||
"author": "Your Name",
|
||||
"authorUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
"id": "llm-agent-plugin",
|
||||
"name": "LLM Agent",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "LLM Agent with graph visualization and chat interface",
|
||||
"author": "tylerfg",
|
||||
"authorUrl": "",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
2235
package-lock.json
generated
Normal file
2235
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
class ChatPanel {
|
||||
constructor(container, props) {
|
||||
this.container = container;
|
||||
this.props = props;
|
||||
this.chatHistory = props.chatHistory || [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container.innerHTML = `
|
||||
<div class="llm-agent-chat-panel">
|
||||
<div class="chat-history" id="chat-history"></div>
|
||||
<div class="chat-input-form">
|
||||
<div class="command-input">
|
||||
<input type="text" id="message-input" placeholder="Введите сообщение или команду (/)..." />
|
||||
<div class="suggestions" id="suggestions" style="display: none;"></div>
|
||||
</div>
|
||||
<button type="button" id="send-button">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.messageInput = this.container.querySelector('#message-input');
|
||||
this.sendButton = this.container.querySelector('#send-button');
|
||||
this.suggestionsContainer = this.container.querySelector('#suggestions');
|
||||
|
||||
this.setupEventListeners();
|
||||
this.renderHistory();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.sendButton.addEventListener('click', () => this.handleSend());
|
||||
|
||||
this.messageInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (this.suggestionsContainer.style.display === 'block') {
|
||||
const firstSuggestion = this.suggestionsContainer.querySelector('.suggestion-item');
|
||||
if (firstSuggestion) {
|
||||
this.messageInput.value = firstSuggestion.textContent;
|
||||
this.hideSuggestions();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.handleSend();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.messageInput.addEventListener('input', (e) => {
|
||||
this.handleInput(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
handleInput(value) {
|
||||
const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat'];
|
||||
|
||||
if (value.startsWith('/')) {
|
||||
const filteredCommands = commands.filter(cmd =>
|
||||
cmd.startsWith(value.toLowerCase())
|
||||
);
|
||||
this.showSuggestions(filteredCommands);
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
showSuggestions(suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestionsContainer.innerHTML = suggestions
|
||||
.map(suggestion => `<div class="suggestion-item">${suggestion}</div>`)
|
||||
.join('');
|
||||
|
||||
this.suggestionsContainer.style.display = 'block';
|
||||
|
||||
// Добавляем обработчики кликов на предложения
|
||||
this.suggestionsContainer.querySelectorAll('.suggestion-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
this.messageInput.value = item.textContent;
|
||||
this.hideSuggestions();
|
||||
this.messageInput.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hideSuggestions() {
|
||||
this.suggestionsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
handleSend() {
|
||||
const message = this.messageInput.value.trim();
|
||||
if (message && this.props.onSendMessage) {
|
||||
this.props.onSendMessage(message);
|
||||
this.messageInput.value = '';
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
renderHistory() {
|
||||
const historyContainer = this.container.querySelector('#chat-history');
|
||||
historyContainer.innerHTML = this.chatHistory
|
||||
.map(msg => `
|
||||
<div class="message ${msg.role}">
|
||||
<div class="message-content">${this.processMessageContent(msg.content)}</div>
|
||||
</div>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
// Прокручиваем к концу
|
||||
historyContainer.scrollTop = historyContainer.scrollHeight;
|
||||
}
|
||||
|
||||
processMessageContent(content) {
|
||||
// Простейшая обработка Markdown для изображений
|
||||
return content
|
||||
.replace(/<img([^>]*?)>/g, '<img$1 style="max-width: 100%; height: auto;">')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
updateHistory(newHistory) {
|
||||
this.chatHistory = newHistory;
|
||||
this.renderHistory();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ChatPanel };
|
||||
147
src/components/ChatPanel.ts
Normal file
147
src/components/ChatPanel.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { ChatHistoryItem } from './models/ChatHistoryItem';
|
||||
|
||||
export class ChatPanel {
|
||||
container: HTMLElement;
|
||||
props: {
|
||||
chatHistory: ChatHistoryItem[];
|
||||
onSendMessage: (message: string) => void;
|
||||
};
|
||||
chatHistory: ChatHistoryItem[];
|
||||
messageInput: HTMLInputElement;
|
||||
sendButton: HTMLButtonElement;
|
||||
suggestionsContainer: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
props: {
|
||||
chatHistory: ChatHistoryItem[];
|
||||
onSendMessage: (message: string) => void;
|
||||
}
|
||||
) {
|
||||
this.container = container;
|
||||
this.props = props;
|
||||
this.chatHistory = props.chatHistory || [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.container.innerHTML = `
|
||||
<div class="llm-agent-chat-panel">
|
||||
<div class="chat-history" id="chat-history"></div>
|
||||
<div class="chat-input-form">
|
||||
<div class="command-input">
|
||||
<input type="text" id="message-input" placeholder="Введите сообщение или команду (/)..." />
|
||||
<div class="suggestions" id="suggestions" style="display: none;"></div>
|
||||
</div>
|
||||
<button type="button" id="send-button">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.messageInput = this.container.querySelector('#message-input') as HTMLInputElement;
|
||||
this.sendButton = this.container.querySelector('#send-button') as HTMLButtonElement;
|
||||
this.suggestionsContainer = this.container.querySelector('#suggestions') as HTMLDivElement;
|
||||
|
||||
this.setupEventListeners();
|
||||
this.renderHistory();
|
||||
}
|
||||
|
||||
setupEventListeners(): void {
|
||||
this.sendButton.addEventListener('click', () => this.handleSend());
|
||||
|
||||
this.messageInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (this.suggestionsContainer.style.display === 'block') {
|
||||
const firstSuggestion = this.suggestionsContainer.querySelector('.suggestion-item');
|
||||
if (firstSuggestion) {
|
||||
this.messageInput.value = firstSuggestion.textContent || '';
|
||||
this.hideSuggestions();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.handleSend();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.messageInput.addEventListener('input', (e) => {
|
||||
this.handleInput((e.target as HTMLInputElement).value);
|
||||
});
|
||||
}
|
||||
|
||||
handleInput(value: string): void {
|
||||
const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat'];
|
||||
|
||||
if (value.startsWith('/')) {
|
||||
const filteredCommands = commands.filter((cmd) => cmd.startsWith(value.toLowerCase()));
|
||||
this.showSuggestions(filteredCommands);
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
showSuggestions(suggestions: string[]): void {
|
||||
if (suggestions.length === 0) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestionsContainer.innerHTML = suggestions.map((suggestion) => `<div class="suggestion-item">${suggestion}</div>`).join('');
|
||||
|
||||
this.suggestionsContainer.style.display = 'block';
|
||||
|
||||
// Добавляем обработчики кликов на предложения
|
||||
this.suggestionsContainer.querySelectorAll('.suggestion-item').forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
this.messageInput.value = item.textContent || '';
|
||||
this.hideSuggestions();
|
||||
this.messageInput.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hideSuggestions(): void {
|
||||
this.suggestionsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
handleSend(): void {
|
||||
const message = this.messageInput.value.trim();
|
||||
if (message && this.props.onSendMessage) {
|
||||
this.props.onSendMessage(message);
|
||||
this.messageInput.value = '';
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
renderHistory(): void {
|
||||
const historyContainer = this.container.querySelector('#chat-history') as HTMLElement;
|
||||
historyContainer.innerHTML = this.chatHistory
|
||||
.map(
|
||||
(msg) => `
|
||||
<div class="message ${msg.role}">
|
||||
<div class="message-content">${this.processMessageContent(msg.content)}</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
// Прокручиваем к концу
|
||||
historyContainer.scrollTop = historyContainer.scrollHeight;
|
||||
}
|
||||
|
||||
processMessageContent(content: string): string {
|
||||
// Простейшая обработка Markdown для изображений
|
||||
return content.replace(/<img([^>]*?)>/g, '<img$1 style="max-width: 100%; height: auto;">').replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
updateHistory(newHistory: ChatHistoryItem[]): void {
|
||||
this.chatHistory = newHistory;
|
||||
this.renderHistory();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
// Адаптация оригинального GraphPanel для работы в Obsidian
|
||||
|
||||
class GraphPanel {
|
||||
constructor(container, props) {
|
||||
this.container = container;
|
||||
this.props = props;
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Создаем структуру для React Flow
|
||||
this.container.innerHTML = `
|
||||
<div class="llm-agent-graph-panel">
|
||||
<div id="reactflow-container" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Загружаем React Flow динамически (если доступен)
|
||||
this.initReactFlow();
|
||||
}
|
||||
|
||||
async initReactFlow() {
|
||||
try {
|
||||
// В среде Obsidian нужно будет подключить React Flow через CDN или bundler
|
||||
// Для демонстрации создаем упрощенную визуализацию
|
||||
this.createSimpleGraph();
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации React Flow:', error);
|
||||
this.createFallbackGraph();
|
||||
}
|
||||
}
|
||||
|
||||
createSimpleGraph() {
|
||||
const container = this.container.querySelector('#reactflow-container');
|
||||
container.innerHTML = `
|
||||
<div class="simple-graph">
|
||||
<div class="graph-nodes" id="graph-nodes"></div>
|
||||
<svg class="graph-edges" id="graph-edges"></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.renderNodes();
|
||||
this.renderEdges();
|
||||
}
|
||||
|
||||
renderNodes() {
|
||||
const nodesContainer = this.container.querySelector('#graph-nodes');
|
||||
nodesContainer.innerHTML = '';
|
||||
|
||||
this.nodes.forEach((node, index) => {
|
||||
const nodeEl = document.createElement('div');
|
||||
nodeEl.className = `graph-node node-${node.type || 'default'}`;
|
||||
nodeEl.style.cssText = `
|
||||
position: absolute;
|
||||
left: ${50 + index * 150}px;
|
||||
top: ${50 + Math.floor(index / 5) * 80}px;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
max-width: 120px;
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
nodeEl.textContent = node.data?.label || `Node ${node.id}`;
|
||||
nodeEl.dataset.nodeId = node.id;
|
||||
|
||||
nodeEl.addEventListener('click', () => {
|
||||
this.handleNodeClick(node.id);
|
||||
});
|
||||
|
||||
nodesContainer.appendChild(nodeEl);
|
||||
});
|
||||
}
|
||||
|
||||
renderEdges() {
|
||||
const edgesContainer = this.container.querySelector('#graph-edges');
|
||||
edgesContainer.innerHTML = '';
|
||||
// Упрощенная отрисовка связей - в реальной реализации нужно будет вычислять позиции
|
||||
}
|
||||
|
||||
createFallbackGraph() {
|
||||
const container = this.container.querySelector('#reactflow-container');
|
||||
container.innerHTML = `
|
||||
<div class="fallback-graph">
|
||||
<p>Упрощенное представление графа</p>
|
||||
<div id="nodes-list"></div>
|
||||
</div>
|
||||
`;
|
||||
this.renderFallbackNodes();
|
||||
}
|
||||
|
||||
renderFallbackNodes() {
|
||||
const nodesList = this.container.querySelector('#nodes-list');
|
||||
nodesList.innerHTML = '';
|
||||
|
||||
this.nodes.forEach(node => {
|
||||
const nodeEl = document.createElement('div');
|
||||
nodeEl.className = 'fallback-node';
|
||||
nodeEl.textContent = `${node.id}: ${node.data?.label || 'No label'}`;
|
||||
nodeEl.addEventListener('click', () => this.handleNodeClick(node.id));
|
||||
nodesList.appendChild(nodeEl);
|
||||
});
|
||||
}
|
||||
|
||||
updateData(graphData) {
|
||||
this.nodes = graphData.nodes || [];
|
||||
this.edges = graphData.edges || [];
|
||||
this.renderNodes();
|
||||
this.renderEdges();
|
||||
}
|
||||
|
||||
handleNodeClick(nodeId) {
|
||||
if (this.props.onNodeClick) {
|
||||
this.props.onNodeClick(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Очистка ресурсов
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GraphPanel };
|
||||
138
src/components/GraphPanel.ts
Normal file
138
src/components/GraphPanel.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
export class GraphPanel {
|
||||
container: HTMLElement;
|
||||
props: {
|
||||
graphData: { nodes: any[]; edges: any[] };
|
||||
onNodeClick: (nodeId: string) => void;
|
||||
};
|
||||
nodes: any[];
|
||||
edges: any[];
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
props: {
|
||||
graphData: { nodes: any[]; edges: any[] };
|
||||
onNodeClick: (nodeId: string) => void;
|
||||
}
|
||||
) {
|
||||
this.container = container;
|
||||
this.props = props;
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init(): void {
|
||||
// Создаем структуру для React Flow
|
||||
this.container.innerHTML = `
|
||||
<div class="llm-agent-graph-panel">
|
||||
<div id="reactflow-container" style="width: 100%; height: 100%;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Загружаем React Flow динамически (если доступен)
|
||||
this.initReactFlow();
|
||||
}
|
||||
|
||||
async initReactFlow(): Promise<void> {
|
||||
try {
|
||||
// В среде Obsidian нужно будет подключить React Flow через CDN или bundler
|
||||
// Для демонстрации создаем упрощенную визуализацию
|
||||
this.createSimpleGraph();
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации React Flow:', error);
|
||||
this.createFallbackGraph();
|
||||
}
|
||||
}
|
||||
|
||||
createSimpleGraph(): void {
|
||||
const container = this.container.querySelector('#reactflow-container') as HTMLElement;
|
||||
container.innerHTML = `
|
||||
<div class="simple-graph">
|
||||
<div class="graph-nodes" id="graph-nodes"></div>
|
||||
<svg class="graph-edges" id="graph-edges"></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.renderNodes();
|
||||
this.renderEdges();
|
||||
}
|
||||
|
||||
renderNodes(): void {
|
||||
const nodesContainer = this.container.querySelector('#graph-nodes') as HTMLElement;
|
||||
nodesContainer.innerHTML = '';
|
||||
|
||||
this.nodes.forEach((node, index) => {
|
||||
const nodeEl = document.createElement('div');
|
||||
nodeEl.className = `graph-node node-${node.type || 'default'}`;
|
||||
nodeEl.style.cssText = `
|
||||
position: absolute;
|
||||
left: ${50 + index * 150}px;
|
||||
top: ${50 + Math.floor(index / 5) * 80}px;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
max-width: 120px;
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
nodeEl.textContent = node.data?.label || `Node ${node.id}`;
|
||||
nodeEl.dataset.nodeId = node.id;
|
||||
|
||||
nodeEl.addEventListener('click', () => {
|
||||
this.handleNodeClick(node.id);
|
||||
});
|
||||
|
||||
nodesContainer.appendChild(nodeEl);
|
||||
});
|
||||
}
|
||||
|
||||
renderEdges(): void {
|
||||
const edgesContainer = this.container.querySelector('#graph-edges') as HTMLElement;
|
||||
edgesContainer.innerHTML = '';
|
||||
// Упрощенная отрисовка связей - в реальной реализации нужно будет вычислять позиции
|
||||
}
|
||||
|
||||
createFallbackGraph(): void {
|
||||
const container = this.container.querySelector('#reactflow-container') as HTMLElement;
|
||||
container.innerHTML = `
|
||||
<div class="fallback-graph">
|
||||
<p>Упрощенное представление графа</p>
|
||||
<div id="nodes-list"></div>
|
||||
</div>
|
||||
`;
|
||||
this.renderFallbackNodes();
|
||||
}
|
||||
|
||||
renderFallbackNodes(): void {
|
||||
const nodesList = this.container.querySelector('#nodes-list') as HTMLElement;
|
||||
nodesList.innerHTML = '';
|
||||
|
||||
this.nodes.forEach((node) => {
|
||||
const nodeEl = document.createElement('div');
|
||||
nodeEl.className = 'fallback-node';
|
||||
nodeEl.textContent = `${node.id}: ${node.data?.label || 'No label'}`;
|
||||
nodeEl.addEventListener('click', () => this.handleNodeClick(node.id));
|
||||
nodesList.appendChild(nodeEl);
|
||||
});
|
||||
}
|
||||
|
||||
updateData(graphData: { nodes: any[]; edges: any[] }): void {
|
||||
this.nodes = graphData.nodes || [];
|
||||
this.edges = graphData.edges || [];
|
||||
this.renderNodes();
|
||||
this.renderEdges();
|
||||
}
|
||||
|
||||
handleNodeClick(nodeId: string): void {
|
||||
if (this.props.onNodeClick) {
|
||||
this.props.onNodeClick(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
// Очистка ресурсов
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
class HistoryPanel {
|
||||
constructor(container, props) {
|
||||
this.container = container;
|
||||
this.props = props;
|
||||
this.graphs = props.graphs || [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container.innerHTML = `
|
||||
<div class="llm-agent-history-panel">
|
||||
<div class="history-header">
|
||||
<h3>История диалогов</h3>
|
||||
<button id="new-chat-button" class="new-chat-button">+ Новый чат</button>
|
||||
</div>
|
||||
<div class="chat-list" id="chat-list"></div>
|
||||
<div class="context-menu" id="context-menu" style="display: none;">
|
||||
<button id="delete-graph-button">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.newChatButton = this.container.querySelector('#new-chat-button');
|
||||
this.chatList = this.container.querySelector('#chat-list');
|
||||
this.contextMenu = this.container.querySelector('#context-menu');
|
||||
this.deleteButton = this.container.querySelector('#delete-graph-button');
|
||||
|
||||
this.setupEventListeners();
|
||||
this.renderGraphs();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.newChatButton.addEventListener('click', () => {
|
||||
if (this.props.onNewChat) {
|
||||
this.props.onNewChat();
|
||||
}
|
||||
});
|
||||
|
||||
this.deleteButton.addEventListener('click', () => {
|
||||
if (this.contextMenuGraphId) {
|
||||
this.handleDeleteGraph(this.contextMenuGraphId);
|
||||
}
|
||||
});
|
||||
|
||||
// Закрытие контекстного меню при клике вне его
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.contextMenu.contains(e.target)) {
|
||||
this.contextMenu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderGraphs() {
|
||||
this.chatList.innerHTML = this.graphs
|
||||
.map(graph => `
|
||||
<div class="chat-list-item" data-graph-id="${graph.id}">
|
||||
<div class="chat-item-content">
|
||||
<div class="chat-item-text">
|
||||
<span class="truncate">
|
||||
${graph.first_message || `Граф ${graph.id}`}
|
||||
</span>
|
||||
</div>
|
||||
<div class="chat-item-actions">
|
||||
<button class="chat-item-options-button" data-graph-id="${graph.id}">
|
||||
⋮
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
// Добавляем обработчики событий
|
||||
this.chatList.querySelectorAll('.chat-list-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (!e.target.classList.contains('chat-item-options-button')) {
|
||||
const graphId = item.dataset.graphId;
|
||||
if (this.props.onGraphSelect) {
|
||||
this.props.onGraphSelect(graphId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.chatList.querySelectorAll('.chat-item-options-button').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.showContextMenu(e, button.dataset.graphId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showContextMenu(event, graphId) {
|
||||
this.contextMenuGraphId = graphId;
|
||||
this.contextMenu.style.display = 'block';
|
||||
this.contextMenu.style.left = `${event.clientX}px`;
|
||||
this.contextMenu.style.top = `${event.clientY}px`;
|
||||
}
|
||||
|
||||
async handleDeleteGraph(graphId) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:5000/api/graphs/${graphId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ошибка! статус: ${response.status}`);
|
||||
}
|
||||
|
||||
// Обновляем список графов
|
||||
this.graphs = this.graphs.filter(graph => graph.id !== graphId);
|
||||
this.renderGraphs();
|
||||
this.contextMenu.style.display = 'none';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления графа:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateGraphs(newGraphs) {
|
||||
this.graphs = newGraphs;
|
||||
this.renderGraphs();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { HistoryPanel };
|
||||
150
src/components/HistoryPanel.ts
Normal file
150
src/components/HistoryPanel.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
export class HistoryPanel {
|
||||
container: HTMLElement;
|
||||
props: {
|
||||
graphs: any[];
|
||||
onGraphSelect: (graphId: string) => void;
|
||||
onNewChat: () => void;
|
||||
};
|
||||
graphs: any[];
|
||||
newChatButton: HTMLButtonElement;
|
||||
chatList: HTMLElement;
|
||||
contextMenu: HTMLElement;
|
||||
deleteButton: HTMLButtonElement;
|
||||
contextMenuGraphId: string | null;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
props: {
|
||||
graphs: any[];
|
||||
onGraphSelect: (graphId: string) => void;
|
||||
onNewChat: () => void;
|
||||
}
|
||||
) {
|
||||
this.container = container;
|
||||
this.props = props;
|
||||
this.graphs = props.graphs || [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.container.innerHTML = `
|
||||
<div class="llm-agent-history-panel">
|
||||
<div class="history-header">
|
||||
<h3>История диалогов</h3>
|
||||
<button id="new-chat-button" class="new-chat-button">+ Новый чат</button>
|
||||
</div>
|
||||
<div class="chat-list" id="chat-list"></div>
|
||||
<div class="context-menu" id="context-menu" style="display: none;">
|
||||
<button id="delete-graph-button">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.newChatButton = this.container.querySelector('#new-chat-button') as HTMLButtonElement;
|
||||
this.chatList = this.container.querySelector('#chat-list') as HTMLElement;
|
||||
this.contextMenu = this.container.querySelector('#context-menu') as HTMLElement;
|
||||
this.deleteButton = this.container.querySelector('#delete-graph-button') as HTMLButtonElement;
|
||||
|
||||
this.setupEventListeners();
|
||||
this.renderGraphs();
|
||||
}
|
||||
|
||||
setupEventListeners(): void {
|
||||
this.newChatButton.addEventListener('click', () => {
|
||||
if (this.props.onNewChat) {
|
||||
this.props.onNewChat();
|
||||
}
|
||||
});
|
||||
|
||||
this.deleteButton.addEventListener('click', () => {
|
||||
if (this.contextMenuGraphId) {
|
||||
this.handleDeleteGraph(this.contextMenuGraphId);
|
||||
}
|
||||
});
|
||||
|
||||
// Закрытие контекстного меню при клике вне его
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.contextMenu.contains(e.target as Node)) {
|
||||
this.contextMenu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderGraphs(): void {
|
||||
this.chatList.innerHTML = this.graphs
|
||||
.map(
|
||||
(graph) => `
|
||||
<div class="chat-list-item" data-graph-id="${graph.id}">
|
||||
<div class="chat-item-content">
|
||||
<div class="chat-item-text">
|
||||
<span class="truncate">
|
||||
${graph.first_message || `Граф ${graph.id}`}
|
||||
</span>
|
||||
</div>
|
||||
<div class="chat-item-actions">
|
||||
<button class="chat-item-options-button" data-graph-id="${graph.id}">
|
||||
⋮
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
// Добавляем обработчики событий
|
||||
this.chatList.querySelectorAll('.chat-list-item').forEach((item) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (!(e.target as HTMLElement).classList.contains('chat-item-options-button')) {
|
||||
const graphId = (item as HTMLElement).dataset.graphId;
|
||||
if (graphId && this.props.onGraphSelect) {
|
||||
this.props.onGraphSelect(graphId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.chatList.querySelectorAll('.chat-item-options-button').forEach((button) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.showContextMenu(e, (button as HTMLElement).dataset.graphId || '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showContextMenu(event: Event, graphId: string): void {
|
||||
this.contextMenuGraphId = graphId;
|
||||
this.contextMenu.style.display = 'block';
|
||||
this.contextMenu.style.left = `${(event as MouseEvent).clientX}px`;
|
||||
this.contextMenu.style.top = `${(event as MouseEvent).clientY}px`;
|
||||
}
|
||||
|
||||
async handleDeleteGraph(graphId: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:5000/api/graphs/${graphId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ошибка! статус: ${response.status}`);
|
||||
}
|
||||
|
||||
// Обновляем список графов
|
||||
this.graphs = this.graphs.filter((graph) => graph.id !== graphId);
|
||||
this.renderGraphs();
|
||||
this.contextMenu.style.display = 'none';
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления графа:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateGraphs(newGraphs: any[]): void {
|
||||
this.graphs = newGraphs;
|
||||
this.renderGraphs();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
4
src/components/models/ChatHistoryItem.ts
Normal file
4
src/components/models/ChatHistoryItem.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface ChatHistoryItem {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// ----------------------------------------------- Event Bus ---------------------------------------------------------------
|
||||
|
||||
class EventBus {
|
||||
constructor() {
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
if (!this.events[eventName]) {
|
||||
this.events[eventName] = [];
|
||||
}
|
||||
this.events[eventName].push(callback);
|
||||
}
|
||||
|
||||
off(eventName, callback) {
|
||||
if (this.events[eventName]) {
|
||||
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
|
||||
}
|
||||
}
|
||||
|
||||
emit(eventName, data) {
|
||||
if (this.events[eventName]) {
|
||||
this.events[eventName].forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { EventBus };
|
||||
24
src/utils/EventBus.ts
Normal file
24
src/utils/EventBus.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// ----------------------------------------------- Event Bus ---------------------------------------------------------------
|
||||
|
||||
export class EventBus {
|
||||
private events: { [key: string]: ((data?: any) => void)[] } = {};
|
||||
|
||||
on(eventName: string, callback: (data?: any) => void): void {
|
||||
if (!this.events[eventName]) {
|
||||
this.events[eventName] = [];
|
||||
}
|
||||
this.events[eventName].push(callback);
|
||||
}
|
||||
|
||||
off(eventName: string, callback: (data?: any) => void): void {
|
||||
if (this.events[eventName]) {
|
||||
this.events[eventName] = this.events[eventName].filter((cb) => cb !== callback);
|
||||
}
|
||||
}
|
||||
|
||||
emit(eventName: string, data?: any): void {
|
||||
if (this.events[eventName]) {
|
||||
this.events[eventName].forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
const { ItemView } = require('obsidian');
|
||||
const { ChatPanel } = require('../components/ChatPanel');
|
||||
const { HistoryPanel } = require('../components/HistoryPanel');
|
||||
|
||||
const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
|
||||
|
||||
// ----------------------------------------------- Chat View ---------------------------------------------------------------
|
||||
|
||||
class ChatView extends ItemView {
|
||||
constructor(leaf, plugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.chatHistory = [];
|
||||
this.graphsList = [];
|
||||
this.selectedGraphId = null;
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return CHAT_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return 'LLM Agent Chat';
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return 'message-circle';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
|
||||
// Создаем контейнеры для React компонентов
|
||||
this.chatContainer = container.createDiv();
|
||||
this.chatContainer.addClass('llm-agent-chat-container');
|
||||
|
||||
this.historyContainer = container.createDiv();
|
||||
this.historyContainer.addClass('llm-agent-history-container');
|
||||
|
||||
// Инициализируем React компоненты
|
||||
this.chatPanel = new ChatPanel(this.chatContainer, {
|
||||
chatHistory: this.chatHistory,
|
||||
onSendMessage: this.handleSendMessage.bind(this)
|
||||
});
|
||||
|
||||
this.historyPanel = new HistoryPanel(this.historyContainer, {
|
||||
graphs: this.graphsList,
|
||||
onGraphSelect: this.handleGraphSelect.bind(this),
|
||||
onNewChat: this.handleNewChat.bind(this)
|
||||
});
|
||||
|
||||
// Подписываемся на события
|
||||
this.plugin.eventBus.on('node-selected', this.handleNodeSelected.bind(this));
|
||||
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
|
||||
|
||||
// Загружаем список графов
|
||||
this.fetchGraphsList();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.destroy();
|
||||
}
|
||||
if (this.historyPanel) {
|
||||
this.historyPanel.destroy();
|
||||
}
|
||||
this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
|
||||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||||
}
|
||||
|
||||
// ----------------------------------------------- API Methods ---------------------------------------------------------------
|
||||
|
||||
async fetchGraphsList() {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`);
|
||||
const data = await response.json();
|
||||
this.graphsList = data;
|
||||
if (this.historyPanel) {
|
||||
this.historyPanel.updateGraphs(this.graphsList);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки списка графов:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchGraphData(graphId) {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
console.error(`Граф с ID ${graphId} не найден.`);
|
||||
}
|
||||
throw new Error(`HTTP ошибка! статус: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Обновляем граф
|
||||
this.plugin.eventBus.emit('graph-updated', {
|
||||
graphData: {
|
||||
nodes: data.graph_nodes || [],
|
||||
edges: data.graph_edges || []
|
||||
},
|
||||
currentNode: data.current_node_id
|
||||
});
|
||||
|
||||
// Обновляем историю чата
|
||||
this.chatHistory = data.messages || [];
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных графа:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMessagesFromRootToNode(nodeId) {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`);
|
||||
const data = await response.json();
|
||||
this.chatHistory = data;
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки сообщений:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||
|
||||
async handleSendMessage(message) {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
graph_id: this.selectedGraphId,
|
||||
parent_node_id: this.currentNode,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ошибка! статус: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Обновляем выбранный ID графа
|
||||
this.selectedGraphId = result.graph_id;
|
||||
|
||||
// Устанавливаем текущий узел
|
||||
const newCurrentNodeId = result.graph_visualization_data.current_node_id;
|
||||
this.currentNode = newCurrentNodeId;
|
||||
|
||||
// Обновляем граф
|
||||
this.plugin.eventBus.emit('graph-updated', {
|
||||
graphData: {
|
||||
nodes: result.graph_visualization_data.nodes || [],
|
||||
edges: result.graph_visualization_data.edges || []
|
||||
},
|
||||
currentNode: newCurrentNodeId
|
||||
});
|
||||
|
||||
// Загружаем сообщения от корня до нового текущего узла
|
||||
if (newCurrentNodeId) {
|
||||
await this.fetchMessagesFromRootToNode(newCurrentNodeId);
|
||||
} else {
|
||||
this.chatHistory = result.messages || [];
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем список графов
|
||||
this.fetchGraphsList();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки сообщения:', error);
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeSelected(data) {
|
||||
this.currentNode = data.nodeId;
|
||||
if (this.selectedGraphId) {
|
||||
this.fetchMessagesFromRootToNode(data.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
handleGraphSelect(graphId) {
|
||||
this.selectedGraphId = graphId;
|
||||
this.fetchGraphData(graphId);
|
||||
}
|
||||
|
||||
handleNewChat() {
|
||||
this.chatHistory = [];
|
||||
this.currentNode = null;
|
||||
this.selectedGraphId = null;
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ChatView, CHAT_VIEW_TYPE };
|
||||
221
src/views/ChatView.ts
Normal file
221
src/views/ChatView.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { ChatPanel } from '../components/ChatPanel';
|
||||
import { HistoryPanel } from '../components/HistoryPanel';
|
||||
import LLMAgentPlugin from 'main';
|
||||
|
||||
export const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
|
||||
|
||||
// ----------------------------------------------- Chat View ---------------------------------------------------------------
|
||||
|
||||
export class ChatView extends ItemView {
|
||||
plugin: LLMAgentPlugin;
|
||||
chatHistory: any[];
|
||||
graphsList: any[];
|
||||
selectedGraphId: string | null;
|
||||
currentNode: string | null;
|
||||
chatContainer: HTMLDivElement;
|
||||
historyContainer: HTMLDivElement;
|
||||
chatPanel: ChatPanel | null;
|
||||
historyPanel: HistoryPanel | null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.chatHistory = [];
|
||||
this.graphsList = [];
|
||||
this.selectedGraphId = null;
|
||||
this.currentNode = null;
|
||||
this.chatPanel = null;
|
||||
this.historyPanel = null;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return CHAT_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'LLM Agent Chat';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'message-circle';
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const container = this.containerEl.children[1] as HTMLElement;
|
||||
container.empty();
|
||||
|
||||
// Создаем контейнеры для React компонентов
|
||||
this.chatContainer = container.createDiv() as HTMLDivElement;
|
||||
this.chatContainer.addClass('llm-agent-chat-container');
|
||||
|
||||
this.historyContainer = container.createDiv() as HTMLDivElement;
|
||||
this.historyContainer.addClass('llm-agent-history-container');
|
||||
|
||||
// Инициализируем React компоненты
|
||||
this.chatPanel = new ChatPanel(this.chatContainer, {
|
||||
chatHistory: this.chatHistory,
|
||||
onSendMessage: this.handleSendMessage.bind(this)
|
||||
});
|
||||
|
||||
this.historyPanel = new HistoryPanel(this.historyContainer, {
|
||||
graphs: this.graphsList,
|
||||
onGraphSelect: this.handleGraphSelect.bind(this),
|
||||
onNewChat: this.handleNewChat.bind(this)
|
||||
});
|
||||
|
||||
// Подписываемся на события
|
||||
this.plugin.eventBus.on('node-selected', this.handleNodeSelected.bind(this));
|
||||
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
|
||||
|
||||
// Загружаем список графов
|
||||
this.fetchGraphsList();
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.destroy();
|
||||
this.chatPanel = null;
|
||||
}
|
||||
if (this.historyPanel) {
|
||||
this.historyPanel.destroy();
|
||||
this.historyPanel = null;
|
||||
}
|
||||
this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
|
||||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||||
}
|
||||
|
||||
// ----------------------------------------------- API Methods ---------------------------------------------------------------
|
||||
|
||||
async fetchGraphsList(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`);
|
||||
const data = await response.json();
|
||||
this.graphsList = data;
|
||||
if (this.historyPanel) {
|
||||
this.historyPanel.updateGraphs(this.graphsList);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки списка графов:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchGraphData(graphId: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
console.error(`Граф с ID ${graphId} не найден.`);
|
||||
}
|
||||
throw new Error(`HTTP ошибка! статус: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Обновляем граф
|
||||
this.plugin.eventBus.emit('graph-updated', {
|
||||
graphData: {
|
||||
nodes: data.graph_nodes || [],
|
||||
edges: data.graph_edges || []
|
||||
},
|
||||
currentNode: data.current_node_id
|
||||
});
|
||||
|
||||
// Обновляем историю чата
|
||||
this.chatHistory = data.messages || [];
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных графа:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMessagesFromRootToNode(nodeId: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`);
|
||||
const data = await response.json();
|
||||
this.chatHistory = data;
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки сообщений:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||
|
||||
async handleSendMessage(message: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
graph_id: this.selectedGraphId,
|
||||
parent_node_id: this.currentNode
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ошибка! статус: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Обновляем выбранный ID графа
|
||||
this.selectedGraphId = result.graph_id;
|
||||
|
||||
// Устанавливаем текущий узел
|
||||
const newCurrentNodeId = result.graph_visualization_data.current_node_id;
|
||||
this.currentNode = newCurrentNodeId;
|
||||
|
||||
// Обновляем граф
|
||||
this.plugin.eventBus.emit('graph-updated', {
|
||||
graphData: {
|
||||
nodes: result.graph_visualization_data.nodes || [],
|
||||
edges: result.graph_visualization_data.edges || []
|
||||
},
|
||||
currentNode: newCurrentNodeId
|
||||
});
|
||||
|
||||
// Загружаем сообщения от корня до нового текущего узла
|
||||
if (newCurrentNodeId) {
|
||||
await this.fetchMessagesFromRootToNode(newCurrentNodeId);
|
||||
} else {
|
||||
this.chatHistory = result.messages || [];
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем список графов
|
||||
this.fetchGraphsList();
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки сообщения:', error);
|
||||
}
|
||||
}
|
||||
|
||||
handleNodeSelected(data: { nodeId: string }): void {
|
||||
this.currentNode = data.nodeId;
|
||||
if (this.selectedGraphId) {
|
||||
this.fetchMessagesFromRootToNode(data.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
handleGraphSelect(graphId: string): void {
|
||||
this.selectedGraphId = graphId;
|
||||
this.fetchGraphData(graphId);
|
||||
}
|
||||
|
||||
handleNewChat(): void {
|
||||
this.chatHistory = [];
|
||||
this.currentNode = null;
|
||||
this.selectedGraphId = null;
|
||||
if (this.chatPanel) {
|
||||
this.chatPanel.updateHistory(this.chatHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
const { ItemView } = require('obsidian');
|
||||
const { GraphPanel } = require('../components/GraphPanel');
|
||||
|
||||
const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
|
||||
|
||||
// ----------------------------------------------- Graph View ---------------------------------------------------------------
|
||||
|
||||
class GraphView extends ItemView {
|
||||
constructor(leaf, plugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.graphData = { nodes: [], edges: [] };
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return GRAPH_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return 'LLM Agent Graph';
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return 'brain-circuit';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
|
||||
// Создаем контейнер для React компонента
|
||||
this.reactContainer = container.createDiv();
|
||||
this.reactContainer.addClass('llm-agent-graph-container');
|
||||
|
||||
// Инициализируем React компонент
|
||||
this.graphPanel = new GraphPanel(this.reactContainer, {
|
||||
graphData: this.graphData,
|
||||
onNodeClick: this.handleNodeClick.bind(this)
|
||||
});
|
||||
|
||||
// Подписываемся на события
|
||||
this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this));
|
||||
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
if (this.graphPanel) {
|
||||
this.graphPanel.destroy();
|
||||
}
|
||||
this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate);
|
||||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||
|
||||
handleNodeClick(nodeId) {
|
||||
this.currentNode = nodeId;
|
||||
this.plugin.eventBus.emit('node-selected', { nodeId });
|
||||
}
|
||||
|
||||
handleGraphUpdate(data) {
|
||||
this.graphData = data.graphData;
|
||||
this.currentNode = data.currentNode;
|
||||
if (this.graphPanel) {
|
||||
this.graphPanel.updateData(this.graphData);
|
||||
}
|
||||
}
|
||||
|
||||
handleNewChat() {
|
||||
this.graphData = { nodes: [], edges: [] };
|
||||
this.currentNode = null;
|
||||
if (this.graphPanel) {
|
||||
this.graphPanel.updateData(this.graphData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GraphView, GRAPH_VIEW_TYPE };
|
||||
86
src/views/GraphView.ts
Normal file
86
src/views/GraphView.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
||||
import { GraphPanel } from '../components/GraphPanel';
|
||||
import LLMAgentPlugin from 'main';
|
||||
|
||||
export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
|
||||
|
||||
// ----------------------------------------------- Graph View ---------------------------------------------------------------
|
||||
|
||||
export class GraphView extends ItemView {
|
||||
plugin: LLMAgentPlugin;
|
||||
graphData: { nodes: any[]; edges: any[] };
|
||||
currentNode: string | null;
|
||||
graphPanel: GraphPanel | null;
|
||||
reactContainer: HTMLDivElement;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.graphData = { nodes: [], edges: [] };
|
||||
this.currentNode = null;
|
||||
this.graphPanel = null;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return GRAPH_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'LLM Agent Graph';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'brain-circuit';
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const container = this.containerEl.children[1] as HTMLElement;
|
||||
container.empty();
|
||||
|
||||
// Создаем контейнер для React компонента
|
||||
this.reactContainer = container.createDiv() as HTMLDivElement;
|
||||
this.reactContainer.addClass('llm-agent-graph-container');
|
||||
|
||||
// Инициализируем React компонент
|
||||
this.graphPanel = new GraphPanel(this.reactContainer, {
|
||||
graphData: this.graphData,
|
||||
onNodeClick: this.handleNodeClick.bind(this)
|
||||
});
|
||||
|
||||
// Подписываемся на события
|
||||
this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this));
|
||||
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
if (this.graphPanel) {
|
||||
this.graphPanel.destroy();
|
||||
this.graphPanel = null;
|
||||
}
|
||||
this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate);
|
||||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||||
}
|
||||
|
||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||
|
||||
handleNodeClick(nodeId: string): void {
|
||||
this.currentNode = nodeId;
|
||||
this.plugin.eventBus.emit('node-selected', { nodeId });
|
||||
}
|
||||
|
||||
handleGraphUpdate(data: { graphData: { nodes: any[]; edges: any[] }; currentNode: string | null }): void {
|
||||
this.graphData = data.graphData;
|
||||
this.currentNode = data.currentNode;
|
||||
if (this.graphPanel) {
|
||||
this.graphPanel.updateData(this.graphData);
|
||||
}
|
||||
}
|
||||
|
||||
handleNewChat(): void {
|
||||
this.graphData = { nodes: [], edges: [] };
|
||||
this.currentNode = null;
|
||||
if (this.graphPanel) {
|
||||
this.graphPanel.updateData(this.graphData);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user