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:
Antigravity
2026-05-09 07:28:03 +00:00
parent 574c8b3166
commit 97b08e5d0b
65 changed files with 2991 additions and 2296 deletions

View File

@@ -1,9 +1,20 @@
import { OpenAIProvider } from './providers/openai';
import { OllamaProvider } from './providers/ollama';
import { CustomOpenAIProvider } from './providers/custom-openai';
import { AnthropicProvider } from './providers/anthropic';
import { AIProvider } from './types';
type ProviderType = 'ollama' | 'openai' | 'custom' | 'deepseek' | 'openrouter' | 'mistral' | 'zai' | 'lmstudio';
type ProviderType =
| 'ollama'
| 'openai'
| 'custom'
| 'deepseek'
| 'openrouter'
| 'mistral'
| 'zai'
| 'lmstudio'
| 'anthropic'
| 'anthropic_custom';
// --- Provider defaults ---
const PROVIDER_DEFAULTS: Record<string, { baseUrl: string; model: string; embeddingModel: string }> = {
@@ -115,6 +126,36 @@ function createLMStudioProvider(config: Record<string, string>, modelName: strin
return new CustomOpenAIProvider(apiKey, baseUrl, modelName, embeddingModelName);
}
function createAnthropicProvider(config: Record<string, string>, modelName: string): AnthropicProvider {
const apiKey = config?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || '';
if (!apiKey) {
throw new Error('ANTHROPIC_API_KEY is required when using Anthropic provider');
}
return new AnthropicProvider(apiKey, modelName || 'claude-sonnet-4-20250514');
}
/**
* Passerelles compatibles **Anthropic Messages API** (ex. MiniMax), pas OpenAI.
* Le SDK envoie les requêtes vers `{baseURL}/messages` avec len-tête `x-api-key`.
*/
function createAnthropicCustomProvider(config: Record<string, string>, modelName: string): AnthropicProvider {
const apiKey = config?.ANTHROPIC_CUSTOM_API_KEY || process.env.ANTHROPIC_CUSTOM_API_KEY || '';
const baseUrl = config?.ANTHROPIC_CUSTOM_BASE_URL || process.env.ANTHROPIC_CUSTOM_BASE_URL || '';
if (!apiKey) {
throw new Error('ANTHROPIC_CUSTOM_API_KEY is required when using Anthropic Custom provider');
}
if (!baseUrl) {
throw new Error('ANTHROPIC_CUSTOM_BASE_URL is required when using Anthropic Custom provider');
}
const resolvedModel =
modelName && modelName.trim() !== '' ? modelName.trim() : 'MiniMax-M2.7';
return new AnthropicProvider(apiKey, resolvedModel, baseUrl.trim());
}
function getProviderInstance(providerType: ProviderType, config: Record<string, string>, modelName: string, embeddingModelName: string, ollamaBaseUrl?: string): AIProvider {
switch (providerType) {
case 'ollama':
@@ -133,6 +174,10 @@ function getProviderInstance(providerType: ProviderType, config: Record<string,
return createZAIProvider(config, modelName, embeddingModelName);
case 'lmstudio':
return createLMStudioProvider(config, modelName, embeddingModelName);
case 'anthropic':
return createAnthropicProvider(config, modelName);
case 'anthropic_custom':
return createAnthropicCustomProvider(config, modelName);
default:
return createOllamaProvider(config, modelName, embeddingModelName, ollamaBaseUrl);
}
@@ -148,6 +193,9 @@ function getProviderConfigKeys(providerType: string): { apiKeyConfigKey: string;
case 'zai': return { apiKeyConfigKey: 'ZAI_API_KEY', baseUrlConfigKey: '' };
case 'lmstudio': return { apiKeyConfigKey: 'LMSTUDIO_API_KEY', baseUrlConfigKey: 'LMSTUDIO_BASE_URL' };
case 'openai': return { apiKeyConfigKey: 'OPENAI_API_KEY', baseUrlConfigKey: '' };
case 'anthropic': return { apiKeyConfigKey: 'ANTHROPIC_API_KEY', baseUrlConfigKey: '' };
case 'anthropic_custom':
return { apiKeyConfigKey: 'ANTHROPIC_CUSTOM_API_KEY', baseUrlConfigKey: 'ANTHROPIC_CUSTOM_BASE_URL' };
case 'custom': return { apiKeyConfigKey: 'CUSTOM_OPENAI_API_KEY', baseUrlConfigKey: 'CUSTOM_OPENAI_BASE_URL' };
default: return { apiKeyConfigKey: '', baseUrlConfigKey: 'OLLAMA_BASE_URL' };
}
@@ -167,7 +215,7 @@ export function getTagsProvider(config?: Record<string, string>): AIProvider {
console.error('[getTagsProvider] FATAL: No provider configured. Config received:', config);
throw new Error(
'AI_PROVIDER_TAGS is not configured. Please set it in the admin settings or environment variables. ' +
'Options: ollama, openai, deepseek, openrouter, mistral, zai, lmstudio, custom'
'Options: ollama, openai, anthropic, anthropic_custom, deepseek, openrouter, mistral, zai, lmstudio, custom'
);
}
@@ -198,6 +246,12 @@ export function getEmbeddingsProvider(config?: Record<string, string>): AIProvid
}
const provider = providerType.toLowerCase() as ProviderType;
if (provider === 'anthropic' || provider === 'anthropic_custom') {
throw new Error(
'AI_PROVIDER_EMBEDDING cannot use "anthropic" or "anthropic_custom": these gateways use the Anthropic Messages API only (no embeddings in Memento). Use ollama, openai, or "custom" with MiniMax OpenAI URL https://api.minimax.io/v1 for embeddings.'
);
}
const modelName = config?.AI_MODEL_TAGS || process.env.AI_MODEL_TAGS || 'granite4:latest';
const embeddingModelName = config?.AI_MODEL_EMBEDDING || process.env.AI_MODEL_EMBEDDING || 'embeddinggemma:latest';
const ollamaBaseUrl = config?.OLLAMA_BASE_URL_EMBEDDING || config?.OLLAMA_BASE_URL;
@@ -225,7 +279,7 @@ export function getChatProvider(config?: Record<string, string>): AIProvider {
console.error('[getChatProvider] FATAL: No provider configured. Config received:', config);
throw new Error(
'AI_PROVIDER_CHAT is not configured. Please set it in the admin settings or environment variables. ' +
'Options: ollama, openai, deepseek, openrouter, mistral, zai, lmstudio, custom'
'Options: ollama, openai, anthropic, anthropic_custom, deepseek, openrouter, mistral, zai, lmstudio, custom'
);
}

View 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;
}
}

View File

@@ -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 []
}
}

View File

@@ -67,4 +67,14 @@ export interface AIProvider {
generateWithTools(options: ToolUseOptions): Promise<ToolCallResult>;
}
export type AIProviderType = 'openai' | 'ollama' | 'custom' | 'deepseek' | 'openrouter';
export type AIProviderType =
| 'openai'
| 'ollama'
| 'custom'
| 'deepseek'
| 'openrouter'
| 'mistral'
| 'zai'
| 'lmstudio'
| 'anthropic'
| 'anthropic_custom';