Skip to content

Commit 49180b3

Browse files
u
1 parent da16039 commit 49180b3

6 files changed

Lines changed: 185 additions & 38 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ coverage/
3838
# Claude flow (generated)
3939
.claude-flow/
4040
.hive-mind/
41+
.sisyphus/

src/entrypoints/background.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@ export default defineBackground(() => {
5252
return;
5353
}
5454

55-
// Perform grammar check
56-
const result = await checkGrammar(text, {
57-
provider: settings.provider,
58-
model: settings.model,
59-
apiKey,
60-
language: settings.language,
61-
});
55+
// Perform grammar check - use customModel if model is 'custom'
56+
const modelId = settings.model === 'custom' ? settings.customModel : settings.model;
57+
const result = await checkGrammar(text, {
58+
provider: settings.provider,
59+
model: modelId || settings.model,
60+
apiKey,
61+
language: settings.language,
62+
});
6263

6364
// Filter out words in personal dictionary
6465
const dictionary = await dictionaryStorage.getValue();
@@ -102,11 +103,12 @@ export default defineBackground(() => {
102103
return;
103104
}
104105

105-
const rewritten = await rewriteText(text, style, {
106-
provider: settings.provider,
107-
model: settings.model,
108-
apiKey,
109-
});
106+
const modelId = settings.model === 'custom' ? settings.customModel : settings.model;
107+
const rewritten = await rewriteText(text, style, {
108+
provider: settings.provider,
109+
model: modelId || settings.model,
110+
apiKey,
111+
});
110112

111113
sendResponse({ success: true, result: rewritten });
112114
break;

src/entrypoints/content.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,124 @@ export default defineContentScript({
4949
console.error("Failed to watch settings:", error);
5050
}
5151

52+
// TextareaObserver for dynamic element detection
53+
class TextareaObserver {
54+
private observer: MutationObserver | null = null;
55+
private processedElements = new WeakSet<HTMLElement>();
56+
private pendingElements = new Set<HTMLElement>();
57+
private rafId: number | null = null;
58+
59+
start() {
60+
if (this.observer) return;
61+
62+
// Process existing textareas first
63+
this.scanExistingElements();
64+
65+
this.observer = new MutationObserver((mutations) => {
66+
this.handleMutations(mutations);
67+
});
68+
69+
this.observer.observe(document.body, {
70+
childList: true,
71+
subtree: true,
72+
attributes: true,
73+
attributeFilter: ["contenteditable", "role"],
74+
});
75+
}
76+
77+
stop() {
78+
if (this.rafId) {
79+
cancelAnimationFrame(this.rafId);
80+
this.rafId = null;
81+
}
82+
if (this.observer) {
83+
this.observer.disconnect();
84+
this.observer = null;
85+
}
86+
this.pendingElements.clear();
87+
}
88+
89+
private scanExistingElements() {
90+
const textareas = document.querySelectorAll(
91+
'textarea, input, [contenteditable="true"], [role="textbox"]'
92+
);
93+
textareas.forEach((el) => {
94+
if (el instanceof HTMLElement && isEditableElement(el)) {
95+
this.scheduleProcessing(el);
96+
}
97+
});
98+
}
99+
100+
private handleMutations(mutations: MutationRecord[]) {
101+
for (const mutation of mutations) {
102+
// Handle attribute changes (contenteditable or role added to existing element)
103+
if (mutation.type === "attributes" && mutation.target instanceof HTMLElement) {
104+
this.checkElement(mutation.target);
105+
}
106+
107+
// Handle new nodes
108+
for (const node of mutation.addedNodes) {
109+
if (node instanceof HTMLElement) {
110+
this.checkElement(node);
111+
}
112+
}
113+
}
114+
}
115+
116+
private checkElement(element: HTMLElement) {
117+
// Check the element itself
118+
if (
119+
isEditableElement(element) &&
120+
!this.processedElements.has(element)
121+
) {
122+
this.scheduleProcessing(element);
123+
}
124+
125+
// Check children
126+
const editables = element.querySelectorAll(
127+
'textarea, input, [contenteditable="true"], [role="textbox"]'
128+
);
129+
editables.forEach((el) => {
130+
if (
131+
el instanceof HTMLElement &&
132+
isEditableElement(el) &&
133+
!this.processedElements.has(el)
134+
) {
135+
this.scheduleProcessing(el);
136+
}
137+
});
138+
}
139+
140+
private scheduleProcessing(element: HTMLElement) {
141+
this.pendingElements.add(element);
142+
143+
if (!this.rafId) {
144+
this.rafId = requestAnimationFrame(() => {
145+
this.processBatch();
146+
});
147+
}
148+
}
149+
150+
private processBatch() {
151+
this.rafId = null;
152+
153+
this.pendingElements.forEach((element) => {
154+
if (
155+
!this.processedElements.has(element) &&
156+
document.contains(element)
157+
) {
158+
this.processedElements.add(element);
159+
handleFocus(element);
160+
}
161+
});
162+
163+
this.pendingElements.clear();
164+
}
165+
}
166+
167+
const textareaObserver = new TextareaObserver();
168+
textareaObserver.start();
169+
52170
// Styles for Shadow DOM
53171
const STYLES = `
54172
* {
@@ -1182,6 +1300,7 @@ export default defineContentScript({
11821300
);
11831301

11841302
ctx.onInvalidated(() => {
1303+
textareaObserver.stop();
11851304
cleanup();
11861305
if (unwatchSettings) unwatchSettings();
11871306
});

src/entrypoints/options/App.tsx

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,18 @@ export default function App() {
7676
</header>
7777

7878
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
79-
{/* Tab navigation */}
80-
<div className="border-b border-gray-200">
81-
<nav className="flex gap-1 p-2">
79+
{/* Tab navigation */}
80+
<div className="border-b border-gray-200">
81+
<nav className="flex flex-col sm:flex-row gap-1 p-2">
8282
{tabs.map((tab) => (
8383
<button
8484
key={tab.id}
8585
onClick={() => setActiveTab(tab.id)}
86-
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${
87-
activeTab === tab.id
88-
? 'bg-blue-100 text-blue-700'
89-
: 'text-gray-600 hover:bg-gray-100'
90-
}`}
86+
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors w-full sm:w-auto ${
87+
activeTab === tab.id
88+
? 'bg-blue-100 text-blue-700'
89+
: 'text-gray-600 hover:bg-gray-100'
90+
}`}
9191
>
9292
{tab.icon}
9393
{tab.label}
@@ -248,20 +248,44 @@ function SettingsTab({
248248
</select>
249249
</div>
250250

251-
<div>
252-
<label className="block font-medium text-gray-900 mb-2">Model</label>
253-
<select
254-
value={settings.model}
255-
onChange={(e) => saveSettings({ ...settings, model: e.target.value })}
256-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
257-
>
258-
{AVAILABLE_MODELS[settings.provider].map((model) => (
259-
<option key={model.id} value={model.id}>
260-
{model.name}
261-
</option>
262-
))}
263-
</select>
264-
</div>
251+
<div>
252+
<label className="block font-medium text-gray-900 mb-2">Model</label>
253+
<select
254+
value={settings.model}
255+
onChange={(e) => {
256+
const model = e.target.value;
257+
if (model === 'custom') {
258+
saveSettings({ ...settings, model: 'custom' });
259+
} else {
260+
saveSettings({ ...settings, model, customModel: undefined });
261+
}
262+
}}
263+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
264+
>
265+
{AVAILABLE_MODELS[settings.provider].map((model) => (
266+
<option key={model.id} value={model.id}>
267+
{model.name}
268+
</option>
269+
))}
270+
<option value="custom">Use custom model...</option>
271+
</select>
272+
</div>
273+
274+
{settings.model === 'custom' && (
275+
<div className="mt-3">
276+
<label className="block font-medium text-gray-900 mb-2">Custom Model ID</label>
277+
<input
278+
type="text"
279+
value={settings.customModel || ''}
280+
onChange={(e) => saveSettings({ ...settings, customModel: e.target.value })}
281+
placeholder="e.g., gpt-4-turbo-preview"
282+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
283+
/>
284+
<p className="text-sm text-gray-500 mt-1">
285+
Enter the exact model ID from your AI provider
286+
</p>
287+
</div>
288+
)}
265289

266290
<div>
267291
<label className="block font-medium text-gray-900 mb-2">Check Mode</label>
@@ -388,8 +412,8 @@ function DictionaryTab({
388412
}
389413

390414
function StatsTab({ stats }: { stats: { checksPerformed: number; errorsFound: number; correctionsApplied: number } }) {
391-
return (
392-
<div className="grid grid-cols-3 gap-6">
415+
return (
416+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
393417
<div className="bg-blue-50 rounded-lg p-6 text-center">
394418
<div className="text-3xl font-bold text-blue-600">{stats.checksPerformed}</div>
395419
<div className="text-sm text-blue-800 mt-1">Checks Performed</div>

src/entrypoints/popup/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ export default function App() {
5555

5656
if (loading) {
5757
return (
58-
<div className="w-80 p-4 flex items-center justify-center">
58+
<div className="w-full min-w-64 max-w-80 p-4 flex items-center justify-center">
5959
<div className="animate-spin w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full" />
6060
</div>
6161
);
6262
}
6363

6464
return (
65-
<div className="w-80 bg-white">
65+
<div className="w-full min-w-64 max-w-80 bg-white">
6666
{/* Header */}
6767
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-4">
6868
<div className="flex items-center justify-between">

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface APIKeys {
1313
export interface Settings {
1414
provider: AIProvider;
1515
model: string;
16+
customModel?: string;
1617
checkMode: CheckMode;
1718
language: string;
1819
enabled: boolean;

0 commit comments

Comments
 (0)