Files
Momento/memento-note/lib/ai/services/paragraph-refactor.service.ts
Antigravity 8c7ca69640
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
fix: brainstorm infinite loop, ghost cursor, embedding ::vector cast, semantic search, billing stats, usage meter accordion
- Fix useBrainstormSocket: stable guestId via useRef, remove setState in cleanup
- Fix GhostCursor: direct DOM manipulation via refs, no useState re-renders
- Fix all SQL embedding queries: add ::vector cast on text columns
- Fix embedding truncation to 15000 chars (under 8192 token limit)
- Fix NoteEmbedding INSERT: remove non-existent updatedAt column
- Fix billing page: show all quota stats in grid instead of single metric
- Fix usage meter: accordion expand/collapse, per-feature detail
- Fix semantic search: rebuild 103 note embeddings, ::vector cast on vectorSearch
- Fix brainstorm expand/manual-idea/create: ::vector cast on embedding SQL
2026-05-16 18:50:34 +00:00

362 lines
13 KiB
TypeScript

/**
* Paragraph Refactor Service
* Provides AI-powered text reformulation with 3 options:
* 1. Clarify - Make ambiguous text clearer
* 2. Shorten - Condense while keeping meaning
* 3. Improve Style - Enhance readability and flow
*/
import { LanguageDetectionService } from './language-detection.service'
import { getTagsProvider } from '../factory'
import { getSystemConfig } from '@/lib/config'
export type RefactorMode = 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar' | 'translate' | 'explain'
export interface RefactorOption {
mode: RefactorMode
label: string
description: string
icon: string
}
export interface RefactorResult {
original: string
refactored: string
mode: RefactorMode
language: string
wordCountChange: {
original: number
refactored: number
difference: number
percentage: number
}
}
export const REFACTOR_OPTIONS: RefactorOption[] = [
{
mode: 'clarify',
label: 'Clarify',
description: 'Make the text clearer and easier to understand',
icon: '💡'
},
{
mode: 'shorten',
label: 'Shorten',
description: 'Condense the text while keeping key information',
icon: '✂️'
},
{
mode: 'improveStyle',
label: 'Improve Style',
description: 'Enhance readability, flow, and expression',
icon: '✨'
}
]
export class ParagraphRefactorService {
private languageDetection: LanguageDetectionService
private readonly MIN_WORDS = 10
private readonly MAX_WORDS = 5000
constructor() {
this.languageDetection = new LanguageDetectionService()
}
/**
* Refactor a paragraph with the specified mode
*/
async refactor(
content: string,
mode: RefactorMode,
format: 'html' | 'markdown' = 'markdown',
targetLanguage?: string
): Promise<RefactorResult> {
// Validate word count
const wordCount = content.split(/\s+/).length
if (wordCount < this.MIN_WORDS || wordCount > this.MAX_WORDS) {
throw new Error(
`Please select ${this.MIN_WORDS}-${this.MAX_WORDS} words to reformulate`
)
}
// Detect language or use provided target language
const { language: detectedLanguage } = await this.languageDetection.detectLanguage(content)
const language = targetLanguage || detectedLanguage
try {
// Build prompts
const systemPrompt = this.getSystemPrompt(mode, format)
const userPrompt = this.getUserPrompt(mode, content, language, format)
// Get AI provider from factory
const config = await getSystemConfig()
const provider = getTagsProvider(config)
// Use provider's generateText method
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`
const refactored = await provider.generateText(fullPrompt)
// Calculate word count change
const refactoredWordCount = refactored.split(/\s+/).length
const wordCountChange = {
original: wordCount,
refactored: refactoredWordCount,
difference: refactoredWordCount - wordCount,
percentage: ((refactoredWordCount - wordCount) / wordCount) * 100
}
return {
original: content,
refactored,
mode,
language,
wordCountChange
}
} catch (error) {
throw new Error('Failed to refactor paragraph. Please try again.')
}
}
/**
* Get all 3 refactor options for a paragraph at once
* More efficient than calling refactor() 3 times
*/
async refactorAllModes(content: string): Promise<RefactorResult[]> {
// Validate word count
const wordCount = content.split(/\s+/).length
if (wordCount < this.MIN_WORDS || wordCount > this.MAX_WORDS) {
throw new Error(
`Please select ${this.MIN_WORDS}-${this.MAX_WORDS} words to reformulate`
)
}
// Detect language
const { language } = await this.languageDetection.detectLanguage(content)
try {
// System prompt for all modes
const systemPrompt = `You are an expert text editor who can improve text in multiple ways.
Your task is to provide 3 different reformulations of the user's text.
For each reformulation:
1. Clarify: Make the text clearer, more explicit, easier to understand
2. Shorten: Condense the text while preserving all key information and meaning
3. Improve Style: Enhance readability, flow, vocabulary, and expression
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text.
- If input is French, ALL 3 outputs MUST be in French
- If input is German, ALL 3 outputs MUST be in German
- If input is Spanish, ALL 3 outputs MUST be in Spanish
- NEVER translate to English unless the input is in English
Maintain the original meaning and intent:
- For "shorten", aim to reduce by 30-50% while keeping all key points
- For "clarify", expand where necessary but keep it natural
- For "improve style", keep similar length but enhance quality
Output Format (JSON):
{
"clarify": "clarified text here...",
"shorten": "shortened text here...",
"improveStyle": "improved text here..."
}`
const userPrompt = `CRITICAL LANGUAGE INSTRUCTION: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please provide 3 reformulations of this ${language} text:
${content}
Original language: ${language}
IMPORTANT: Provide all 3 versions in ${language}. No English, no explanations.`
// Get AI provider from factory
const config = await getSystemConfig()
const provider = getTagsProvider(config)
// Use provider's generateText method
const fullPrompt = `${systemPrompt}\n\n${userPrompt}`
const response = await provider.generateText(fullPrompt)
// Parse JSON response
const jsonResponse = JSON.parse(response)
const modes: RefactorMode[] = ['clarify', 'shorten', 'improveStyle']
const results: RefactorResult[] = []
for (const mode of modes) {
if (!jsonResponse[mode]) continue
const refactored = this.extractRefactoredText(jsonResponse[mode])
const refactoredWordCount = refactored.split(/\s+/).length
results.push({
original: content,
refactored,
mode,
language,
wordCountChange: {
original: wordCount,
refactored: refactoredWordCount,
difference: refactoredWordCount - wordCount,
percentage: ((refactoredWordCount - wordCount) / wordCount) * 100
}
})
}
return results
} catch (error) {
throw new Error('Failed to generate refactor options. Please try again.')
}
}
/**
* Get mode-specific system prompt
*/
private getSystemPrompt(mode: RefactorMode, format: 'html' | 'markdown' = 'markdown'): string {
const formatInstruction = format === 'html'
? "\nCRITICAL FORMAT RULE: You MUST return your response as valid HTML fragments (e.g., using <p>, <strong>, <em>, <ul>, etc.) without markdown symbols. Do not wrap in a markdown code block."
: "\nCRITICAL FORMAT RULE: You MUST return your response as plain text or Markdown."
const prompts = {
clarify: `You are an expert at making text clearer and more understandable.
Your goal: Rewrite the text to eliminate ambiguity, add necessary context, and improve clarity.
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text. If input is French, output MUST be French. If input is German, output MUST be German. NEVER translate to English.
Maintain the original meaning and tone, just make it clearer.${formatInstruction}`,
shorten: `You are an expert at concise writing.
Your goal: Reduce the text length by 30-50% while preserving ALL key information and meaning.
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text. If input is French, output MUST be French. If input is German, output MUST be German. NEVER translate to English.
Remove fluff, repetition, and unnecessary words, but keep the substance.${formatInstruction}`,
improveStyle: `You are an expert editor with a focus on readability and flow.
Your goal: Enhance the text's style, vocabulary, sentence structure, and overall quality.
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text. If input is French, output MUST be French. If input is German, output MUST be German. NEVER translate to English.
Maintain similar length but make it sound more professional and polished.${formatInstruction}`,
fix_grammar: `You are an expert proofreader.
Your goal: Fix spelling, grammar, and punctuation errors in the text without changing its meaning, tone, or style.
CRITICAL LANGUAGE RULE: You MUST respond in the EXACT SAME LANGUAGE as the input text. NEVER translate to English.
Make minimal changes, only correcting errors.${formatInstruction}`,
translate: `You are a professional translator.
Your goal: Translate the text perfectly into the target language requested by the user.
Ensure the translation sounds natural and preserves the original tone and formatting.${formatInstruction}`,
explain: `You are an expert teacher and encyclopedia.
Your goal: Explain the selected text, concept, or word clearly and concisely.
Provide context, definitions, or relevant information to help the user understand it.
CRITICAL LANGUAGE RULE: You MUST explain it in the requested language (which is the user's interface language).
Keep it concise but informative.${formatInstruction}`
}
return prompts[mode]
}
/**
* Get mode-specific user prompt
*/
private getUserPrompt(mode: RefactorMode, content: string, language: string, format: 'html' | 'markdown' = 'markdown'): string {
const instructions = {
clarify: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please clarify and make this ${language} text easier to understand:`,
shorten: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please shorten this ${language} text while keeping all key information:`,
improveStyle: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please improve the style and readability of this ${language} text:`,
fix_grammar: `IMPORTANT: The text below is in ${language}. Your response MUST be in ${language}. Do NOT translate to English.
Please fix any spelling, grammar, or punctuation errors in this ${language} text:`,
translate: `Please translate the following text into ${language}.
Only return the translated text, nothing else.`,
explain: `Please explain the following text/concept in ${language}.
Keep the explanation clear, educational, and concise.`
}
const systemSuffix = mode === 'explain'
? `CRITICAL: Respond ONLY with your explanation in ${language}. No meta-commentary, no English. Format strictly as ${format === 'html' ? 'HTML tags' : 'Markdown'}.`
: `CRITICAL: Respond ONLY with the refactored text in ${language}. No explanations, no meta-commentary, no English. Format strictly as ${format === 'html' ? 'HTML tags' : 'Markdown'}.`
return `${instructions[mode]}
${content}
${systemSuffix}`
}
/**
* Extract refactored text from AI response
* Handles JSON, markdown code blocks, or HTML/plain text
*/
private extractRefactoredText(response: string): string {
// Try JSON first
if (response.trim().startsWith('{')) {
try {
const parsed = JSON.parse(response)
// Look for common response fields
return parsed.refactored || parsed.text || parsed.result || response
} catch {
// Not valid JSON, continue
}
}
// Try markdown code block (HTML or Markdown)
const codeBlockMatch = response.match(/```(?:markdown|html)?\n([\s\S]+?)\n```/)
if (codeBlockMatch) {
return codeBlockMatch[1].trim()
}
// Try bare HTML if wrapped in a single code block
const genericCodeBlockMatch = response.match(/```\n([\s\S]+?)\n```/)
if (genericCodeBlockMatch) {
return genericCodeBlockMatch[1].trim()
}
// Fallback: trim whitespace and quotes
return response.trim().replace(/^["']|["']$/g, '')
}
/**
* Validate that text is within acceptable word count range
*/
validateWordCount(content: string): { valid: boolean; error?: string; errorKey?: string; params?: Record<string, number> } {
const wordCount = content.split(/\s+/).length
if (wordCount < this.MIN_WORDS) {
return {
valid: false,
errorKey: 'ai.wordCountMin',
params: { min: this.MIN_WORDS, current: wordCount },
error: `min:${this.MIN_WORDS}:current:${wordCount}`
}
}
if (wordCount > this.MAX_WORDS) {
return {
valid: false,
errorKey: 'ai.wordCountMax',
params: { max: this.MAX_WORDS, current: wordCount },
error: `max:${this.MAX_WORDS}:current:${wordCount}`
}
}
return { valid: true }
}
}
// Singleton instance
export const paragraphRefactorService = new ParagraphRefactorService()