Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Code Ownership Boundaries for filler-de
# See docs/ARCHITECTURE.md for domain descriptions.
#
# Each source file is assigned to exactly one ownership domain.
# Patterns are ordered so that no two rules match the same file.

# --- plugin-core: lifecycle, configuration, shared types ---
/src/main.ts @domain/plugin-core
/src/settings.ts @domain/plugin-core
/src/types.ts @domain/plugin-core

# --- commands: user-facing Obsidian commands ---
/src/commands/ @domain/commands

# --- ai-api: external LLM integration ---
/src/api.ts @domain/ai-api

# --- prompt-engineering: prompt templates and builders ---
/src/prompts/ @domain/prompt-engineering

# --- filesystem: vault file read/write helpers ---
/src/utils.ts @domain/filesystem
/src/file.ts @domain/filesystem
90 changes: 90 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# filler-de Architecture

## Ownership Domains

The plugin is divided into five logical ownership domains. Each domain
groups files by responsibility so that changes within a domain can be
reviewed by the team most familiar with that area.

### 1. plugin-core

Entry point, settings UI, and shared type definitions.

| File | Purpose |
|------|---------|
| `src/main.ts` | Plugin lifecycle (`onload` / `onunload`), command registration |
| `src/settings.ts` | Settings tab, defaults, persistence |
| `src/types.ts` | Shared TypeScript interfaces and type aliases |

### 2. commands

User-facing Obsidian commands that orchestrate the other domains.

| File | Purpose |
|------|---------|
| `src/commands/addBacklinksToCurrentFile.ts` | Add backlinks to current note |
| `src/commands/endgame.ts` | Endgame command logic |
| `src/commands/fillTemplate.ts` | Fill a note template via AI |
| `src/commands/formatSelectionWithNumber.ts` | Format selected text with numbering |
| `src/commands/functions.ts` | Shared command helpers |
| `src/commands/getInfinitiveAndEmoji.ts` | Look up infinitive + emoji |
| `src/commands/insertReplyFromC1Richter.ts` | Insert C1 Richter reply |
| `src/commands/insertReplyFromKeymaker.ts` | Insert Keymaker reply |
| `src/commands/normalizeSelection.ts` | Normalize selected text |
| `src/commands/translateSelection.ts` | Translate selected text |

### 3. ai-api

HTTP client for the external LLM provider.

| File | Purpose |
|------|---------|
| `src/api.ts` | API request construction, response parsing, error handling |

### 4. prompt-engineering

Prompt templates and builders sent to the LLM.

| File | Purpose |
|------|---------|
| `src/prompts/index.ts` | Re-exports / prompt registry |
| `src/prompts/baseDict.ts` | Base dictionary prompt |
| `src/prompts/c1Richter.ts` | C1 Richter prompt |
| `src/prompts/determine-infinitive-and-pick-emoji.ts` | Infinitive + emoji prompt |
| `src/prompts/full-dict-enrtie.ts` | Full dictionary entry prompt |
| `src/prompts/generate-forms.ts` | Word-form generation prompt |
| `src/prompts/keymaker.ts` | Keymaker prompt |
| `src/prompts/morphems.ts` | Morpheme analysis prompt |
| `src/prompts/normalize.ts` | Normalization prompt |
| `src/prompts/translate-de-to-eng.ts` | DE-to-EN translation prompt |
| `src/prompts/valence.ts` | Verb valence prompt |
| `src/prompts/wip_keymaker.ts` | Keymaker (work-in-progress) prompt |

### 5. filesystem

Vault file-system helpers for reading and writing notes.

| File | Purpose |
|------|---------|
| `src/utils.ts` | Path resolution, directory sharding, file creation helpers |
| `src/file.ts` | File read/write operations |

## Dependency Flow

```
plugin-core
|
v
commands
/ \
v v
ai-api filesystem
|
v
prompt-engineering
```

`plugin-core` registers commands. Each command may call into `ai-api`
(which uses prompts from `prompt-engineering`) and `filesystem` to read
or write vault files. Domains at the bottom of the graph should never
import from domains above them.
17 changes: 6 additions & 11 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { z } from 'zod';

import {
GoogleGenerativeAI,
GenerationConfig,
HarmCategory,
HarmBlockThreshold,
ResponseSchema,
} from '@google/generative-ai';
import { TextEaterSettings } from './types';
import { TFile, Vault, Notice, TAbstractFile, requestUrl } from 'obsidian';
import { Vault, Notice } from 'obsidian';
import { prompts } from './prompts';

export class ApiService {
Expand All @@ -34,7 +29,6 @@ export class ApiService {
userInput: string,
responseSchema?: boolean
): Promise<string> {
const startTime = performance.now();
try {
let response: string | null = null;
// Remove leading tab characters from the system prompt
Expand Down Expand Up @@ -66,7 +60,7 @@ export class ApiService {
responseMimeType: `application/json`,
};

const chatKey = systemPrompt;
const chatKey = `${systemPrompt}::${!!responseSchema}`;
if (!this.chatSessions[chatKey]) {
const model = this.genAI.getGenerativeModel({
model: this.model,
Expand All @@ -80,14 +74,15 @@ export class ApiService {

const chatSession = this.chatSessions[chatKey];

const result = await chatSession.sendMessage(userInput);
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('API request timed out after 30 seconds')), 30_000)
);
const result = await Promise.race([chatSession.sendMessage(userInput), timeout]);
response = result.response.text();

const logResponse = response === null ? '' : response;
return logResponse;
} catch (error: any) {
const endTime = performance.now();
const duration = endTime - startTime;
throw new Error(error.message);
}
}
Expand Down
74 changes: 0 additions & 74 deletions src/commands/endgame.ts

This file was deleted.

20 changes: 12 additions & 8 deletions src/commands/fillTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Editor, MarkdownView, Notice, TFile } from 'obsidian';
import { Editor, Notice, TFile } from 'obsidian';
import TextEaterPlugin from '../main';
import { prompts } from '../prompts';
import { longDash } from '../utils';

function blockOrEmpty(block: string): string {
return block.replace('\n', '') === longDash ? '' : block;
}

function extractFirstBracketedWord(text: string) {
const match = text.match(/\[\[([^\]]+)\]\]/);
return match ? match[1] : null;
Expand Down Expand Up @@ -78,6 +82,7 @@ export default async function fillTemplate(
callBack?: () => void
) {
const word = file.basename;
const notice = new Notice('Generating…', 0);

try {
const [dictionaryEntry, froms, morphems, valence] = await Promise.all([
Expand All @@ -97,13 +102,10 @@ export default async function fillTemplate(
const baseBlock = await incertClipbordContentsInContextsBlock(
incertYouglishLinkInIpa(trimmedBaseEntrie)
);
const morphemsBlock =
morphems.replace('\n', '') === longDash ? '' : `${morphems}\n`;
const valenceBlock =
valence.replace('\n', '') === longDash ? '' : `${valence}`;
const fromsBlock = froms.replace('\n', '') === longDash ? '' : `${froms}`;
const adjFormsBlock =
adjForms.replace('\n', '') === longDash ? '' : `${adjForms}`;
const morphemsBlock = blockOrEmpty(morphems);
const valenceBlock = blockOrEmpty(valence);
const fromsBlock = blockOrEmpty(froms);
const adjFormsBlock = blockOrEmpty(adjForms);

const blocks = [
baseBlock,
Expand All @@ -127,6 +129,8 @@ export default async function fillTemplate(
}
} catch (error) {
new Notice(`Error: ${error.message}`);
} finally {
notice.hide();
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/commands/getInfinitiveAndEmoji.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor, MarkdownView, Notice, TFile } from 'obsidian';
import { Editor, Notice, TFile } from 'obsidian';
import TextEaterPlugin from '../main';

export default async function getInfinitiveAndEmoji(
Expand All @@ -7,6 +7,7 @@ export default async function getInfinitiveAndEmoji(
file: TFile
) {
const word = file.basename;
const notice = new Notice('Generating…', 0);

try {
let response = await plugin.apiService.determineInfinitiveAndEmoji(word);
Expand All @@ -17,5 +18,7 @@ export default async function getInfinitiveAndEmoji(
}
} catch (error) {
new Notice(`Error: ${error.message}`);
} finally {
notice.hide();
}
}
7 changes: 5 additions & 2 deletions src/commands/insertReplyFromC1Richter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Editor } from 'obsidian';
import { Editor, Notice } from 'obsidian';
import TextEaterPlugin from '../main';
import { cleanMarkdownFormatting } from './functions';

Expand All @@ -7,6 +7,7 @@ export default async function insertReplyFromC1Richter(
editor: Editor,
selection: string
) {
const notice = new Notice('Generating…', 0);
try {
const response = await plugin.apiService.consultC1Richter(
cleanMarkdownFormatting(selection)
Expand All @@ -15,6 +16,8 @@ export default async function insertReplyFromC1Richter(
editor.replaceSelection(selection + '\n' + response.trim());
}
} catch (error) {
console.error('Error in C1 Richter command:', error);
new Notice(`Error: ${error.message}`);
} finally {
notice.hide();
}
}
3 changes: 3 additions & 0 deletions src/commands/insertReplyFromKeymaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ export default async function insertReplyFromKeymaker(
editor: Editor,
selection: string
) {
const notice = new Notice('Generating…', 0);
try {
const response = await plugin.apiService.consultKeymaker(selection);
if (response) {
editor.replaceSelection(selection + '\n' + response + '\n');
}
} catch (error) {
new Notice(`Error: ${error.message}`);
} finally {
notice.hide();
}
}
3 changes: 3 additions & 0 deletions src/commands/normalizeSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ export default async function normalizeSelection(
file: TFile,
selection: string
) {
const notice = new Notice('Generating…', 0);
try {
const response = await plugin.apiService.normalize(selection);
if (response) {
editor.replaceSelection(response);
}
} catch (error) {
new Notice(`Error: ${error.message}`);
} finally {
notice.hide();
}
}
3 changes: 3 additions & 0 deletions src/commands/translateSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default async function translateSelection(
editor: Editor,
selection: string
) {
const notice = new Notice('Generating…', 0);
try {
const cursor = editor.getCursor();
const response = await plugin.apiService.translateText(selection);
Expand All @@ -18,5 +19,7 @@ export default async function translateSelection(
}
} catch (error) {
new Notice(`Error: ${error.message}`);
} finally {
notice.hide();
}
}
Loading