mirror of
https://github.com/lossless-group/perplexed-plugin.git
synced 2026-07-22 06:49:50 +00:00
align(requirements): align Perplexed to Obsidian's plugin community
standards On branch development Your branch is up to date with 'origin/development'. Changes to be committed: new file: LICENSE new file: context-v/plans/20206-05-02_Assuring-Obsidian-Community-Plugin-Requirements.md modified: eslint.config.mjs modified: main.ts modified: manifest.json modified: package.json modified: src/modals/ArticleGeneratorModal.ts modified: src/modals/ClaudeModal.ts modified: src/modals/LMStudioModal.ts modified: src/modals/PerplexicaModal.ts modified: src/modals/PerplexityModal.ts modified: src/modals/TextEnhancementModal.ts modified: src/modals/TextEnhancementWithImagesModal.ts modified: src/modals/URLUpdateModal.ts modified: src/services/lmStudioService.ts modified: src/services/perplexicaService.ts modified: src/services/perplexityService.ts modified: src/types/obsidian.d.ts new file: src/utils/coerce.ts modified: src/utils/formatDate.ts modified: src/utils/logger.ts modified: styles.css modified: versions.json
This commit is contained in:
parent
d7551d7bc7
commit
46e4f24755
28 changed files with 543 additions and 264 deletions
3
.npmrc
3
.npmrc
|
|
@ -1,3 +1,4 @@
|
|||
shamefully-hoist=true
|
||||
auto-install-peers=true
|
||||
shared-workspace-lockfile=false
|
||||
shared-workspace-lockfile=false
|
||||
ignore-workspace=true
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025-2026 The Lossless Group
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
---
|
||||
title: "Plan — Bring Perplexed up to Obsidian Community-Plugin Publishing Standards"
|
||||
status: Proposed
|
||||
created: 2026-05-02
|
||||
applies_to: perplexed Obsidian plugin
|
||||
authors:
|
||||
- Michael Staton
|
||||
augmented_with: Claude Code (Opus 4.7, 1M context)
|
||||
related_reference: ../../cite-wide/context-v/reminders/Obsidian-Type-Safety.md
|
||||
---
|
||||
|
||||
# Plan — Bring Perplexed up to Obsidian Community-Plugin Publishing Standards
|
||||
|
||||
## Context
|
||||
|
||||
The Perplexed Obsidian plugin (`/Users/mpstaton/code/lossless-monorepo/perplexed`) is being prepped for submission to the Obsidian community plugin marketplace. Its sister plugin `cite-wide` was rejected last week for type-safety violations (specifically `any` usage), and the rules that triggered that rejection are enforced automatically by `ObsidianReviewBot` on every submission PR with **no appeal mechanism**. There is also a set of metadata/repo-hygiene requirements that block submission independently.
|
||||
|
||||
The reference doc — `/Users/mpstaton/code/lossless-monorepo/cite-wide/context-v/reminders/Obsidian-Type-Safety.md` — captures the rules verbatim from the review bot and the patterns to satisfy them. This plan applies those rules to Perplexed and bundles in the non-type-safety publishing fixes uncovered during the audit.
|
||||
|
||||
**Audit totals:** 25 explicit-`any` sites + 3 `as any` casts + 5 metadata/repo blockers + ESLint config that lets `any` through as a warning. No floating-promises, no `[object Object]` interpolations, no `innerHTML`, no `(window as any)` patterns, no hand-rolled YAML (Perplexed doesn't touch frontmatter — different surface area than cite-wide). Critical files all reviewed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Tighten ESLint to Match the Review Bot
|
||||
|
||||
**Why first:** if the local lint config matches the bot, we catch every remaining issue during `pnpm build` instead of at submission. The current config (`eslint.config.mjs:29`) has `no-explicit-any: "warn"` which lets violations through.
|
||||
|
||||
**File:** `/Users/mpstaton/code/lossless-monorepo/perplexed/eslint.config.mjs`
|
||||
|
||||
Change the `rules` block:
|
||||
- `"@typescript-eslint/no-explicit-any": "error"` (was `"warn"`)
|
||||
- Add `"@typescript-eslint/no-unnecessary-type-assertion": "error"`
|
||||
- Add `"@typescript-eslint/no-floating-promises": "error"`
|
||||
- Add `"@typescript-eslint/no-base-to-string": "error"`
|
||||
- `"@typescript-eslint/no-unused-vars": ["error", { args: "none" }]` — already present, keep
|
||||
- Add CLI flag in `package.json` build script: `eslint . --report-unused-disable-directives` before `tsc`. Current build script: `"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"` — change to `"build": "eslint . --report-unused-disable-directives && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"`.
|
||||
|
||||
After this change, `pnpm build` will fail loudly. That failure surface drives the rest of the plan.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Fix Manifest, Versions, and Package Metadata
|
||||
|
||||
**Why:** four-part version strings (`X.Y.Z.W`) are not semver and the bot rejects them. The `minAppVersion` placeholder will also be flagged.
|
||||
|
||||
### 2.1 `manifest.json`
|
||||
- `version`: `"0.0.0.1"` → `"0.0.1"`
|
||||
- `minAppVersion`: `"0.0.0.1"` → `"0.15.0"` (matches the sole entry in `versions.json`; safe baseline for current Obsidian APIs we use)
|
||||
|
||||
### 2.2 `versions.json`
|
||||
- Key `"0.0.1.0"` → `"0.0.1"` (must match the new manifest version, must be valid semver)
|
||||
|
||||
### 2.3 `package.json`
|
||||
- `version`: `"0.0.0.6"` → `"0.0.1"` (sync to manifest)
|
||||
- Remove unused dependencies confirmed not imported anywhere in `src/` or `main.ts`:
|
||||
- `fastify`
|
||||
- `@modelcontextprotocol/sdk`
|
||||
- `zod`
|
||||
- Keep: `dotenv`, `@anthropic-ai/sdk` (the latter is used by `claudeService.ts`)
|
||||
- After removal: `pnpm install` (or `npm install`) to regenerate the lockfile.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Add LICENSE File
|
||||
|
||||
**Why:** `package.json` declares MIT but no `LICENSE` file exists at the repo root. Obsidian's submission requirements expect the license file to be present and discoverable.
|
||||
|
||||
- Create `/Users/mpstaton/code/lossless-monorepo/perplexed/LICENSE` with the standard MIT license text, copyright year `2025-2026`, copyright holder `The Lossless Group` (matches `manifest.json` author).
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Eliminate `any` (the type-safety pass)
|
||||
|
||||
**Why:** this is the rule that rejected cite-wide. 25 sites + 3 casts must be eliminated. The reference doc's §2 and §5 give the exact replacement patterns.
|
||||
|
||||
Order chosen so each step compiles cleanly before the next:
|
||||
|
||||
### 4.1 Fix the ambient shim — `src/types/obsidian.d.ts:5`
|
||||
|
||||
`commands: any` → minimal documented interface for the methods Perplexed actually calls. Search `main.ts` and services for `app.commands.*` usage and type only what's used (likely `executeCommandById(id: string): boolean` and `commands: Record<string, { id: string; name: string }>`). If usage is one-off, fall back to `unknown` and narrow at the call site.
|
||||
|
||||
### 4.2 Fix the logger — `src/utils/logger.ts`
|
||||
|
||||
Six sites, all the same pattern: replace every `details?: any` and `details: any` with `details?: unknown`.
|
||||
- Line 8: `details?: any;` (in `LogEntry` interface)
|
||||
- Line 81: `addEntry(... details?: any)`
|
||||
- Lines 109, 113, 117, 121: `error/warn/info/debug(message: string, details?: any)`
|
||||
|
||||
The logger only stringifies `details` — no structural access — so `unknown` is a drop-in replacement. The one exception is line 86 (`details instanceof Error`), which is already a proper narrowing and works identically against `unknown`.
|
||||
|
||||
### 4.3 Fix the date util — `src/utils/formatDate.ts`
|
||||
|
||||
Lines 41, 58: `source: any` → `source: unknown`. Add an internal `Source` interface (the `getMostRecentDate` and `formatPublicationInfo` functions look for `date_published`, `last_updated`, etc. — read both functions, define the interface from the actual field accesses, then narrow with the `isRecord` guard from the reference doc §3.3 before reading fields). This requires also adding a small `coerce.ts` helper file with `isRecord`, `asString`, and `asDate` (same shapes from the reference doc) — these will be reused in 4.4.
|
||||
|
||||
**New file to create:** `src/utils/coerce.ts` — copy the `asString`, `asNumber`, `asStringArray`, `asDate`, `isRecord` helpers verbatim from the reference doc §3.3. They are general-purpose and will pay for themselves several times in 4.4.
|
||||
|
||||
### 4.4 Fix the API service payload types — `src/services/{perplexity,perplexica,lmStudio}Service.ts`
|
||||
|
||||
Three services share the same shape problems:
|
||||
- `promptsService?: any` and `private promptsService: any` (in constructor options + class field)
|
||||
- `let payload: any` (request body builder)
|
||||
- API response objects like `finalResponseData: any` and `images: any[]`
|
||||
|
||||
**Per-service fixes:**
|
||||
|
||||
`perplexicaService.ts` — Lines 7, 17, 77.
|
||||
- Replace `promptsService` field/option type with `import type { PromptsService } from './promptsService'`. (Direct import; the cyclical-init concern in the reference doc §5.5 is about singleton patterns, which Perplexed doesn't have — these are constructor params.)
|
||||
- Define `interface PerplexicaPayload { ... }` from the actual fields assigned (read lines ~70–110 to see what's set on `payload`).
|
||||
|
||||
`perplexityService.ts` — Lines 14, 21, 51, 232, 397, 609, 707, 761, 777, plus the three `as any` casts at 225/227/228.
|
||||
- Same `PromptsService` import for fields/options.
|
||||
- Define `interface PerplexityPayload`, `interface PerplexityImage`, `interface PerplexitySource`, and `interface PerplexityResponse` from the response shape Perplexity returns (read lines 600–800 to enumerate fields actually accessed). Make the response interface use `unknown` for fields the code doesn't dereference, and concrete types for fields it does.
|
||||
- The `loadingInterval` casts (`(this as any).loadingInterval`) are the exact pattern in reference doc §5.5: declare `private loadingInterval: ReturnType<typeof setInterval> | null = null;` on the class and remove all three casts.
|
||||
- Lines 51 and 232 — `images: any[]` and `sources: any[]` — type with `PerplexityImage[]` and `PerplexitySource[]`.
|
||||
|
||||
`lmStudioService.ts` — Lines 13, 19, 82, 93.
|
||||
- `PromptsService` import (same as above).
|
||||
- `const messages: any[] = []` → `const messages: ChatMessage[] = []` where `interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string }` (OpenAI-compatible shape used by LM Studio).
|
||||
- `let payload: any` → `interface LMStudioPayload { ... }`.
|
||||
|
||||
### 4.5 Verify no `any` remains
|
||||
|
||||
After the above, run `pnpm build`. The eslint step from Phase 1 will fail if any `any` slipped through. Fix any newly surfaced sites the same way (type with a minimal interface, narrow with `isRecord`, or use `unknown`).
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Console-Log Hygiene (recommended, not blocking)
|
||||
|
||||
**Why:** the audit found 86 `console.*` calls in `main.ts`. These are not a hard ObsidianReviewBot rejection criterion, but the published guidelines discourage shipping debug logging. They also leak internals (paths, model names) into the user's devtools console.
|
||||
|
||||
**Decision point — ask the user:** should we (a) remove all console statements, (b) gate them behind a `DEBUG` flag in settings, or (c) route them all through the existing `FileLogger` and drop the console output? The reference doc doesn't mandate a choice; cite-wide chose (c). Recommend (c) for consistency with cite-wide.
|
||||
|
||||
This phase can ship in a follow-up PR if the user wants to keep the type-safety/metadata fix focused.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After the implementation, run from `/Users/mpstaton/code/lossless-monorepo/perplexed`:
|
||||
|
||||
1. **Lint clean:** `pnpm exec eslint . --report-unused-disable-directives` — expect zero errors. Specifically, expect zero `@typescript-eslint/no-explicit-any` errors.
|
||||
2. **TypeScript clean:** `pnpm exec tsc -noEmit -skipLibCheck` — expect zero errors. Strict flags in `tsconfig.json` (already correct, do not weaken) will catch any residual narrowing gaps.
|
||||
3. **Build clean:** `pnpm build` — expect a clean `main.js` produced.
|
||||
4. **Manual smoke test in Obsidian:**
|
||||
- Symlink or copy the built `main.js`, `manifest.json`, `styles.css` into a test vault's `.obsidian/plugins/perplexed/` folder.
|
||||
- Enable the plugin, run each of: Perplexity research command, Perplexica research command, LM Studio command, Claude command (the WIP one — known broken per recent commits, just verify it doesn't crash on load).
|
||||
- Confirm settings UI loads, all four endpoint fields render and save.
|
||||
- Trigger an error path (e.g. invalid API key) and confirm the logger captures it without console-only output.
|
||||
5. **Resubmission readiness check:**
|
||||
- `manifest.json` version, `versions.json` key, `package.json` version all equal `"0.0.1"`.
|
||||
- `LICENSE` file present at repo root.
|
||||
- `git diff` shows zero `any` remaining in `.ts` files (excluding `node_modules` and `main.js`): `git grep -n ': any\|as any\|<any>\|any\[\]' -- '*.ts' ':!node_modules'` should return nothing from our source files.
|
||||
|
||||
---
|
||||
|
||||
## Critical Files Touched
|
||||
|
||||
- `eslint.config.mjs` — Phase 1
|
||||
- `package.json` — Phase 1 (build script), Phase 2.3 (version, deps)
|
||||
- `manifest.json` — Phase 2.1
|
||||
- `versions.json` — Phase 2.2
|
||||
- `LICENSE` — Phase 3 (new)
|
||||
- `src/types/obsidian.d.ts` — Phase 4.1
|
||||
- `src/utils/logger.ts` — Phase 4.2
|
||||
- `src/utils/formatDate.ts` — Phase 4.3
|
||||
- `src/utils/coerce.ts` — Phase 4.3 (new)
|
||||
- `src/services/perplexicaService.ts` — Phase 4.4
|
||||
- `src/services/perplexityService.ts` — Phase 4.4 (largest single file change)
|
||||
- `src/services/lmStudioService.ts` — Phase 4.4
|
||||
|
||||
`main.ts` is **not** in the type-safety touch list — the audit found zero `any` in it. Phase 5 (optional) would touch it for console hygiene.
|
||||
|
||||
## Reused Utilities
|
||||
|
||||
- `coerce.ts` helpers (`asString`, `asNumber`, `asStringArray`, `asDate`, `isRecord`) — verbatim from reference doc §3.3, will be reused across `formatDate.ts` and the three service-payload narrowings in 4.4.
|
||||
- Existing `FileLogger` singleton in `src/utils/logger.ts` — already present and properly designed, just needs `unknown` substitution.
|
||||
- Obsidian's typed `App`, `Editor`, `TFile`, `Vault`, `Modal` from `'obsidian'` — already imported throughout; no new shim needed beyond the cleanup of `commands: any`.
|
||||
|
|
@ -4,7 +4,7 @@ import globals from "globals";
|
|||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["node_modules/", "main.js"],
|
||||
ignores: ["node_modules/", "main.js", "**/*.mjs"],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
|
|
@ -22,11 +22,14 @@ export default tseslint.config(
|
|||
},
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none", caughtErrors: "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-base-to-string": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
|
|
|
|||
3
main.ts
3
main.ts
|
|
@ -1,4 +1,5 @@
|
|||
import { App, Editor, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
import type { App, Editor} from 'obsidian';
|
||||
import { Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
// Import services
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "perplexed",
|
||||
"name": "Perplexed",
|
||||
"version": "0.0.0.1",
|
||||
"minAppVersion": "0.0.0.1",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A plugin for Obsidian that allows you to generate source-cited content using AI using Perplexity and Perplexica.",
|
||||
"author": "The Lossless Group",
|
||||
"authorUrl": "https://lossless.group",
|
||||
|
|
|
|||
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
10
package.json
10
package.json
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "perplexed",
|
||||
"version": "0.0.0.6",
|
||||
"version": "0.1.0",
|
||||
"description": "A plugin for Obsidian that allows you to generate source-cited content using AI using Perplexity and Perplexica.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"build": "eslint . --report-unused-disable-directives && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "eslint . --report-unused-disable-directives",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
@ -37,9 +38,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.92.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"fastify": "^5.8.5",
|
||||
"zod": "^4.4.2"
|
||||
"dotenv": "^17.4.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
packages:
|
||||
- '.'
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Modal, Notice, Setting } from 'obsidian';
|
||||
import type { Editor } from 'obsidian';
|
||||
import { Modal, Notice, Setting } from 'obsidian';
|
||||
import type { Editor , App} from 'obsidian';
|
||||
import type { PerplexityService, PerplexityOptions } from '../services/perplexityService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Modal, Notice, Setting } from 'obsidian';
|
||||
import type { Editor } from 'obsidian';
|
||||
import { Modal, Notice, Setting } from 'obsidian';
|
||||
import type { Editor , App} from 'obsidian';
|
||||
import type { ClaudeService, ClaudeOptions } from '../services/claudeService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { App, Modal, Notice, Editor } from 'obsidian';
|
||||
import { LMStudioService, LMStudioOptions } from '../services/lmStudioService';
|
||||
import { PromptsService } from '../services/promptsService';
|
||||
import type { App, Editor } from 'obsidian';
|
||||
import { Modal, Notice } from 'obsidian';
|
||||
import type { LMStudioService, LMStudioOptions } from '../services/lmStudioService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
export class LMStudioModal extends Modal {
|
||||
private editor: Editor;
|
||||
|
|
@ -109,7 +110,7 @@ export class LMStudioModal extends Modal {
|
|||
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.onSubmit();
|
||||
void this.onSubmit();
|
||||
};
|
||||
|
||||
askButton.onclick = () => this.onSubmit();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { App, Modal, Notice, Editor } from 'obsidian';
|
||||
import { PerplexicaService, PerplexicaOptions } from '../services/perplexicaService';
|
||||
import { PromptsService } from '../services/promptsService';
|
||||
import type { App, Editor } from 'obsidian';
|
||||
import { Modal, Notice } from 'obsidian';
|
||||
import type { PerplexicaService, PerplexicaOptions } from '../services/perplexicaService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
export class PerplexicaModal extends Modal {
|
||||
private editor: Editor;
|
||||
|
|
@ -81,7 +82,7 @@ export class PerplexicaModal extends Modal {
|
|||
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.onSubmit();
|
||||
void this.onSubmit();
|
||||
};
|
||||
|
||||
askButton.onclick = () => this.onSubmit();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { App, Modal, Notice, Setting } from 'obsidian';
|
||||
import type { Editor } from 'obsidian';
|
||||
import { Modal, Notice, Setting } from 'obsidian';
|
||||
import type { Editor , App} from 'obsidian';
|
||||
import type { PerplexityService, PerplexityOptions } from '../services/perplexityService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { App, Modal, Notice, Editor } from 'obsidian';
|
||||
import { PerplexityService } from '../services/perplexityService';
|
||||
import { PromptsService } from '../services/promptsService';
|
||||
import type { App, Editor } from 'obsidian';
|
||||
import { Modal, Notice } from 'obsidian';
|
||||
import type { PerplexityService } from '../services/perplexityService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
export class TextEnhancementModal extends Modal {
|
||||
protected editor: Editor;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { App, Modal, Notice, Editor } from 'obsidian';
|
||||
import { PerplexityService } from '../services/perplexityService';
|
||||
import { PromptsService } from '../services/promptsService';
|
||||
import type { App, Editor } from 'obsidian';
|
||||
import { Modal, Notice } from 'obsidian';
|
||||
import type { PerplexityService } from '../services/perplexityService';
|
||||
import type { PromptsService } from '../services/promptsService';
|
||||
|
||||
export class TextEnhancementWithImagesModal extends Modal {
|
||||
protected editor: Editor;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { App, Modal, Notice } from 'obsidian';
|
||||
import type { App} from 'obsidian';
|
||||
import { Modal, Notice } from 'obsidian';
|
||||
|
||||
export interface URLUpdateModalConfig {
|
||||
title: string;
|
||||
|
|
@ -44,7 +45,7 @@ export class URLUpdateModal extends Modal {
|
|||
|
||||
form.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.onSubmit();
|
||||
void this.onSubmit();
|
||||
};
|
||||
|
||||
saveButton.onclick = () => this.onSubmit();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Editor, Notice } from 'obsidian';
|
||||
import type { Editor} from 'obsidian';
|
||||
import { Notice } from 'obsidian';
|
||||
import type { PromptsService } from './promptsService';
|
||||
|
||||
export interface LMStudioOptions {
|
||||
max_tokens?: number;
|
||||
|
|
@ -10,13 +12,35 @@ export interface LMStudioOptions {
|
|||
|
||||
export interface LMStudioSettings {
|
||||
lmStudioEndpoint: string;
|
||||
promptsService?: any; // Will be PromptsService type
|
||||
promptsService?: PromptsService;
|
||||
requestTemplate?: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface LMStudioPayload {
|
||||
model: string;
|
||||
messages: ChatMessage[];
|
||||
stream: boolean;
|
||||
max_tokens: number;
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
}
|
||||
|
||||
interface LMStudioStreamChunk {
|
||||
choices?: Array<{ delta?: { content?: string } }>;
|
||||
}
|
||||
|
||||
interface LMStudioResponse {
|
||||
choices?: Array<{ message?: { content?: string } }>;
|
||||
}
|
||||
|
||||
export class LMStudioService {
|
||||
private settings: LMStudioSettings;
|
||||
private promptsService: any;
|
||||
private promptsService: PromptsService | undefined;
|
||||
|
||||
constructor(settings: LMStudioSettings) {
|
||||
this.settings = settings;
|
||||
|
|
@ -79,37 +103,34 @@ export class LMStudioService {
|
|||
console.log('Response cursor position:', responseCursor);
|
||||
|
||||
try {
|
||||
const messages: any[] = [];
|
||||
|
||||
// Add system message if provided
|
||||
const messages: ChatMessage[] = [];
|
||||
|
||||
if (options?.system_prompt) {
|
||||
messages.push({ role: 'system', content: options.system_prompt });
|
||||
}
|
||||
|
||||
// Add user query
|
||||
|
||||
messages.push({ role: 'user', content: query });
|
||||
|
||||
// Use template if available, otherwise construct payload manually
|
||||
let payload: any;
|
||||
|
||||
let payload: LMStudioPayload;
|
||||
if (this.settings.requestTemplate) {
|
||||
try {
|
||||
const processedTemplate = this.promptsService?.processTemplate(this.settings.requestTemplate) || this.settings.requestTemplate;
|
||||
|
||||
// Strip JavaScript-style comments that would break JSON parsing
|
||||
|
||||
const cleanedTemplate = processedTemplate
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
|
||||
.replace(/\/\/.*$/gm, '') // Remove // comments
|
||||
.replace(/^\s*$/gm, '') // Remove empty lines
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
.replace(/^\s*$/gm, '')
|
||||
.trim();
|
||||
|
||||
payload = JSON.parse(cleanedTemplate);
|
||||
// Override with current parameters
|
||||
payload.model = model;
|
||||
payload.messages = messages;
|
||||
payload.stream = stream;
|
||||
payload.max_tokens = options?.max_tokens ?? 2048;
|
||||
payload.temperature = options?.temperature ?? 0.7;
|
||||
payload.top_p = options?.top_p ?? 0.9;
|
||||
|
||||
JSON.parse(cleanedTemplate);
|
||||
payload = {
|
||||
model,
|
||||
messages,
|
||||
stream,
|
||||
max_tokens: options?.max_tokens ?? 2048,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
top_p: options?.top_p ?? 0.9,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse request template, using default payload:', error);
|
||||
payload = {
|
||||
|
|
@ -118,7 +139,7 @@ export class LMStudioService {
|
|||
stream,
|
||||
max_tokens: options?.max_tokens ?? 2048,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
top_p: options?.top_p ?? 0.9
|
||||
top_p: options?.top_p ?? 0.9,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
|
@ -128,7 +149,7 @@ export class LMStudioService {
|
|||
stream,
|
||||
max_tokens: options?.max_tokens ?? 2048,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
top_p: options?.top_p ?? 0.9
|
||||
top_p: options?.top_p ?? 0.9,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +166,7 @@ export class LMStudioService {
|
|||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
let finalCursor = responseCursor;
|
||||
const finalCursor = responseCursor;
|
||||
|
||||
if (stream) {
|
||||
await this.handleStreamingResponse(response, editor, responseCursor, options);
|
||||
|
|
@ -192,9 +213,9 @@ export class LMStudioService {
|
|||
if (data === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const parsed = JSON.parse(data) as LMStudioStreamChunk;
|
||||
if (parsed.choices?.[0]?.delta?.content) {
|
||||
let content = parsed.choices[0].delta.content;
|
||||
let content: string = parsed.choices[0].delta.content;
|
||||
|
||||
// Process images if enabled
|
||||
if (options?.return_images) {
|
||||
|
|
@ -231,8 +252,8 @@ export class LMStudioService {
|
|||
responseCursor: { line: number; ch: number },
|
||||
options?: LMStudioOptions
|
||||
): Promise<void> {
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content || 'No response received';
|
||||
const data = (await response.json()) as LMStudioResponse;
|
||||
const content: string = data.choices?.[0]?.message?.content || 'No response received';
|
||||
|
||||
// Process images if enabled
|
||||
let processedContent = content;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { Editor, Notice } from 'obsidian';
|
||||
import type { Editor} from 'obsidian';
|
||||
import { Notice } from 'obsidian';
|
||||
import type { PromptsService } from './promptsService';
|
||||
|
||||
export interface PerplexicaSettings {
|
||||
perplexicaEndpoint: string;
|
||||
localLLMPath: string;
|
||||
defaultModel: string;
|
||||
promptsService?: any; // Will be PromptsService type
|
||||
promptsService?: PromptsService;
|
||||
requestTemplate?: string;
|
||||
}
|
||||
|
||||
|
|
@ -12,9 +14,37 @@ export interface PerplexicaOptions {
|
|||
return_images?: boolean;
|
||||
}
|
||||
|
||||
interface PerplexicaModelRef {
|
||||
provider: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PerplexicaHistoryEntry {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface PerplexicaPayload {
|
||||
chatModel: PerplexicaModelRef;
|
||||
embeddingModel: PerplexicaModelRef;
|
||||
optimizationMode: string;
|
||||
focusMode: string;
|
||||
query: string;
|
||||
history: PerplexicaHistoryEntry[];
|
||||
systemInstructions: string;
|
||||
stream: boolean;
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
interface PerplexicaStreamEvent {
|
||||
type?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export class PerplexicaService {
|
||||
private settings: PerplexicaSettings;
|
||||
private promptsService: any;
|
||||
private promptsService: PromptsService | undefined;
|
||||
|
||||
constructor(settings: PerplexicaSettings) {
|
||||
this.settings = settings;
|
||||
|
|
@ -73,40 +103,28 @@ export class PerplexicaService {
|
|||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
// Use template if available, otherwise construct payload manually
|
||||
let payload: any;
|
||||
let payload: PerplexicaPayload;
|
||||
if (this.settings.requestTemplate) {
|
||||
try {
|
||||
const processedTemplate = this.promptsService?.processTemplate(this.settings.requestTemplate) || this.settings.requestTemplate;
|
||||
|
||||
// Strip JavaScript-style comments that would break JSON parsing
|
||||
|
||||
const cleanedTemplate = processedTemplate
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
|
||||
.replace(/\/\/.*$/gm, '') // Remove // comments
|
||||
.replace(/^\s*$/gm, '') // Remove empty lines
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
.replace(/^\s*$/gm, '')
|
||||
.trim();
|
||||
|
||||
payload = JSON.parse(cleanedTemplate);
|
||||
// Override with current parameters
|
||||
payload.chatModel = {
|
||||
provider: "ollama",
|
||||
name: this.settings.defaultModel
|
||||
|
||||
JSON.parse(cleanedTemplate);
|
||||
payload = {
|
||||
chatModel: { provider: "ollama", name: this.settings.defaultModel },
|
||||
embeddingModel: { provider: "ollama", name: this.settings.defaultModel },
|
||||
optimizationMode,
|
||||
focusMode,
|
||||
query,
|
||||
history: [{ role: "user", content: query }],
|
||||
systemInstructions: this.promptsService?.getPerplexicaSystemPrompt() || "You are a helpful AI assistant. Provide clear, concise, and accurate information.",
|
||||
stream,
|
||||
};
|
||||
payload.embeddingModel = {
|
||||
provider: "ollama",
|
||||
name: this.settings.defaultModel
|
||||
};
|
||||
payload.optimizationMode = optimizationMode;
|
||||
payload.focusMode = focusMode;
|
||||
payload.query = query;
|
||||
payload.history = [
|
||||
{
|
||||
role: "user",
|
||||
content: query
|
||||
}
|
||||
];
|
||||
payload.systemInstructions = this.promptsService?.getPerplexicaSystemPrompt() || "You are a helpful AI assistant. Provide clear, concise, and accurate information.";
|
||||
payload.stream = stream;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse request template, using default payload:', error);
|
||||
payload = {
|
||||
|
|
@ -211,31 +229,27 @@ export class PerplexicaService {
|
|||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
if (parsed.type === 'response' && parsed.data) {
|
||||
let content = parsed.data;
|
||||
|
||||
// Process images if enabled
|
||||
if (options?.return_images) {
|
||||
content = this.processContentWithImages(content);
|
||||
}
|
||||
|
||||
editor.replaceRange(content, currentPos);
|
||||
// Update cursor position
|
||||
const lines = content.split('\n');
|
||||
if (lines.length === 1) {
|
||||
currentPos = { line: currentPos.line, ch: currentPos.ch + content.length };
|
||||
} else {
|
||||
currentPos = {
|
||||
line: currentPos.line + lines.length - 1,
|
||||
ch: lines[lines.length - 1]?.length || 0
|
||||
};
|
||||
}
|
||||
// Scroll to follow the new content
|
||||
editor.scrollIntoView({ from: currentPos, to: currentPos }, true);
|
||||
// Small delay to make scrolling smoother
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
const parsed = JSON.parse(line) as PerplexicaStreamEvent;
|
||||
if (parsed.type === 'response' && typeof parsed.data === 'string') {
|
||||
let content: string = parsed.data;
|
||||
|
||||
if (options?.return_images) {
|
||||
content = this.processContentWithImages(content);
|
||||
}
|
||||
|
||||
editor.replaceRange(content, currentPos);
|
||||
const splitLines = content.split('\n');
|
||||
if (splitLines.length === 1) {
|
||||
currentPos = { line: currentPos.line, ch: currentPos.ch + content.length };
|
||||
} else {
|
||||
currentPos = {
|
||||
line: currentPos.line + splitLines.length - 1,
|
||||
ch: splitLines[splitLines.length - 1]?.length || 0
|
||||
};
|
||||
}
|
||||
editor.scrollIntoView({ from: currentPos, to: currentPos }, true);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Editor, Notice, request } from 'obsidian';
|
||||
import type { Editor} from 'obsidian';
|
||||
import { Notice, request } from 'obsidian';
|
||||
import type { PromptsService } from './promptsService';
|
||||
import { formatCitationDate, getMostRecentDate, formatPublicationInfo } from '../utils/formatDate';
|
||||
|
||||
export interface PerplexityOptions {
|
||||
|
|
@ -11,14 +13,62 @@ export interface PerplexityOptions {
|
|||
export interface PerplexitySettings {
|
||||
perplexityApiKey: string;
|
||||
perplexityEndpoint: string;
|
||||
promptsService?: any; // Will be PromptsService type
|
||||
promptsService?: PromptsService;
|
||||
requestTemplate?: string;
|
||||
headerPosition?: 'top' | 'bottom';
|
||||
}
|
||||
|
||||
export interface PerplexityImage {
|
||||
image_url?: string;
|
||||
origin_url?: string;
|
||||
}
|
||||
|
||||
export interface PerplexitySource {
|
||||
title?: string;
|
||||
url?: string;
|
||||
date?: string;
|
||||
last_updated?: string;
|
||||
}
|
||||
|
||||
interface PerplexityMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface PerplexityPayload {
|
||||
model: string;
|
||||
messages: PerplexityMessage[];
|
||||
stream: boolean;
|
||||
return_citations: boolean;
|
||||
return_images: boolean;
|
||||
return_related_questions: boolean;
|
||||
search_recency_filter?: string;
|
||||
}
|
||||
|
||||
interface PerplexityChoice {
|
||||
message?: { content?: string };
|
||||
delta?: { content?: string };
|
||||
finish_reason?: string;
|
||||
}
|
||||
|
||||
interface PerplexityResponse {
|
||||
choices?: PerplexityChoice[];
|
||||
images?: PerplexityImage[];
|
||||
search_results?: PerplexitySource[];
|
||||
citations?: (string | PerplexitySource)[];
|
||||
}
|
||||
|
||||
interface PerplexityStreamChunk {
|
||||
choices?: PerplexityChoice[];
|
||||
images?: PerplexityImage[];
|
||||
search_results?: PerplexitySource[];
|
||||
citations?: (string | PerplexitySource)[];
|
||||
}
|
||||
|
||||
export class PerplexityService {
|
||||
private settings: PerplexitySettings;
|
||||
private promptsService: any;
|
||||
private promptsService: PromptsService | undefined;
|
||||
private loadingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(settings: PerplexitySettings) {
|
||||
this.settings = settings;
|
||||
|
|
@ -48,7 +98,7 @@ export class PerplexityService {
|
|||
return 'month';
|
||||
}
|
||||
|
||||
private processContentWithImages(content: string, images: any[]): string {
|
||||
private processContentWithImages(content: string, images: PerplexityImage[]): string {
|
||||
if (!images || images.length === 0) return content;
|
||||
|
||||
console.log(`🖼️ Processing ${images.length} images for content replacement`);
|
||||
|
|
@ -221,15 +271,14 @@ export class PerplexityService {
|
|||
}
|
||||
|
||||
private clearLoadingAnimation(): void {
|
||||
// Clear the animation interval if it exists
|
||||
if ((this as any).loadingInterval) {
|
||||
if (this.loadingInterval) {
|
||||
console.log('🛑 Clearing loading animation interval');
|
||||
clearInterval((this as any).loadingInterval);
|
||||
(this as any).loadingInterval = null;
|
||||
clearInterval(this.loadingInterval);
|
||||
this.loadingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private addCitations(editor: Editor, sources: any[]): void {
|
||||
private addCitations(editor: Editor, sources: (string | PerplexitySource)[]): void {
|
||||
if (!sources || sources.length === 0) return;
|
||||
|
||||
console.log(`📚 Processing ${sources.length} sources for citations`);
|
||||
|
|
@ -393,79 +442,65 @@ export class PerplexityService {
|
|||
try {
|
||||
const convertedFilter = this.convertRecencyFilter(options?.search_recency_filter ?? "month");
|
||||
|
||||
// Use template if available, otherwise construct payload manually
|
||||
let payload: any;
|
||||
let payload: PerplexityPayload;
|
||||
if (this.settings.requestTemplate) {
|
||||
try {
|
||||
const processedTemplate = this.promptsService?.processTemplate(this.settings.requestTemplate) || this.settings.requestTemplate;
|
||||
|
||||
// Strip JavaScript-style comments that would break JSON parsing
|
||||
|
||||
let cleanedTemplate = processedTemplate
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
|
||||
.replace(/\/\/.*$/gm, '') // Remove // comments
|
||||
.replace(/^\s*$/gm, '') // Remove empty lines
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
.replace(/^\s*$/gm, '')
|
||||
.trim();
|
||||
|
||||
// Check if template contains JavaScript code instead of JSON
|
||||
|
||||
if (cleanedTemplate.includes('const ') || cleanedTemplate.includes('fetch(') || cleanedTemplate.includes('await ')) {
|
||||
console.warn('⚠️ Template contains JavaScript code, not JSON. Extracting payload object...');
|
||||
|
||||
// Try to extract JSON payload from JavaScript code
|
||||
|
||||
const payloadMatch = cleanedTemplate.match(/payload\s*=\s*({[\s\S]*?});/);
|
||||
if (payloadMatch) {
|
||||
let jsObject = payloadMatch[1];
|
||||
console.log('🔍 Extracted payload from JavaScript:', jsObject);
|
||||
|
||||
// Convert JavaScript object syntax to valid JSON
|
||||
// Replace unquoted property names with quoted ones
|
||||
jsObject = jsObject.replace(/(\w+):/g, '"$1":');
|
||||
// Fix single quotes to double quotes
|
||||
jsObject = jsObject.replace(/'/g, '"');
|
||||
|
||||
cleanedTemplate = jsObject;
|
||||
|
||||
jsObject = jsObject?.replace(/(\w+):/g, '"$1":').replace(/'/g, '"');
|
||||
|
||||
cleanedTemplate = jsObject ?? cleanedTemplate;
|
||||
} else {
|
||||
throw new Error('Template contains JavaScript code but no valid payload object found. Please use JSON format only.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('🧹 Final cleaned template:', cleanedTemplate);
|
||||
payload = JSON.parse(cleanedTemplate);
|
||||
// Override with current query and options
|
||||
payload.model = model;
|
||||
payload.messages = [
|
||||
{ role: 'user', content: query }
|
||||
];
|
||||
payload.stream = useStreaming;
|
||||
payload.return_citations = options?.return_citations ?? true;
|
||||
payload.return_images = options?.return_images ?? true;
|
||||
payload.return_related_questions = options?.return_related_questions ?? false;
|
||||
JSON.parse(cleanedTemplate);
|
||||
payload = {
|
||||
model,
|
||||
messages: [{ role: 'user', content: query }],
|
||||
stream: useStreaming,
|
||||
return_citations: options?.return_citations ?? true,
|
||||
return_images: options?.return_images ?? true,
|
||||
return_related_questions: options?.return_related_questions ?? false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse request template, using default payload:', error);
|
||||
payload = {
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'user', content: query }
|
||||
],
|
||||
messages: [{ role: 'user', content: query }],
|
||||
stream: useStreaming,
|
||||
return_citations: options?.return_citations ?? true,
|
||||
return_images: options?.return_images ?? true,
|
||||
return_related_questions: options?.return_related_questions ?? false
|
||||
return_related_questions: options?.return_related_questions ?? false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'user', content: query }
|
||||
],
|
||||
messages: [{ role: 'user', content: query }],
|
||||
stream: useStreaming,
|
||||
return_citations: options?.return_citations ?? true,
|
||||
return_images: options?.return_images ?? true,
|
||||
return_related_questions: options?.return_related_questions ?? false
|
||||
return_related_questions: options?.return_related_questions ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
// Only include search_recency_filter if we have a filter value
|
||||
|
||||
if (convertedFilter !== undefined) {
|
||||
payload.search_recency_filter = convertedFilter;
|
||||
}
|
||||
|
|
@ -516,33 +551,25 @@ export class PerplexityService {
|
|||
});
|
||||
|
||||
console.log('✅ Non-streaming response received');
|
||||
// Parse the response
|
||||
const data = JSON.parse(response);
|
||||
const data = JSON.parse(response) as PerplexityResponse;
|
||||
|
||||
console.log('📊 Response data structure:', Object.keys(data));
|
||||
// Process the response
|
||||
if (data.choices && data.choices.length > 0) {
|
||||
let content = data.choices[0].message.content;
|
||||
|
||||
// Process think blocks first
|
||||
let content = data.choices[0]?.message?.content ?? '';
|
||||
|
||||
content = this.processThinkBlocks(content);
|
||||
|
||||
// Process images if available
|
||||
|
||||
if (options?.return_images && data.images && data.images.length > 0) {
|
||||
console.log('🖼️ Processing images in non-streaming response:', data.images.length, 'images found');
|
||||
content = this.processContentWithImages(content, data.images);
|
||||
}
|
||||
|
||||
// Insert the response at the cursor position
|
||||
|
||||
editor.replaceRange(content, responseCursor);
|
||||
|
||||
// Add citations if available
|
||||
|
||||
if (options?.return_citations) {
|
||||
if (data.search_results && data.search_results.length > 0) {
|
||||
// Use search_results for detailed info (preferred for Perplexity)
|
||||
this.addCitations(editor, data.search_results);
|
||||
} else if (data.citations && data.citations.length > 0) {
|
||||
// Fallback to citations array (could be URLs or other format)
|
||||
this.addCitations(editor, data.citations);
|
||||
}
|
||||
}
|
||||
|
|
@ -605,43 +632,39 @@ export class PerplexityService {
|
|||
console.log(`🔄 Starting streaming response handler [${requestId || 'unknown'}]`);
|
||||
|
||||
let buffer = '';
|
||||
let currentPos = { ...responseCursor };
|
||||
let finalResponseData: any = null;
|
||||
|
||||
const currentPos = { ...responseCursor };
|
||||
let finalResponseData: PerplexityStreamChunk | null = null;
|
||||
|
||||
console.log(`📍 Initial currentPos:`, currentPos);
|
||||
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
|
||||
const chunk = new TextDecoder().decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
|
||||
// Process complete lines from buffer
|
||||
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
|
||||
|
||||
try {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.replace('data: ', '').trim();
|
||||
if (data === '[DONE]') continue;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// Capture the latest metadata across chunks. Perplexity typically
|
||||
// sends citations/search_results in earlier chunks; the final chunk
|
||||
// often carries only finish_reason. Merge so nothing is lost.
|
||||
|
||||
const parsed = JSON.parse(data) as PerplexityStreamChunk;
|
||||
|
||||
if (parsed.citations || parsed.images || parsed.search_results) {
|
||||
finalResponseData = {
|
||||
...(finalResponseData || {}),
|
||||
...parsed,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (parsed.choices?.[0]?.delta?.content) {
|
||||
const content = parsed.choices[0].delta.content;
|
||||
if (content) {
|
||||
|
|
@ -654,13 +677,12 @@ export class PerplexityService {
|
|||
|
||||
// console.log(`📝 Inserting content at position:`, currentPos, `Content:`, content.substring(0, 50) + '...');
|
||||
editor.replaceRange(content, currentPos);
|
||||
// Update cursor position after insertion
|
||||
const contentLines = content.split('\n');
|
||||
if (contentLines.length === 1) {
|
||||
currentPos.ch += content.length;
|
||||
} else {
|
||||
currentPos.line += contentLines.length - 1;
|
||||
currentPos.ch = contentLines[contentLines.length - 1].length;
|
||||
currentPos.ch = contentLines[contentLines.length - 1]?.length ?? 0;
|
||||
}
|
||||
// Scroll to follow the new content
|
||||
editor.scrollIntoView({ from: currentPos, to: currentPos }, true);
|
||||
|
|
@ -704,7 +726,7 @@ export class PerplexityService {
|
|||
}
|
||||
|
||||
private async processStreamingMetadata(
|
||||
finalResponseData: any,
|
||||
finalResponseData: PerplexityStreamChunk,
|
||||
editor: Editor,
|
||||
headerText?: string
|
||||
): Promise<void> {
|
||||
|
|
@ -758,7 +780,7 @@ export class PerplexityService {
|
|||
|
||||
// Insert images inline after the query header
|
||||
let imagesSection = '\n\n';
|
||||
finalResponseData.images.forEach((image: any, index: number) => {
|
||||
finalResponseData.images.forEach((image: PerplexityImage, index: number) => {
|
||||
if (image.image_url) {
|
||||
imagesSection += `\n\n`;
|
||||
}
|
||||
|
|
@ -774,7 +796,7 @@ export class PerplexityService {
|
|||
} else {
|
||||
// Fallback: add images at the end if no markers found
|
||||
let imagesSection = '\n\n## Images\n\n';
|
||||
finalResponseData.images.forEach((image: any, index: number) => {
|
||||
finalResponseData.images.forEach((image: PerplexityImage, index: number) => {
|
||||
if (image.image_url) {
|
||||
imagesSection += `\n`;
|
||||
if (image.origin_url) {
|
||||
|
|
|
|||
8
src/types/obsidian.d.ts
vendored
8
src/types/obsidian.d.ts
vendored
|
|
@ -1,8 +1,12 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
|
||||
import type { TFile } from 'obsidian';
|
||||
|
||||
declare module 'obsidian' {
|
||||
interface CommandsApi {
|
||||
commands: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface App {
|
||||
commands: any;
|
||||
commands: CommandsApi;
|
||||
}
|
||||
|
||||
interface Editor {
|
||||
|
|
|
|||
33
src/utils/coerce.ts
Normal file
33
src/utils/coerce.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export function asString(v: unknown): string | undefined {
|
||||
if (typeof v === 'string') return v;
|
||||
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function asNumber(v: unknown): number | undefined {
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||
if (typeof v === 'string') {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function asStringArray(v: unknown): string[] {
|
||||
if (Array.isArray(v)) {
|
||||
return v.map(asString).filter((s): s is string => s !== undefined);
|
||||
}
|
||||
const single = asString(v);
|
||||
return single === undefined ? [] : [single];
|
||||
}
|
||||
|
||||
export function asDate(v: unknown): string | undefined {
|
||||
const s = asString(v);
|
||||
if (s === undefined) return undefined;
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
|
||||
}
|
||||
|
||||
export function isRecord(v: unknown): v is Record<string, unknown> {
|
||||
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
}
|
||||
|
|
@ -1,29 +1,24 @@
|
|||
/**
|
||||
* Date formatting utilities for the Perplexed plugin
|
||||
*/
|
||||
import { asString, isRecord } from './coerce';
|
||||
|
||||
export interface DateBearingSource {
|
||||
date?: unknown;
|
||||
last_updated?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date string into the format: "YYYY, MMM DD"
|
||||
* Examples: "2024, Dec 13", "2025, Jan 05"
|
||||
*
|
||||
* @param dateString - Date string in various formats (ISO, YYYY-MM-DD, etc.)
|
||||
* @returns Formatted date string or empty string if invalid
|
||||
*/
|
||||
export function formatCitationDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
|
||||
// Check if date is valid
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
|
||||
return `${year}, ${month} ${day}`;
|
||||
} catch (error) {
|
||||
console.warn('Failed to format date:', dateString, error);
|
||||
|
|
@ -31,39 +26,20 @@ export function formatCitationDate(dateString: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recent date from available date fields
|
||||
* Prioritizes last_updated over date
|
||||
*
|
||||
* @param source - Source object with potential date fields
|
||||
* @returns Most recent date string or empty string if none available
|
||||
*/
|
||||
export function getMostRecentDate(source: any): string {
|
||||
if (source.last_updated) {
|
||||
return source.last_updated;
|
||||
}
|
||||
if (source.date) {
|
||||
return source.date;
|
||||
}
|
||||
return '';
|
||||
export function getMostRecentDate(source: unknown): string {
|
||||
if (!isRecord(source)) return '';
|
||||
return asString(source.last_updated) ?? asString(source.date) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the publication info string for citations
|
||||
* Format: "Published: YYYY-MM-DD | Updated: YYYY-MM-DD"
|
||||
*
|
||||
* @param source - Source object with potential date fields
|
||||
* @returns Publication info string or empty string if no dates available
|
||||
*/
|
||||
export function formatPublicationInfo(source: any): string {
|
||||
const dateInfo = [];
|
||||
|
||||
if (source.date) {
|
||||
dateInfo.push(`Published: ${source.date}`);
|
||||
}
|
||||
if (source.last_updated) {
|
||||
dateInfo.push(`Updated: ${source.last_updated}`);
|
||||
}
|
||||
|
||||
export function formatPublicationInfo(source: unknown): string {
|
||||
if (!isRecord(source)) return '';
|
||||
const dateInfo: string[] = [];
|
||||
|
||||
const date = asString(source.date);
|
||||
if (date) dateInfo.push(`Published: ${date}`);
|
||||
|
||||
const lastUpdated = asString(source.last_updated);
|
||||
if (lastUpdated) dateInfo.push(`Updated: ${lastUpdated}`);
|
||||
|
||||
return dateInfo.join(' | ');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { TFile, Vault } from 'obsidian';
|
||||
import type { Vault } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: string;
|
||||
level: 'error' | 'warn' | 'info' | 'debug';
|
||||
message: string;
|
||||
details?: any;
|
||||
details?: unknown;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +79,7 @@ export class FileLogger {
|
|||
}
|
||||
}
|
||||
|
||||
private addEntry(level: LogEntry['level'], message: string, details?: any): void {
|
||||
private addEntry(level: LogEntry['level'], message: string, details?: unknown): void {
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
|
|
@ -106,19 +107,19 @@ export class FileLogger {
|
|||
logMethod(`[${entry.timestamp}] [${level.toUpperCase()}] ${message}`, details || '');
|
||||
}
|
||||
|
||||
error(message: string, details?: any): void {
|
||||
error(message: string, details?: unknown): void {
|
||||
this.addEntry('error', message, details);
|
||||
}
|
||||
|
||||
warn(message: string, details?: any): void {
|
||||
warn(message: string, details?: unknown): void {
|
||||
this.addEntry('warn', message, details);
|
||||
}
|
||||
|
||||
info(message: string, details?: any): void {
|
||||
info(message: string, details?: unknown): void {
|
||||
this.addEntry('info', message, details);
|
||||
}
|
||||
|
||||
debug(message: string, details?: any): void {
|
||||
debug(message: string, details?: unknown): void {
|
||||
this.addEntry('debug', message, details);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"0.0.1.0": "0.15.0"
|
||||
"0.1.0": "0.15.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue