feat: icon-only toolbar, versioning fixes, history modal, PanelRight repositioning
- Toolbar: remove text labels from all icon buttons (AI, Save, Preview, Convert) all buttons now icon-only with title tooltip for accessibility - Toolbar: reposition PanelRight (info panel toggle) to far right after three-dot menu - Versioning: decouple getNoteHistory/restoreNoteVersion from global userAISettings.noteHistory now checks note.historyEnabled directly — unblocks manual per-note history - Versioning: add 'Sauvegarder cette version' button in Versions tab of info panel calls commitNoteHistory with visual feedback (spinner → success state) - note-document-info-panel: import commitNoteHistory, add isSavingVersion state - notes.ts: fix double guard that silently blocked all history operations
This commit is contained in:
122
memento-note/lib/ai/providers/anthropic.ts
Normal file
122
memento-note/lib/ai/providers/anthropic.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { generateObject, generateText as aiGenerateText, stepCountIs } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types';
|
||||
|
||||
export class AnthropicProvider implements AIProvider {
|
||||
private model: any;
|
||||
|
||||
/**
|
||||
* @param baseURL Optional Messages API root (no trailing slash). The SDK calls `{baseURL}/messages`.
|
||||
* MiniMax: `https://api.minimax.io/anthropic` (China: `https://api.minimaxi.com/anthropic`).
|
||||
*/
|
||||
constructor(apiKey: string, modelName: string = 'claude-sonnet-4-20250514', baseURL?: string) {
|
||||
const trimmedBase = baseURL?.trim().replace(/\/+$/, '');
|
||||
const anthropicClient = createAnthropic(trimmedBase ? { apiKey, baseURL: trimmedBase } : { apiKey });
|
||||
this.model = anthropicClient.chat(modelName);
|
||||
}
|
||||
|
||||
async generateTags(content: string): Promise<TagSuggestion[]> {
|
||||
try {
|
||||
const { object } = await generateObject({
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
tags: z.array(z.object({
|
||||
tag: z.string().describe('Short tag name in lowercase'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1'),
|
||||
})),
|
||||
}),
|
||||
prompt: `Analyze the following note and suggest 1 to 5 relevant tags.
|
||||
Note content: "${content}"`,
|
||||
});
|
||||
|
||||
return object.tags;
|
||||
} catch (e) {
|
||||
console.error('Error generating tags (Anthropic):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getEmbeddings(_text: string): Promise<number[]> {
|
||||
throw new Error(
|
||||
'Anthropic does not expose embedding models in Memento. Choose another provider for embeddings (e.g. Ollama or OpenAI).'
|
||||
);
|
||||
}
|
||||
|
||||
async generateTitles(prompt: string): Promise<TitleSuggestion[]> {
|
||||
try {
|
||||
const { object } = await generateObject({
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
titles: z.array(z.object({
|
||||
title: z.string().describe('Suggested title'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1'),
|
||||
})),
|
||||
}),
|
||||
prompt,
|
||||
});
|
||||
|
||||
return object.titles;
|
||||
} catch (e) {
|
||||
console.error('Error generating titles (Anthropic):', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async generateText(prompt: string): Promise<string> {
|
||||
try {
|
||||
const { text } = await aiGenerateText({
|
||||
model: this.model,
|
||||
prompt,
|
||||
});
|
||||
|
||||
return text.trim();
|
||||
} catch (e) {
|
||||
console.error('Error generating text (Anthropic):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async chat(messages: any[], systemPrompt?: string): Promise<any> {
|
||||
try {
|
||||
const { text } = await aiGenerateText({
|
||||
model: this.model,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
});
|
||||
|
||||
return { text: text.trim() };
|
||||
} catch (e) {
|
||||
console.error('Error in chat (Anthropic):', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async generateWithTools(options: ToolUseOptions): Promise<ToolCallResult> {
|
||||
const { tools, maxSteps = 10, systemPrompt, messages, prompt } = options;
|
||||
const opts: Record<string, any> = {
|
||||
model: this.model,
|
||||
tools,
|
||||
stopWhen: stepCountIs(maxSteps),
|
||||
};
|
||||
if (systemPrompt) opts.system = systemPrompt;
|
||||
if (messages) opts.messages = messages;
|
||||
else if (prompt) opts.prompt = prompt;
|
||||
|
||||
const result = await aiGenerateText(opts as any);
|
||||
return {
|
||||
toolCalls: result.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
||||
toolResults: result.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
||||
text: result.text,
|
||||
steps: result.steps?.map((step: any) => ({
|
||||
text: step.text,
|
||||
toolCalls: step.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
||||
toolResults: step.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
||||
})) || [],
|
||||
};
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
@@ -83,21 +83,23 @@ export class CustomOpenAIProvider implements AIProvider {
|
||||
|
||||
async generateTitles(prompt: string): Promise<TitleSuggestion[]> {
|
||||
try {
|
||||
const { object } = await generateObject({
|
||||
// Use generateText instead of generateObject — DeepSeek doesn't support
|
||||
// response_format: json_schema via the OpenAI compat layer
|
||||
const { text } = await aiGenerateText({
|
||||
model: this.model,
|
||||
schema: z.object({
|
||||
titles: z.array(z.object({
|
||||
title: z.string().describe('Suggested title'),
|
||||
confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1')
|
||||
}))
|
||||
}),
|
||||
prompt: prompt,
|
||||
});
|
||||
})
|
||||
|
||||
return object.titles;
|
||||
// Parse the JSON array from the text response — strip markdown code fences if present
|
||||
const parsed = JSON.parse(text.replace(/^```json\n?/,'').replace(/\n?```$/,'').trim())
|
||||
const titles = Array.isArray(parsed) ? parsed : (parsed.titles || parsed.suggestions || [])
|
||||
return titles.map((t: any) => ({
|
||||
title: typeof t === 'string' ? t : t.title || t.name || '',
|
||||
confidence: typeof t === 'number' ? t : (t.confidence || t.score || 0.5),
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('Error generating titles (Custom OpenAI):', e);
|
||||
return [];
|
||||
console.error('Error generating titles (Custom OpenAI):', e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user