diff --git a/README.md b/README.md index 18c872550..0d8f54f2d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # Bolt.new Fork by Cole Medin +[Lire en français](README_FR.md) + This fork of bolt.new allows you to choose the LLM that you use for each prompt! Currently you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See instructions below for running this locally and extending to include more models. # Requested Additions to this Fork - Feel Free to Contribute!! @@ -39,13 +41,14 @@ Bolt.new is an AI-powered web development agent that allows you to prompt, run, Claude, v0, etc are incredible- but you can't install packages, run backends or edit code. That’s where Bolt.new stands out: - **Full-Stack in the Browser**: Bolt.new integrates cutting-edge AI models with an in-browser development environment powered by **StackBlitz’s WebContainers**. This allows you to: + - Install and run npm tools and libraries (like Vite, Next.js, and more) - Run Node.js servers - Interact with third-party APIs - Deploy to production from chat - Share your work via a URL -- **AI with Environment Control**: Unlike traditional dev environments where the AI can only assist in code generation, Bolt.new gives AI models **complete control** over the entire environment including the filesystem, node server, package manager, terminal, and browser console. This empowers AI agents to handle the entire app lifecycle—from creation to deployment. +- **AI with Environment Control**: Unlike traditional dev environments where the AI can only assist in code generation, Bolt.new gives AI models **complete control** over the entire environment including the filesystem, node server, package manager, terminal, and browser console. This empowers AI agents to handle the entire app lifecycle—from creation to deployment. Whether you’re an experienced developer, a PM or designer, Bolt.new allows you to build production-grade full-stack applications with ease. @@ -90,7 +93,7 @@ VITE_LOG_LEVEL=debug ## Adding New LLMs: -To make new LLMs available to use in this version of Bolt.new, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a lable for the frontend model dropdown, and the provider. +To make new LLMs available to use in this version of Bolt.new, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a lable for the frontend model dropdown, and the provider. By default, Anthropic, OpenAI, Groq, and Ollama are implemented as providers, but the YouTube video for this repo covers how to extend this to work with more providers if you wish! diff --git a/README_FR.md b/README_FR.md new file mode 100644 index 000000000..b0a6456c2 --- /dev/null +++ b/README_FR.md @@ -0,0 +1,133 @@ +# Bolt.new: Développement Web Full-Stack Powered by AI dans le Navigateur + +![Bolt.new: Développement Web Full-Stack Powered by AI dans le Navigateur](./public/social_preview_index.jpg) + +# Bolt.new Fork par Cole Medin + +Ce fork de bolt.new vous permet de choisir le LLM que vous utilisez pour chaque prompt ! Actuellement, vous pouvez utiliser les modèles OpenAI, Anthropic, Ollama, OpenRouter, Gemini, ou Groq - et il est facilement extensible pour utiliser tout autre modèle supporté par le Vercel AI SDK ! Consultez les instructions ci-dessous pour exécuter ceci localement et étendre pour inclure plus de modèles. + +# Additions Requises pour ce Fork - N'hésitez pas à Contribuer !! + +- ✅ Intégration OpenRouter (@coleam00) +- ✅ Intégration Gemini (@jonathands) +- ✅ Autogénérer les modèles Ollama à partir de ce qui est téléchargé (@mosquet) +- ✅ Filtrer les modèles par fournisseur (@jasonm23) +- ✅ Télécharger le projet en tant que ZIP (@fabwaseem) +- ⬜ Intégration LM Studio +- ⬜ Intégration DeepSeek API +- ⬜ Intégration Together +- ⬜ Intégration Azure Open AI API +- ⬜ Intégration HuggingFace +- ⬜ Intégration Perplexity +- ⬜ Conteneuriser l'application avec Docker pour une installation facile +- ⬜ Meilleure incitation pour les LLMs plus petits (la fenêtre de code ne démarre parfois pas) +- ⬜ Joindre des images aux prompts +- ⬜ Exécuter des agents dans le backend au lieu d'un seul appel de modèle +- ⬜ Publier des projets directement sur GitHub +- ⬜ Déployer directement sur Vercel/Netlify/autres plateformes similaires +- ⬜ Charger des projets locaux dans l'application +- ⬜ Améliorations de la prompt principale de Bolt.new dans `app\lib\.server\llm\prompts.ts` (il y a définitivement une opportunité là) +- ⬜ Capacité à revenir au code à une version antérieure +- ⬜ Mise en cache des prompts +- ⬜ Capacité à entrer des clés API dans l'interface utilisateur +- ⬜ Empêcher Bolt de réécrire les fichiers aussi souvent + +# Bolt.new: Développement Web Full-Stack Powered by AI dans le Navigateur + +Bolt.new est un agent de développement web powered by AI qui vous permet de prompter, exécuter, éditer et déployer des applications full-stack directement depuis votre navigateur—aucune configuration locale requise. Si vous êtes ici pour construire votre propre agent de développement web powered by AI en utilisant le code source open source de Bolt, [cliquez ici pour commencer!](./CONTRIBUTING.md) + +## Ce qui Rend Bolt.new Différent + +Claude, v0, etc. sont incroyables - mais vous ne pouvez pas installer de packages, exécuter des backends ou éditer du code. C'est là que Bolt.new se distingue : + +- **Full-Stack dans le Navigateur** : Bolt.new intègre des modèles AI de pointe avec un environnement de développement dans le navigateur powered by **StackBlitz’s WebContainers**. Cela vous permet de : + + - Installer et exécuter des outils et bibliothèques npm (comme Vite, Next.js, et plus) + - Exécuter des serveurs Node.js + - Interagir avec des API tierces + - Déployer en production depuis le chat + - Partager votre travail via une URL + +- **AI avec Contrôle de l'Environnement** : Contrairement aux environnements de développement traditionnels où l'AI ne peut qu'aider à la génération de code, Bolt.new donne aux modèles AI un **contrôle complet** sur tout l'environnement, y compris le système de fichiers, le serveur node, le gestionnaire de packages, le terminal, et la console du navigateur. Cela permet aux agents AI de gérer tout le cycle de vie de l'application—de la création au déploiement. + +Que vous soyez un développeur expérimenté, un chef de projet ou un designer, Bolt.new vous permet de construire des applications full-stack de qualité production avec facilité. + +Pour les développeurs intéressés par la construction de leurs propres outils de développement powered by AI avec WebContainers, consultez le code source open source de Bolt dans ce dépôt ! + +## Prérequis + +Avant de commencer, assurez-vous d'avoir installé les éléments suivants : + +- Node.js (v20.15.1) +- pnpm (v9.4.0) + +## Configuration + +1. Clonez le dépôt (si ce n'est pas déjà fait) : + +```bash +git clone https://github.com/coleam00/bolt.new-any-llm.git +``` + +2. Installez les dépendances : + +```bash +pnpm install +``` + +3. Renommez `.env.example` en .env.local et ajoutez vos clés API LLM (vous n'avez qu'à définir celles que vous souhaitez utiliser et Ollama n'a pas besoin d'une clé API car il s'exécute localement sur votre ordinateur) : + +``` +GROQ_API_KEY=XXX +OPENAI_API_KEY=XXX +ANTHROPIC_API_KEY=XXX +``` + +Optionnellement, vous pouvez définir le niveau de débogage : + +``` +VITE_LOG_LEVEL=debug +``` + +**Important** : Ne commettez jamais votre fichier `.env.local` dans le contrôle de version. Il est déjà inclus dans .gitignore. + +## Ajout de Nouveaux LLMs : + +Pour rendre de nouveaux LLMs disponibles à l'utilisation dans cette version de Bolt.new, rendez-vous sur `app/utils/constants.ts` et trouvez la constante MODEL_LIST. Chaque élément de ce tableau est un objet qui a l'ID du modèle pour le nom (obtenez ceci à partir de la documentation de l'API du fournisseur), une étiquette pour le menu déroulant des modèles frontend, et le fournisseur. + +Par défaut, Anthropic, OpenAI, Groq, et Ollama sont implémentés en tant que fournisseurs, mais la vidéo YouTube pour ce dépôt couvre comment étendre ceci pour fonctionner avec plus de fournisseurs si vous le souhaitez ! + +Lorsque vous ajoutez un nouveau modèle au tableau MODEL_LIST, il sera immédiatement disponible à l'utilisation lorsque vous exécutez l'application localement ou la rechargez. Pour les modèles Ollama, assurez-vous d'avoir le modèle installé avant d'essayer de l'utiliser ici ! + +## Scripts Disponibles + +- `pnpm run dev` : Démarre le serveur de développement. +- `pnpm run build` : Construit le projet. +- `pnpm run start` : Exécute l'application construite localement en utilisant Wrangler Pages. Ce script utilise `bindings.sh` pour configurer les liaisons nécessaires afin que vous n'ayez pas à dupliquer les variables d'environnement. +- `pnpm run preview` : Construit le projet puis le démarre localement, utile pour tester la build de production. Notez que le streaming HTTP ne fonctionne pas comme prévu avec `wrangler pages dev`. +- `pnpm test` : Exécute la suite de tests en utilisant Vitest. +- `pnpm run typecheck` : Exécute la vérification des types TypeScript. +- `pnpm run typegen` : Génère les types TypeScript en utilisant Wrangler. +- `pnpm run deploy` : Construit le projet et le déploie sur Cloudflare Pages. + +## Développement + +Pour démarrer le serveur de développement : + +```bash +pnpm run dev +``` + +Cela démarrera le serveur de développement Remix Vite. Vous aurez besoin de Google Chrome Canary pour exécuter ceci localement ! C'est une installation très facile et un bon navigateur pour le développement web de toute façon. + +## Astuces et Conseils + +Voici quelques conseils pour tirer le meilleur parti de Bolt.new : + +- **Soyez spécifique sur votre stack** : Si vous souhaitez utiliser des frameworks ou bibliothèques spécifiques (comme Astro, Tailwind, ShadCN, ou tout autre framework JavaScript populaire), mentionnez-les dans votre prompt initial pour vous assurer que Bolt échafaude le projet en conséquence. + +- **Utilisez l'icône de prompt d'amélioration** : Avant d'envoyer votre prompt, essayez de cliquer sur l'icône 'améliorer' pour que le modèle AI vous aide à affiner votre prompt, puis éditez les résultats avant de les soumettre. + +- **Échafaudez les bases d'abord, puis ajoutez des fonctionnalités** : Assurez-vous que la structure de base de votre application est en place avant de plonger dans des fonctionnalités plus avancées. Cela aide Bolt à comprendre la fondation de votre projet et à s'assurer que tout est bien câblé avant de construire des fonctionnalités plus avancées. + +- **Regroupez les instructions simples** : Gagnez du temps en combinant des instructions simples en un seul message. Par exemple, vous pouvez demander à Bolt de changer le schéma de couleurs, d'ajouter une réactivité mobile, et de redémarrer le serveur de développement, le tout en une seule fois, vous faisant gagner du temps et réduisant considérablement la consommation de crédits API. diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index b7421349e..5b0d625df 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -10,9 +10,11 @@ import { classNames } from '~/utils/classNames'; import { MODEL_LIST, DEFAULT_PROVIDER } from '~/utils/constants'; import { Messages } from './Messages.client'; import { SendButton } from './SendButton.client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { openDatabase, setProvider, getProviderById } from '~/lib/persistence/db'; import styles from './BaseChat.module.scss'; +import type { Provider } from '~/types/provider'; const EXAMPLE_PROMPTS = [ { text: 'Build a todo app in React using Tailwind' }, @@ -22,47 +24,101 @@ const EXAMPLE_PROMPTS = [ { text: 'How do I center a div?' }, ]; -const providerList = [...new Set(MODEL_LIST.map((model) => model.provider))] +const ModelSelector = ({ model, setModel }) => { + const [providerList, setProviderList] = useState([]); + const [selectedProvider, setSelectedProvider] = useState(); + const [apiKeyOrUrl, setApiKeyOrUrl] = useState(''); + + useEffect(() => { + const loadProviders = async () => { + const providerIdList = [...new Set(MODEL_LIST.map((model) => model.provider))]; + const db = await openDatabase(); + if (db) { + const providerPromises = providerIdList.map(async (id) => { + return (await getProviderById(db, id)) || { id, models: MODEL_LIST.filter((m) => m.provider === id) }; + }); + + setProviderList(await Promise.all(providerPromises)); + setSelectedProvider(providerList.find((p) => p.id === DEFAULT_PROVIDER)); + } + }; + loadProviders(); + }, []); + + useEffect(() => { + if (selectedProvider) { + console.log('Selected Provider:', selectedProvider); + setApiKeyOrUrl(selectedProvider.apiKey ?? ''); + } + }, [selectedProvider]); + + const saveProvider = async () => { + if (selectedProvider) { + selectedProvider.apiKey = apiKeyOrUrl; + const db = await openDatabase(); + if (db) { + await setProvider(db, selectedProvider); + } + } + }; -const ModelSelector = ({ model, setModel, modelList, providerList }) => { - const [provider, setProvider] = useState(DEFAULT_PROVIDER); return (
- - +
+ +
+ {selectedProvider?.id === 'Ollama' || ( +
+ setApiKeyOrUrl(e.target.value)} + className="w-full p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none" + /> + +
+ )} +
+ +
); }; const TEXTAREA_MIN_HEIGHT = 76; - interface BaseChatProps { textareaRef?: React.RefObject | undefined; messageRef?: RefCallback | undefined; @@ -150,12 +206,7 @@ export const BaseChat = React.forwardRef( 'sticky bottom-0': chatStarted, })} > - +
(
); }, -); \ No newline at end of file +); diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 458bd8364..763991fc0 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -7,14 +7,15 @@ import { useAnimate } from 'framer-motion'; import { memo, useEffect, useRef, useState } from 'react'; import { cssTransition, toast, ToastContainer } from 'react-toastify'; import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks'; -import { useChatHistory } from '~/lib/persistence'; +import { useChatHistory, openDatabase, getProviderById } from '~/lib/persistence'; import { chatStore } from '~/lib/stores/chat'; import { workbenchStore } from '~/lib/stores/workbench'; import { fileModificationsToHTML } from '~/utils/diff'; -import { DEFAULT_MODEL } from '~/utils/constants'; +import { MODEL_LIST, DEFAULT_MODEL } from '~/utils/constants'; import { cubicEasingFn } from '~/utils/easings'; import { createScopedLogger, renderLogger } from '~/utils/logger'; import { BaseChat } from './BaseChat'; +import type { Provider } from '~/types/provider'; const toastAnimation = cssTransition({ enter: 'animated fadeInRight', @@ -172,6 +173,16 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp runAnimation(); + let apiKey = ''; + let provider: Provider | undefined; + const providerId = MODEL_LIST.find((m) => m.name === model)?.provider; + if (providerId) { + const db = await openDatabase(); + if (db) { + provider = await getProviderById(db, providerId); + } + } + if (fileModifications !== undefined) { const diff = fileModificationsToHTML(fileModifications); @@ -182,7 +193,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp * manually reset the input and we'd have to manually pass in file attachments. However, those * aren't relevant here. */ - append({ role: 'user', content: `[Model: ${model}]\n\n${diff}\n\n${_input}` }); + append({ role: 'user', content: `[Model: ${model}][APIKey: ${provider?.apiKey || ''}]\n\n${diff}\n\n${_input}` }); /** * After sending a new message we reset all modifications since the model @@ -190,7 +201,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp */ workbenchStore.resetAllFileModifications(); } else { - append({ role: 'user', content: `[Model: ${model}]\n\n${_input}` }); + append({ role: 'user', content: `[Model: ${model}][APIKey: ${provider?.apiKey || ''}]\n\n${_input}` }); } setInput(''); diff --git a/app/lib/.server/llm/api-key.ts b/app/lib/.server/llm/api-key.ts index 0e91bd486..d2340bd62 100644 --- a/app/lib/.server/llm/api-key.ts +++ b/app/lib/.server/llm/api-key.ts @@ -1,8 +1,9 @@ // @ts-nocheck // Preventing TS checks with files presented in the video for a better presentation. import { env } from 'node:process'; +import { openDatabase, getProviderById } from '~/lib/persistence/db'; -export function getAPIKey(cloudflareEnv: Env, provider: string) { +export async function getAPIKey(cloudflareEnv: Env, provider: string) { /** * The `cloudflareEnv` is only used when deployed or when previewing locally. * In development the environment variables are available through `env`. @@ -20,8 +21,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string) { case 'OpenRouter': return env.OPEN_ROUTER_API_KEY || cloudflareEnv.OPEN_ROUTER_API_KEY; case 'Mistral': - return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY; + return env.MISTRAL_AI_API_KEY || cloudflareEnv.MISTRAL_AI_API_KEY; default: - return ""; + return ''; } } diff --git a/app/lib/.server/llm/model.ts b/app/lib/.server/llm/model.ts index b62e4f2bc..dc24dd135 100644 --- a/app/lib/.server/llm/model.ts +++ b/app/lib/.server/llm/model.ts @@ -31,7 +31,7 @@ export function getGoogleModel(apiKey: string, model: string) { } export function getMistralModel(apiKey: string, model: string) { - const mistral = createMistral(apiKey); + const mistral = createMistral({ apiKey }); return mistral(model); } @@ -57,8 +57,13 @@ export function getOpenRouterModel(apiKey: string, model: string) { return openRouter.chat(model); } -export function getModel(provider: string, model: string, env: Env) { - const apiKey = getAPIKey(env, provider); +export function getModel(provider: string, customApiKey: string, model: string, env: Env) { + let apiKey = getAPIKey(env, provider); + + // If a custom API key is provided, use it instead of the environment variable. + if (customApiKey !== '') { + apiKey = customApiKey; + } switch (provider) { case 'Anthropic': diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index de3d5bfa8..7ce23d148 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -24,27 +24,33 @@ export type Messages = Message[]; export type StreamingOptions = Omit[0], 'model'>; -function extractModelFromMessage(message: Message): { model: string; content: string } { - const modelRegex = /^\[Model: (.*?)\]\n\n/; +function extractModelFromMessage(message: Message): { model: string; apiKey: string; content: string } { + const modelRegex = /^\[Model: (.*?)\]\[APIKey: (.*?)\]\n\n/; const match = message.content.match(modelRegex); if (match) { const model = match[1]; + const apiKey = match[2]; + + console.log('APIKey:', apiKey); + const content = message.content.replace(modelRegex, ''); - return { model, content }; + return { model, apiKey, content }; } // Default model if not specified - return { model: DEFAULT_MODEL, content: message.content }; + return { model: DEFAULT_MODEL, apiKey: '', content: message.content }; } export function streamText(messages: Messages, env: Env, options?: StreamingOptions) { let currentModel = DEFAULT_MODEL; + let customApiKey = ''; const processedMessages = messages.map((message) => { if (message.role === 'user') { - const { model, content } = extractModelFromMessage(message); + const { model, apiKey, content } = extractModelFromMessage(message); if (model && MODEL_LIST.find((m) => m.name === model)) { currentModel = model; // Update the current model + customApiKey = apiKey; // Update the API key if provided } return { ...message, content }; } @@ -54,7 +60,7 @@ export function streamText(messages: Messages, env: Env, options?: StreamingOpti const provider = MODEL_LIST.find((model) => model.name === currentModel)?.provider || DEFAULT_PROVIDER; return _streamText({ - model: getModel(provider, currentModel, env), + model: getModel(provider, customApiKey, currentModel, env), system: getSystemPrompt(), maxTokens: MAX_TOKENS, // headers: { diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts index 7a952e344..62ab9c002 100644 --- a/app/lib/persistence/db.ts +++ b/app/lib/persistence/db.ts @@ -1,13 +1,14 @@ import type { Message } from 'ai'; import { createScopedLogger } from '~/utils/logger'; import type { ChatHistoryItem } from './useChatHistory'; +import type { Provider } from '~/types/provider'; const logger = createScopedLogger('ChatHistory'); // this is used at the top level and never rejects export async function openDatabase(): Promise { return new Promise((resolve) => { - const request = indexedDB.open('boltHistory', 1); + const request = indexedDB.open('boltHistory', 4); request.onupgradeneeded = (event: IDBVersionChangeEvent) => { const db = (event.target as IDBOpenDBRequest).result; @@ -17,6 +18,14 @@ export async function openDatabase(): Promise { store.createIndex('id', 'id', { unique: true }); store.createIndex('urlId', 'urlId', { unique: true }); } + + if (!db.objectStoreNames.contains('theme')) { + db.createObjectStore('theme', { keyPath: 'id' }); + } + + if (!db.objectStoreNames.contains('providers')) { + db.createObjectStore('providers', { keyPath: 'id' }); + } }; request.onsuccess = (event: Event) => { @@ -158,3 +167,49 @@ async function getUrlIds(db: IDBDatabase): Promise { }; }); } + +export async function saveTheme(db: IDBDatabase, theme: string): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('theme', 'readwrite'); + const store = transaction.objectStore('theme'); + + const request = store.put({ id: 'theme', value: theme }); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +export async function getTheme(db: IDBDatabase): Promise<{ id: string; value: string } | undefined> { + return new Promise((resolve, reject) => { + const transaction = db.transaction('theme', 'readonly'); + const store = transaction.objectStore('theme'); + const request = store.get('theme'); + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +export async function setProvider(db: IDBDatabase, provider: Provider): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('providers', 'readwrite'); + const store = transaction.objectStore('providers'); + + const request = store.put(provider); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +export async function getProviderById(db: IDBDatabase, id: string): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('providers', 'readonly'); + const store = transaction.objectStore('providers'); + const request = store.get(id); + + request.onsuccess = () => resolve(request.result as Provider); + request.onerror = () => reject(request.error); + }); +} diff --git a/app/lib/stores/theme.ts b/app/lib/stores/theme.ts index 4f3e47bd4..470df33a6 100644 --- a/app/lib/stores/theme.ts +++ b/app/lib/stores/theme.ts @@ -1,4 +1,5 @@ import { atom } from 'nanostores'; +import { openDatabase, saveTheme, getTheme } from '~/lib/persistence/db'; export type Theme = 'dark' | 'light'; @@ -10,26 +11,38 @@ export function themeIsDark() { export const DEFAULT_THEME = 'light'; -export const themeStore = atom(initStore()); +export const themeStore = atom(await initStore()); -function initStore() { +async function getThemeFromDB(): Promise { + const db = await openDatabase(); + if (db) { + const themeResult = await getTheme(db); + return (themeResult?.value as Theme) || DEFAULT_THEME; + } + return DEFAULT_THEME; +} + +async function initStore() { if (!import.meta.env.SSR) { - const persistedTheme = localStorage.getItem(kTheme) as Theme | undefined; const themeAttribute = document.querySelector('html')?.getAttribute('data-theme'); + const dbTheme = await getThemeFromDB(); - return persistedTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME; + return dbTheme ?? (themeAttribute as Theme) ?? DEFAULT_THEME; } return DEFAULT_THEME; } -export function toggleTheme() { +export async function toggleTheme() { const currentTheme = themeStore.get(); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; themeStore.set(newTheme); - localStorage.setItem(kTheme, newTheme); - document.querySelector('html')?.setAttribute('data-theme', newTheme); + + const db = await openDatabase(); + if (db) { + await saveTheme(db, newTheme); + } } diff --git a/app/types/provider.ts b/app/types/provider.ts new file mode 100644 index 000000000..de1150edc --- /dev/null +++ b/app/types/provider.ts @@ -0,0 +1,7 @@ +import type { ModelInfo } from '~/utils/types'; + +export interface Provider { + id: string; + apiKey: string; + models: ModelInfo[]; +} diff --git a/app/utils/constants.ts b/app/utils/constants.ts index b6b4dd344..d56ff7f27 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -30,7 +30,7 @@ const staticModels: ModelInfo[] = [ { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' }, { name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI' }, { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI' }, - { name: 'mistral-large', label: 'Mistral-large', provider: 'Mistral' }, + { name: 'mistral-large-latest', label: 'Mistral-large-latest', provider: 'Mistral' }, ]; export let MODEL_LIST: ModelInfo[] = [...staticModels]; diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index f3259893e..58dd9eef6 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -3,5 +3,6 @@ interface Env { OPENAI_API_KEY: string; GROQ_API_KEY: string; OPEN_ROUTER_API_KEY: string; + MISTRAL_AI_API_KEY: string; OLLAMA_API_BASE_URL: string; }