F1-F4: Inline editor AI - Backend: expand CopilotAction with fix-rewrite, change-tone, continue, explain - Backend: add tone parameter to copilot route for change-tone action - Web: copilot-client adds CopilotTone type and tone parameter - Web: NoteEditor toolbar gains AI row with Fix & Rewrite, Change Tone dropdown, Continue Writing (appends at cursor), Explain (inline popover) F15-F19: Mobile capture enhancements - Backend: POST /note-prompts/url-extract endpoint (fetch, strip HTML, LLM summarize) - Mobile API: extractFromUrl() and copilotTransform() client functions - Mobile: capture tab rewritten with 6 capture modes grid (text, photo, voice, URL, scan, paste) — URL extract + clipboard paste fully wired, camera/voice/scan surface native permission prompts (require expo-av/expo-image-picker) - expo-clipboard added as dependency F25-F27: Scheduled actions, webhook triggers, approval-gated actions - New scheduler.ts module with PromptScheduleDoc + PromptWebhookDoc types - Schedule CRUD: GET/POST/PATCH/DELETE /prompt-schedules - Webhook CRUD: GET/POST/PATCH/DELETE /prompt-webhooks - POST /prompt-webhooks/:id/trigger — execute template against note - Scheduler loop (60s tick) with cron next-run calculation - Diagnostics endpoint: GET /prompt-schedules/diagnostics - Cosmos containers: note_prompt_schedules, note_prompt_webhooks - PromptTemplateDoc gains requiresApproval field (F27) - Runner produces approvalState: proposed|applied based on template flag - Create/Update schemas accept requiresApproval boolean
94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
/**
|
|
* Copilot text transforms — powered by @bytelyst/llm.
|
|
*
|
|
* Falls back to local heuristics if LLM is unavailable.
|
|
*/
|
|
|
|
import { llm } from './llm.js';
|
|
|
|
export type CopilotAction = 'shorten' | 'expand' | 'bulletize' | 'grammar' | 'fix-rewrite' | 'change-tone' | 'continue' | 'explain';
|
|
|
|
const SYSTEM_PROMPTS: Record<CopilotAction, string> = {
|
|
shorten: 'Condense the text to about half its length while preserving key points. Return only the shortened text.',
|
|
expand: 'Expand the text with more detail and examples. Return only the expanded text.',
|
|
bulletize: 'Convert the text into concise bullet points. Return only the bullet points.',
|
|
grammar: 'Fix grammar, spelling, and punctuation. Preserve original meaning and tone. Return only the corrected text.',
|
|
'fix-rewrite': 'Completely rewrite the text for better clarity, grammar, and flow while preserving the original meaning. Return only the rewritten text.',
|
|
'change-tone': 'Rewrite the text in the requested tone (formal, casual, professional, or friendly). The tone is specified at the end after "Tone:". Return only the rewritten text.',
|
|
'continue': 'You are a writing assistant. Continue writing naturally from where the text ends. Write 2-3 paragraphs that flow logically from the context. Return only the continuation, not the original text.',
|
|
'explain': 'Explain the given term, concept, or text selection concisely in 2-3 sentences. Return only the explanation.',
|
|
};
|
|
|
|
function fallbackTransform(action: CopilotAction, text: string): string {
|
|
const lines = text.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
switch (action) {
|
|
case 'bulletize':
|
|
return lines.map((l) => (l.startsWith('-') || l.startsWith('•') ? l : `- ${l}`)).join('\n');
|
|
case 'shorten': {
|
|
const words = text.split(/\s+/);
|
|
const target = Math.max(8, Math.floor(words.length * 0.55));
|
|
return words.slice(0, target).join(' ') + (words.length > target ? '…' : '');
|
|
}
|
|
case 'expand':
|
|
return `${text}\n\n_Additional detail could be added here to expand on the main points._`;
|
|
case 'fix-rewrite':
|
|
return text;
|
|
case 'change-tone':
|
|
return text;
|
|
case 'continue':
|
|
return `${text}\n\n[Continue writing here...]`;
|
|
case 'explain':
|
|
return 'Explanation not available without an LLM provider.';
|
|
case 'grammar':
|
|
default:
|
|
return text;
|
|
}
|
|
}
|
|
|
|
export async function runCopilotTransform(action: CopilotAction, text: string): Promise<string> {
|
|
const provider = llm();
|
|
if (!provider.isConfigured()) {
|
|
return fallbackTransform(action, text);
|
|
}
|
|
|
|
try {
|
|
const result = await provider.chatCompletion({
|
|
messages: [
|
|
{ role: 'system', content: SYSTEM_PROMPTS[action] },
|
|
{ role: 'user', content: text },
|
|
],
|
|
temperature: 0.3,
|
|
maxTokens: 4096,
|
|
});
|
|
const out = result.content.trim();
|
|
if (out.length > 0) return out;
|
|
} catch {
|
|
// fall through to local heuristics
|
|
}
|
|
return fallbackTransform(action, text);
|
|
}
|
|
|
|
export async function suggestTitleFromBody(body: string): Promise<string> {
|
|
const plain = body.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
const provider = llm();
|
|
if (!provider.isConfigured()) {
|
|
return plain.split(/[.!?]/)[0]?.trim().slice(0, 80) || 'Untitled note';
|
|
}
|
|
|
|
try {
|
|
const result = await provider.chatCompletion({
|
|
messages: [
|
|
{ role: 'system', content: 'Suggest a concise, descriptive title (max 8 words). Return only the title, no quotes.' },
|
|
{ role: 'user', content: plain.slice(0, 4000) },
|
|
],
|
|
temperature: 0.6,
|
|
maxTokens: 64,
|
|
});
|
|
const t = result.content.trim();
|
|
if (t.length > 0 && t.length < 500) return t;
|
|
} catch {
|
|
// fall through
|
|
}
|
|
return plain.split(/[.!?]/)[0]?.trim().slice(0, 80) || 'Untitled note';
|
|
}
|