Use Supabase for problems and user login (#85)

This commit is contained in:
Brian Hackett
2025-03-30 05:31:56 -07:00
committed by GitHub
parent f5259243b8
commit ac1858b84e
127 changed files with 1430 additions and 9887 deletions

View File

@@ -1,110 +1,8 @@
# Rename this file to .env once you have filled in the below environment variables!
# Get your GROQ API Key here -
# https://console.groq.com/keys
# You only need this environment variable set if you want to use Groq models
GROQ_API_KEY=
# Get your HuggingFace API Key here -
# https://huggingface.co/settings/tokens
# You only need this environment variable set if you want to use HuggingFace models
HuggingFace_API_KEY=
# Get your Open AI API Key by following these instructions -
# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
# You only need this environment variable set if you want to use GPT models
OPENAI_API_KEY=
# Get your Anthropic API Key in your account settings -
# https://console.anthropic.com/settings/keys
# You only need this environment variable set if you want to use Claude models
ANTHROPIC_API_KEY=
# Get your OpenRouter API Key in your account settings -
# https://openrouter.ai/settings/keys
# You only need this environment variable set if you want to use OpenRouter models
OPEN_ROUTER_API_KEY=
# Get your Google Generative AI API Key by following these instructions -
# https://console.cloud.google.com/apis/credentials
# You only need this environment variable set if you want to use Google Generative AI models
GOOGLE_GENERATIVE_AI_API_KEY=
# You only need this environment variable set if you want to use oLLAMA models
# DONT USE http://localhost:11434 due to IPV6 issues
# USE EXAMPLE http://127.0.0.1:11434
OLLAMA_API_BASE_URL=
# You only need this environment variable set if you want to use OpenAI Like models
OPENAI_LIKE_API_BASE_URL=
# You only need this environment variable set if you want to use Together AI models
TOGETHER_API_BASE_URL=
# You only need this environment variable set if you want to use DeepSeek models through their API
DEEPSEEK_API_KEY=
# Get your OpenAI Like API Key
OPENAI_LIKE_API_KEY=
# Get your Together API Key
TOGETHER_API_KEY=
# You only need this environment variable set if you want to use Hyperbolic models
#Get your Hyperbolics API Key at https://app.hyperbolic.xyz/settings
#baseURL="https://api.hyperbolic.xyz/v1/chat/completions"
HYPERBOLIC_API_KEY=
HYPERBOLIC_API_BASE_URL=
# Get your Mistral API Key by following these instructions -
# https://console.mistral.ai/api-keys/
# You only need this environment variable set if you want to use Mistral models
MISTRAL_API_KEY=
# Get the Cohere Api key by following these instructions -
# https://dashboard.cohere.com/api-keys
# You only need this environment variable set if you want to use Cohere models
COHERE_API_KEY=
# Get LMStudio Base URL from LM Studio Developer Console
# Make sure to enable CORS
# DONT USE http://localhost:1234 due to IPV6 issues
# Example: http://127.0.0.1:1234
LMSTUDIO_API_BASE_URL=
# Get your xAI API key
# https://x.ai/api
# You only need this environment variable set if you want to use xAI models
XAI_API_KEY=
# Get your Perplexity API Key here -
# https://www.perplexity.ai/settings/api
# You only need this environment variable set if you want to use Perplexity models
PERPLEXITY_API_KEY=
# Get your AWS configuration
# https://console.aws.amazon.com/iam/home
# The JSON should include the following keys:
# - region: The AWS region where Bedrock is available.
# - accessKeyId: Your AWS access key ID.
# - secretAccessKey: Your AWS secret access key.
# - sessionToken (optional): Temporary session token if using an IAM role or temporary credentials.
# Example JSON:
# {"region": "us-east-1", "accessKeyId": "yourAccessKeyId", "secretAccessKey": "yourSecretAccessKey", "sessionToken": "yourSessionToken"}
AWS_BEDROCK_CONFIG=
# Copy this file to .env once you have filled in the below environment variables!
# Include this environment variable if you want more logging for debugging locally
VITE_LOG_LEVEL=debug
# Example Context Values for qwen2.5-coder:32b
#
# DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM
# DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM
# DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM
# DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM
DEFAULT_NUM_CTX=
# set these to enable opentelemetry
HONEYCOMB_API_KEY=
HONEYCOMB_DATASET=
@@ -113,9 +11,5 @@ HONEYCOMB_DATASET=
SENTRY_AUTH_TOKEN=
# Supabase Configuration
# Get your Supabase URL and anon key from your project settings -> API
# https://app.supabase.com/project/_/settings/api
SUPABASE_URL=your_supabase_project_url
SUPABASE_ANON_KEY=your_supabase_anon_key
USE_SUPABASE=false
SUPABASE_URL=https://zbkcavxidjyslqmnbfux.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpia2NhdnhpZGp5c2xxbW5iZnV4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAwMDU0MjAsImV4cCI6MjA1NTU4MTQyMH0.xHKTareBFW0LW0AmYXH0vOvU3mLB3jkQGwhoNWqpnTw

View File

@@ -1,150 +0,0 @@
# Contribution Guidelines
Welcome! This guide provides all the details you need to contribute effectively to the project. Thank you for helping us make **bolt.diy** a better tool for developers worldwide. 💡
---
## 📋 Table of Contents
1. [Code of Conduct](#code-of-conduct)
2. [How Can I Contribute?](#how-can-i-contribute)
3. [Pull Request Guidelines](#pull-request-guidelines)
4. [Coding Standards](#coding-standards)
5. [Development Setup](#development-setup)
6. [Testing](#testing)
7. [Deployment](#deployment)
8. [Docker Deployment](#docker-deployment)
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
---
## 🛡️ Code of Conduct
This project is governed by our **Code of Conduct**. By participating, you agree to uphold this code. Report unacceptable behavior to the project maintainers.
---
## 🛠️ How Can I Contribute?
### 1⃣ Reporting Bugs or Feature Requests
- Check the [issue tracker](#) to avoid duplicates.
- Use issue templates (if available).
- Provide detailed, relevant information and steps to reproduce bugs.
### 2⃣ Code Contributions
1. Fork the repository.
2. Create a feature or fix branch.
3. Write and test your code.
4. Submit a pull request (PR).
### 3⃣ Join as a Core Contributor
Interested in maintaining and growing the project? Fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7).
---
## ✅ Pull Request Guidelines
### PR Checklist
- Branch from the **main** branch.
- Update documentation, if needed.
- Test all functionality manually.
- Focus on one feature/bug per PR.
### Review Process
1. Manual testing by reviewers.
2. At least one maintainer review required.
3. Address review comments.
4. Maintain a clean commit history.
---
## 📏 Coding Standards
### General Guidelines
- Follow existing code style.
- Comment complex logic.
- Keep functions small and focused.
- Use meaningful variable names.
---
## 🖥️ Development Setup
### 1⃣ Initial Setup
- Clone the repository:
```bash
git clone https://github.com/stackblitz-labs/bolt.diy.git
```
- Install dependencies:
```bash
pnpm install
```
- Set up environment variables:
1. Rename `.env.example` to `.env.local`.
2. Add your API keys:
```bash
GROQ_API_KEY=XXX
HuggingFace_API_KEY=XXX
OPENAI_API_KEY=XXX
...
```
3. Optionally set:
- Debug level: `VITE_LOG_LEVEL=debug`
- Context size: `DEFAULT_NUM_CTX=32768`
**Note**: Never commit your `.env.local` file to version control. Its already in `.gitignore`.
### 2⃣ Run Development Server
```bash
pnpm run dev
```
**Tip**: Use **Google Chrome Canary** for local testing.
---
## 🧪 Testing
Run the test suite with:
```bash
pnpm test
```
---
### Coolify Deployment
For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
1. Import your Git repository into Coolify.
2. Choose **Docker Compose** as the build pack.
3. Configure environment variables (e.g., API keys).
4. Set the start command:
```bash
docker compose --profile production up
```
---
## 🛠️ VS Code Dev Containers Integration
The `docker-compose.yaml` configuration is compatible with **VS Code Dev Containers**, making it easy to set up a development environment directly in Visual Studio Code.
### Steps to Use Dev Containers
1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
2. Select **Dev Containers: Reopen in Container**.
3. Choose the **development** profile when prompted.
4. VS Code will rebuild the container and open it with the pre-configured environment.
---
## 🔑 Environment Variables
Ensure `.env.local` is configured correctly with:
- API keys.
- Context-specific configurations.
Example for the `DEFAULT_NUM_CTX` variable:
```bash
DEFAULT_NUM_CTX=24576 # Uses 32GB VRAM
```

View File

@@ -1,92 +0,0 @@
ARG BASE=node:20.18.0
FROM ${BASE} AS base
WORKDIR /app
# Install dependencies (this step is cached as long as the dependencies don't change)
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install
# Copy the rest of your app's source code
COPY . .
# Expose the port the app runs on
EXPOSE 5173
# Production image
FROM base AS bolt-ai-production
# Define environment variables with default values or let them be overridden
ARG GROQ_API_KEY
ARG HuggingFace_API_KEY
ARG OPENAI_API_KEY
ARG ANTHROPIC_API_KEY
ARG OPEN_ROUTER_API_KEY
ARG GOOGLE_GENERATIVE_AI_API_KEY
ARG OLLAMA_API_BASE_URL
ARG XAI_API_KEY
ARG TOGETHER_API_KEY
ARG TOGETHER_API_BASE_URL
ARG AWS_BEDROCK_CONFIG
ARG VITE_LOG_LEVEL=debug
ARG DEFAULT_NUM_CTX
ENV WRANGLER_SEND_METRICS=false \
GROQ_API_KEY=${GROQ_API_KEY} \
HuggingFace_KEY=${HuggingFace_API_KEY} \
OPENAI_API_KEY=${OPENAI_API_KEY} \
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
XAI_API_KEY=${XAI_API_KEY} \
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\
RUNNING_IN_DOCKER=true
# Pre-configure wrangler to disable metrics
RUN mkdir -p /root/.config/.wrangler && \
echo '{"enabled":false}' > /root/.config/.wrangler/metrics.json
RUN pnpm run build
CMD [ "pnpm", "run", "dockerstart"]
# Development image
FROM base AS bolt-ai-development
# Define the same environment variables for development
ARG GROQ_API_KEY
ARG HuggingFace
ARG OPENAI_API_KEY
ARG ANTHROPIC_API_KEY
ARG OPEN_ROUTER_API_KEY
ARG GOOGLE_GENERATIVE_AI_API_KEY
ARG OLLAMA_API_BASE_URL
ARG XAI_API_KEY
ARG TOGETHER_API_KEY
ARG TOGETHER_API_BASE_URL
ARG VITE_LOG_LEVEL=debug
ARG DEFAULT_NUM_CTX
ENV GROQ_API_KEY=${GROQ_API_KEY} \
HuggingFace_API_KEY=${HuggingFace_API_KEY} \
OPENAI_API_KEY=${OPENAI_API_KEY} \
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} \
OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY} \
GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY} \
OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL} \
XAI_API_KEY=${XAI_API_KEY} \
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG} \
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}\
RUNNING_IN_DOCKER=true
RUN mkdir -p ${WORKDIR}/run
CMD pnpm run dev --host

91
FAQ.md
View File

@@ -1,91 +0,0 @@
# Frequently Asked Questions (FAQ)
<details>
<summary><strong>What are the best models for bolt.diy?</strong></summary>
For the best experience with bolt.diy, we recommend using the following models:
- **Claude 3.5 Sonnet (old)**: Best overall coder, providing excellent results across all use cases
- **Gemini 2.0 Flash**: Exceptional speed while maintaining good performance
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
- **DeepSeekCoder V2 236b**: Best open source model (available through OpenRouter, DeepSeek API, or self-hosted)
- **Qwen 2.5 Coder 32b**: Best model for self-hosting with reasonable hardware requirements
**Note**: Models with less than 7b parameters typically lack the capability to properly interact with bolt!
</details>
<details>
<summary><strong>How do I get the best results with bolt.diy?</strong></summary>
- **Be specific about your stack**:
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
- **Use the enhance prompt icon**:
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
- **Scaffold the basics first, then add features**:
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
- **Batch simple instructions**:
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
</details>
<details>
<summary><strong>How do I contribute to bolt.diy?</strong></summary>
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
</details>
<details>
<summary><strong>What are the future plans for bolt.diy?</strong></summary>
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
New features and improvements are on the way!
</details>
<details>
<summary><strong>Why are there so many open issues/pull requests?</strong></summary>
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.
</details>
<details>
<summary><strong>How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?</strong></summary>
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
</details>
<details>
<summary><strong>Common Errors and Troubleshooting</strong></summary>
### **"There was an error processing this request"**
This generic error message means something went wrong. Check both:
- The terminal (if you started the app with Docker or `pnpm`).
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
### **"x-api-key header missing"**
This error is sometimes resolved by restarting the Docker container.
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
### **Blank preview when running the app**
A blank preview often occurs due to hallucinated bad code or incorrect commands.
To troubleshoot:
- Check the developer console for errors.
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
### **"Everything works, but the results are bad"**
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like GPT-4o, Claude 3.5 Sonnet, or DeepSeek Coder V2 236b.
### **"Received structured exception #0xc0000005: access violation"**
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
### **"Miniflare or Wrangler errors in Windows"**
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here https://github.com/stackblitz-labs/bolt.diy/issues/19.
</details>
---
Got more questions? Feel free to reach out or open an issue in our GitHub repo!

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 StackBlitz, Inc. and bolt.diy contributors
Copyright (c) 2024 StackBlitz, Inc., bolt.diy contributors, and Record Replay Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,57 +0,0 @@
# Project management of bolt.diy
First off: this sounds funny, we know. "Project management" comes from a world of enterprise stuff and this project is
far from being enterprisy- it's still anarchy all over the place 😉
But we need to organize ourselves somehow, right?
> tl;dr: We've got a project board with epics and features. We use PRs as change log and as materialized features. Find it [here](https://github.com/orgs/stackblitz-labs/projects/4).
Here's how we structure long-term vision, mid-term capabilities of the software and short term improvements.
## Strategic epics (long-term)
Strategic epics define areas in which the product evolves. Usually, these epics dont overlap. They shall allow the core
team to define what they believe is most important and should be worked on with the highest priority.
You can find the [epics as issues](https://github.com/stackblitz-labs/bolt.diy/labels/epic) which are probably never
going to be closed.
What's the benefit / purpose of epics?
1. Prioritization
E. g. we could say “managing files is currently more important that quality”. Then, we could thing about which features
would bring “managing files” forward. It may be different features, such as “upload local files”, “import from a repo”
or also undo/redo/commit.
In a more-or-less regular meeting dedicated for that, the core team discusses which epics matter most, sketch features
and then check who can work on them. After the meeting, they update the roadmap (at least for the next development turn)
and this way communicate where the focus currently is.
2. Grouping of features
By linking features with epics, we can keep them together and document *why* we invest work into a particular thing.
## Features (mid-term)
We all know probably a dozen of methodologies following which features are being described (User story, business
function, you name it).
However, we intentionally describe features in a more vague manner. Why? Everybody loves crisp, well-defined
acceptance-criteria, no? Well, every product owner loves it. because he knows what hell get once its done.
But: **here is no owner of this product**. Therefore, we grant *maximum flexibility to the developer contributing a feature* so that he can bring in his ideas and have most fun implementing it.
The feature therefore tries to describe *what* should be improved but not in detail *how*.
## PRs as materialized features (short-term)
Once a developer starts working on a feature, a draft-PR *can* be opened asap to share, describe and discuss, how the feature shall be implemented. But: this is not a must. It just helps to get early feedback and get other developers involved. Sometimes, the developer just wants to get started and then open a PR later.
In a loosely organized project, it may as well happen that multiple PRs are opened for the same feature. This is no real issue: Usually, peoply being passionate about a solution are willing to join forces and get it done together. And if a second developer was just faster getting the same feature realized: Be happy that it's been done, close the PR and look out for the next feature to implement 🤓
## PRs as change log
Once a PR is merged, a squashed commit contains the whole PR description which allows for a good change log.
All authors of commits in the PR are mentioned in the squashed commit message and become contributors 🙌

View File

@@ -85,19 +85,29 @@ export function ClientAuth() {
toast.success('Signed out successfully');
};
const handleGoogleSignIn = async () => {
const { error } = await getSupabase().auth.signInWithOAuth({
provider: 'google',
});
console.log('GoogleSignIn', error);
};
if (loading) {
return <div className="w-8 h-8 rounded-full bg-gray-300 animate-pulse" />;
}
// Avatar URLs are disabled due to broken links from CORS issues.
const useAvatarURL = false;
return (
<>
{user ? (
<div className="relative">
<button
className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-500 text-white"
className="flex items-center justify-center w-8 h-8 rounded-full bg-green-500 text-white"
onClick={() => setShowDropdown(!showDropdown)}
>
{user.user_metadata?.avatar_url ? (
{useAvatarURL && user.user_metadata?.avatar_url ? (
<img
src={user.user_metadata.avatar_url}
alt="User avatar"
@@ -109,7 +119,7 @@ export function ClientAuth() {
</button>
{showDropdown && (
<div className="absolute right-0 mt-2 w-48 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded shadow-lg z-10">
<div className="absolute right-0 mt-2 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded shadow-lg z-10">
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
{user.email}
</div>
@@ -125,7 +135,7 @@ export function ClientAuth() {
) : (
<button
onClick={() => setShowSignIn(true)}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 font-bold"
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
>
Sign In
</button>
@@ -141,6 +151,14 @@ export function ClientAuth() {
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-2xl font-bold mb-4 text-bolt-elements-textPrimary">Sign In</h2>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isSigningIn}
className="w-full mb-4 p-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
{isSigningIn ? 'Processing...' : 'Use Google'}
</button>
<form onSubmit={handleSignIn}>
<div className="mb-4">
<label htmlFor="email" className="block mb-1 text-bolt-elements-textPrimary">
@@ -172,7 +190,7 @@ export function ClientAuth() {
<button
type="submit"
disabled={isSigningIn}
className="flex-1 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
className="flex-1 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
{isSigningIn ? 'Processing...' : 'Sign In'}
</button>

View File

@@ -1,169 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { IconButton } from '~/components/ui/IconButton';
import type { ProviderInfo } from '~/types/model';
import Cookies from 'js-cookie';
interface APIKeyManagerProps {
provider: ProviderInfo;
apiKey: string;
setApiKey: (key: string) => void;
getApiKeyLink?: string;
labelForGetApiKey?: string;
}
// cache which stores whether the provider's API key is set via environment variable
const providerEnvKeyStatusCache: Record<string, boolean> = {};
const apiKeyMemoizeCache: { [k: string]: Record<string, string> } = {};
export function getApiKeysFromCookies() {
const storedApiKeys = Cookies.get('apiKeys');
let parsedKeys: Record<string, string> = {};
if (storedApiKeys) {
parsedKeys = apiKeyMemoizeCache[storedApiKeys];
if (!parsedKeys) {
parsedKeys = apiKeyMemoizeCache[storedApiKeys] = JSON.parse(storedApiKeys);
}
}
return parsedKeys;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
const [isEditing, setIsEditing] = useState(false);
const [tempKey, setTempKey] = useState(apiKey);
const [isEnvKeySet, setIsEnvKeySet] = useState(false);
// Reset states and load saved key when provider changes
useEffect(() => {
// Load saved API key from cookies for this provider
const savedKeys = getApiKeysFromCookies();
const savedKey = savedKeys[provider.name] || '';
setTempKey(savedKey);
setApiKey(savedKey);
setIsEditing(false);
}, [provider.name]);
const checkEnvApiKey = useCallback(async () => {
// Check cache first
if (providerEnvKeyStatusCache[provider.name] !== undefined) {
setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]);
return;
}
try {
const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`);
const data = await response.json();
const isSet = (data as { isSet: boolean }).isSet;
// Cache the result
providerEnvKeyStatusCache[provider.name] = isSet;
setIsEnvKeySet(isSet);
} catch (error) {
console.error('Failed to check environment API key:', error);
setIsEnvKeySet(false);
}
}, [provider.name]);
useEffect(() => {
checkEnvApiKey();
}, [checkEnvApiKey]);
const handleSave = () => {
// Save to parent state
setApiKey(tempKey);
// Save to cookies
const currentKeys = getApiKeysFromCookies();
const newKeys = { ...currentKeys, [provider.name]: tempKey };
Cookies.set('apiKeys', JSON.stringify(newKeys));
setIsEditing(false);
};
return (
<div className="flex items-center justify-between py-3 px-1">
<div className="flex items-center gap-2 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
{!isEditing && (
<div className="flex items-center gap-2">
{apiKey ? (
<>
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
<span className="text-xs text-green-500">Set via UI</span>
</>
) : isEnvKeySet ? (
<>
<div className="i-ph:check-circle-fill text-green-500 w-4 h-4" />
<span className="text-xs text-green-500">Set via environment variable</span>
</>
) : (
<>
<div className="i-ph:x-circle-fill text-red-500 w-4 h-4" />
<span className="text-xs text-red-500">Not Set (Please set via UI or ENV_VAR)</span>
</>
)}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{isEditing ? (
<div className="flex items-center gap-2">
<input
type="password"
value={tempKey}
placeholder="Enter API Key"
onChange={(e) => setTempKey(e.target.value)}
className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor
bg-bolt-elements-prompt-background text-bolt-elements-textPrimary
focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
/>
<IconButton
onClick={handleSave}
title="Save API Key"
className="bg-green-500/10 hover:bg-green-500/20 text-green-500"
>
<div className="i-ph:check w-4 h-4" />
</IconButton>
<IconButton
onClick={() => setIsEditing(false)}
title="Cancel"
className="bg-red-500/10 hover:bg-red-500/20 text-red-500"
>
<div className="i-ph:x w-4 h-4" />
</IconButton>
</div>
) : (
<>
{
<IconButton
onClick={() => setIsEditing(true)}
title="Edit API Key"
className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500"
>
<div className="i-ph:pencil-simple w-4 h-4" />
</IconButton>
}
{provider?.getApiKeyLink && !apiKey && (
<IconButton
onClick={() => window.open(provider?.getApiKeyLink)}
title="Get API Key"
className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2"
>
<span className="text-xs whitespace-nowrap">{provider?.labelForGetApiKey || 'Get API Key'}</span>
<div className={`${provider?.icon || 'i-ph:key'} w-4 h-4`} />
</IconButton>
)}
</>
)}
</div>
</div>
);
};

View File

@@ -9,13 +9,11 @@ import { IconButton } from '~/components/ui/IconButton';
import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames';
import { Messages } from './Messages.client';
import { getPreviousRepositoryId } from '~/lib/persistence/useChatHistory';
import type { Message } from '~/lib/persistence/message';
import { getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
import { SendButton } from './SendButton.client';
import * as Tooltip from '@radix-ui/react-tooltip';
import styles from './BaseChat.module.scss';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
@@ -36,7 +34,6 @@ interface BaseChatProps {
chatStarted?: boolean;
isStreaming?: boolean;
messages?: Message[];
description?: string;
enhancingPrompt?: boolean;
promptEnhanced?: boolean;
input?: string;
@@ -45,8 +42,7 @@ interface BaseChatProps {
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
_enhancingPrompt?: boolean;
_enhancePrompt?: () => void;
importChat?: (description: string, messages: Message[]) => Promise<void>;
exportChat?: () => void;
importChat?: (title: string, messages: Message[]) => Promise<void>;
uploadedFiles?: File[];
setUploadedFiles?: (files: File[]) => void;
imageDataList?: string[];
@@ -67,14 +63,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
chatStarted = false,
isStreaming = false,
input = '',
_enhancingPrompt,
handleInputChange,
_enhancePrompt,
sendMessage,
handleStop,
importChat,
exportChat,
uploadedFiles = [],
setUploadedFiles,
imageDataList = [],
@@ -350,7 +343,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
onStop={stopListening}
disabled={isStreaming}
/>
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
</div>
{input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary">

View File

@@ -4,17 +4,15 @@
*/
import { useStore } from '@nanostores/react';
import { useAnimate } from 'framer-motion';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { useSnapScroll } from '~/lib/hooks';
import { description, useChatHistory } from '~/lib/persistence';
import { useChatHistory } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { PROMPT_COOKIE_KEY } from '~/utils/constants';
import { cubicEasingFn } from '~/utils/easings';
import { renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
import Cookies from 'js-cookie';
import { debounce } from '~/utils/debounce';
import { useSearchParams } from '@remix-run/react';
import { createSampler } from '~/utils/sampler';
import {
@@ -28,13 +26,13 @@ import {
import { getIFrameSimulationData } from '~/lib/replay/Recording';
import { getCurrentIFrame } from '~/components/workbench/Preview';
import { getCurrentMouseData } from '~/components/workbench/PointSelector';
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, maxFreeUses } from '~/utils/freeUses';
import { getNutLoginKey, submitFeedback } from '~/lib/replay/Problems';
import { anthropicNumFreeUsesCookieName, maxFreeUses } from '~/utils/freeUses';
import { submitFeedback } from '~/lib/replay/Problems';
import { ChatMessageTelemetry, pingTelemetry } from '~/lib/hooks/pingTelemetry';
import type { RejectChangeData } from './ApproveChange';
import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient';
import { getMessagesRepositoryId, getPreviousRepositoryId } from '~/lib/persistence/useChatHistory';
import type { Message } from '~/lib/persistence/message';
import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
import { useAuthStatus } from '~/lib/stores/auth';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -75,19 +73,12 @@ setInterval(async () => {
export function Chat() {
renderLogger.trace('Chat');
const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
const title = useStore(description);
const { ready, initialMessages, storeMessageHistory, importChat } = useChatHistory();
return (
<>
{ready && (
<ChatImpl
description={title}
initialMessages={initialMessages}
exportChat={exportChat}
storeMessageHistory={storeMessageHistory}
importChat={importChat}
/>
<ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} importChat={importChat} />
)}
<ToastContainer
closeButton={({ closeToast }) => {
@@ -139,8 +130,6 @@ interface ChatProps {
initialMessages: Message[];
storeMessageHistory: (messages: Message[]) => Promise<void>;
importChat: (description: string, messages: Message[]) => Promise<void>;
exportChat: () => void;
description?: string;
}
let gNumAborts = 0;
@@ -151,406 +140,377 @@ async function clearActiveChat() {
gActiveChatMessageTelemetry = undefined;
}
export const ChatImpl = memo(
({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
const [searchParams, setSearchParams] = useSearchParams();
const [approveChangesMessageId, setApproveChangesMessageId] = useState<string | undefined>(undefined);
export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat }: ChatProps) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
const [searchParams, setSearchParams] = useSearchParams();
const [approveChangesMessageId, setApproveChangesMessageId] = useState<string | undefined>(undefined);
const { isLoggedIn } = useAuthStatus();
// Input currently in the textarea.
const [input, setInput] = useState('');
// Input currently in the textarea.
const [input, setInput] = useState('');
/*
* This is set when the user has triggered a chat message and the response hasn't finished
* being generated.
*/
const [activeChatId, setActiveChatId] = useState<string | undefined>(undefined);
const isLoading = activeChatId !== undefined;
/*
* This is set when the user has triggered a chat message and the response hasn't finished
* being generated.
*/
const [activeChatId, setActiveChatId] = useState<string | undefined>(undefined);
const isLoading = activeChatId !== undefined;
const [messages, setMessages] = useState<Message[]>(initialMessages);
const [messages, setMessages] = useState<Message[]>(initialMessages);
const { showChat } = useStore(chatStore);
const { showChat } = useStore(chatStore);
const [animationScope, animate] = useAnimate();
const [animationScope, animate] = useAnimate();
useEffect(() => {
const prompt = searchParams.get('prompt');
useEffect(() => {
const prompt = searchParams.get('prompt');
if (prompt) {
setSearchParams({});
sendMessage(prompt);
}
}, [searchParams]);
if (prompt) {
setSearchParams({});
sendMessage(prompt);
}
}, [searchParams]);
// Load any repository in the initial messages.
useEffect(() => {
const repositoryId = getMessagesRepositoryId(initialMessages);
// Load any repository in the initial messages.
useEffect(() => {
const repositoryId = getMessagesRepositoryId(initialMessages);
if (repositoryId) {
simulationRepositoryUpdated(repositoryId);
}
}, [initialMessages]);
if (repositoryId) {
simulationRepositoryUpdated(repositoryId);
}
}, [initialMessages]);
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
useEffect(() => {
chatStore.setKey('started', initialMessages.length > 0);
}, []);
useEffect(() => {
chatStore.setKey('started', initialMessages.length > 0);
}, []);
useEffect(() => {
processSampledMessages({
messages,
initialMessages,
storeMessageHistory,
});
}, [messages, isLoading]);
useEffect(() => {
processSampledMessages({
messages,
initialMessages,
storeMessageHistory,
});
}, [messages, isLoading]);
const abort = () => {
stop();
gNumAborts++;
chatStore.setKey('aborted', true);
setActiveChatId(undefined);
const abort = () => {
stop();
gNumAborts++;
chatStore.setKey('aborted', true);
setActiveChatId(undefined);
if (gActiveChatMessageTelemetry) {
gActiveChatMessageTelemetry.abort('StopButtonClicked');
if (gActiveChatMessageTelemetry) {
gActiveChatMessageTelemetry.abort('StopButtonClicked');
clearActiveChat();
simulationReset();
}
};
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
}
}, [input, textareaRef]);
const runAnimation = async () => {
if (chatStarted) {
return;
}
await Promise.all([
animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
]);
chatStore.setKey('started', true);
setChatStarted(true);
};
const sendMessage = async (messageInput?: string) => {
const _input = messageInput || input;
const numAbortsAtStart = gNumAborts;
if (_input.length === 0 || isLoading) {
return;
}
gActiveChatMessageTelemetry = new ChatMessageTelemetry(messages.length);
if (!isLoggedIn) {
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
if (numFreeUses >= maxFreeUses) {
toast.error('All free uses consumed. Please login to continue using Nut.');
gActiveChatMessageTelemetry.abort('NoFreeUses');
clearActiveChat();
simulationReset();
}
};
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
}
}, [input, textareaRef]);
const runAnimation = async () => {
if (chatStarted) {
return;
}
await Promise.all([
animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
]);
Cookies.set(anthropicNumFreeUsesCookieName, (numFreeUses + 1).toString());
}
chatStore.setKey('started', true);
const chatId = generateRandomId();
setActiveChatId(chatId);
setChatStarted(true);
const userMessage: Message = {
id: `user-${chatId}`,
role: 'user',
type: 'text',
content: _input,
};
const sendMessage = async (messageInput?: string) => {
const _input = messageInput || input;
const numAbortsAtStart = gNumAborts;
let newMessages = [...messages, userMessage];
if (_input.length === 0 || isLoading) {
return;
}
gActiveChatMessageTelemetry = new ChatMessageTelemetry(messages.length);
const loginKey = getNutLoginKey();
const apiKeyCookie = Cookies.get(anthropicApiKeyCookieName);
const anthropicApiKey = apiKeyCookie?.length ? apiKeyCookie : undefined;
if (!loginKey && !anthropicApiKey) {
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
if (numFreeUses >= maxFreeUses) {
toast.error(
'All free uses consumed. Please set a login key or Anthropic API key in the "User Info" settings.',
);
gActiveChatMessageTelemetry.abort('NoFreeUses');
clearActiveChat();
return;
}
Cookies.set(anthropicNumFreeUsesCookieName, (numFreeUses + 1).toString());
}
const chatId = generateRandomId();
setActiveChatId(chatId);
const userMessage: Message = {
id: `user-${chatId}`,
imageDataList.forEach((imageData, index) => {
const imageMessage: Message = {
id: `image-${chatId}-${index}`,
role: 'user',
type: 'text',
content: _input,
type: 'image',
dataURL: imageData,
};
newMessages.push(imageMessage);
});
let newMessages = [...messages, userMessage];
setMessages(newMessages);
imageDataList.forEach((imageData, index) => {
const imageMessage: Message = {
id: `image-${chatId}-${index}`,
role: 'user',
type: 'image',
dataURL: imageData,
};
newMessages.push(imageMessage);
});
// Add file cleanup here
setUploadedFiles([]);
setImageDataList([]);
setMessages(newMessages);
await flushSimulationData();
simulationFinishData();
// Add file cleanup here
setUploadedFiles([]);
setImageDataList([]);
chatStore.setKey('aborted', false);
await flushSimulationData();
simulationFinishData();
runAnimation();
chatStore.setKey('aborted', false);
runAnimation();
const existingRepositoryId = getMessagesRepositoryId(messages);
let updatedRepository = false;
const addResponseMessage = (msg: Message) => {
if (gNumAborts != numAbortsAtStart) {
return;
}
newMessages = [...newMessages];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage.id == msg.id) {
newMessages.pop();
assert(lastMessage.type == 'text', 'Last message must be a text message');
assert(msg.type == 'text', 'Message must be a text message');
newMessages.push({
...msg,
content: lastMessage.content + msg.content,
});
} else {
newMessages.push(msg);
}
setMessages(newMessages);
// Update the repository as soon as it has changed.
const responseRepositoryId = getMessagesRepositoryId(newMessages);
if (responseRepositoryId && existingRepositoryId != responseRepositoryId) {
simulationRepositoryUpdated(responseRepositoryId);
updatedRepository = true;
}
};
const references: ChatReference[] = [];
const mouseData = getCurrentMouseData();
if (mouseData) {
references.push({
kind: 'element',
selector: mouseData.selector,
x: mouseData.x,
y: mouseData.y,
width: mouseData.width,
height: mouseData.height,
});
}
try {
await sendChatMessage(newMessages, references, addResponseMessage);
} catch (e) {
toast.error('Error sending message');
console.error('Error sending message', e);
}
const existingRepositoryId = getMessagesRepositoryId(messages);
let updatedRepository = false;
const addResponseMessage = (msg: Message) => {
if (gNumAborts != numAbortsAtStart) {
return;
}
gActiveChatMessageTelemetry.finish();
clearActiveChat();
newMessages = [...newMessages];
setActiveChatId(undefined);
const lastMessage = newMessages[newMessages.length - 1];
setInput('');
Cookies.remove(PROMPT_COOKIE_KEY);
textareaRef.current?.blur();
if (updatedRepository) {
const lastMessage = newMessages[newMessages.length - 1];
setApproveChangesMessageId(lastMessage.id);
if (lastMessage.id == msg.id) {
newMessages.pop();
assert(lastMessage.type == 'text', 'Last message must be a text message');
assert(msg.type == 'text', 'Message must be a text message');
newMessages.push({
...msg,
content: lastMessage.content + msg.content,
});
} else {
simulationReset();
newMessages.push(msg);
}
setMessages(newMessages);
// Update the repository as soon as it has changed.
const responseRepositoryId = getMessagesRepositoryId(newMessages);
if (responseRepositoryId && existingRepositoryId != responseRepositoryId) {
simulationRepositoryUpdated(responseRepositoryId);
updatedRepository = true;
}
};
// Rewind far enough to erase the specified message.
const onRewind = async (messageId: string) => {
console.log('Rewinding', messageId);
const references: ChatReference[] = [];
const messageIndex = messages.findIndex((message) => message.id === messageId);
const mouseData = getCurrentMouseData();
if (messageIndex < 0) {
toast.error('Rewind message not found');
return;
}
const previousRepositoryId = getPreviousRepositoryId(messages, messageIndex);
if (!previousRepositoryId) {
toast.error('No repository ID found for rewind');
return;
}
setMessages(messages.slice(0, messageIndex));
simulationRepositoryUpdated(previousRepositoryId);
pingTelemetry('RewindChat', {
numMessages: messages.length,
rewindIndex: messageIndex,
loginKey: getNutLoginKey(),
if (mouseData) {
references.push({
kind: 'element',
selector: mouseData.selector,
x: mouseData.x,
y: mouseData.y,
width: mouseData.width,
height: mouseData.height,
});
};
}
const flashScreen = async () => {
const flash = document.createElement('div');
flash.style.position = 'fixed';
flash.style.top = '0';
flash.style.left = '0';
flash.style.width = '100%';
flash.style.height = '100%';
flash.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
flash.style.zIndex = '9999';
flash.style.pointerEvents = 'none';
document.body.appendChild(flash);
try {
await sendChatMessage(newMessages, references, addResponseMessage);
} catch (e) {
toast.error('Error sending message');
console.error('Error sending message', e);
}
// Fade out and remove after 500ms
if (gNumAborts != numAbortsAtStart) {
return;
}
gActiveChatMessageTelemetry.finish();
clearActiveChat();
setActiveChatId(undefined);
setInput('');
textareaRef.current?.blur();
if (updatedRepository) {
const lastMessage = newMessages[newMessages.length - 1];
setApproveChangesMessageId(lastMessage.id);
} else {
simulationReset();
}
};
// Rewind far enough to erase the specified message.
const onRewind = async (messageId: string) => {
console.log('Rewinding', messageId);
const messageIndex = messages.findIndex((message) => message.id === messageId);
if (messageIndex < 0) {
toast.error('Rewind message not found');
return;
}
const previousRepositoryId = getPreviousRepositoryId(messages, messageIndex);
if (!previousRepositoryId) {
toast.error('No repository ID found for rewind');
return;
}
setMessages(messages.slice(0, messageIndex));
simulationRepositoryUpdated(previousRepositoryId);
pingTelemetry('RewindChat', {
numMessages: messages.length,
rewindIndex: messageIndex,
});
};
const flashScreen = async () => {
const flash = document.createElement('div');
flash.style.position = 'fixed';
flash.style.top = '0';
flash.style.left = '0';
flash.style.width = '100%';
flash.style.height = '100%';
flash.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
flash.style.zIndex = '9999';
flash.style.pointerEvents = 'none';
document.body.appendChild(flash);
// Fade out and remove after 500ms
setTimeout(() => {
flash.style.transition = 'opacity 0.5s';
flash.style.opacity = '0';
setTimeout(() => {
flash.style.transition = 'opacity 0.5s';
flash.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(flash);
}, 500);
}, 200);
};
document.body.removeChild(flash);
}, 500);
}, 200);
};
const onApproveChange = async (messageId: string) => {
console.log('ApproveChange', messageId);
const onApproveChange = async (messageId: string) => {
console.log('ApproveChange', messageId);
setApproveChangesMessageId(undefined);
setApproveChangesMessageId(undefined);
await flashScreen();
await flashScreen();
pingTelemetry('ApproveChange', {
numMessages: messages.length,
loginKey: getNutLoginKey(),
});
};
pingTelemetry('ApproveChange', {
numMessages: messages.length,
});
};
const onRejectChange = async (messageId: string, data: RejectChangeData) => {
console.log('RejectChange', messageId, data);
const onRejectChange = async (messageId: string, data: RejectChangeData) => {
console.log('RejectChange', messageId, data);
setApproveChangesMessageId(undefined);
setApproveChangesMessageId(undefined);
const message = messages.find((message) => message.id === messageId);
assert(message, 'Message not found');
assert(message == messages[messages.length - 1], 'Message must be the last message');
const message = messages.find((message) => message.id === messageId);
assert(message, 'Message not found');
assert(message == messages[messages.length - 1], 'Message must be the last message');
// Erase all messages since the last user message.
let rewindMessageId = message.id;
// Erase all messages since the last user message.
let rewindMessageId = message.id;
for (let i = messages.length - 2; i >= 0; i--) {
if (messages[i].role == 'user') {
break;
}
rewindMessageId = messages[i].id;
}
await onRewind(rewindMessageId);
let shareProjectSuccess = false;
if (data.shareProject) {
const feedbackData: any = {
explanation: data.explanation,
chatMessages: messages,
loginKey: getNutLoginKey(),
};
shareProjectSuccess = await submitFeedback(feedbackData);
for (let i = messages.length - 2; i >= 0; i--) {
if (messages[i].role == 'user') {
break;
}
pingTelemetry('RejectChange', {
shareProject: data.shareProject,
shareProjectSuccess,
numMessages: messages.length,
loginKey: getNutLoginKey(),
});
};
rewindMessageId = messages[i].id;
}
await onRewind(rewindMessageId);
/**
* Handles the change event for the textarea and updates the input state.
* @param event - The change event from the textarea.
*/
const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(event.target.value);
};
let shareProjectSuccess = false;
/**
* Debounced function to cache the prompt in cookies.
* Caches the trimmed value of the textarea input after a delay to optimize performance.
*/
const debouncedCachePrompt = useCallback(
debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => {
const trimmedValue = event.target.value.trim();
Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 });
}, 1000),
[],
);
if (data.shareProject) {
const feedbackData: any = {
explanation: data.explanation,
chatMessages: messages,
};
const [messageRef, scrollRef] = useSnapScroll();
shareProjectSuccess = await submitFeedback(feedbackData);
}
gLastChatMessages = messages;
pingTelemetry('RejectChange', {
shareProject: data.shareProject,
shareProjectSuccess,
numMessages: messages.length,
});
};
return (
<BaseChat
ref={animationScope}
textareaRef={textareaRef}
input={input}
showChat={showChat}
chatStarted={chatStarted}
isStreaming={isLoading}
sendMessage={sendMessage}
messageRef={messageRef}
scrollRef={scrollRef}
handleInputChange={(e) => {
onTextareaChange(e);
debouncedCachePrompt(e);
}}
handleStop={abort}
description={description}
importChat={importChat}
exportChat={exportChat}
messages={messages}
uploadedFiles={uploadedFiles}
setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList}
setImageDataList={setImageDataList}
onRewind={onRewind}
approveChangesMessageId={approveChangesMessageId}
onApproveChange={onApproveChange}
onRejectChange={onRejectChange}
/>
);
},
);
/**
* Handles the change event for the textarea and updates the input state.
* @param event - The change event from the textarea.
*/
const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(event.target.value);
};
const [messageRef, scrollRef] = useSnapScroll();
gLastChatMessages = messages;
return (
<BaseChat
ref={animationScope}
textareaRef={textareaRef}
input={input}
showChat={showChat}
chatStarted={chatStarted}
isStreaming={isLoading}
sendMessage={sendMessage}
messageRef={messageRef}
scrollRef={scrollRef}
handleInputChange={(e) => {
onTextareaChange(e);
}}
handleStop={abort}
importChat={importChat}
messages={messages}
uploadedFiles={uploadedFiles}
setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList}
setImageDataList={setImageDataList}
onRewind={onRewind}
approveChangesMessageId={approveChangesMessageId}
onApproveChange={onApproveChange}
onRejectChange={onRejectChange}
/>
);
});

View File

@@ -1,108 +0,0 @@
import { AnimatePresence, motion } from 'framer-motion';
import type { ActionAlert } from '~/types/actions';
import { classNames } from '~/utils/classNames';
interface Props {
alert: ActionAlert;
clearAlert: () => void;
postMessage: (message: string) => void;
}
export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
const { description, content, source } = alert;
const isPreview = source === 'preview';
const title = isPreview ? 'Preview Error' : 'Terminal Error';
const message = isPreview
? 'We encountered an error while running the preview. Would you like Bolt to analyze and help resolve this issue?'
: 'We encountered an error while running terminal commands. Would you like Bolt to analyze and help resolve this issue?';
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4`}
>
<div className="flex items-start">
{/* Icon */}
<motion.div
className="flex-shrink-0"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
<div className={`i-ph:warning-duotone text-xl text-bolt-elements-button-danger-text`}></div>
</motion.div>
{/* Content */}
<div className="ml-3 flex-1">
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className={`text-sm font-medium text-bolt-elements-textPrimary`}
>
{title}
</motion.h3>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className={`mt-2 text-sm text-bolt-elements-textSecondary`}
>
<p>{message}</p>
{description && (
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
Error: {description}
</div>
)}
</motion.div>
{/* Actions */}
<motion.div
className="mt-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className={classNames(' flex gap-2')}>
<button
onClick={() =>
postMessage(
`*Fix this ${isPreview ? 'preview' : 'terminal'} error* \n\`\`\`${isPreview ? 'js' : 'sh'}\n${content}\n\`\`\`\n`,
)
}
className={classNames(
`px-2 py-1.5 rounded-md text-sm font-medium`,
'bg-bolt-elements-button-primary-background',
'hover:bg-bolt-elements-button-primary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
'text-bolt-elements-button-primary-text',
'flex items-center gap-1.5',
)}
>
<div className="i-ph:chat-circle-duotone"></div>
Ask Bolt
</button>
<button
onClick={clearAlert}
className={classNames(
`px-2 py-1.5 rounded-md text-sm font-medium`,
'bg-bolt-elements-button-secondary-background',
'hover:bg-bolt-elements-button-secondary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
'text-bolt-elements-button-secondary-text',
)}
>
Dismiss
</button>
</div>
</motion.div>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}

View File

@@ -1,11 +1,9 @@
import React from 'react';
const EXAMPLE_PROMPTS = [
{ text: 'Build a todo app in React using Tailwind' },
{ text: 'Build a simple blog using Astro' },
{ text: 'Create a cookie consent form using Material UI' },
{ text: 'Build a todo app in React' },
{ text: 'Build a simple blog' },
{ text: 'Make a space invaders game' },
{ text: 'Make a Tic Tac Toe game in html, css and js only' },
];
export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInput?: string): void | undefined }) {

View File

@@ -3,7 +3,7 @@ import { toast } from 'react-toastify';
import { createChatFromFolder } from '~/utils/folderImport';
import { logStore } from '~/lib/stores/logs';
import { assert } from '~/lib/replay/ReplayProtocolClient';
import type { BoltProblem } from '~/lib/replay/Problems';
import type { NutProblem } from '~/lib/replay/Problems';
import { getProblem } from '~/lib/replay/Problems';
import type { Message } from '~/lib/persistence/message';
@@ -12,12 +12,12 @@ interface LoadProblemButtonProps {
importChat?: (description: string, messages: Message[]) => Promise<void>;
}
export function setLastLoadedProblem(problem: BoltProblem) {
export function setLastLoadedProblem(problem: NutProblem) {
localStorage.setItem('loadedProblemId', problem.problemId);
}
export async function getOrFetchLastLoadedProblem(): Promise<BoltProblem | null> {
let problem: BoltProblem | null = null;
export async function getOrFetchLastLoadedProblem(): Promise<NutProblem | null> {
let problem: NutProblem | null = null;
const problemId = localStorage.getItem('loadedProblemId');
if (!problemId) {

View File

@@ -1,48 +0,0 @@
import { describe, expect, it } from 'vitest';
import { stripCodeFenceFromArtifact } from './Markdown';
describe('stripCodeFenceFromArtifact', () => {
it('should remove code fences around artifact element', () => {
const input = "```xml\n<div class='__boltArtifact__'></div>\n```";
const expected = "\n<div class='__boltArtifact__'></div>\n";
expect(stripCodeFenceFromArtifact(input)).toBe(expected);
});
it('should handle code fence with language specification', () => {
const input = "```typescript\n<div class='__boltArtifact__'></div>\n```";
const expected = "\n<div class='__boltArtifact__'></div>\n";
expect(stripCodeFenceFromArtifact(input)).toBe(expected);
});
it('should not modify content without artifacts', () => {
const input = '```\nregular code block\n```';
expect(stripCodeFenceFromArtifact(input)).toBe(input);
});
it('should handle empty input', () => {
expect(stripCodeFenceFromArtifact('')).toBe('');
});
it('should handle artifact without code fences', () => {
const input = "<div class='__boltArtifact__'></div>";
expect(stripCodeFenceFromArtifact(input)).toBe(input);
});
it('should handle multiple artifacts but only remove fences around them', () => {
const input = [
'Some text',
'```typescript',
"<div class='__boltArtifact__'></div>",
'```',
'```',
'regular code',
'```',
].join('\n');
const expected = ['Some text', '', "<div class='__boltArtifact__'></div>", '', '```', 'regular code', '```'].join(
'\n',
);
expect(stripCodeFenceFromArtifact(input)).toBe(expected);
});
});

View File

@@ -57,51 +57,7 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
remarkPlugins={remarkPlugins(limitedMarkdown)}
rehypePlugins={rehypePlugins(html)}
>
{stripCodeFenceFromArtifact(children)}
{children}
</ReactMarkdown>
);
});
/**
* Removes code fence markers (```) surrounding an artifact element while preserving the artifact content.
* This is necessary because artifacts should not be wrapped in code blocks when rendered for rendering action list.
*
* @param content - The markdown content to process
* @returns The processed content with code fence markers removed around artifacts
*
* @example
* // Removes code fences around artifact
* const input = "```xml\n<div class='__boltArtifact__'></div>\n```";
* stripCodeFenceFromArtifact(input);
* // Returns: "\n<div class='__boltArtifact__'></div>\n"
*
* @remarks
* - Only removes code fences that directly wrap an artifact (marked with __boltArtifact__ class)
* - Handles code fences with optional language specifications (e.g. ```xml, ```typescript)
* - Preserves original content if no artifact is found
* - Safely handles edge cases like empty input or artifacts at start/end of content
*/
export const stripCodeFenceFromArtifact = (content: string) => {
if (!content || !content.includes('__boltArtifact__')) {
return content;
}
const lines = content.split('\n');
const artifactLineIndex = lines.findIndex((line) => line.includes('__boltArtifact__'));
// Return original content if artifact line not found
if (artifactLineIndex === -1) {
return content;
}
// Check previous line for code fence
if (artifactLineIndex > 0 && lines[artifactLineIndex - 1]?.trim().match(/^```\w*$/)) {
lines[artifactLineIndex - 1] = '';
}
if (artifactLineIndex < lines.length - 1 && lines[artifactLineIndex + 1]?.trim().match(/^```$/)) {
lines[artifactLineIndex + 1] = '';
}
return lines.join('\n');
};

View File

@@ -2,7 +2,6 @@
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
import { Markdown } from './Markdown';
import type { Message } from '~/lib/persistence/message';
@@ -15,7 +14,7 @@ export function MessageContents({ message }: MessageContentsProps) {
case 'text':
return (
<div data-testid="message-content" className="overflow-hidden pt-[4px]">
<Markdown html>{stripMetadata(message.content)}</Markdown>
<Markdown html>{message.content}</Markdown>
</div>
);
case 'image':
@@ -32,7 +31,3 @@ export function MessageContents({ message }: MessageContentsProps) {
);
}
}
function stripMetadata(content: string) {
return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
}

View File

@@ -1,8 +1,7 @@
import React, { Suspense } from 'react';
import { classNames } from '~/utils/classNames';
import WithTooltip from '~/components/ui/Tooltip';
import { getPreviousRepositoryId } from '~/lib/persistence/useChatHistory';
import type { Message } from '~/lib/persistence/message';
import { getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
import { MessageContents } from './MessageContents';
interface MessagesProps {

View File

@@ -1,106 +0,0 @@
import type { ProviderInfo } from '~/types/model';
import { useEffect } from 'react';
import type { ModelInfo } from '~/lib/modules/llm/types';
interface ModelSelectorProps {
model?: string;
setModel?: (model: string) => void;
provider?: ProviderInfo;
setProvider?: (provider: ProviderInfo) => void;
modelList: ModelInfo[];
providerList: ProviderInfo[];
apiKeys: Record<string, string>;
modelLoading?: string;
}
export const ModelSelector = ({
model,
setModel,
provider,
setProvider,
modelList,
providerList,
modelLoading,
}: ModelSelectorProps) => {
// Load enabled providers from cookies
// Update enabled providers when cookies change
useEffect(() => {
// If current provider is disabled, switch to first enabled provider
if (providerList.length == 0) {
return;
}
if (provider && !providerList.map((p) => p.name).includes(provider.name)) {
const firstEnabledProvider = providerList[0];
setProvider?.(firstEnabledProvider);
// Also update the model to the first available one for the new provider
const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);
if (firstModel) {
setModel?.(firstModel.name);
}
}
}, [providerList, provider, setProvider, modelList, setModel]);
if (providerList.length === 0) {
return (
<div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
<p className="text-center">
No providers are currently enabled. Please enable at least one provider in the settings to start using the
chat.
</p>
</div>
);
}
return (
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
<select
value={provider?.name ?? ''}
onChange={(e) => {
const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
if (newProvider && setProvider) {
setProvider(newProvider);
}
const firstModel = [...modelList].find((m) => m.provider === e.target.value);
if (firstModel && setModel) {
setModel(firstModel.name);
}
}}
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
>
{providerList.map((provider: ProviderInfo) => (
<option key={provider.name} value={provider.name}>
{provider.name}
</option>
))}
</select>
<select
key={provider?.name}
value={model}
onChange={(e) => setModel?.(e.target.value)}
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%]"
disabled={modelLoading === 'all' || modelLoading === provider?.name}
>
{modelLoading == 'all' || modelLoading == provider?.name ? (
<option key={0} value="">
Loading...
</option>
) : (
[...modelList]
.filter((e) => e.provider == provider?.name && e.name)
.map((modelOption, index) => (
<option key={index} value={modelOption.name}>
{modelOption.label}
</option>
))
)}
</select>
</div>
);
};

View File

@@ -1,37 +0,0 @@
import React from 'react';
import type { Template } from '~/types/template';
import { STARTER_TEMPLATES } from '~/utils/constants';
interface FrameworkLinkProps {
template: Template;
}
const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
<a
href={`/git?url=https://github.com/${template.githubRepo}.git`}
data-state="closed"
data-discover="true"
className="items-center justify-center "
>
<div
className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-75 transition-all`}
/>
</a>
);
const StarterTemplates: React.FC = () => {
return (
<div className="flex flex-col items-center gap-4">
<span className="text-sm text-gray-500">or start a blank app with your favorite stack</span>
<div className="flex justify-center">
<div className="flex w-70 flex-wrap items-center justify-center gap-4">
{STARTER_TEMPLATES.map((template) => (
<FrameworkLink key={template.name} template={template} />
))}
</div>
</div>
</div>
);
};
export default StarterTemplates;

View File

@@ -1,13 +0,0 @@
import WithTooltip from '~/components/ui/Tooltip';
import { IconButton } from '~/components/ui/IconButton';
import React from 'react';
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
return (
<WithTooltip tooltip="Export Chat">
<IconButton title="Export Chat" onClick={() => exportChat?.()}>
<div className="i-ph:download-simple text-xl"></div>
</IconButton>
</WithTooltip>
);
};

View File

@@ -1,72 +1,11 @@
import { toast } from 'react-toastify';
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
import type { Message } from '~/lib/persistence/message';
type ChatData = {
messages?: Message[]; // Standard Bolt format
description?: string; // Optional description
};
export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
return (
<div className="flex flex-col items-center justify-center w-auto">
<input
type="file"
id="chat-import"
className="hidden"
accept=".json"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file && importChat) {
try {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const content = e.target?.result as string;
const data = JSON.parse(content) as ChatData;
// Standard format
if (Array.isArray(data.messages)) {
await importChat(data.description || 'Imported Chat', data.messages);
toast.success('Chat imported successfully');
return;
}
toast.error('Invalid chat file format');
} catch (error: unknown) {
if (error instanceof Error) {
toast.error('Failed to parse chat file: ' + error.message);
} else {
toast.error('Failed to parse chat file');
}
}
};
reader.onerror = () => toast.error('Failed to read chat file');
reader.readAsText(file);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to import chat');
}
e.target.value = ''; // Reset file input
} else {
toast.error('Something went wrong');
}
}}
/>
<div className="flex flex-col items-center gap-4 max-w-2xl text-center">
<div className="flex gap-2">
<button
onClick={() => {
const input = document.getElementById('chat-import');
input?.click();
}}
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
>
<div className="i-ph:upload-simple" />
Import Chat
</button>
<ImportFolderButton
importChat={importChat}
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"

View File

@@ -3,7 +3,6 @@ import ReactModal from 'react-modal';
import { useState } from 'react';
import { submitFeedback } from '~/lib/replay/Problems';
import { getLastChatMessages } from '~/components/chat/Chat.client';
import { shouldUseSupabase } from '~/lib/supabase/client';
ReactModal.setAppElement('#root');
@@ -35,25 +34,13 @@ export function Feedback() {
return;
}
if (!shouldUseSupabase() && !formData.email) {
toast.error('Please fill in the email field');
return;
}
toast.info('Submitting feedback...');
const feedbackData: any = shouldUseSupabase()
? {
description: formData.description,
share: formData.share,
source: 'feedback_modal',
}
: {
feedback: formData.description,
email: formData.email,
share: formData.share,
};
const feedbackData: any = {
description: formData.description,
share: formData.share,
source: 'feedback_modal',
};
if (feedbackData.share) {
feedbackData.chatMessages = getLastChatMessages();
@@ -74,8 +61,6 @@ export function Feedback() {
}
};
console.log(shouldUseSupabase() ? 'supabase true' : 'supabase false');
return (
<>
<button
@@ -130,25 +115,6 @@ export function Feedback() {
/>
</div>
{!shouldUseSupabase() && (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Your Email:</label>
<input
type="email"
name="email"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 w-full border border-gray-300"
value={formData.email}
placeholder="Enter your email address"
onChange={(e) => {
setFormData((prev) => ({
...prev,
email: e.target.value,
}));
}}
/>
</div>
)}
<div className="flex items-center gap-2 mb-6">
<input
type="checkbox"

View File

@@ -7,7 +7,6 @@ import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
import { Feedback } from './Feedback';
import { Suspense } from 'react';
import { ClientAuth } from '~/components/auth/ClientAuth';
import { shouldUseSupabase } from '~/lib/supabase/client';
export function Header() {
const chat = useStore(chatStore);
@@ -47,15 +46,13 @@ export function Header() {
</div>
</div>
{shouldUseSupabase() && (
<ClientOnly>
{() => (
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-gray-300 animate-pulse" />}>
<ClientAuth />
</Suspense>
)}
</ClientOnly>
)}
<ClientOnly>
{() => (
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-gray-300 animate-pulse" />}>
<ClientAuth />
</Suspense>
)}
</ClientOnly>
</header>
);
}

View File

@@ -5,13 +5,7 @@ import { classNames } from '~/utils/classNames';
import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog';
import { IconButton } from '~/components/ui/IconButton';
import styles from './Settings.module.scss';
import APIKeysTab from './providers/APIKeysTab';
import { useSettings } from '~/lib/hooks/useSettings';
import FeaturesTab from './features/FeaturesTab';
import DebugTab from './debug/DebugTab';
import EventLogsTab from './event-logs/EventLogsTab';
import ConnectionsTab from './connections/ConnectionsTab';
import DataTab from './data/DataTab';
interface SettingsProps {
open: boolean;
@@ -21,34 +15,10 @@ interface SettingsProps {
type TabType = 'data' | 'apiKeys' | 'features' | 'debug' | 'event-logs' | 'connection';
export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
const { debug, eventLogs } = useSettings();
const [activeTab, setActiveTab] = useState<TabType>('data');
const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
{ id: 'data', label: 'Data', icon: 'i-ph:database', component: <DataTab /> },
{ id: 'apiKeys', label: 'User Info', icon: 'i-ph:key', component: <APIKeysTab /> },
{ id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
...(debug
? [
{
id: 'debug' as TabType,
label: 'Debug Tab',
icon: 'i-ph:bug',
component: <DebugTab />,
},
]
: []),
...(eventLogs
? [
{
id: 'event-logs' as TabType,
label: 'Event Logs',
icon: 'i-ph:list-bullets',
component: <EventLogsTab />,
},
]
: []),
];
return (
@@ -91,26 +61,6 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
{tab.label}
</button>
))}
<div className="mt-auto flex flex-col gap-2">
<a
href="https://github.com/stackblitz-labs/bolt.diy"
target="_blank"
rel="noopener noreferrer"
className={classNames(styles['settings-button'], 'flex items-center gap-2')}
>
<div className="i-ph:github-logo" />
GitHub
</a>
<a
href="https://stackblitz-labs.github.io/bolt.diy/"
target="_blank"
rel="noopener noreferrer"
className={classNames(styles['settings-button'], 'flex items-center gap-2')}
>
<div className="i-ph:book" />
Docs
</a>
</div>
</div>
<div className="flex-1 flex flex-col p-8 pt-10 bg-bolt-elements-background-depth-2">

View File

@@ -1,389 +0,0 @@
import React, { useState } from 'react';
import { useNavigate } from '@remix-run/react';
import Cookies from 'js-cookie';
import { toast } from 'react-toastify';
import { database, deleteById, getAll, setMessages } from '~/lib/persistence';
import { logStore } from '~/lib/stores/logs';
import { classNames } from '~/utils/classNames';
import type { Message } from '~/lib/persistence/message';
// List of supported providers that can have API keys
const API_KEY_PROVIDERS = [
'Anthropic',
'OpenAI',
'Google',
'Groq',
'HuggingFace',
'OpenRouter',
'Deepseek',
'Mistral',
'OpenAILike',
'Together',
'xAI',
'Perplexity',
'Cohere',
'AzureOpenAI',
'AmazonBedrock',
] as const;
interface ApiKeys {
[key: string]: string;
}
export default function DataTab() {
const db = database?.read();
const navigate = useNavigate();
const [isDeleting, setIsDeleting] = useState(false);
const downloadAsJson = (data: any, filename: string) => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleExportAllChats = async () => {
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to export chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
const allChats = await getAll(db);
const exportData = {
chats: allChats,
exportDate: new Date().toISOString(),
};
downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
logStore.logSystem('Chats exported successfully', { count: allChats.length });
toast.success('Chats exported successfully');
} catch (error) {
logStore.logError('Failed to export chats', error);
toast.error('Failed to export chats');
console.error(error);
}
};
const handleDeleteAllChats = async () => {
const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.');
if (!confirmDelete) {
return;
}
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to delete chats - DB unavailable', error);
toast.error('Database is not available');
return;
}
try {
setIsDeleting(true);
const allChats = await getAll(db);
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
logStore.logSystem('All chats deleted successfully', { count: allChats.length });
toast.success('All chats deleted successfully');
navigate('/', { replace: true });
} catch (error) {
logStore.logError('Failed to delete chats', error);
toast.error('Failed to delete chats');
console.error(error);
} finally {
setIsDeleting(false);
}
};
const handleExportSettings = () => {
const settings = {
providers: Cookies.get('providers'),
isDebugEnabled: Cookies.get('isDebugEnabled'),
isEventLogsEnabled: Cookies.get('isEventLogsEnabled'),
isLocalModelsEnabled: Cookies.get('isLocalModelsEnabled'),
promptId: Cookies.get('promptId'),
isLatestBranch: Cookies.get('isLatestBranch'),
commitHash: Cookies.get('commitHash'),
eventLogs: Cookies.get('eventLogs'),
selectedModel: Cookies.get('selectedModel'),
selectedProvider: Cookies.get('selectedProvider'),
githubUsername: Cookies.get('githubUsername'),
githubToken: Cookies.get('githubToken'),
bolt_theme: localStorage.getItem('bolt_theme'),
};
downloadAsJson(settings, 'bolt-settings.json');
toast.success('Settings exported successfully');
};
const handleImportSettings = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const settings = JSON.parse(e.target?.result as string);
Object.entries(settings).forEach(([key, value]) => {
if (key === 'bolt_theme') {
if (value) {
localStorage.setItem(key, value as string);
}
} else if (value) {
Cookies.set(key, value as string);
}
});
toast.success('Settings imported successfully. Please refresh the page for changes to take effect.');
} catch (error) {
toast.error('Failed to import settings. Make sure the file is a valid JSON file.');
console.error('Failed to import settings:', error);
}
};
reader.readAsText(file);
event.target.value = '';
};
const handleExportApiKeyTemplate = () => {
const template: ApiKeys = {};
API_KEY_PROVIDERS.forEach((provider) => {
template[`${provider}_API_KEY`] = '';
});
template.OPENAI_LIKE_API_BASE_URL = '';
template.LMSTUDIO_API_BASE_URL = '';
template.OLLAMA_API_BASE_URL = '';
template.TOGETHER_API_BASE_URL = '';
downloadAsJson(template, 'api-keys-template.json');
toast.success('API keys template exported successfully');
};
const handleImportApiKeys = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const apiKeys = JSON.parse(e.target?.result as string);
let importedCount = 0;
const consolidatedKeys: Record<string, string> = {};
API_KEY_PROVIDERS.forEach((provider) => {
const keyName = `${provider}_API_KEY`;
if (apiKeys[keyName]) {
consolidatedKeys[provider] = apiKeys[keyName];
importedCount++;
}
});
if (importedCount > 0) {
// Store all API keys in a single cookie as JSON
Cookies.set('apiKeys', JSON.stringify(consolidatedKeys));
// Also set individual cookies for backward compatibility
Object.entries(consolidatedKeys).forEach(([provider, key]) => {
Cookies.set(`${provider}_API_KEY`, key);
});
toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`);
// Reload the page after a short delay to allow the toast to be seen
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
toast.warn('No valid API keys found in the file');
}
// Set base URLs if they exist
['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(
(baseUrl) => {
if (apiKeys[baseUrl]) {
Cookies.set(baseUrl, apiKeys[baseUrl]);
}
},
);
} catch (error) {
toast.error('Failed to import API keys. Make sure the file is a valid JSON file.');
console.error('Failed to import API keys:', error);
}
};
reader.readAsText(file);
event.target.value = '';
};
const processChatData = (
data: any,
): Array<{
id: string;
messages: Message[];
description: string;
urlId?: string;
}> => {
// Handle Bolt standard format (single chat)
if (data.messages && Array.isArray(data.messages)) {
const chatId = crypto.randomUUID();
return [
{
id: chatId,
messages: data.messages,
description: data.description || 'Imported Chat',
urlId: chatId,
},
];
}
// Handle Bolt export format (multiple chats)
if (data.chats && Array.isArray(data.chats)) {
return data.chats.map((chat: { id?: string; messages: Message[]; description?: string; urlId?: string }) => ({
id: chat.id || crypto.randomUUID(),
messages: chat.messages,
description: chat.description || 'Imported Chat',
urlId: chat.urlId,
}));
}
console.error('No matching format found for:', data);
throw new Error('Unsupported chat format');
};
const handleImportChats = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file || !db) {
toast.error('Something went wrong');
return;
}
try {
const content = await file.text();
const data = JSON.parse(content);
const chatsToImport = processChatData(data);
for (const chat of chatsToImport) {
await setMessages(db, chat.id, chat.messages, chat.urlId, chat.description);
}
logStore.logSystem('Chats imported successfully', { count: chatsToImport.length });
toast.success(`Successfully imported ${chatsToImport.length} chat${chatsToImport.length > 1 ? 's' : ''}`);
window.location.reload();
} catch (error) {
if (error instanceof Error) {
logStore.logError('Failed to import chats:', error);
toast.error('Failed to import chats: ' + error.message);
} else {
toast.error('Failed to import chats');
}
console.error(error);
}
};
input.click();
};
return (
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
<div className="mb-6">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Data Management</h3>
<div className="space-y-8">
<div className="flex flex-col gap-4">
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">Chat History</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
<div className="flex gap-4">
<button
onClick={handleExportAllChats}
className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
>
Export All Chats
</button>
<button
onClick={handleImportChats}
className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
>
Import Chats
</button>
<button
onClick={handleDeleteAllChats}
disabled={isDeleting}
className={classNames(
'px-4 py-2 bg-bolt-elements-button-danger-background hover:bg-bolt-elements-button-danger-backgroundHover text-bolt-elements-button-danger-text rounded-lg transition-colors',
isDeleting ? 'opacity-50 cursor-not-allowed' : '',
)}
>
{isDeleting ? 'Deleting...' : 'Delete All Chats'}
</button>
</div>
</div>
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">Settings Backup</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
Export your settings to a JSON file or import settings from a previously exported file.
</p>
<div className="flex gap-4">
<button
onClick={handleExportSettings}
className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
>
Export Settings
</button>
<label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
Import Settings
<input type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
</label>
</div>
</div>
<div>
<h4 className="text-bolt-elements-textPrimary mb-2">API Keys Management</h4>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
Import API keys from a JSON file or download a template to fill in your keys.
</p>
<div className="flex gap-4">
<button
onClick={handleExportApiKeyTemplate}
className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
>
Download Template
</button>
<label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
Import API Keys
<input type="file" accept=".json" onChange={handleImportApiKeys} className="hidden" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,639 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSettings } from '~/lib/hooks/useSettings';
import { toast } from 'react-toastify';
import { providerBaseUrlEnvKeys } from '~/utils/constants';
interface ProviderStatus {
name: string;
enabled: boolean;
isLocal: boolean;
isRunning: boolean | null;
error?: string;
lastChecked: Date;
responseTime?: number;
url: string | null;
}
interface SystemInfo {
os: string;
browser: string;
screen: string;
language: string;
timezone: string;
memory: string;
cores: number;
deviceType: string;
colorDepth: string;
pixelRatio: number;
online: boolean;
cookiesEnabled: boolean;
doNotTrack: boolean;
}
interface IProviderConfig {
name: string;
settings: {
enabled: boolean;
baseUrl?: string;
};
}
interface CommitData {
commit: string;
version?: string;
}
const connitJson: CommitData = {
commit: __COMMIT_HASH,
version: __APP_VERSION,
};
const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
const versionHash = connitJson.commit;
const versionTag = connitJson.version;
const GITHUB_URLS = {
original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main',
fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main',
commitJson: async (branch: string) => {
try {
const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
const data: { sha: string } = await response.json();
const packageJsonResp = await fetch(
`https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/package.json`,
);
const packageJson: { version: string } = await packageJsonResp.json();
return {
commit: data.sha.slice(0, 7),
version: packageJson.version,
};
} catch (error) {
console.log('Failed to fetch local commit info:', error);
throw new Error('Failed to fetch local commit info');
}
},
};
function getSystemInfo(): SystemInfo {
const formatBytes = (bytes: number): string => {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getBrowserInfo = (): string => {
const ua = navigator.userAgent;
let browser = 'Unknown';
if (ua.includes('Firefox/')) {
browser = 'Firefox';
} else if (ua.includes('Chrome/')) {
if (ua.includes('Edg/')) {
browser = 'Edge';
} else if (ua.includes('OPR/')) {
browser = 'Opera';
} else {
browser = 'Chrome';
}
} else if (ua.includes('Safari/')) {
if (!ua.includes('Chrome')) {
browser = 'Safari';
}
}
// Extract version number
const match = ua.match(new RegExp(`${browser}\\/([\\d.]+)`));
const version = match ? ` ${match[1]}` : '';
return `${browser}${version}`;
};
const getOperatingSystem = (): string => {
const ua = navigator.userAgent;
const platform = navigator.platform;
if (ua.includes('Win')) {
return 'Windows';
}
if (ua.includes('Mac')) {
if (ua.includes('iPhone') || ua.includes('iPad')) {
return 'iOS';
}
return 'macOS';
}
if (ua.includes('Linux')) {
return 'Linux';
}
if (ua.includes('Android')) {
return 'Android';
}
return platform || 'Unknown';
};
const getDeviceType = (): string => {
const ua = navigator.userAgent;
if (ua.includes('Mobile')) {
return 'Mobile';
}
if (ua.includes('Tablet')) {
return 'Tablet';
}
return 'Desktop';
};
// Get more detailed memory info if available
const getMemoryInfo = (): string => {
if ('memory' in performance) {
const memory = (performance as any).memory;
return `${formatBytes(memory.jsHeapSizeLimit)} (Used: ${formatBytes(memory.usedJSHeapSize)})`;
}
return 'Not available';
};
return {
os: getOperatingSystem(),
browser: getBrowserInfo(),
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
memory: getMemoryInfo(),
cores: navigator.hardwareConcurrency || 0,
deviceType: getDeviceType(),
// Add new fields
colorDepth: `${window.screen.colorDepth}-bit`,
pixelRatio: window.devicePixelRatio,
online: navigator.onLine,
cookiesEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack === '1',
};
}
const checkProviderStatus = async (url: string | null, providerName: string): Promise<ProviderStatus> => {
if (!url) {
console.log(`[Debug] No URL provided for ${providerName}`);
return {
name: providerName,
enabled: false,
isLocal: true,
isRunning: false,
error: 'No URL configured',
lastChecked: new Date(),
url: null,
};
}
console.log(`[Debug] Checking status for ${providerName} at ${url}`);
const startTime = performance.now();
try {
if (providerName.toLowerCase() === 'ollama') {
// Special check for Ollama root endpoint
try {
console.log(`[Debug] Checking Ollama root endpoint: ${url}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
const response = await fetch(url, {
signal: controller.signal,
headers: {
Accept: 'text/plain,application/json',
},
});
clearTimeout(timeoutId);
const text = await response.text();
console.log(`[Debug] Ollama root response:`, text);
if (text.includes('Ollama is running')) {
console.log(`[Debug] Ollama running confirmed via root endpoint`);
return {
name: providerName,
enabled: false,
isLocal: true,
isRunning: true,
lastChecked: new Date(),
responseTime: performance.now() - startTime,
url,
};
}
} catch (error) {
console.log(`[Debug] Ollama root check failed:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
if (errorMessage.includes('aborted')) {
return {
name: providerName,
enabled: false,
isLocal: true,
isRunning: false,
error: 'Connection timeout',
lastChecked: new Date(),
responseTime: performance.now() - startTime,
url,
};
}
}
}
// Try different endpoints based on provider
const checkUrls = [`${url}/api/health`, url.endsWith('v1') ? `${url}/models` : `${url}/v1/models`];
console.log(`[Debug] Checking additional endpoints:`, checkUrls);
const results = await Promise.all(
checkUrls.map(async (checkUrl) => {
try {
console.log(`[Debug] Trying endpoint: ${checkUrl}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(checkUrl, {
signal: controller.signal,
headers: {
Accept: 'application/json',
},
});
clearTimeout(timeoutId);
const ok = response.ok;
console.log(`[Debug] Endpoint ${checkUrl} response:`, ok);
if (ok) {
try {
const data = await response.json();
console.log(`[Debug] Endpoint ${checkUrl} data:`, data);
} catch {
console.log(`[Debug] Could not parse JSON from ${checkUrl}`);
}
}
return ok;
} catch (error) {
console.log(`[Debug] Endpoint ${checkUrl} failed:`, error);
return false;
}
}),
);
const isRunning = results.some((result) => result);
console.log(`[Debug] Final status for ${providerName}:`, isRunning);
return {
name: providerName,
enabled: false,
isLocal: true,
isRunning,
lastChecked: new Date(),
responseTime: performance.now() - startTime,
url,
};
} catch (error) {
console.log(`[Debug] Provider check failed for ${providerName}:`, error);
return {
name: providerName,
enabled: false,
isLocal: true,
isRunning: false,
error: error instanceof Error ? error.message : 'Unknown error',
lastChecked: new Date(),
responseTime: performance.now() - startTime,
url,
};
}
};
export default function DebugTab() {
const { providers, isLatestBranch } = useSettings();
const [activeProviders, setActiveProviders] = useState<ProviderStatus[]>([]);
const [updateMessage, setUpdateMessage] = useState<string>('');
const [systemInfo] = useState<SystemInfo>(getSystemInfo());
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
const updateProviderStatuses = async () => {
if (!providers) {
return;
}
try {
const entries = Object.entries(providers) as [string, IProviderConfig][];
const statuses = await Promise.all(
entries
.filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name))
.map(async ([, provider]) => {
const envVarName =
providerBaseUrlEnvKeys[provider.name].baseUrlKey || `REACT_APP_${provider.name.toUpperCase()}_URL`;
// Access environment variables through import.meta.env
let settingsUrl = provider.settings.baseUrl;
if (settingsUrl && settingsUrl.trim().length === 0) {
settingsUrl = undefined;
}
const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used
console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`);
const status = await checkProviderStatus(url, provider.name);
return {
...status,
enabled: provider.settings.enabled ?? false,
};
}),
);
setActiveProviders(statuses);
} catch (error) {
console.error('[Debug] Failed to update provider statuses:', error);
}
};
useEffect(() => {
updateProviderStatuses();
const interval = setInterval(updateProviderStatuses, 30000);
return () => clearInterval(interval);
}, [providers]);
const handleCheckForUpdate = useCallback(async () => {
if (isCheckingUpdate) {
return;
}
try {
setIsCheckingUpdate(true);
setUpdateMessage('Checking for updates...');
const branchToCheck = isLatestBranch ? 'main' : 'stable';
console.log(`[Debug] Checking for updates against ${branchToCheck} branch`);
const latestCommitResp = await GITHUB_URLS.commitJson(branchToCheck);
const remoteCommitHash = latestCommitResp.commit;
const currentCommitHash = versionHash;
if (remoteCommitHash !== currentCommitHash) {
setUpdateMessage(
`Update available from ${branchToCheck} branch!\n` +
`Current: ${currentCommitHash.slice(0, 7)}\n` +
`Latest: ${remoteCommitHash.slice(0, 7)}`,
);
} else {
setUpdateMessage(`You are on the latest version from the ${branchToCheck} branch`);
}
} catch (error) {
setUpdateMessage('Failed to check for updates');
console.error('[Debug] Failed to check for updates:', error);
} finally {
setIsCheckingUpdate(false);
}
}, [isCheckingUpdate, isLatestBranch]);
const handleCopyToClipboard = useCallback(() => {
const debugInfo = {
System: systemInfo,
Providers: activeProviders.map((provider) => ({
name: provider.name,
enabled: provider.enabled,
isLocal: provider.isLocal,
running: provider.isRunning,
error: provider.error,
lastChecked: provider.lastChecked,
responseTime: provider.responseTime,
url: provider.url,
})),
Version: {
hash: versionHash.slice(0, 7),
branch: isLatestBranch ? 'main' : 'stable',
},
Timestamp: new Date().toISOString(),
};
navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
toast.success('Debug information copied to clipboard!');
});
}, [activeProviders, systemInfo, isLatestBranch]);
return (
<div className="p-4 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Debug Information</h3>
<div className="flex gap-2">
<button
onClick={handleCopyToClipboard}
className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
>
Copy Debug Info
</button>
<button
onClick={handleCheckForUpdate}
disabled={isCheckingUpdate}
className={`bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200
${!isCheckingUpdate ? 'hover:bg-bolt-elements-button-primary-backgroundHover' : 'opacity-75 cursor-not-allowed'}
text-bolt-elements-button-primary-text`}
>
{isCheckingUpdate ? 'Checking...' : 'Check for Updates'}
</button>
</div>
</div>
{updateMessage && (
<div
className={`bg-bolt-elements-surface rounded-lg p-3 ${
updateMessage.includes('Update available') ? 'border-l-4 border-yellow-400' : ''
}`}
>
<p className="text-bolt-elements-textSecondary whitespace-pre-line">{updateMessage}</p>
{updateMessage.includes('Update available') && (
<div className="mt-3 text-sm">
<p className="font-medium text-bolt-elements-textPrimary">To update:</p>
<ol className="list-decimal ml-4 mt-1 text-bolt-elements-textSecondary">
<li>
Pull the latest changes:{' '}
<code className="bg-bolt-elements-surface-hover px-1 rounded">git pull upstream main</code>
</li>
<li>
Install any new dependencies:{' '}
<code className="bg-bolt-elements-surface-hover px-1 rounded">pnpm install</code>
</li>
<li>Restart the application</li>
</ol>
</div>
)}
</div>
)}
<section className="space-y-4">
<div>
<h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">System Information</h4>
<div className="bg-bolt-elements-surface rounded-lg p-4">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<p className="text-xs text-bolt-elements-textSecondary">Operating System</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.os}</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Device Type</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Browser</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Display</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">
{systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x
</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Connection</p>
<p className="text-sm font-medium flex items-center gap-2">
<span
className={`inline-block w-2 h-2 rounded-full ${systemInfo.online ? 'bg-green-500' : 'bg-red-500'}`}
/>
<span className={`${systemInfo.online ? 'text-green-600' : 'text-red-600'}`}>
{systemInfo.online ? 'Online' : 'Offline'}
</span>
</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Screen Resolution</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.screen}</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Language</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.language}</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">Timezone</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.timezone}</p>
</div>
<div>
<p className="text-xs text-bolt-elements-textSecondary">CPU Cores</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.cores}</p>
</div>
</div>
<div className="mt-3 pt-3 border-t border-bolt-elements-surface-hover">
<p className="text-xs text-bolt-elements-textSecondary">Version</p>
<p className="text-sm font-medium text-bolt-elements-textPrimary font-mono">
{connitJson.commit.slice(0, 7)}
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
(v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'}
</span>
</p>
</div>
</div>
</div>
<div>
<h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">Local LLM Status</h4>
<div className="bg-bolt-elements-surface rounded-lg">
<div className="grid grid-cols-1 divide-y">
{activeProviders.map((provider) => (
<div key={provider.name} className="p-3 flex flex-col space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<div
className={`w-2 h-2 rounded-full ${
!provider.enabled ? 'bg-gray-300' : provider.isRunning ? 'bg-green-400' : 'bg-red-400'
}`}
/>
</div>
<div>
<p className="text-sm font-medium text-bolt-elements-textPrimary">{provider.name}</p>
{provider.url && (
<p className="text-xs text-bolt-elements-textSecondary truncate max-w-[300px]">
{provider.url}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<span
className={`px-2 py-0.5 text-xs rounded-full ${
provider.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
{provider.enabled ? 'Enabled' : 'Disabled'}
</span>
{provider.enabled && (
<span
className={`px-2 py-0.5 text-xs rounded-full ${
provider.isRunning ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{provider.isRunning ? 'Running' : 'Not Running'}
</span>
)}
</div>
</div>
<div className="pl-5 flex flex-col space-y-1 text-xs">
{/* Status Details */}
<div className="flex flex-wrap gap-2">
<span className="text-bolt-elements-textSecondary">
Last checked: {new Date(provider.lastChecked).toLocaleTimeString()}
</span>
{provider.responseTime && (
<span className="text-bolt-elements-textSecondary">
Response time: {Math.round(provider.responseTime)}ms
</span>
)}
</div>
{/* Error Message */}
{provider.error && (
<div className="mt-1 text-red-600 bg-red-50 rounded-md p-2">
<span className="font-medium">Error:</span> {provider.error}
</div>
)}
{/* Connection Info */}
{provider.url && (
<div className="text-bolt-elements-textSecondary">
<span className="font-medium">Endpoints checked:</span>
<ul className="list-disc list-inside pl-2 mt-1">
<li>{provider.url} (root)</li>
<li>{provider.url}/api/health</li>
<li>{provider.url}/v1/models</li>
</ul>
</div>
)}
</div>
</div>
))}
{activeProviders.length === 0 && (
<div className="p-4 text-center text-bolt-elements-textSecondary">No local LLMs configured</div>
)}
</div>
</div>
</div>
</section>
</div>
);
}

View File

@@ -1,219 +0,0 @@
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { useSettings } from '~/lib/hooks/useSettings';
import { toast } from 'react-toastify';
import { Switch } from '~/components/ui/Switch';
import { logStore, type LogEntry } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
import { classNames } from '~/utils/classNames';
export default function EventLogsTab() {
const {} = useSettings();
const showLogs = useStore(logStore.showLogs);
const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info');
const [autoScroll, setAutoScroll] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [, forceUpdate] = useState({});
const filteredLogs = useMemo(() => {
const logs = logStore.getLogs();
return logs.filter((log) => {
const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all';
const matchesSearch =
!searchQuery ||
log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
return matchesLevel && matchesSearch;
});
}, [logLevel, searchQuery]);
// Effect to initialize showLogs
useEffect(() => {
logStore.showLogs.set(true);
}, []);
useEffect(() => {
// System info logs
logStore.logSystem('Application initialized', {
version: process.env.NEXT_PUBLIC_APP_VERSION,
environment: process.env.NODE_ENV,
});
// Debug logs for system state
logStore.logDebug('System configuration loaded', {
runtime: 'Next.js',
features: ['AI Chat', 'Event Logging'],
});
// Warning logs for potential issues
logStore.logWarning('Resource usage threshold approaching', {
memoryUsage: '75%',
cpuLoad: '60%',
});
// Error logs with detailed context
logStore.logError('API connection failed', new Error('Connection timeout'), {
endpoint: '/api/chat',
retryCount: 3,
lastAttempt: new Date().toISOString(),
});
}, []);
useEffect(() => {
const container = document.querySelector('.logs-container');
if (container && autoScroll) {
container.scrollTop = container.scrollHeight;
}
}, [filteredLogs, autoScroll]);
const handleClearLogs = useCallback(() => {
if (confirm('Are you sure you want to clear all logs?')) {
logStore.clearLogs();
toast.success('Logs cleared successfully');
forceUpdate({}); // Force a re-render after clearing logs
}
}, []);
const handleExportLogs = useCallback(() => {
try {
const logText = logStore
.getLogs()
.map(
(log) =>
`[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${
log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : ''
}`,
)
.join('\n\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `event-logs-${new Date().toISOString()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Logs exported successfully');
} catch (error) {
toast.error('Failed to export logs');
console.error('Export error:', error);
}
}, []);
const getLevelColor = (level: LogEntry['level']) => {
switch (level) {
case 'info':
return 'text-blue-500';
case 'warning':
return 'text-yellow-500';
case 'error':
return 'text-red-500';
case 'debug':
return 'text-gray-500';
default:
return 'text-bolt-elements-textPrimary';
}
};
return (
<div className="p-4 h-full flex flex-col">
<div className="flex flex-col space-y-4 mb-4">
{/* Title and Toggles Row */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center space-x-2">
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span>
<Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} />
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span>
<Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
</div>
</div>
</div>
{/* Controls Row */}
<div className="flex flex-wrap items-center gap-2">
<select
value={logLevel}
onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[20%] text-sm min-w-[100px]"
>
<option value="all">All</option>
<option value="info">Info</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
<option value="debug">Debug</option>
</select>
<div className="flex-1 min-w-[200px]">
<input
type="text"
placeholder="Search logs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
{showLogs && (
<div className="flex items-center gap-2 flex-nowrap">
<button
onClick={handleExportLogs}
className={classNames(
'bg-bolt-elements-button-primary-background',
'rounded-lg px-4 py-2 transition-colors duration-200',
'hover:bg-bolt-elements-button-primary-backgroundHover',
'text-bolt-elements-button-primary-text',
)}
>
Export Logs
</button>
<button
onClick={handleClearLogs}
className={classNames(
'bg-bolt-elements-button-danger-background',
'rounded-lg px-4 py-2 transition-colors duration-200',
'hover:bg-bolt-elements-button-danger-backgroundHover',
'text-bolt-elements-button-danger-text',
)}
>
Clear Logs
</button>
</div>
)}
</div>
</div>
<div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[calc(100vh - 250px)] min-h-[400px] overflow-y-auto logs-container overflow-y-auto">
{filteredLogs.length === 0 ? (
<div className="text-center text-bolt-elements-textSecondary py-8">No logs found</div>
) : (
filteredLogs.map((log, index) => (
<div
key={index}
className="text-sm mb-3 font-mono border-b border-bolt-elements-borderColor pb-2 last:border-0"
>
<div className="flex items-start space-x-2 flex-wrap">
<span className={`font-bold ${getLevelColor(log.level)} whitespace-nowrap`}>
[{log.level.toUpperCase()}]
</span>
<span className="text-bolt-elements-textSecondary whitespace-nowrap">
{new Date(log.timestamp).toLocaleString()}
</span>
<span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
</div>
{log.details && (
<pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto whitespace-pre-wrap break-all">
{JSON.stringify(log.details, null, 2)}
</pre>
)}
</div>
))
)}
</div>
</div>
);
}

View File

@@ -1,91 +0,0 @@
import React from 'react';
import { Switch } from '~/components/ui/Switch';
import { useSettings } from '~/lib/hooks/useSettings';
export default function FeaturesTab() {
const {
debug,
enableDebugMode,
isLocalModel,
enableLocalModels,
enableEventLogs,
isLatestBranch,
enableLatestBranch,
// Unused variables but needed from useSettings
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
promptId,
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
setPromptId,
autoSelectTemplate,
setAutoSelectTemplate,
enableContextOptimization,
contextOptimizationEnabled,
} = useSettings();
const handleToggle = (enabled: boolean) => {
enableDebugMode(enabled);
enableEventLogs(enabled);
};
return (
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
<div className="mb-6">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-bolt-elements-textPrimary">Debug Features</span>
<Switch className="ml-auto" checked={debug} onCheckedChange={handleToggle} />
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-bolt-elements-textPrimary">Use Main Branch</span>
<p className="text-xs text-bolt-elements-textTertiary">
Check for updates against the main branch instead of stable
</p>
</div>
<Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} />
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-bolt-elements-textPrimary">Auto Select Code Template</span>
<p className="text-xs text-bolt-elements-textTertiary">
Let Bolt select the best starter template for your project.
</p>
</div>
<Switch className="ml-auto" checked={autoSelectTemplate} onCheckedChange={setAutoSelectTemplate} />
</div>
<div className="flex items-center justify-between">
<div>
<span className="text-bolt-elements-textPrimary">Use Context Optimization</span>
<p className="text-sm text-bolt-elements-textSecondary">
redact file contents form chat and puts the latest file contents on the system prompt
</p>
</div>
<Switch
className="ml-auto"
checked={contextOptimizationEnabled}
onCheckedChange={enableContextOptimization}
/>
</div>
</div>
</div>
<div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
<p className="text-sm text-bolt-elements-textSecondary mb-10">
Disclaimer: Experimental features may be unstable and are subject to change.
</p>
<div className="flex flex-col">
<div className="flex items-center justify-between mb-2">
<span className="text-bolt-elements-textPrimary">Experimental Providers</span>
<Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
</div>
<p className="text-xs text-bolt-elements-textTertiary mb-4">
Enable experimental providers such as Ollama, LMStudio, and OpenAILike.
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,99 +0,0 @@
import { useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, maxFreeUses } from '~/utils/freeUses';
import { saveNutLoginKey, saveUsername, getNutLoginKey, getUsername } from '~/lib/replay/Problems';
import { debounce } from '~/utils/debounce';
export default function ConnectionsTab() {
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
const [username, setUsername] = useState(getUsername() || '');
const [loginKey, setLoginKey] = useState(getNutLoginKey() || '');
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
const [loginKeyIsLoading, setLoginKeyIsLoading] = useState(false);
const handleSaveAPIKey = async (key: string) => {
if (key && !key.startsWith('sk-ant-')) {
toast.error('Please provide a valid Anthropic API key');
return;
}
Cookies.set('anthropicApiKey', key);
setApiKey(key);
};
const saveUsernameWithToast = (username: string) => {
saveUsername(username);
toast.success('Username saved!');
};
const debouncedSaveUsername = useCallback(debounce(saveUsernameWithToast, 1000), []);
const handleSaveUsername = async (username: string) => {
setUsername(username);
debouncedSaveUsername(username);
};
const handleSaveLoginKey = async (key: string) => {
setLoginKey(key);
try {
setLoginKeyIsLoading(true);
await saveNutLoginKey(key);
toast.success('Login key saved');
} catch {
toast.error('Failed to save login key');
} finally {
setLoginKeyIsLoading(false);
}
};
return (
<div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Anthropic API Key</h3>
<div className="flex mb-4">
<div className="flex-1 mr-2">
<input
type="text"
value={apiKey}
onChange={(e) => handleSaveAPIKey(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
/>
</div>
</div>
{numFreeUses < maxFreeUses && (
<div className="flex mb-4">
<div className="flex-1 mr-2">
{maxFreeUses - numFreeUses} / {maxFreeUses} free uses remaining
</div>
</div>
)}
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Problems User Name</h3>
<div className="flex mb-4">
<div className="flex-1 mr-2">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => handleSaveUsername(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
/>
</div>
</div>
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Nut Login Key</h3>
<div className="flex mb-4">
<div className="flex-1 mr-2">
<input
type="text"
placeholder="Enter your login key"
value={loginKey}
data-testid="login-key-input"
data-isloading={loginKeyIsLoading}
onChange={(e) => handleSaveLoginKey(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
/>
</div>
</div>
</div>
);
}

View File

@@ -1,147 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Switch } from '~/components/ui/Switch';
import { useSettings } from '~/lib/hooks/useSettings';
import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
import type { IProviderConfig } from '~/types/model';
import { logStore } from '~/lib/stores/logs';
// Import a default fallback icon
import { providerBaseUrlEnvKeys } from '~/utils/constants';
const DefaultIcon = '/icons/Default.svg'; // Adjust the path as necessary
export default function ProvidersTab() {
const { providers, updateProviderSettings, isLocalModel } = useSettings();
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
// Load base URLs from cookies
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({
...value,
name: key,
}));
if (searchTerm && searchTerm.length > 0) {
newFilteredProviders = newFilteredProviders.filter((provider) =>
provider.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
if (!isLocalModel) {
newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name));
}
newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
// Split providers into regular and URL-configurable
const regular = newFilteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
setFilteredProviders([...regular, ...urlConfigurable]);
}, [providers, searchTerm, isLocalModel]);
const renderProviderCard = (provider: IProviderConfig) => {
const envBaseUrlKey = providerBaseUrlEnvKeys[provider.name].baseUrlKey;
const envBaseUrl = envBaseUrlKey ? import.meta.env[envBaseUrlKey] : undefined;
const isUrlConfigurable = URL_CONFIGURABLE_PROVIDERS.includes(provider.name);
return (
<div
key={provider.name}
className="flex flex-col provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<img
src={`/icons/${provider.name}.svg`}
onError={(e) => {
e.currentTarget.src = DefaultIcon;
}}
alt={`${provider.name} icon`}
className="w-6 h-6 dark:invert"
/>
<span className="text-bolt-elements-textPrimary">{provider.name}</span>
</div>
<Switch
className="ml-auto"
checked={provider.settings.enabled}
onCheckedChange={(enabled) => {
updateProviderSettings(provider.name, { ...provider.settings, enabled });
if (enabled) {
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
} else {
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
}
}}
/>
</div>
{isUrlConfigurable && provider.settings.enabled && (
<div className="mt-2">
{envBaseUrl && (
<label className="block text-xs text-bolt-elements-textSecondary text-green-300 mb-2">
Set On (.env) : {envBaseUrl}
</label>
)}
<label className="block text-sm text-bolt-elements-textSecondary mb-2">
{envBaseUrl ? 'Override Base Url' : 'Base URL '}:{' '}
</label>
<input
type="text"
value={provider.settings.baseUrl || ''}
onChange={(e) => {
let newBaseUrl: string | undefined = e.target.value;
if (newBaseUrl && newBaseUrl.trim().length === 0) {
newBaseUrl = undefined;
}
updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
logStore.logProvider(`Base URL updated for ${provider.name}`, {
provider: provider.name,
baseUrl: newBaseUrl,
});
}}
placeholder={`Enter ${provider.name} base URL`}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
)}
</div>
);
};
const regularProviders = filteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
const urlConfigurableProviders = filteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
return (
<div className="p-4">
<div className="flex mb-4">
<input
type="text"
placeholder="Search providers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
{/* Regular Providers Grid */}
<div className="grid grid-cols-2 gap-4 mb-8">{regularProviders.map(renderProviderCard)}</div>
{/* URL Configurable Providers Section */}
{urlConfigurableProviders.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-semibold mb-2 text-bolt-elements-textPrimary">Experimental Providers</h3>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
These providers are experimental and allow you to run AI models locally or connect to your own
infrastructure. They require additional setup but offer more flexibility.
</p>
<div className="space-y-4">{urlConfigurableProviders.map(renderProviderCard)}</div>
</div>
)}
</div>
);
}

View File

@@ -1,25 +1,24 @@
import { useParams } from '@remix-run/react';
import { classNames } from '~/utils/classNames';
import * as Dialog from '@radix-ui/react-dialog';
import { type ChatHistoryItem } from '~/lib/persistence';
import { type ChatContents } from '~/lib/persistence/db';
import WithTooltip from '~/components/ui/Tooltip';
import { useEditChatDescription } from '~/lib/hooks';
import { useEditChatTitle } from '~/lib/hooks/useEditChatDescription';
import { forwardRef, type ForwardedRef } from 'react';
interface HistoryItemProps {
item: ChatHistoryItem;
item: ChatContents;
onDelete?: (event: React.UIEvent) => void;
onDuplicate?: (id: string) => void;
exportChat: (id?: string) => void;
}
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
const { id: urlId } = useParams();
const isActiveChat = urlId === item.urlId;
const isActiveChat = urlId === item.id;
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
useEditChatDescription({
initialDescription: item.description,
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentTitle, toggleEditMode } =
useEditChatTitle({
initialTitle: item.title,
customChatId: item.id,
syncWithGlobalStore: isActiveChat,
});
@@ -30,7 +29,7 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
type="text"
className="flex-1 bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2"
autoFocus
value={currentDescription}
value={currentTitle}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
@@ -53,8 +52,8 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
{editing ? (
renderDescriptionForm
) : (
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
{currentDescription}
<a href={`/chat/${item.id}`} className="flex w-full relative truncate block">
{item.title}
<div
className={classNames(
'absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-22 group-hover:from-99%',
@@ -62,14 +61,6 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
)}
>
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity">
<ChatActionButton
toolTipContent="Export chat"
icon="i-ph:download-simple"
onClick={(event) => {
event.preventDefault();
exportChat(item.id);
}}
/>
{onDuplicate && (
<ChatActionButton
toolTipContent="Duplicate chat"

View File

@@ -5,7 +5,8 @@ import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
import { SettingsWindow } from '~/components/settings/SettingsWindow';
import { SettingsButton } from '~/components/ui/SettingsButton';
import { database, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
import { deleteById, getAllChats, currentChatId } from '~/lib/persistence';
import type { ChatContents } from '~/lib/persistence/db';
import { cubicEasingFn } from '~/utils/easings';
import { logger } from '~/utils/logger';
import { HistoryItem } from './HistoryItem';
@@ -35,13 +36,11 @@ const menuVariants = {
},
} satisfies Variants;
type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
type DialogContent = { type: 'delete'; item: ChatContents } | null;
export const Menu = () => {
const db = database?.read();
const { duplicateCurrentChat, exportChat } = useChatHistory();
const menuRef = useRef<HTMLDivElement>(null);
const [list, setList] = useState<ChatHistoryItem[]>([]);
const [list, setList] = useState<ChatContents[]>([]);
const [open, setOpen] = useState(false);
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
@@ -49,36 +48,31 @@ export const Menu = () => {
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
items: list,
searchFields: ['description'],
searchFields: ['title'],
});
const loadEntries = useCallback(() => {
if (db) {
getAll(db)
.then((list) => list.filter((item) => item.urlId && item.description))
.then(setList)
.catch((error) => toast.error(error.message));
}
getAllChats()
.then(setList)
.catch((error) => toast.error(error.message));
}, []);
const deleteItem = useCallback((event: React.UIEvent, item: ChatHistoryItem) => {
const deleteItem = useCallback((event: React.UIEvent, item: ChatContents) => {
event.preventDefault();
if (db) {
deleteById(db, item.id)
.then(() => {
loadEntries();
deleteById(item.id)
.then(() => {
loadEntries();
if (chatId.get() === item.id) {
// hard page navigation to clear the stores
window.location.pathname = '/';
}
})
.catch((error) => {
toast.error('Failed to delete conversation');
logger.error(error);
});
}
if (currentChatId.get() === item.id) {
// hard page navigation to clear the stores
window.location.pathname = '/';
}
})
.catch((error) => {
toast.error('Failed to delete conversation');
logger.error(error);
});
}, []);
const closeDialog = () => {
@@ -112,16 +106,11 @@ export const Menu = () => {
};
}, []);
const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
const handleDeleteClick = (event: React.UIEvent, item: ChatContents) => {
event.preventDefault();
setDialogContent({ type: 'delete', item });
};
const handleDuplicate = async (id: string) => {
await duplicateCurrentChat(id);
loadEntries(); // Reload the list after duplication
};
return (
<motion.div
ref={menuRef}
@@ -179,13 +168,7 @@ export const Menu = () => {
{category}
</div>
{items.map((item) => (
<HistoryItem
key={item.id}
item={item}
exportChat={exportChat}
onDelete={(event) => handleDeleteClick(event, item)}
onDuplicate={() => handleDuplicate(item.id)}
/>
<HistoryItem key={item.id} item={item} onDelete={(event) => handleDeleteClick(event, item)} />
))}
</div>
))}
@@ -196,7 +179,7 @@ export const Menu = () => {
<DialogDescription asChild>
<div>
<p>
You are about to delete <strong>{dialogContent.item.description}</strong>.
You are about to delete <strong>{dialogContent.item.title}</strong>.
</p>
<p className="mt-1">Are you sure you want to delete this chat?</p>
</div>

View File

@@ -1,10 +1,10 @@
import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { workbenchStore } from '~/lib/stores/workbench';
import { submitProblem, BoltProblemStatus } from '~/lib/replay/Problems';
import type { BoltProblemInput, BoltProblemSolution } from '~/lib/replay/Problems';
import { shouldUseSupabase, getCurrentUser } from '~/lib/supabase/client';
import { submitProblem, NutProblemStatus } from '~/lib/replay/Problems';
import type { NutProblemInput, NutProblemSolution } from '~/lib/replay/Problems';
import { getCurrentUser } from '~/lib/supabase/client';
import { authModalStore } from '~/lib/stores/authModal';
import { authStatusStore } from '~/lib/stores/auth';
import { useStore } from '@nanostores/react';
@@ -30,11 +30,6 @@ async function saveProblem(
return null;
}
if (!shouldUseSupabase() && !username) {
toast.error('Please enter a username');
return null;
}
toast.info('Submitting problem...');
const repositoryId = workbenchStore.repositoryId.get();
@@ -44,19 +39,18 @@ async function saveProblem(
return null;
}
const solution: BoltProblemSolution = {
const solution: NutProblemSolution = {
evaluator: undefined,
...reproData,
};
const problem: BoltProblemInput = {
const problem: NutProblemInput = {
version: 2,
title,
description,
username: shouldUseSupabase() ? (undefined as any) : username,
user_id: shouldUseSupabase() ? (await getCurrentUser())?.id || '' : undefined,
user_id: (await getCurrentUser())?.id || '',
repositoryId,
status: BoltProblemStatus.Pending,
status: NutProblemStatus.Pending,
solution,
};
@@ -112,14 +106,6 @@ export function SaveProblem() {
const [problemId, setProblemId] = useState<string | null>(null);
const [reproData, setReproData] = useState<any>(null);
const isLoggedIn = useStore(authStatusStore.isLoggedIn);
const username = useStore(authStatusStore.username);
// Update the username from the store when component mounts
useEffect(() => {
if (username) {
setFormData((prev) => ({ ...prev, username }));
}
}, [username]);
const handleSaveProblem = (e: React.MouseEvent) => {
e.preventDefault();
@@ -142,11 +128,6 @@ export function SaveProblem() {
...prev,
[name]: value,
}));
// Update username in the store if it's the username field
if (name === 'username') {
authStatusStore.updateUsername(value);
}
};
const handleSubmitProblem = async () => {
@@ -195,7 +176,7 @@ export function SaveProblem() {
},
}}
>
{shouldUseSupabase() && !isLoggedIn && (
{!isLoggedIn && (
<div className="text-center">
<div className="mb-4">Please log in to save a problem</div>
<button
@@ -209,7 +190,7 @@ export function SaveProblem() {
</button>
</div>
)}
{(!shouldUseSupabase() || isLoggedIn) && problemId && (
{isLoggedIn && problemId && (
<>
<div className="text-center mb-2">Problem Submitted: {problemId}</div>
<div className="text-center">
@@ -224,7 +205,7 @@ export function SaveProblem() {
</div>
</>
)}
{(!shouldUseSupabase() || isLoggedIn) && !problemId && (
{isLoggedIn && !problemId && (
<>
<div className="text-center">
Save prompts as new problems when AI results are unsatisfactory. Problems are publicly visible and are
@@ -232,19 +213,6 @@ export function SaveProblem() {
</div>
<div style={{ marginTop: '10px' }}>
<div className="grid grid-cols-[auto_1fr] gap-4 max-w-md mx-auto">
{!shouldUseSupabase() && (
<>
<div className="flex items-center">Username:</div>
<input
type="text"
name="username"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.username}
onChange={handleInputChange}
/>
</>
)}
<div className="flex items-center">Title:</div>
<input
type="text"

View File

@@ -1,16 +1,16 @@
import { format, isAfter, isThisWeek, isThisYear, isToday, isYesterday, subDays } from 'date-fns';
import type { ChatHistoryItem } from '~/lib/persistence';
import type { ChatContents } from '~/lib/persistence/db';
type Bin = { category: string; items: ChatHistoryItem[] };
type Bin = { category: string; items: ChatContents[] };
export function binDates(_list: ChatHistoryItem[]) {
const list = [..._list].sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
export function binDates(_list: ChatContents[]) {
const list = [..._list].sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
const binLookup: Record<string, Bin> = {};
const bins: Array<Bin> = [];
list.forEach((item) => {
const category = dateCategory(new Date(item.timestamp));
const category = dateCategory(new Date(item.createdAt));
if (!(category in binLookup)) {
const bin = {

1
app/env.d.ts vendored
View File

@@ -6,7 +6,6 @@
interface WindowEnv {
SUPABASE_URL: string;
SUPABASE_ANON_KEY: string;
USE_SUPABASE?: string;
}
declare global {

View File

@@ -1,7 +1,5 @@
// FIXME ping telemetry server directly instead of going through the backend.
import { getNutLoginKey } from '~/lib/replay/Problems';
// We do this to work around CORS insanity.
export async function pingTelemetry(event: string, data: any) {
const requestBody: any = {
@@ -30,7 +28,6 @@ export class ChatMessageTelemetry {
private _ping(event: string, data: any = {}) {
pingTelemetry(event, {
...data,
loginKey: getNutLoginKey(),
messageId: this.id,
numMessages: this.numMessages,
});

View File

@@ -1,16 +1,10 @@
import { useStore } from '@nanostores/react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import {
chatId as chatIdStore,
database,
description as descriptionStore,
getMessages,
updateChatDescription,
} from '~/lib/persistence';
import { currentChatId, currentChatTitle, getChatContents, updateChatTitle } from '~/lib/persistence';
interface EditChatDescriptionOptions {
initialDescription?: string;
initialTitle?: string;
customChatId?: string;
syncWithGlobalStore?: boolean;
}
@@ -21,7 +15,7 @@ type EditChatDescriptionHook = {
handleBlur: () => Promise<void>;
handleSubmit: (event: React.FormEvent) => Promise<void>;
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => Promise<void>;
currentDescription: string;
currentTitle: string;
toggleEditMode: () => void;
};
@@ -39,15 +33,14 @@ type EditChatDescriptionHook = {
* @param {boolean} options.syncWithGlobalStore - Flag to indicate global description store synchronization.
* @returns {EditChatDescriptionHook} Methods and state for managing description edits.
*/
export function useEditChatDescription({
initialDescription = descriptionStore.get()!,
export function useEditChatTitle({
initialTitle = currentChatTitle.get()!,
customChatId,
syncWithGlobalStore,
}: EditChatDescriptionOptions): EditChatDescriptionHook {
const db = database?.read();
const chatIdFromStore = useStore(chatIdStore);
const chatIdFromStore = useStore(currentChatId);
const [editing, setEditing] = useState(false);
const [currentDescription, setCurrentDescription] = useState(initialDescription);
const [currentTitle, setCurrentTitle] = useState(initialTitle);
const [chatId, setChatId] = useState<string>();
@@ -55,55 +48,55 @@ export function useEditChatDescription({
setChatId(customChatId || chatIdFromStore);
}, [customChatId, chatIdFromStore]);
useEffect(() => {
setCurrentDescription(initialDescription);
}, [initialDescription]);
setCurrentTitle(initialTitle);
}, [initialTitle]);
const toggleEditMode = useCallback(() => setEditing((prev) => !prev), []);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setCurrentDescription(e.target.value);
setCurrentTitle(e.target.value);
}, []);
const fetchLatestDescription = useCallback(async () => {
if (!db || !chatId) {
return initialDescription;
const fetchLatestTitle = useCallback(async () => {
if (!chatId) {
return initialTitle;
}
try {
const chat = await getMessages(db, chatId);
return chat?.description || initialDescription;
const chat = await getChatContents(chatId);
return chat?.title || initialTitle;
} catch (error) {
console.error('Failed to fetch latest description:', error);
return initialDescription;
return initialTitle;
}
}, [db, chatId, initialDescription]);
}, [chatId, initialTitle]);
const handleBlur = useCallback(async () => {
const latestDescription = await fetchLatestDescription();
setCurrentDescription(latestDescription);
const latestTitle = await fetchLatestTitle();
setCurrentTitle(latestTitle);
toggleEditMode();
}, [fetchLatestDescription, toggleEditMode]);
}, [fetchLatestTitle, toggleEditMode]);
const isValidDescription = useCallback((desc: string): boolean => {
const trimmedDesc = desc.trim();
const isValidTitle = useCallback((title: string): boolean => {
const trimmedTitle = title.trim();
if (trimmedDesc === initialDescription) {
if (trimmedTitle === initialTitle) {
toggleEditMode();
return false; // No change, skip validation
}
const lengthValid = trimmedDesc.length > 0 && trimmedDesc.length <= 100;
const lengthValid = trimmedTitle.length > 0 && trimmedTitle.length <= 100;
// Allow letters, numbers, spaces, and common punctuation but exclude characters that could cause issues
const characterValid = /^[a-zA-Z0-9\s\-_.,!?()[\]{}'"]+$/.test(trimmedDesc);
const characterValid = /^[a-zA-Z0-9\s\-_.,!?()[\]{}'"]+$/.test(trimmedTitle);
if (!lengthValid) {
toast.error('Description must be between 1 and 100 characters.');
toast.error('Title must be between 1 and 100 characters.');
return false;
}
if (!characterValid) {
toast.error('Description can only contain letters, numbers, spaces, and basic punctuation.');
toast.error('Title can only contain letters, numbers, spaces, and basic punctuation.');
return false;
}
@@ -114,35 +107,30 @@ export function useEditChatDescription({
async (event: React.FormEvent) => {
event.preventDefault();
if (!isValidDescription(currentDescription)) {
if (!isValidTitle(currentTitle)) {
return;
}
try {
if (!db) {
toast.error('Chat persistence is not available');
return;
}
if (!chatId) {
toast.error('Chat Id is not available');
return;
}
await updateChatDescription(db, chatId, currentDescription);
await updateChatTitle(chatId, currentTitle);
if (syncWithGlobalStore) {
descriptionStore.set(currentDescription);
currentChatTitle.set(currentTitle);
}
toast.success('Chat description updated successfully');
toast.success('Chat title updated successfully');
} catch (error) {
toast.error('Failed to update chat description: ' + (error as Error).message);
toast.error('Failed to update chat title: ' + (error as Error).message);
}
toggleEditMode();
},
[currentDescription, db, chatId, initialDescription, customChatId],
[currentTitle, chatId, customChatId],
);
const handleKeyDown = useCallback(
@@ -160,7 +148,7 @@ export function useEditChatDescription({
handleBlur,
handleSubmit,
handleKeyDown,
currentDescription,
currentTitle,
toggleEditMode,
};
}

View File

@@ -1,18 +1,14 @@
import { useState, useMemo, useCallback } from 'react';
import { debounce } from '~/utils/debounce';
import type { ChatHistoryItem } from '~/lib/persistence';
import type { ChatContents } from '~/lib/persistence/db';
interface UseSearchFilterOptions {
items: ChatHistoryItem[];
searchFields?: (keyof ChatHistoryItem)[];
items: ChatContents[];
searchFields?: (keyof ChatContents)[];
debounceMs?: number;
}
export function useSearchFilter({
items = [],
searchFields = ['description'],
debounceMs = 300,
}: UseSearchFilterOptions) {
export function useSearchFilter({ items = [], searchFields = ['title'], debounceMs = 300 }: UseSearchFilterOptions) {
const [searchQuery, setSearchQuery] = useState('');
const debouncedSetSearch = useCallback(debounce(setSearchQuery, debounceMs), []);

View File

@@ -1,229 +0,0 @@
import { useStore } from '@nanostores/react';
import {
isDebugMode,
isEventLogsEnabled,
isLocalModelsEnabled,
LOCAL_PROVIDERS,
promptStore,
providersStore,
latestBranchStore,
autoSelectStarterTemplate,
enableContextOptimizationStore,
} from '~/lib/stores/settings';
import { useCallback, useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
interface CommitData {
commit: string;
version?: string;
}
const versionData: CommitData = {
commit: __COMMIT_HASH,
version: __APP_VERSION,
};
export function useSettings() {
const providers = useStore(providersStore);
const debug = useStore(isDebugMode);
const eventLogs = useStore(isEventLogsEnabled);
const promptId = useStore(promptStore);
const isLocalModel = useStore(isLocalModelsEnabled);
const isLatestBranch = useStore(latestBranchStore);
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
// Function to check if we're on stable version
const checkIsStableVersion = async () => {
try {
const response = await fetch(
`https://api.github.com/repos/stackblitz-labs/bolt.diy/git/refs/tags/v${versionData.version}`,
);
const data: { object: { sha: string } } = await response.json();
return versionData.commit.slice(0, 7) === data.object.sha.slice(0, 7);
} catch (error) {
console.warn('Error checking stable version:', error);
return false;
}
};
// reading values from cookies on mount
useEffect(() => {
const savedProviders = Cookies.get('providers');
if (savedProviders) {
try {
const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders);
Object.keys(providers).forEach((provider) => {
const currentProviderSettings = parsedProviders[provider];
if (currentProviderSettings) {
providersStore.setKey(provider, {
...providers[provider],
settings: {
...currentProviderSettings,
enabled: currentProviderSettings.enabled ?? true,
},
});
}
});
} catch (error) {
console.error('Failed to parse providers from cookies:', error);
}
}
// load debug mode from cookies
const savedDebugMode = Cookies.get('isDebugEnabled');
if (savedDebugMode) {
isDebugMode.set(savedDebugMode === 'true');
}
// load event logs from cookies
const savedEventLogs = Cookies.get('isEventLogsEnabled');
if (savedEventLogs) {
isEventLogsEnabled.set(savedEventLogs === 'true');
}
// load local models from cookies
const savedLocalModels = Cookies.get('isLocalModelsEnabled');
if (savedLocalModels) {
isLocalModelsEnabled.set(savedLocalModels === 'true');
}
const promptId = Cookies.get('promptId');
if (promptId) {
promptStore.set(promptId);
}
// load latest branch setting from cookies or determine based on version
const savedLatestBranch = Cookies.get('isLatestBranch');
let checkCommit = Cookies.get('commitHash');
if (checkCommit === undefined) {
checkCommit = versionData.commit;
}
if (savedLatestBranch === undefined || checkCommit !== versionData.commit) {
// If setting hasn't been set by user, check version
checkIsStableVersion().then((isStable) => {
const shouldUseLatest = !isStable;
latestBranchStore.set(shouldUseLatest);
Cookies.set('isLatestBranch', String(shouldUseLatest));
Cookies.set('commitHash', String(versionData.commit));
});
} else {
latestBranchStore.set(savedLatestBranch === 'true');
}
const autoSelectTemplate = Cookies.get('autoSelectTemplate');
if (autoSelectTemplate) {
autoSelectStarterTemplate.set(autoSelectTemplate === 'true');
}
const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
if (savedContextOptimizationEnabled) {
enableContextOptimizationStore.set(savedContextOptimizationEnabled === 'true');
}
}, []);
// writing values to cookies on change
useEffect(() => {
const providers = providersStore.get();
const providerSetting: Record<string, IProviderSetting> = {};
Object.keys(providers).forEach((provider) => {
providerSetting[provider] = providers[provider].settings;
});
Cookies.set('providers', JSON.stringify(providerSetting));
}, [providers]);
useEffect(() => {
let active = Object.entries(providers)
.filter(([_key, provider]) => provider.settings.enabled)
.map(([_k, p]) => p);
if (!isLocalModel) {
active = active.filter((p) => !LOCAL_PROVIDERS.includes(p.name));
}
setActiveProviders(active);
}, [providers, isLocalModel]);
// helper function to update settings
const updateProviderSettings = useCallback(
(provider: string, config: IProviderSetting) => {
const settings = providers[provider].settings;
providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } });
},
[providers],
);
const enableDebugMode = useCallback((enabled: boolean) => {
isDebugMode.set(enabled);
logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isDebugEnabled', String(enabled));
}, []);
const enableEventLogs = useCallback((enabled: boolean) => {
isEventLogsEnabled.set(enabled);
logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isEventLogsEnabled', String(enabled));
}, []);
const enableLocalModels = useCallback((enabled: boolean) => {
isLocalModelsEnabled.set(enabled);
logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isLocalModelsEnabled', String(enabled));
}, []);
const setPromptId = useCallback((promptId: string) => {
promptStore.set(promptId);
Cookies.set('promptId', promptId);
}, []);
const enableLatestBranch = useCallback((enabled: boolean) => {
latestBranchStore.set(enabled);
logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('isLatestBranch', String(enabled));
}, []);
const setAutoSelectTemplate = useCallback((enabled: boolean) => {
autoSelectStarterTemplate.set(enabled);
logStore.logSystem(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('autoSelectTemplate', String(enabled));
}, []);
const enableContextOptimization = useCallback((enabled: boolean) => {
enableContextOptimizationStore.set(enabled);
logStore.logSystem(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
Cookies.set('contextOptimizationEnabled', String(enabled));
}, []);
return {
providers,
activeProviders,
updateProviderSettings,
debug,
enableDebugMode,
eventLogs,
enableEventLogs,
isLocalModel,
enableLocalModels,
promptId,
setPromptId,
isLatestBranch,
enableLatestBranch,
autoSelectTemplate,
setAutoSelectTemplate,
contextOptimizationEnabled,
enableContextOptimization,
};
}

View File

@@ -1,129 +0,0 @@
import type { LanguageModelV1 } from 'ai';
import type { ProviderInfo, ProviderConfig, ModelInfo } from './types';
import type { IProviderSetting } from '~/types/model';
import { createOpenAI } from '@ai-sdk/openai';
import { LLMManager } from './manager';
export abstract class BaseProvider implements ProviderInfo {
abstract name: string;
abstract staticModels: ModelInfo[];
abstract config: ProviderConfig;
cachedDynamicModels?: {
cacheId: string;
models: ModelInfo[];
};
getApiKeyLink?: string;
labelForGetApiKey?: string;
icon?: string;
getProviderBaseUrlAndKey(options: {
apiKeys?: Record<string, string>;
providerSettings?: IProviderSetting;
serverEnv?: Record<string, string>;
defaultBaseUrlKey: string;
defaultApiTokenKey: string;
}) {
const { apiKeys, providerSettings, serverEnv, defaultBaseUrlKey, defaultApiTokenKey } = options;
let settingsBaseUrl = providerSettings?.baseUrl;
const manager = LLMManager.getInstance();
if (settingsBaseUrl && settingsBaseUrl.length == 0) {
settingsBaseUrl = undefined;
}
const baseUrlKey = this.config.baseUrlKey || defaultBaseUrlKey;
let baseUrl =
settingsBaseUrl ||
serverEnv?.[baseUrlKey] ||
process?.env?.[baseUrlKey] ||
manager.env?.[baseUrlKey] ||
this.config.baseUrl;
if (baseUrl && baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey;
const apiKey =
apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[apiTokenKey];
return {
baseUrl,
apiKey,
};
}
getModelsFromCache(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}): ModelInfo[] | null {
if (!this.cachedDynamicModels) {
// console.log('no dynamic models',this.name);
return null;
}
const cacheKey = this.cachedDynamicModels.cacheId;
const generatedCacheKey = this.getDynamicModelsCacheKey(options);
if (cacheKey !== generatedCacheKey) {
// console.log('cache key mismatch',this.name,cacheKey,generatedCacheKey);
this.cachedDynamicModels = undefined;
return null;
}
return this.cachedDynamicModels.models;
}
getDynamicModelsCacheKey(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}) {
return JSON.stringify({
apiKeys: options.apiKeys?.[this.name],
providerSettings: options.providerSettings?.[this.name],
serverEnv: options.serverEnv,
});
}
storeDynamicModels(
options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
},
models: ModelInfo[],
) {
const cacheId = this.getDynamicModelsCacheKey(options);
// console.log('caching dynamic models',this.name,cacheId);
this.cachedDynamicModels = {
cacheId,
models,
};
}
// Declare the optional getDynamicModels method
getDynamicModels?(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv?: Record<string, string>,
): Promise<ModelInfo[]>;
abstract getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1;
}
type OptionalApiKey = string | undefined;
export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) {
const openai = createOpenAI({
baseURL,
apiKey,
});
return openai(model);
}

View File

@@ -1,203 +0,0 @@
import type { IProviderSetting } from '~/types/model';
import { BaseProvider } from './base-provider';
import type { ModelInfo, ProviderInfo } from './types';
import * as providers from './registry';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('LLMManager');
export class LLMManager {
private static _instance: LLMManager;
private _providers: Map<string, BaseProvider> = new Map();
private _modelList: ModelInfo[] = [];
private readonly _env: any = {};
private constructor(_env: Record<string, string>) {
this._registerProvidersFromDirectory();
this._env = _env;
}
static getInstance(env: Record<string, string> = {}): LLMManager {
if (!LLMManager._instance) {
LLMManager._instance = new LLMManager(env);
}
return LLMManager._instance;
}
get env() {
return this._env;
}
private async _registerProvidersFromDirectory() {
try {
/*
* Dynamically import all files from the providers directory
* const providerModules = import.meta.glob('./providers/*.ts', { eager: true });
*/
// Look for exported classes that extend BaseProvider
for (const exportedItem of Object.values(providers)) {
if (typeof exportedItem === 'function' && exportedItem.prototype instanceof BaseProvider) {
const provider = new exportedItem();
try {
this.registerProvider(provider);
} catch (error: any) {
logger.warn('Failed To Register Provider: ', provider.name, 'error:', error.message);
}
}
}
} catch (error) {
logger.error('Error registering providers:', error);
}
}
registerProvider(provider: BaseProvider) {
if (this._providers.has(provider.name)) {
logger.warn(`Provider ${provider.name} is already registered. Skipping.`);
return;
}
logger.info('Registering Provider: ', provider.name);
this._providers.set(provider.name, provider);
this._modelList = [...this._modelList, ...provider.staticModels];
}
getProvider(name: string): BaseProvider | undefined {
return this._providers.get(name);
}
getAllProviders(): BaseProvider[] {
return Array.from(this._providers.values());
}
getModelList(): ModelInfo[] {
return this._modelList;
}
async updateModelList(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}): Promise<ModelInfo[]> {
const { apiKeys, providerSettings, serverEnv } = options;
let enabledProviders = Array.from(this._providers.values()).map((p) => p.name);
if (providerSettings) {
enabledProviders = enabledProviders.filter((p) => providerSettings[p].enabled);
}
// Get dynamic models from all providers that support them
const dynamicModels = await Promise.all(
Array.from(this._providers.values())
.filter((provider) => enabledProviders.includes(provider.name))
.filter(
(provider): provider is BaseProvider & Required<Pick<ProviderInfo, 'getDynamicModels'>> =>
!!provider.getDynamicModels,
)
.map(async (provider) => {
const cachedModels = provider.getModelsFromCache(options);
if (cachedModels) {
return cachedModels;
}
const dynamicModels = await provider
.getDynamicModels(apiKeys, providerSettings?.[provider.name], serverEnv)
.then((models) => {
logger.info(`Caching ${models.length} dynamic models for ${provider.name}`);
provider.storeDynamicModels(options, models);
return models;
})
.catch((err) => {
logger.error(`Error getting dynamic models ${provider.name} :`, err);
return [];
});
return dynamicModels;
}),
);
// Combine static and dynamic models
const modelList = [
...dynamicModels.flat(),
...Array.from(this._providers.values()).flatMap((p) => p.staticModels || []),
];
this._modelList = modelList;
return modelList;
}
getStaticModelList() {
return [...this._providers.values()].flatMap((p) => p.staticModels || []);
}
async getModelListFromProvider(
providerArg: BaseProvider,
options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
},
): Promise<ModelInfo[]> {
const provider = this._providers.get(providerArg.name);
if (!provider) {
throw new Error(`Provider ${providerArg.name} not found`);
}
const staticModels = provider.staticModels || [];
if (!provider.getDynamicModels) {
return staticModels;
}
const { apiKeys, providerSettings, serverEnv } = options;
const cachedModels = provider.getModelsFromCache({
apiKeys,
providerSettings,
serverEnv,
});
if (cachedModels) {
logger.info(`Found ${cachedModels.length} cached models for ${provider.name}`);
return [...cachedModels, ...staticModels];
}
logger.info(`Getting dynamic models for ${provider.name}`);
const dynamicModels = await provider
.getDynamicModels?.(apiKeys, providerSettings?.[provider.name], serverEnv)
.then((models) => {
logger.info(`Got ${models.length} dynamic models for ${provider.name}`);
provider.storeDynamicModels(options, models);
return models;
})
.catch((err) => {
logger.error(`Error getting dynamic models ${provider.name} :`, err);
return [];
});
return [...dynamicModels, ...staticModels];
}
getStaticModelListFromProvider(providerArg: BaseProvider) {
const provider = this._providers.get(providerArg.name);
if (!provider) {
throw new Error(`Provider ${providerArg.name} not found`);
}
return [...(provider.staticModels || [])];
}
getDefaultProvider(): BaseProvider {
const firstProvider = this._providers.values().next().value;
if (!firstProvider) {
throw new Error('No providers registered');
}
return firstProvider;
}
}

View File

@@ -1,113 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { LanguageModelV1 } from 'ai';
import type { IProviderSetting } from '~/types/model';
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
interface AWSBedRockConfig {
region: string;
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
}
export default class AmazonBedrockProvider extends BaseProvider {
name = 'AmazonBedrock';
getApiKeyLink = 'https://console.aws.amazon.com/iam/home';
config = {
apiTokenKey: 'AWS_BEDROCK_CONFIG',
};
staticModels: ModelInfo[] = [
{
name: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
label: 'Claude 3.5 Sonnet (Bedrock)',
provider: 'AmazonBedrock',
maxTokenAllowed: 4096,
},
{
name: 'anthropic.claude-3-sonnet-20240229-v1:0',
label: 'Claude 3 Sonnet (Bedrock)',
provider: 'AmazonBedrock',
maxTokenAllowed: 4096,
},
{
name: 'anthropic.claude-3-haiku-20240307-v1:0',
label: 'Claude 3 Haiku (Bedrock)',
provider: 'AmazonBedrock',
maxTokenAllowed: 4096,
},
{
name: 'amazon.nova-pro-v1:0',
label: 'Amazon Nova Pro (Bedrock)',
provider: 'AmazonBedrock',
maxTokenAllowed: 5120,
},
{
name: 'amazon.nova-lite-v1:0',
label: 'Amazon Nova Lite (Bedrock)',
provider: 'AmazonBedrock',
maxTokenAllowed: 5120,
},
{
name: 'mistral.mistral-large-2402-v1:0',
label: 'Mistral Large 24.02 (Bedrock)',
provider: 'AmazonBedrock',
maxTokenAllowed: 8192,
},
];
private _parseAndValidateConfig(apiKey: string): AWSBedRockConfig {
let parsedConfig: AWSBedRockConfig;
try {
parsedConfig = JSON.parse(apiKey);
} catch {
throw new Error(
'Invalid AWS Bedrock configuration format. Please provide a valid JSON string containing region, accessKeyId, and secretAccessKey.',
);
}
const { region, accessKeyId, secretAccessKey, sessionToken } = parsedConfig;
if (!region || !accessKeyId || !secretAccessKey) {
throw new Error(
'Missing required AWS credentials. Configuration must include region, accessKeyId, and secretAccessKey.',
);
}
return {
region,
accessKeyId,
secretAccessKey,
...(sessionToken && { sessionToken }),
};
}
getModelInstance(options: {
model: string;
serverEnv: any;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'AWS_BEDROCK_CONFIG',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const config = this._parseAndValidateConfig(apiKey);
const bedrock = createAmazonBedrock(config);
return bedrock(model);
}
}

View File

@@ -1,58 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { LanguageModelV1 } from 'ai';
import type { IProviderSetting } from '~/types/model';
import { createAnthropic } from '@ai-sdk/anthropic';
export default class AnthropicProvider extends BaseProvider {
name = 'Anthropic';
getApiKeyLink = 'https://console.anthropic.com/settings/keys';
config = {
apiTokenKey: 'ANTHROPIC_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'claude-3-5-sonnet-latest',
label: 'Claude 3.5 Sonnet (new)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{
name: 'claude-3-5-sonnet-20240620',
label: 'Claude 3.5 Sonnet (old)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{
name: 'claude-3-5-haiku-latest',
label: 'Claude 3.5 Haiku (new)',
provider: 'Anthropic',
maxTokenAllowed: 8000,
},
{ name: 'claude-3-opus-latest', label: 'Claude 3 Opus', provider: 'Anthropic', maxTokenAllowed: 8000 },
{ name: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet', provider: 'Anthropic', maxTokenAllowed: 8000 },
{ name: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku', provider: 'Anthropic', maxTokenAllowed: 8000 },
];
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings,
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'ANTHROPIC_API_KEY',
});
const anthropic = createAnthropic({
apiKey,
});
return anthropic(model);
};
}

View File

@@ -1,54 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createCohere } from '@ai-sdk/cohere';
export default class CohereProvider extends BaseProvider {
name = 'Cohere';
getApiKeyLink = 'https://dashboard.cohere.com/api-keys';
config = {
apiTokenKey: 'COHERE_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'command-r-plus-08-2024', label: 'Command R plus Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r-08-2024', label: 'Command R Latest', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r-plus', label: 'Command R plus', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-r', label: 'Command R', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command', label: 'Command', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-nightly', label: 'Command Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-light', label: 'Command Light', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'command-light-nightly', label: 'Command Light Nightly', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'c4ai-aya-expanse-8b', label: 'c4AI Aya Expanse 8b', provider: 'Cohere', maxTokenAllowed: 4096 },
{ name: 'c4ai-aya-expanse-32b', label: 'c4AI Aya Expanse 32b', provider: 'Cohere', maxTokenAllowed: 4096 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'COHERE_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const cohere = createCohere({
apiKey,
});
return cohere(model);
}
}

View File

@@ -1,47 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class DeepseekProvider extends BaseProvider {
name = 'Deepseek';
getApiKeyLink = 'https://platform.deepseek.com/apiKeys';
config = {
apiTokenKey: 'DEEPSEEK_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'deepseek-coder', label: 'Deepseek-Coder', provider: 'Deepseek', maxTokenAllowed: 8000 },
{ name: 'deepseek-chat', label: 'Deepseek-Chat', provider: 'Deepseek', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'DEEPSEEK_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api.deepseek.com/beta',
apiKey,
});
return openai(model);
}
}

View File

@@ -1,51 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
export default class GoogleProvider extends BaseProvider {
name = 'Google';
getApiKeyLink = 'https://aistudio.google.com/app/apikey';
config = {
apiTokenKey: 'GOOGLE_GENERATIVE_AI_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-flash-002', label: 'Gemini 1.5 Flash-002', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-flash-8b', label: 'Gemini 1.5 Flash-8b', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro-002', provider: 'Google', maxTokenAllowed: 8192 },
{ name: 'gemini-exp-1206', label: 'Gemini exp-1206', provider: 'Google', maxTokenAllowed: 8192 },
];
getModelInstance(options: {
model: string;
serverEnv: any;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'GOOGLE_GENERATIVE_AI_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const google = createGoogleGenerativeAI({
apiKey,
});
return google(model);
}
}

View File

@@ -1,51 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class GroqProvider extends BaseProvider {
name = 'Groq';
getApiKeyLink = 'https://console.groq.com/keys';
config = {
apiTokenKey: 'GROQ_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'GROQ_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api.groq.com/openai/v1',
apiKey,
});
return openai(model);
}
}

View File

@@ -1,111 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class HuggingFaceProvider extends BaseProvider {
name = 'HuggingFace';
getApiKeyLink = 'https://huggingface.co/settings/tokens';
config = {
apiTokenKey: 'HuggingFace_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: '01-ai/Yi-1.5-34B-Chat',
label: 'Yi-1.5-34B-Chat (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'codellama/CodeLlama-34b-Instruct-hf',
label: 'CodeLlama-34b-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'NousResearch/Hermes-3-Llama-3.1-8B',
label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen2.5-Coder-32B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'Qwen/Qwen2.5-72B-Instruct',
label: 'Qwen2.5-72B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.1-70B-Instruct',
label: 'Llama-3.1-70B-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.1-405B',
label: 'Llama-3.1-405B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: '01-ai/Yi-1.5-34B-Chat',
label: 'Yi-1.5-34B-Chat (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'codellama/CodeLlama-34b-Instruct-hf',
label: 'CodeLlama-34b-Instruct (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
{
name: 'NousResearch/Hermes-3-Llama-3.1-8B',
label: 'Hermes-3-Llama-3.1-8B (HuggingFace)',
provider: 'HuggingFace',
maxTokenAllowed: 8000,
},
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'HuggingFace_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api-inference.huggingface.co/v1/',
apiKey,
});
return openai(model);
}
}

View File

@@ -1,111 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class HyperbolicProvider extends BaseProvider {
name = 'Hyperbolic';
getApiKeyLink = 'https://app.hyperbolic.xyz/settings';
config = {
apiTokenKey: 'HYPERBOLIC_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen 2.5 Coder 32B Instruct',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'Qwen/Qwen2.5-72B-Instruct',
label: 'Qwen2.5-72B-Instruct',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'deepseek-ai/DeepSeek-V2.5',
label: 'DeepSeek-V2.5',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'Qwen/QwQ-32B-Preview',
label: 'QwQ-32B-Preview',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
{
name: 'Qwen/Qwen2-VL-72B-Instruct',
label: 'Qwen2-VL-72B-Instruct',
provider: 'Hyperbolic',
maxTokenAllowed: 8192,
},
];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl: fetchBaseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'HYPERBOLIC_API_KEY',
});
const baseUrl = fetchBaseUrl || 'https://api.hyperbolic.xyz/v1';
if (!apiKey) {
throw `Missing Api Key configuration for ${this.name} provider`;
}
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
const data = res.data.filter((model: any) => model.object === 'model' && model.supports_chat);
return data.map((m: any) => ({
name: m.id,
label: `${m.id} - context ${m.context_length ? Math.floor(m.context_length / 1000) + 'k' : 'N/A'}`,
provider: this.name,
maxTokenAllowed: m.context_length || 8000,
}));
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'HYPERBOLIC_API_KEY',
});
if (!apiKey) {
throw `Missing Api Key configuration for ${this.name} provider`;
}
const openai = createOpenAI({
baseURL: 'https://api.hyperbolic.xyz/v1/',
apiKey,
});
return openai(model);
}
}

View File

@@ -1,93 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import { createOpenAI } from '@ai-sdk/openai';
import type { LanguageModelV1 } from 'ai';
import { logger } from '~/utils/logger';
export default class LMStudioProvider extends BaseProvider {
name = 'LMStudio';
getApiKeyLink = 'https://lmstudio.ai/';
labelForGetApiKey = 'Get LMStudio';
icon = 'i-ph:cloud-arrow-down';
config = {
baseUrlKey: 'LMSTUDIO_API_BASE_URL',
baseUrl: 'http://localhost:1234/',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL',
defaultApiTokenKey: '',
});
if (!baseUrl) {
throw new Error('No baseUrl found for LMStudio provider');
}
if (typeof window === 'undefined') {
/*
* Running in Server
* Backend: Check if we're running in Docker
*/
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
const response = await fetch(`${baseUrl}/v1/models`);
const data = (await response.json()) as { data: Array<{ id: string }> };
return data.data.map((model) => ({
name: model.id,
label: model.id,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'LMSTUDIO_API_BASE_URL',
defaultApiTokenKey: '',
});
if (!baseUrl) {
throw new Error('No baseUrl found for LMStudio provider');
}
if (typeof window === 'undefined') {
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
logger.debug('LMStudio Base Url used: ', baseUrl);
const lmstudio = createOpenAI({
baseUrl: `${baseUrl}/v1`,
apiKey: '',
});
return lmstudio(model);
};
}

View File

@@ -1,53 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createMistral } from '@ai-sdk/mistral';
export default class MistralProvider extends BaseProvider {
name = 'Mistral';
getApiKeyLink = 'https://console.mistral.ai/api-keys/';
config = {
apiTokenKey: 'MISTRAL_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'open-mistral-7b', label: 'Mistral 7B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mixtral-8x7b', label: 'Mistral 8x7B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mixtral-8x22b', label: 'Mistral 8x22B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-codestral-mamba', label: 'Codestral Mamba', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'open-mistral-nemo', label: 'Mistral Nemo', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'ministral-8b-latest', label: 'Mistral 8B', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'mistral-small-latest', label: 'Mistral Small', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'codestral-latest', label: 'Codestral', provider: 'Mistral', maxTokenAllowed: 8000 },
{ name: 'mistral-large-latest', label: 'Mistral Large Latest', provider: 'Mistral', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'MISTRAL_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const mistral = createMistral({
apiKey,
});
return mistral(model);
}
}

View File

@@ -1,118 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { ollama } from 'ollama-ai-provider';
import { logger } from '~/utils/logger';
interface OllamaModelDetails {
parent_model: string;
format: string;
family: string;
families: string[];
parameter_size: string;
quantization_level: string;
}
export interface OllamaModel {
name: string;
model: string;
modified_at: string;
size: number;
digest: string;
details: OllamaModelDetails;
}
export interface OllamaApiResponse {
models: OllamaModel[];
}
export const DEFAULT_NUM_CTX = import.meta.env.DEFAULT_NUM_CTX ? parseInt(import.meta.env.DEFAULT_NUM_CTX, 10) : 32768;
export default class OllamaProvider extends BaseProvider {
name = 'Ollama';
getApiKeyLink = 'https://ollama.com/download';
labelForGetApiKey = 'Download Ollama';
icon = 'i-ph:cloud-arrow-down';
config = {
baseUrlKey: 'OLLAMA_API_BASE_URL',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultApiTokenKey: '',
});
if (!baseUrl) {
throw new Error('No baseUrl found for OLLAMA provider');
}
if (typeof window === 'undefined') {
/*
* Running in Server
* Backend: Check if we're running in Docker
*/
const isDocker = process.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
}
const response = await fetch(`${baseUrl}/api/tags`);
const data = (await response.json()) as OllamaApiResponse;
// console.log({ ollamamodels: data.models });
return data.models.map((model: OllamaModel) => ({
name: model.name,
label: `${model.name} (${model.details.parameter_size})`,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultApiTokenKey: '',
});
// Backend: Check if we're running in Docker
if (!baseUrl) {
throw new Error('No baseUrl found for OLLAMA provider');
}
const isDocker = import.meta.env.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
logger.debug('Ollama Base Url used: ', baseUrl);
const ollamaInstance = ollama(model, {
numCtx: DEFAULT_NUM_CTX,
}) as LanguageModelV1 & { config: any };
ollamaInstance.config.baseURL = `${baseUrl}/api`;
return ollamaInstance;
};
}

View File

@@ -1,131 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
interface OpenRouterModel {
name: string;
id: string;
context_length: number;
pricing: {
prompt: number;
completion: number;
};
}
interface OpenRouterModelsResponse {
data: OpenRouterModel[];
}
export default class OpenRouterProvider extends BaseProvider {
name = 'OpenRouter';
getApiKeyLink = 'https://openrouter.ai/settings/keys';
config = {
apiTokenKey: 'OPEN_ROUTER_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'anthropic/claude-3.5-sonnet',
label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'anthropic/claude-3-haiku',
label: 'Anthropic: Claude 3 Haiku (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'deepseek/deepseek-coder',
label: 'Deepseek-Coder V2 236B (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'google/gemini-flash-1.5',
label: 'Google Gemini Flash 1.5 (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'google/gemini-pro-1.5',
label: 'Google Gemini Pro 1.5 (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{ name: 'x-ai/grok-beta', label: 'xAI Grok Beta (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 8000 },
{
name: 'mistralai/mistral-nemo',
label: 'OpenRouter Mistral Nemo (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{
name: 'qwen/qwen-110b-chat',
label: 'OpenRouter Qwen 110b Chat (OpenRouter)',
provider: 'OpenRouter',
maxTokenAllowed: 8000,
},
{ name: 'cohere/command', label: 'Cohere Command (OpenRouter)', provider: 'OpenRouter', maxTokenAllowed: 4096 },
];
async getDynamicModels(
_apiKeys?: Record<string, string>,
_settings?: IProviderSetting,
_serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
try {
const response = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Content-Type': 'application/json',
},
});
const data = (await response.json()) as OpenRouterModelsResponse;
return data.data
.sort((a, b) => a.name.localeCompare(b.name))
.map((m) => ({
name: m.id,
label: `${m.name} - in:$${(m.pricing.prompt * 1_000_000).toFixed(2)} out:$${(m.pricing.completion * 1_000_000).toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
provider: this.name,
maxTokenAllowed: 8000,
}));
} catch (error) {
console.error('Error getting OpenRouter models:', error);
return [];
}
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'OPEN_ROUTER_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openRouter = createOpenRouter({
apiKey,
});
const instance = openRouter.chat(model) as LanguageModelV1;
return instance;
}
}

View File

@@ -1,72 +0,0 @@
import { BaseProvider, getOpenAILikeModel } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
export default class OpenAILikeProvider extends BaseProvider {
name = 'OpenAILike';
getApiKeyLink = undefined;
config = {
baseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
apiTokenKey: 'OPENAI_LIKE_API_KEY',
};
staticModels: ModelInfo[] = [];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
defaultApiTokenKey: 'OPENAI_LIKE_API_KEY',
});
if (!baseUrl || !apiKey) {
return [];
}
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
return res.data.map((model: any) => ({
name: model.id,
label: model.id,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'OPENAI_LIKE_API_BASE_URL',
defaultApiTokenKey: 'OPENAI_LIKE_API_KEY',
});
if (!baseUrl || !apiKey) {
throw new Error(`Missing configuration for ${this.name} provider`);
}
return getOpenAILikeModel(baseUrl, apiKey, model);
}
}

View File

@@ -1,49 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class OpenAIProvider extends BaseProvider {
name = 'OpenAI';
getApiKeyLink = 'https://platform.openai.com/api-keys';
config = {
apiTokenKey: 'OPENAI_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-4', label: 'GPT-4', provider: 'OpenAI', maxTokenAllowed: 8000 },
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'OpenAI', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'OPENAI_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
apiKey,
});
return openai(model);
}
}

View File

@@ -1,63 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class PerplexityProvider extends BaseProvider {
name = 'Perplexity';
getApiKeyLink = 'https://www.perplexity.ai/settings/api';
config = {
apiTokenKey: 'PERPLEXITY_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'llama-3.1-sonar-small-128k-online',
label: 'Sonar Small Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
{
name: 'llama-3.1-sonar-large-128k-online',
label: 'Sonar Large Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
{
name: 'llama-3.1-sonar-huge-128k-online',
label: 'Sonar Huge Online',
provider: 'Perplexity',
maxTokenAllowed: 8192,
},
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'PERPLEXITY_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const perplexity = createOpenAI({
baseURL: 'https://api.perplexity.ai/',
apiKey,
});
return perplexity(model);
}
}

View File

@@ -1,95 +0,0 @@
import { BaseProvider, getOpenAILikeModel } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
export default class TogetherProvider extends BaseProvider {
name = 'Together';
getApiKeyLink = 'https://api.together.xyz/settings/api-keys';
config = {
baseUrlKey: 'TOGETHER_API_BASE_URL',
apiTokenKey: 'TOGETHER_API_KEY',
};
staticModels: ModelInfo[] = [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen/Qwen2.5-Coder-32B-Instruct',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
label: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
label: 'Mixtral 8x7B Instruct',
provider: 'Together',
maxTokenAllowed: 8192,
},
];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
const { baseUrl: fetchBaseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'TOGETHER_API_BASE_URL',
defaultApiTokenKey: 'TOGETHER_API_KEY',
});
const baseUrl = fetchBaseUrl || 'https://api.together.xyz/v1';
if (!baseUrl || !apiKey) {
return [];
}
// console.log({ baseUrl, apiKey });
const response = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
const res = (await response.json()) as any;
const data = (res || []).filter((model: any) => model.type === 'chat');
return data.map((m: any) => ({
name: m.id,
label: `${m.display_name} - in:$${m.pricing.input.toFixed(2)} out:$${m.pricing.output.toFixed(2)} - context ${Math.floor(m.context_length / 1000)}k`,
provider: this.name,
maxTokenAllowed: 8000,
}));
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'TOGETHER_API_BASE_URL',
defaultApiTokenKey: 'TOGETHER_API_KEY',
});
if (!baseUrl || !apiKey) {
throw new Error(`Missing configuration for ${this.name} provider`);
}
return getOpenAILikeModel(baseUrl, apiKey, model);
}
}

View File

@@ -1,47 +0,0 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
export default class XAIProvider extends BaseProvider {
name = 'xAI';
getApiKeyLink = 'https://docs.x.ai/docs/quickstart#creating-an-api-key';
config = {
apiTokenKey: 'XAI_API_KEY',
};
staticModels: ModelInfo[] = [
{ name: 'grok-beta', label: 'xAI Grok Beta', provider: 'xAI', maxTokenAllowed: 8000 },
{ name: 'grok-2-1212', label: 'xAI Grok2 1212', provider: 'xAI', maxTokenAllowed: 8000 },
];
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: '',
defaultApiTokenKey: 'XAI_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
const openai = createOpenAI({
baseURL: 'https://api.x.ai/v1',
apiKey,
});
return openai(model);
}
}

View File

@@ -1,37 +0,0 @@
import AnthropicProvider from './providers/anthropic';
import CohereProvider from './providers/cohere';
import DeepseekProvider from './providers/deepseek';
import GoogleProvider from './providers/google';
import GroqProvider from './providers/groq';
import HuggingFaceProvider from './providers/huggingface';
import LMStudioProvider from './providers/lmstudio';
import MistralProvider from './providers/mistral';
import OllamaProvider from './providers/ollama';
import OpenRouterProvider from './providers/open-router';
import OpenAILikeProvider from './providers/openai-like';
import OpenAIProvider from './providers/openai';
import PerplexityProvider from './providers/perplexity';
import TogetherProvider from './providers/together';
import XAIProvider from './providers/xai';
import HyperbolicProvider from './providers/hyperbolic';
import AmazonBedrockProvider from './providers/amazon-bedrock';
export {
AnthropicProvider,
CohereProvider,
DeepseekProvider,
GoogleProvider,
GroqProvider,
HuggingFaceProvider,
HyperbolicProvider,
MistralProvider,
OllamaProvider,
OpenAIProvider,
OpenRouterProvider,
OpenAILikeProvider,
PerplexityProvider,
XAIProvider,
TogetherProvider,
LMStudioProvider,
AmazonBedrockProvider,
};

View File

@@ -1,33 +0,0 @@
import type { LanguageModelV1 } from 'ai';
import type { IProviderSetting } from '~/types/model';
export interface ModelInfo {
name: string;
label: string;
provider: string;
maxTokenAllowed: number;
}
export interface ProviderInfo {
name: string;
staticModels: ModelInfo[];
getDynamicModels?: (
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv?: Record<string, string>,
) => Promise<ModelInfo[]>;
getModelInstance: (options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}) => LanguageModelV1;
getApiKeyLink?: string;
labelForGetApiKey?: string;
icon?: string;
}
export interface ProviderConfig {
baseUrlKey?: string;
baseUrl?: string;
apiTokenKey?: string;
}

View File

@@ -1,19 +1,19 @@
import { useStore } from '@nanostores/react';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import WithTooltip from '~/components/ui/Tooltip';
import { useEditChatDescription } from '~/lib/hooks';
import { description as descriptionStore } from '~/lib/persistence';
import { useEditChatTitle } from '~/lib/hooks/useEditChatDescription';
import { currentChatTitle } from '~/lib/persistence';
export function ChatDescription() {
const initialDescription = useStore(descriptionStore)!;
const initialTitle = useStore(currentChatTitle)!;
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
useEditChatDescription({
initialDescription,
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentTitle, toggleEditMode } =
useEditChatTitle({
initialTitle,
syncWithGlobalStore: true,
});
if (!initialDescription) {
if (!initialTitle) {
// doing this to prevent showing edit button until chat description is set
return null;
}
@@ -26,11 +26,11 @@ export function ChatDescription() {
type="text"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2 w-fit"
autoFocus
value={currentDescription}
value={currentTitle}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
style={{ width: `${Math.max(currentDescription.length * 8, 100)}px` }}
style={{ width: `${Math.max(currentTitle.length * 8, 100)}px` }}
/>
<TooltipProvider>
<WithTooltip tooltip="Save title">
@@ -46,7 +46,7 @@ export function ChatDescription() {
</form>
) : (
<>
{currentDescription}
{currentTitle}
<TooltipProvider>
<WithTooltip tooltip="Rename chat">
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent ml-2">

View File

@@ -1,236 +1,94 @@
import { createScopedLogger } from '~/utils/logger';
import type { ChatHistoryItem } from './useChatHistory';
import type { Message } from './message';
import { getSupabase } from '~/lib/supabase/client';
import { v4 as uuid } from 'uuid';
import { getMessagesRepositoryId, type Message } from './message';
const logger = createScopedLogger('ChatHistory');
export interface ChatContents {
id: string;
createdAt: string;
updatedAt: string;
title: string;
repositoryId: string | undefined;
messages: Message[];
}
// this is used at the top level and never rejects
export async function openDatabase(): Promise<IDBDatabase | undefined> {
if (typeof indexedDB === 'undefined') {
console.error('indexedDB is not available in this environment.');
return undefined;
function databaseRowToChatContents(d: any): ChatContents {
return {
id: d.id,
createdAt: d.created_at,
updatedAt: d.updated_at,
title: d.title,
messages: d.messages,
repositoryId: d.repository_id,
};
}
export async function getAllChats(): Promise<ChatContents[]> {
const { data, error } = await getSupabase().from('chats').select('*');
if (error) {
throw error;
}
return new Promise((resolve) => {
const request = indexedDB.open('boltHistory', 1);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('chats')) {
const store = db.createObjectStore('chats', { keyPath: 'id' });
store.createIndex('id', 'id', { unique: true });
store.createIndex('urlId', 'urlId', { unique: true });
}
};
request.onsuccess = (event: Event) => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onerror = (event: Event) => {
resolve(undefined);
logger.error((event.target as IDBOpenDBRequest).error);
};
});
return data.map(databaseRowToChatContents);
}
export async function getAll(db: IDBDatabase): Promise<ChatHistoryItem[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const request = store.getAll();
export async function setChatContents(id: string, title: string, messages: Message[]): Promise<void> {
const { data: user } = await getSupabase().auth.getUser();
const userId = user.user?.id;
request.onsuccess = () => resolve(request.result as ChatHistoryItem[]);
request.onerror = () => reject(request.error);
});
}
export async function setMessages(
db: IDBDatabase,
id: string,
messages: Message[],
urlId?: string,
description?: string,
timestamp?: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readwrite');
const store = transaction.objectStore('chats');
if (timestamp && isNaN(Date.parse(timestamp))) {
reject(new Error('Invalid timestamp'));
return;
}
const request = store.put({
id,
messages,
urlId,
description,
timestamp: timestamp ?? new Date().toISOString(),
});
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function getMessages(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {
return (await getMessagesById(db, id)) || (await getMessagesByUrlId(db, id));
}
export async function getMessagesByUrlId(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const index = store.index('urlId');
const request = index.get(id);
request.onsuccess = () => resolve(request.result as ChatHistoryItem);
request.onerror = () => reject(request.error);
});
}
export async function getMessagesById(db: IDBDatabase, id: string): Promise<ChatHistoryItem> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const request = store.get(id);
request.onsuccess = () => resolve(request.result as ChatHistoryItem);
request.onerror = () => reject(request.error);
});
}
export async function deleteById(db: IDBDatabase, id: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readwrite');
const store = transaction.objectStore('chats');
const request = store.delete(id);
request.onsuccess = () => resolve(undefined);
request.onerror = () => reject(request.error);
});
}
export async function getNextId(db: IDBDatabase): Promise<string> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const request = store.getAllKeys();
request.onsuccess = () => {
const highestId = request.result.reduce((cur, acc) => Math.max(+cur, +acc), 0);
resolve(String(+highestId + 1));
};
request.onerror = () => reject(request.error);
});
}
export async function getUrlId(db: IDBDatabase, id: string): Promise<string> {
const idList = await getUrlIds(db);
if (!idList.includes(id)) {
return id;
} else {
let i = 2;
while (idList.includes(`${id}-${i}`)) {
i++;
}
return `${id}-${i}`;
}
}
async function getUrlIds(db: IDBDatabase): Promise<string[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
const store = transaction.objectStore('chats');
const idList: string[] = [];
const request = store.openCursor();
request.onsuccess = (event: Event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
if (cursor) {
idList.push(cursor.value.urlId);
cursor.continue();
} else {
resolve(idList);
}
};
request.onerror = () => {
reject(request.error);
};
});
}
export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise<string> {
const chat = await getMessages(db, chatId);
if (!chat) {
throw new Error('Chat not found');
if (!userId) {
throw new Error('Not logged in');
}
// Find the index of the message to fork at
const messageIndex = chat.messages.findIndex((msg) => msg.id === messageId);
const repositoryId = getMessagesRepositoryId(messages);
if (messageIndex === -1) {
throw new Error('Message not found');
}
// Get messages up to and including the selected message
const messages = chat.messages.slice(0, messageIndex + 1);
return createChatFromMessages(db, chat.description ? `${chat.description} (fork)` : 'Forked chat', messages);
}
export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
const chat = await getMessages(db, id);
if (!chat) {
throw new Error('Chat not found');
}
return createChatFromMessages(db, `${chat.description || 'Chat'} (copy)`, chat.messages);
}
export async function createChatFromMessages(
db: IDBDatabase,
description: string,
messages: Message[],
): Promise<string> {
const newId = await getNextId(db);
const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
// TODO: Call setLastLoadedProblem(null).
await setMessages(
db,
newId,
const { error } = await getSupabase().from('chats').upsert({
id,
messages,
newUrlId, // Use the new urlId
description,
);
title,
user_id: userId,
repository_id: repositoryId,
});
return newUrlId; // Return the urlId instead of id for navigation
if (error) {
throw error;
}
}
export async function updateChatDescription(db: IDBDatabase, id: string, description: string): Promise<void> {
const chat = await getMessages(db, id);
export async function getChatContents(id: string): Promise<ChatContents> {
const { data, error } = await getSupabase().from('chats').select('*').eq('id', id);
if (!chat) {
throw new Error('Chat not found');
if (error) {
throw error;
}
if (!description.trim()) {
throw new Error('Description cannot be empty');
if (data.length != 1) {
throw new Error('Unexpected chat contents returned');
}
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp);
return databaseRowToChatContents(data[0]);
}
export async function deleteById(id: string): Promise<void> {
const { error } = await getSupabase().from('chats').delete().eq('id', id);
if (error) {
throw error;
}
}
export async function createChat(title: string, messages: Message[]): Promise<string> {
const id = uuid();
await setChatContents(id, title, messages);
return id;
}
export async function updateChatTitle(id: string, title: string): Promise<void> {
const chat = await getChatContents(id);
if (!title.trim()) {
throw new Error('Title cannot be empty');
}
await setChatContents(id, title, chat.messages);
}

View File

@@ -19,3 +19,20 @@ interface MessageImage extends MessageBase {
}
export type Message = MessageText | MessageImage;
// Get the repositoryId before any changes in the message at the given index.
export function getPreviousRepositoryId(messages: Message[], index: number): string | undefined {
for (let i = index - 1; i >= 0; i--) {
const message = messages[i];
if (message.repositoryId) {
return message.repositoryId;
}
}
return undefined;
}
// Get the repositoryId after applying some messages.
export function getMessagesRepositoryId(messages: Message[]): string | undefined {
return getPreviousRepositoryId(messages, messages.length);
}

View File

@@ -1,54 +1,25 @@
import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react';
import { useLoaderData, useNavigate } from '@remix-run/react';
import { useState, useEffect } from 'react';
import { atom } from 'nanostores';
import { toast } from 'react-toastify';
import { logStore } from '~/lib/stores/logs'; // Import logStore
import { getMessages, getNextId, openDatabase, setMessages, duplicateChat, createChatFromMessages } from './db';
import { createChat, getChatContents, setChatContents } from './db';
import { loadProblem } from '~/components/chat/LoadProblemButton';
import { createAsyncSuspenseValue } from '~/lib/asyncSuspenseValue';
import type { Message } from './message';
export interface ChatState {
description: string;
messages: Message[];
}
export interface ChatHistoryItem {
id: string;
urlId?: string;
description?: string;
messages: Message[];
timestamp: string;
}
const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;
export const database = persistenceEnabled ? createAsyncSuspenseValue(openDatabase) : undefined;
if (typeof document !== 'undefined') {
database?.preload();
}
export const chatId = atom<string | undefined>(undefined);
export const description = atom<string | undefined>(undefined);
export const currentChatId = atom<string | undefined>(undefined);
export const currentChatTitle = atom<string | undefined>(undefined);
export function useChatHistory() {
const db = database?.read();
const navigate = useNavigate();
const { id: mixedId, problemId } = useLoaderData<{ id?: string; problemId?: string }>() ?? {};
const [searchParams] = useSearchParams();
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
const [ready, setReady] = useState<boolean>(!mixedId && !problemId);
const [urlId, setUrlId] = useState<string | undefined>();
const importChat = async (description: string, messages: Message[]) => {
if (!db) {
return;
}
try {
const newId = await createChatFromMessages(db, description, messages);
const newId = await createChat(description, messages);
window.location.href = `/chat/${newId}`;
toast.success('Chat imported successfully');
} catch (error) {
@@ -61,31 +32,13 @@ export function useChatHistory() {
};
useEffect(() => {
if (!db) {
setReady(true);
if (persistenceEnabled) {
const error = new Error('Chat persistence is unavailable');
logStore.logError('Chat persistence initialization failed', error);
toast.error('Chat persistence is unavailable');
}
return;
}
if (mixedId) {
getMessages(db, mixedId)
.then((storedMessages) => {
if (storedMessages && storedMessages.messages.length > 0) {
const rewindId = searchParams.get('rewindTo');
const filteredMessages = rewindId
? storedMessages.messages.slice(0, storedMessages.messages.findIndex((m) => m.id === rewindId) + 1)
: storedMessages.messages;
setInitialMessages(filteredMessages);
setUrlId(storedMessages.urlId);
description.set(storedMessages.description);
chatId.set(storedMessages.id);
getChatContents(mixedId)
.then((chatContents) => {
if (chatContents && chatContents.messages.length > 0) {
setInitialMessages(chatContents.messages);
currentChatTitle.set(chatContents.title);
currentChatId.set(mixedId);
} else {
navigate('/', { replace: true });
}
@@ -105,59 +58,21 @@ export function useChatHistory() {
ready,
initialMessages,
storeMessageHistory: async (messages: Message[]) => {
if (!db || messages.length === 0) {
if (messages.length === 0) {
return;
}
if (initialMessages.length === 0 && !chatId.get()) {
const nextId = await getNextId(db);
const title = currentChatTitle.get() ?? 'New Chat';
chatId.set(nextId);
if (!urlId) {
navigateChat(nextId);
}
if (!currentChatId.get()) {
const id = await createChat(title, initialMessages);
currentChatId.set(id);
navigateChat(id);
}
await setMessages(db, chatId.get() as string, messages, urlId, description.get());
},
duplicateCurrentChat: async (listItemId: string) => {
if (!db || (!mixedId && !listItemId)) {
return;
}
try {
const newId = await duplicateChat(db, mixedId || listItemId);
navigate(`/chat/${newId}`);
toast.success('Chat duplicated successfully');
} catch (error) {
toast.error('Failed to duplicate chat');
console.log(error);
}
await setChatContents(currentChatId.get() as string, title, messages);
},
importChat,
exportChat: async (id = urlId) => {
if (!db || !id) {
return;
}
const chat = await getMessages(db, id);
const chatData = {
messages: chat.messages,
description: chat.description,
exportDate: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat-${new Date().toISOString()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
};
}
@@ -172,20 +87,3 @@ function navigateChat(nextId: string) {
window.history.replaceState({}, '', url);
}
// Get the repositoryId before any changes in the message at the given index.
export function getPreviousRepositoryId(messages: Message[], index: number): string | undefined {
for (let i = index - 1; i >= 0; i--) {
const message = messages[i];
if (message.repositoryId) {
return message.repositoryId;
}
}
return undefined;
}
// Get the repositoryId after applying some messages.
export function getMessagesRepositoryId(messages: Message[]): string | undefined {
return getPreviousRepositoryId(messages, messages.length);
}

View File

@@ -57,6 +57,8 @@ class DevelopmentServerManager {
let gActiveDevelopmentServer: DevelopmentServerManager | undefined;
export async function updateDevelopmentServer(repositoryId: string) {
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryId);
workbenchStore.showWorkbench.set(true);
workbenchStore.repositoryId.set(repositoryId);
workbenchStore.previewURL.set(undefined);

View File

@@ -1,10 +1,6 @@
// Accessors for the API to access saved problems.
import { toast } from 'react-toastify';
import { assert, sendCommandDedicatedClient } from './ReplayProtocolClient';
import type { Message } from '~/lib/persistence/message';
import Cookies from 'js-cookie';
import { shouldUseSupabase } from '~/lib/supabase/client';
import {
supabaseListAllProblems,
supabaseGetProblem,
@@ -14,29 +10,28 @@ import {
supabaseDeleteProblem,
} from '~/lib/supabase/problems';
import { getNutIsAdmin as getNutIsAdminFromSupabase } from '~/lib/supabase/client';
import { updateIsAdmin, updateUsername } from '~/lib/stores/user';
// Add global declaration for the problem property
declare global {
interface Window {
__currentProblem__?: BoltProblem;
__currentProblem__?: NutProblem;
}
}
export interface BoltProblemComment {
export interface NutProblemComment {
id?: string;
username?: string;
content: string;
timestamp: number;
}
export interface BoltProblemSolution {
export interface NutProblemSolution {
simulationData: any;
messages: Message[];
evaluator?: string;
}
export enum BoltProblemStatus {
export enum NutProblemStatus {
// Problem has been submitted but not yet reviewed.
Pending = 'Pending',
@@ -48,103 +43,32 @@ export enum BoltProblemStatus {
}
// Information about each problem stored in the index file.
export interface BoltProblemDescription {
export interface NutProblemDescription {
version: number;
problemId: string;
timestamp: number;
title: string;
description: string;
status?: BoltProblemStatus;
status?: NutProblemStatus;
keywords?: string[];
}
export interface BoltProblem extends BoltProblemDescription {
export interface NutProblem extends NutProblemDescription {
username?: string;
user_id?: string;
repositoryId: string;
comments?: BoltProblemComment[];
solution?: BoltProblemSolution;
comments?: NutProblemComment[];
solution?: NutProblemSolution;
}
export type BoltProblemInput = Omit<BoltProblem, 'problemId' | 'timestamp'>;
export type NutProblemInput = Omit<NutProblem, 'problemId' | 'timestamp'>;
export async function listAllProblems(): Promise<BoltProblemDescription[]> {
let problems: BoltProblemDescription[] = [];
if (shouldUseSupabase()) {
problems = await supabaseListAllProblems();
} else {
try {
const rv = await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'listBoltProblems',
},
});
console.log('ListProblemsRval', rv);
problems = (rv as any).rval.problems.reverse();
const filteredProblems = problems.filter((problem) => {
// if ?showAll=true is not in the url, filter out [test] problems
if (window.location.search.includes('showAll=true')) {
return true;
}
return !problem.title.includes('[test]');
});
return filteredProblems;
} catch (error) {
console.error('Error fetching problems', error);
toast.error('Failed to fetch problems');
return [];
}
}
return problems;
export async function listAllProblems(): Promise<NutProblemDescription[]> {
return supabaseListAllProblems();
}
export async function getProblem(problemId: string): Promise<BoltProblem | null> {
let problem: BoltProblem | null = null;
if (shouldUseSupabase()) {
problem = await supabaseGetProblem(problemId);
} else {
try {
if (!problemId) {
toast.error('Invalid problem ID');
return null;
}
const rv = await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'fetchBoltProblem',
params: { problemId },
},
});
problem = (rv as { rval: { problem: BoltProblem } }).rval.problem;
if (!problem) {
toast.error('Problem not found');
return null;
}
assert(problem.repositoryId, 'Problem probably has outdated data format. Must have a repositoryId.');
} catch (error) {
console.error('Error fetching problem', error);
// Check for specific protocol error
if (error instanceof Error && error.message.includes('Unknown problem ID')) {
toast.error('Problem not found');
} else {
toast.error('Failed to fetch problem');
}
}
}
export async function getProblem(problemId: string): Promise<NutProblem | null> {
const problem = await supabaseGetProblem(problemId);
/*
* Only used for testing
@@ -156,147 +80,26 @@ export async function getProblem(problemId: string): Promise<BoltProblem | null>
return problem;
}
export async function submitProblem(problem: BoltProblemInput): Promise<string | null> {
if (shouldUseSupabase()) {
return supabaseSubmitProblem(problem);
}
try {
const rv = await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'submitBoltProblem',
params: { problem },
},
});
console.log('SubmitProblemRval', rv);
return (rv as any).rval.problemId;
} catch (error) {
console.error('Error submitting problem', error);
toast.error('Failed to submit problem');
return null;
}
export async function submitProblem(problem: NutProblemInput): Promise<string | null> {
return supabaseSubmitProblem(problem);
}
export async function deleteProblem(problemId: string): Promise<void | undefined> {
if (shouldUseSupabase()) {
return supabaseDeleteProblem(problemId);
}
return undefined;
return supabaseDeleteProblem(problemId);
}
const nutLoginKeyCookieName = 'nutLoginKey';
const nutIsAdminCookieName = 'nutIsAdmin';
const nutUsernameCookieName = 'nutUsername';
export async function updateProblem(problemId: string, problem: BoltProblemInput): Promise<BoltProblem | null> {
if (shouldUseSupabase()) {
await supabaseUpdateProblem(problemId, problem);
} else {
try {
if (!getNutIsAdmin()) {
toast.error('Admin user required');
return null;
}
const loginKey = Cookies.get(nutLoginKeyCookieName);
await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'updateBoltProblem',
params: { problemId, problem, loginKey },
},
});
} catch (error) {
console.error('Error updating problem', error);
toast.error('Failed to update problem');
}
}
export async function updateProblem(problemId: string, problem: NutProblemInput): Promise<NutProblem | null> {
await supabaseUpdateProblem(problemId, problem);
const updatedProblem = await getProblem(problemId);
return updatedProblem;
}
export function getNutLoginKey(): string | undefined {
const cookieValue = Cookies.get(nutLoginKeyCookieName);
return cookieValue?.length ? cookieValue : undefined;
}
export async function getNutIsAdmin(): Promise<boolean> {
if (shouldUseSupabase()) {
return getNutIsAdminFromSupabase();
}
return Cookies.get(nutIsAdminCookieName) === 'true';
}
interface UserInfo {
username: string;
loginKey: string;
details: string;
admin: boolean;
}
export async function saveNutLoginKey(key: string) {
const {
rval: { userInfo },
} = (await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'getUserInfo',
params: { loginKey: key },
},
})) as { rval: { userInfo: UserInfo } };
console.log('UserInfo', userInfo);
Cookies.set(nutLoginKeyCookieName, key);
setNutIsAdmin(userInfo.admin);
}
export function setNutIsAdmin(isAdmin: boolean) {
Cookies.set(nutIsAdminCookieName, isAdmin ? 'true' : 'false');
// Update the store
updateIsAdmin(isAdmin);
}
export function getUsername(): string | undefined {
const cookieValue = Cookies.get(nutUsernameCookieName);
return cookieValue?.length ? cookieValue : undefined;
}
export function saveUsername(username: string) {
Cookies.set(nutUsernameCookieName, username);
// Update the store
updateUsername(username);
return getNutIsAdminFromSupabase();
}
export async function submitFeedback(feedback: any): Promise<boolean> {
if (shouldUseSupabase()) {
return supabaseSubmitFeedback(feedback);
}
try {
const rv = await sendCommandDedicatedClient({
method: 'Recording.globalExperimentalCommand',
params: {
name: 'submitFeedback',
params: { feedback },
},
});
console.log('SubmitFeedbackRval', rv);
return true;
} catch (error) {
console.error('Error submitting feedback', error);
toast.error('Failed to submit feedback');
return false;
}
return supabaseSubmitFeedback(feedback);
}

View File

@@ -1,238 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onActionClose 1`] = `
{
"action": {
"content": "npm install",
"type": "shell",
},
"actionId": "0",
"artifactId": "artifact_1",
"messageId": "message_1",
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onActionOpen 1`] = `
{
"action": {
"content": "",
"type": "shell",
},
"actionId": "0",
"artifactId": "artifact_1",
"messageId": "message_1",
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionClose 1`] = `
{
"action": {
"content": "npm install",
"type": "shell",
},
"actionId": "0",
"artifactId": "artifact_1",
"messageId": "message_1",
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionClose 2`] = `
{
"action": {
"content": "some content
",
"filePath": "index.js",
"type": "file",
},
"actionId": "1",
"artifactId": "artifact_1",
"messageId": "message_1",
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionOpen 1`] = `
{
"action": {
"content": "",
"type": "shell",
},
"actionId": "0",
"artifactId": "artifact_1",
"messageId": "message_1",
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onActionOpen 2`] = `
{
"action": {
"content": "",
"filePath": "index.js",
"type": "file",
},
"actionId": "1",
"artifactId": "artifact_1",
"messageId": "message_1",
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts with actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (0) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": "bundled",
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (1) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": "bundled",
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (2) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (2) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (3) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (3) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (4) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (4) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (5) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (5) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (6) > onArtifactClose 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;
exports[`StreamingMessageParser > valid artifacts without actions > should correctly parse chunks and strip out bolt artifacts (6) > onArtifactOpen 1`] = `
{
"id": "artifact_1",
"messageId": "message_1",
"title": "Some title",
"type": undefined,
}
`;

View File

@@ -1,211 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import { StreamingMessageParser, type ActionCallback, type ArtifactCallback } from './message-parser';
interface ExpectedResult {
output: string;
callbacks?: {
onArtifactOpen?: number;
onArtifactClose?: number;
onActionOpen?: number;
onActionClose?: number;
};
}
describe('StreamingMessageParser', () => {
it('should pass through normal text', () => {
const parser = new StreamingMessageParser();
expect(parser.parse('test_id', 'Hello, world!')).toBe('Hello, world!');
});
it('should allow normal HTML tags', () => {
const parser = new StreamingMessageParser();
expect(parser.parse('test_id', 'Hello <strong>world</strong>!')).toBe('Hello <strong>world</strong>!');
});
describe('no artifacts', () => {
it.each<[string | string[], ExpectedResult | string]>([
['Foo bar', 'Foo bar'],
['Foo bar <', 'Foo bar '],
['Foo bar <p', 'Foo bar <p'],
[['Foo bar <', 's', 'p', 'an>some text</span>'], 'Foo bar <span>some text</span>'],
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
runTest(input, expected);
});
});
describe('invalid or incomplete artifacts', () => {
it.each<[string | string[], ExpectedResult | string]>([
['Foo bar <b', 'Foo bar '],
['Foo bar <ba', 'Foo bar <ba'],
['Foo bar <bol', 'Foo bar '],
['Foo bar <bolt', 'Foo bar '],
['Foo bar <bolta', 'Foo bar <bolta'],
['Foo bar <boltA', 'Foo bar '],
['Foo bar <boltArtifacs></boltArtifact>', 'Foo bar <boltArtifacs></boltArtifact>'],
['Before <oltArtfiact>foo</boltArtifact> After', 'Before <oltArtfiact>foo</boltArtifact> After'],
['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'],
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
runTest(input, expected);
});
});
describe('valid artifacts without actions', () => {
it.each<[string | string[], ExpectedResult | string]>([
[
'Some text before <boltArtifact title="Some title" id="artifact_1">foo bar</boltArtifact> Some more text',
{
output: 'Some text before Some more text',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
[
[
'Some text before <boltArti',
'fact',
' title="Some title" id="artifact_1" type="bundled" >foo</boltArtifact> Some more text',
],
{
output: 'Some text before Some more text',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
[
[
'Some text before <boltArti',
'fac',
't title="Some title" id="artifact_1"',
' ',
'>',
'foo</boltArtifact> Some more text',
],
{
output: 'Some text before Some more text',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
[
[
'Some text before <boltArti',
'fact',
' title="Some title" id="artifact_1"',
' >fo',
'o</boltArtifact> Some more text',
],
{
output: 'Some text before Some more text',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
[
[
'Some text before <boltArti',
'fact tit',
'le="Some ',
'title" id="artifact_1">fo',
'o',
'<',
'/boltArtifact> Some more text',
],
{
output: 'Some text before Some more text',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
[
[
'Some text before <boltArti',
'fact title="Some title" id="artif',
'act_1">fo',
'o<',
'/boltArtifact> Some more text',
],
{
output: 'Some text before Some more text',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
[
'Before <boltArtifact title="Some title" id="artifact_1">foo</boltArtifact> After',
{
output: 'Before After',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
},
],
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
runTest(input, expected);
});
});
describe('valid artifacts with actions', () => {
it.each<[string | string[], ExpectedResult | string]>([
[
'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction></boltArtifact> After',
{
output: 'Before After',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 1, onActionClose: 1 },
},
],
[
'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction><boltAction type="file" filePath="index.js">some content</boltAction></boltArtifact> After',
{
output: 'Before After',
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 2, onActionClose: 2 },
},
],
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
runTest(input, expected);
});
});
});
function runTest(input: string | string[], outputOrExpectedResult: string | ExpectedResult) {
let expected: ExpectedResult;
if (typeof outputOrExpectedResult === 'string') {
expected = { output: outputOrExpectedResult };
} else {
expected = outputOrExpectedResult;
}
const callbacks = {
onArtifactOpen: vi.fn<ArtifactCallback>((data) => {
expect(data).toMatchSnapshot('onArtifactOpen');
}),
onArtifactClose: vi.fn<ArtifactCallback>((data) => {
expect(data).toMatchSnapshot('onArtifactClose');
}),
onActionOpen: vi.fn<ActionCallback>((data) => {
expect(data).toMatchSnapshot('onActionOpen');
}),
onActionClose: vi.fn<ActionCallback>((data) => {
expect(data).toMatchSnapshot('onActionClose');
}),
};
const parser = new StreamingMessageParser({
artifactElement: () => '',
callbacks,
});
let message = '';
let result = '';
const chunks = Array.isArray(input) ? input : input.split('');
for (const chunk of chunks) {
message += chunk;
result += parser.parse('message_1', message);
}
for (const name in expected.callbacks) {
const callbackName = name;
expect(callbacks[callbackName as keyof typeof callbacks]).toHaveBeenCalledTimes(
expected.callbacks[callbackName as keyof typeof expected.callbacks] ?? 0,
);
}
expect(result).toEqual(expected.output);
}

View File

@@ -1,326 +0,0 @@
import type { ActionType, BoltAction, BoltActionData, FileAction, ShellAction } from '~/types/actions';
import type { BoltArtifactData } from '~/types/artifact';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
const ARTIFACT_TAG_OPEN = '<boltArtifact';
const ARTIFACT_TAG_CLOSE = '</boltArtifact>';
const ARTIFACT_ACTION_TAG_OPEN = '<boltAction';
const ARTIFACT_ACTION_TAG_CLOSE = '</boltAction>';
const logger = createScopedLogger('MessageParser');
export interface ArtifactCallbackData extends BoltArtifactData {
messageId: string;
}
export interface ActionCallbackData {
artifactId: string;
messageId: string;
actionId: string;
action: BoltAction;
}
export type ArtifactCallback = (data: ArtifactCallbackData) => void;
export type ActionCallback = (data: ActionCallbackData) => void;
export interface ParserCallbacks {
onArtifactOpen?: ArtifactCallback;
onArtifactClose?: ArtifactCallback;
onActionOpen?: ActionCallback;
onActionStream?: ActionCallback;
onActionClose?: ActionCallback;
}
interface ElementFactoryProps {
messageId: string;
}
type ElementFactory = (props: ElementFactoryProps) => string;
export interface StreamingMessageParserOptions {
callbacks?: ParserCallbacks;
artifactElement?: ElementFactory;
}
interface MessageState {
position: number;
insideArtifact: boolean;
insideAction: boolean;
currentArtifact?: BoltArtifactData;
currentAction: BoltActionData;
actionId: number;
}
function cleanoutMarkdownSyntax(content: string) {
const codeBlockRegex = /^\s*```\w*\n([\s\S]*?)\n\s*```\s*$/;
const match = content.match(codeBlockRegex);
// console.log('matching', !!match, content);
if (match) {
return match[1]; // Remove common leading 4-space indent
} else {
return content;
}
}
export class StreamingMessageParser {
#messages = new Map<string, MessageState>();
constructor(private _options: StreamingMessageParserOptions = {}) {}
parse(messageId: string, input: string) {
let state = this.#messages.get(messageId);
if (!state) {
state = {
position: 0,
insideAction: false,
insideArtifact: false,
currentAction: { content: '' },
actionId: 0,
};
this.#messages.set(messageId, state);
}
let output = '';
let i = state.position;
let earlyBreak = false;
while (i < input.length) {
if (state.insideArtifact) {
const currentArtifact = state.currentArtifact;
if (currentArtifact === undefined) {
unreachable('Artifact not initialized');
}
if (state.insideAction) {
const closeIndex = input.indexOf(ARTIFACT_ACTION_TAG_CLOSE, i);
const currentAction = state.currentAction;
if (closeIndex !== -1) {
currentAction.content += input.slice(i, closeIndex);
let content = currentAction.content.trim();
if ('type' in currentAction && currentAction.type === 'file') {
// Remove markdown code block syntax if present and file is not markdown
if (!currentAction.filePath.endsWith('.md')) {
content = cleanoutMarkdownSyntax(content);
}
content += '\n';
}
currentAction.content = content;
this._options.callbacks?.onActionClose?.({
artifactId: currentArtifact.id,
messageId,
/**
* We decrement the id because it's been incremented already
* when `onActionOpen` was emitted to make sure the ids are
* the same.
*/
actionId: String(state.actionId - 1),
action: currentAction as BoltAction,
});
state.insideAction = false;
state.currentAction = { content: '' };
i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
} else {
if ('type' in currentAction && currentAction.type === 'file') {
let content = input.slice(i);
if (!currentAction.filePath.endsWith('.md')) {
content = cleanoutMarkdownSyntax(content);
}
this._options.callbacks?.onActionStream?.({
artifactId: currentArtifact.id,
messageId,
actionId: String(state.actionId - 1),
action: {
...(currentAction as FileAction),
content,
filePath: currentAction.filePath,
},
});
}
break;
}
} else {
const actionOpenIndex = input.indexOf(ARTIFACT_ACTION_TAG_OPEN, i);
const artifactCloseIndex = input.indexOf(ARTIFACT_TAG_CLOSE, i);
if (actionOpenIndex !== -1 && (artifactCloseIndex === -1 || actionOpenIndex < artifactCloseIndex)) {
const actionEndIndex = input.indexOf('>', actionOpenIndex);
if (actionEndIndex !== -1) {
state.insideAction = true;
state.currentAction = this.#parseActionTag(input, actionOpenIndex, actionEndIndex);
this._options.callbacks?.onActionOpen?.({
artifactId: currentArtifact.id,
messageId,
actionId: String(state.actionId++),
action: state.currentAction as BoltAction,
});
i = actionEndIndex + 1;
} else {
break;
}
} else if (artifactCloseIndex !== -1) {
this._options.callbacks?.onArtifactClose?.({ messageId, ...currentArtifact });
state.insideArtifact = false;
state.currentArtifact = undefined;
i = artifactCloseIndex + ARTIFACT_TAG_CLOSE.length;
} else {
break;
}
}
} else if (input[i] === '<' && input[i + 1] !== '/') {
let j = i;
let potentialTag = '';
while (j < input.length && potentialTag.length < ARTIFACT_TAG_OPEN.length) {
potentialTag += input[j];
if (potentialTag === ARTIFACT_TAG_OPEN) {
const nextChar = input[j + 1];
if (nextChar && nextChar !== '>' && nextChar !== ' ') {
output += input.slice(i, j + 1);
i = j + 1;
break;
}
const openTagEnd = input.indexOf('>', j);
if (openTagEnd !== -1) {
const artifactTag = input.slice(i, openTagEnd + 1);
const artifactTitle = this.#extractAttribute(artifactTag, 'title') as string;
const type = this.#extractAttribute(artifactTag, 'type') as string;
const artifactId = this.#extractAttribute(artifactTag, 'id') as string;
if (!artifactTitle) {
logger.warn('Artifact title missing');
}
if (!artifactId) {
logger.warn('Artifact id missing');
}
state.insideArtifact = true;
const currentArtifact = {
id: artifactId,
title: artifactTitle,
type,
} satisfies BoltArtifactData;
state.currentArtifact = currentArtifact;
this._options.callbacks?.onArtifactOpen?.({ messageId, ...currentArtifact });
/*
* const artifactFactory = this._options.artifactElement ?? createArtifactElement;
* output += artifactFactory({ messageId });
*/
i = openTagEnd + 1;
} else {
earlyBreak = true;
}
break;
} else if (!ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {
output += input.slice(i, j + 1);
i = j + 1;
break;
}
j++;
}
if (j === input.length && ARTIFACT_TAG_OPEN.startsWith(potentialTag)) {
break;
}
} else {
output += input[i];
i++;
}
if (earlyBreak) {
break;
}
}
state.position = i;
return output;
}
reset() {
this.#messages.clear();
}
#parseActionTag(input: string, actionOpenIndex: number, actionEndIndex: number) {
const actionTag = input.slice(actionOpenIndex, actionEndIndex + 1);
const actionType = this.#extractAttribute(actionTag, 'type') as ActionType;
const actionAttributes = {
type: actionType,
content: '',
};
if (actionType === 'file') {
const filePath = this.#extractAttribute(actionTag, 'filePath') as string;
if (!filePath) {
logger.debug('File path not specified');
}
(actionAttributes as FileAction).filePath = filePath;
} else if (!['shell', 'start'].includes(actionType)) {
logger.warn(`Unknown action type '${actionType}'`);
}
return actionAttributes as FileAction | ShellAction;
}
#extractAttribute(tag: string, attributeName: string): string | undefined {
const match = tag.match(new RegExp(`${attributeName}="([^"]*)"`, 'i'));
return match ? match[1] : undefined;
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const createArtifactElement: ElementFactory = (props) => {
const elementProps = [
'class="__boltArtifact__"',
...Object.entries(props).map(([key, value]) => {
return `data-${camelToDashCase(key)}=${JSON.stringify(value)}`;
}),
];
return `<div ${elementProps.join(' ')}></div>`;
};
function camelToDashCase(input: string) {
return input.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}

View File

@@ -3,59 +3,32 @@ import { getSupabase } from '~/lib/supabase/client';
import type { User, Session } from '@supabase/supabase-js';
import { logStore } from './logs';
import { useEffect, useState } from 'react';
import { shouldUseSupabase, isAuthenticated } from '~/lib/supabase/client';
import { getUsername, saveUsername } from '~/lib/replay/Problems';
import { isAuthenticated } from '~/lib/supabase/client';
export const userStore = atom<User | null>(null);
export const sessionStore = atom<Session | null>(null);
export const isLoadingStore = atom<boolean>(true);
// Auth status store for both Supabase and non-Supabase modes
export const authStatusStore = {
isLoggedIn: atom<boolean | null>(null),
username: atom<string>(''),
// Initialize auth status store
async init() {
if (shouldUseSupabase()) {
// For Supabase, subscribe to the userStore
userStore.listen((user) => {
this.isLoggedIn.set(!!user);
});
// subscribe to the userStore
userStore.listen((user) => {
this.isLoggedIn.set(!!user);
});
// Check initial auth state
const authenticated = await isAuthenticated();
this.isLoggedIn.set(authenticated);
} else {
// For non-Supabase, always logged in
this.isLoggedIn.set(true);
// Get username from storage
const storedUsername = getUsername();
if (storedUsername) {
this.username.set(storedUsername);
}
}
},
// Update username (only meaningful in non-Supabase mode)
updateUsername(newUsername: string) {
this.username.set(newUsername);
if (!shouldUseSupabase()) {
saveUsername(newUsername);
}
// Check initial auth state
const authenticated = await isAuthenticated();
this.isLoggedIn.set(authenticated);
},
};
// Initialize auth status store
if (typeof window !== 'undefined') {
authStatusStore.init();
}
export async function initializeAuth() {
try {
authStatusStore.init();
isLoadingStore.set(true);
// Get initial session
@@ -191,24 +164,16 @@ export async function signOut() {
}
}
// Keep the hook for backwards compatibility, but implement it using the store
export function useAuthStatus() {
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(authStatusStore.isLoggedIn.get());
const [username, setUsername] = useState<string>(authStatusStore.username.get());
useEffect(() => {
const unsubscribeIsLoggedIn = authStatusStore.isLoggedIn.listen(setIsLoggedIn);
const unsubscribeUsername = authStatusStore.username.listen(setUsername);
return () => {
unsubscribeIsLoggedIn();
unsubscribeUsername();
};
}, []);
const updateUsername = (newUsername: string) => {
authStatusStore.updateUsername(newUsername);
};
return { isLoggedIn, username, updateUsername };
return { isLoggedIn };
}

View File

@@ -1,49 +0,0 @@
import { atom, map } from 'nanostores';
import { PROVIDER_LIST } from '~/utils/constants';
import type { IProviderConfig } from '~/types/model';
export interface Shortcut {
key: string;
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
ctrlOrMetaKey?: boolean;
action: () => void;
}
export interface Shortcuts {
toggleTerminal: Shortcut;
}
export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama'];
export type ProviderSetting = Record<string, IProviderConfig>;
const initialProviderSettings: ProviderSetting = {};
PROVIDER_LIST.forEach((provider) => {
initialProviderSettings[provider.name] = {
...provider,
settings: {
enabled: true,
},
};
});
//TODO: need to create one single map for all these flags
export const providersStore = map<ProviderSetting>(initialProviderSettings);
export const isDebugMode = atom(false);
export const isEventLogsEnabled = atom(false);
export const isLocalModelsEnabled = atom(true);
export const promptStore = atom<string>('default');
export const latestBranchStore = atom(false);
export const autoSelectStarterTemplate = atom(false);
export const enableContextOptimizationStore = atom(false);

View File

@@ -1,5 +1,5 @@
import { atom } from 'nanostores';
import { getNutIsAdmin, getUsername } from '~/lib/replay/Problems';
import { getNutIsAdmin } from '~/lib/replay/Problems';
import { userStore } from './auth';
import { useStore } from '@nanostores/react';
import { useEffect } from 'react';
@@ -7,22 +7,6 @@ import { useEffect } from 'react';
// Store for admin status
export const isAdminStore = atom<boolean>(false);
// Store for username
export const usernameStore = atom<string | undefined>(undefined);
// Safe store updaters that check for browser environment
export function updateIsAdmin(value: boolean) {
if (typeof window !== 'undefined') {
isAdminStore.set(value);
}
}
export function updateUsername(username: string | undefined) {
if (typeof window !== 'undefined') {
usernameStore.set(username);
}
}
export function useAdminStatus() {
const isAdmin = useStore(isAdminStore);
@@ -49,9 +33,6 @@ export async function initializeUserStores() {
const isAdmin = await getNutIsAdmin();
isAdminStore.set(isAdmin);
const username = getUsername();
usernameStore.set(username);
// Subscribe to user changes to update admin status
return userStore.subscribe(async (user) => {
if (user) {

View File

@@ -64,25 +64,6 @@ let supabaseAnonKey = '';
// Add a singleton client instance
let supabaseClientInstance: ReturnType<typeof createClient<Database>> | null = null;
/**
* Determines whether Supabase should be used based on URL parameters and environment variables.
* URL parameters take precedence over environment variables.
*/
export function shouldUseSupabase(): boolean {
// Check URL parameters (client-side only)
const urlParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null;
const useSupabaseFromUrl = urlParams ? urlParams.get('supabase') === 'true' : false;
// Check environment variables
const useSupabaseFromEnv =
typeof window === 'object' ? window.ENV?.USE_SUPABASE === 'true' : process.env.USE_SUPABASE === 'true';
// URL param takes precedence over environment variable
const shouldUse = useSupabaseFromUrl || useSupabaseFromEnv;
return shouldUse;
}
export async function getCurrentUser(): Promise<SupabaseUser | null> {
try {
const {
@@ -141,11 +122,6 @@ export function getSupabase() {
supabaseAnonKey = process.env.SUPABASE_ANON_KEY || '';
}
// If neither URL param nor environment variable is set to true, log a warning
if (!shouldUseSupabase()) {
console.log('Supabase is not enabled. Set USE_SUPABASE=true or use ?supabase=true query parameter.');
}
// Log warning if environment variables are missing
if (!supabaseUrl || !supabaseAnonKey) {
console.warn('Missing Supabase environment variables. Some features may not work properly.');

View File

@@ -2,8 +2,8 @@
import { toast } from 'react-toastify';
import { getSupabase, type Database } from './client';
import type { BoltProblem, BoltProblemDescription, BoltProblemInput, BoltProblemStatus } from '~/lib/replay/Problems';
import { getUsername, getNutIsAdmin } from '~/lib/replay/Problems';
import type { NutProblem, NutProblemDescription, NutProblemInput, NutProblemStatus } from '~/lib/replay/Problems';
import { getNutIsAdmin } from '~/lib/replay/Problems';
async function downloadBlob(bucket: string, path: string) {
const supabase = getSupabase();
@@ -17,7 +17,7 @@ async function downloadBlob(bucket: string, path: string) {
return data.text();
}
export async function supabaseListAllProblems(): Promise<BoltProblemDescription[]> {
export async function supabaseListAllProblems(): Promise<NutProblemDescription[]> {
try {
const { data, error } = await getSupabase()
.from('problems')
@@ -28,7 +28,7 @@ export async function supabaseListAllProblems(): Promise<BoltProblemDescription[
throw error;
}
const problems: BoltProblemDescription[] = data.map((problem) => ({
const problems: NutProblemDescription[] = data.map((problem) => ({
version: 1,
problemId: problem.id,
timestamp: new Date(problem.created_at).getTime(),
@@ -47,7 +47,7 @@ export async function supabaseListAllProblems(): Promise<BoltProblemDescription[
}
}
export async function supabaseGetProblem(problemId: string): Promise<BoltProblem | null> {
export async function supabaseGetProblem(problemId: string): Promise<NutProblem | null> {
try {
if (!problemId) {
toast.error('Invalid problem ID');
@@ -115,7 +115,7 @@ export async function supabaseGetProblem(problemId: string): Promise<BoltProblem
timestamp: new Date(data.created_at).getTime(),
title: data.title,
description: data.description,
status: data.status as BoltProblemStatus,
status: data.status as NutProblemStatus,
keywords: data.keywords,
repositoryId: data.repository_id,
username,
@@ -140,13 +140,13 @@ export async function supabaseGetProblem(problemId: string): Promise<BoltProblem
return null;
}
export async function supabaseSubmitProblem(problem: BoltProblemInput): Promise<string | null> {
export async function supabaseSubmitProblem(problem: NutProblemInput): Promise<string | null> {
try {
const supabaseProblem = {
id: undefined as any, // This will be set by Supabase
title: problem.title,
description: problem.description,
status: problem.status as BoltProblemStatus,
status: problem.status as NutProblemStatus,
keywords: problem.keywords || [],
repository_id: problem.repositoryId,
user_id: problem.user_id,
@@ -183,7 +183,7 @@ export async function supabaseDeleteProblem(problemId: string): Promise<void | u
}
}
export async function supabaseUpdateProblem(problemId: string, problem: BoltProblemInput): Promise<void> {
export async function supabaseUpdateProblem(problemId: string, problem: NutProblemInput): Promise<void> {
try {
if (!getNutIsAdmin()) {
toast.error('Admin user required');
@@ -225,7 +225,7 @@ export async function supabaseUpdateProblem(problemId: string, problem: BoltProb
return {
problem_id: problemId,
content: comment.content,
username: comment.username || getUsername() || 'Anonymous',
username: comment.username || 'Anonymous',
};
});

View File

@@ -25,7 +25,6 @@ interface LoaderData {
ENV: {
SUPABASE_URL: string;
SUPABASE_ANON_KEY: string;
USE_SUPABASE?: string;
};
}
@@ -57,15 +56,11 @@ export const links: LinksFunction = () => [
export const loader: LoaderFunction = async () => {
const supabaseUrl = process.env.SUPABASE_URL as string;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY as string;
const useSupabase = process.env.USE_SUPABASE as string;
console.log('useSupabase', useSupabase);
return json<LoaderData>({
ENV: {
SUPABASE_URL: supabaseUrl,
SUPABASE_ANON_KEY: supabaseAnonKey,
USE_SUPABASE: useSupabase,
},
});
};

View File

@@ -1,18 +0,0 @@
import type { LoaderFunction } from '~/lib/remix-types';
import { providerBaseUrlEnvKeys } from '~/utils/constants';
export const loader: LoaderFunction = async ({ context: _, request }) => {
const url = new URL(request.url);
const provider = url.searchParams.get('provider');
if (!provider || !providerBaseUrlEnvKeys[provider].apiTokenKey) {
return Response.json({ isSet: false });
}
const envVarName = providerBaseUrlEnvKeys[provider].apiTokenKey;
// Use only process.env since context.env might be undefined
const isSet = !!process.env[envVarName];
return Response.json({ isSet });
};

View File

@@ -1,6 +0,0 @@
import { json } from '~/lib/remix-types';
import { MODEL_LIST } from '~/utils/constants';
export async function loader() {
return json(MODEL_LIST);
}

View File

@@ -7,17 +7,16 @@ import { ToastContainerWrapper, Status, Keywords } from './problems';
import { toast } from 'react-toastify';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { useParams } from '@remix-run/react';
import { useStore } from '@nanostores/react';
import {
getProblem,
updateProblem as backendUpdateProblem,
deleteProblem as backendDeleteProblem,
BoltProblemStatus,
NutProblemStatus,
} from '~/lib/replay/Problems';
import { useAdminStatus, usernameStore } from '~/lib/stores/user';
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
import { useAdminStatus } from '~/lib/stores/user';
import type { NutProblem, NutProblemComment } from '~/lib/replay/Problems';
function Comments({ comments }: { comments: BoltProblemComment[] }) {
function Comments({ comments }: { comments: NutProblemComment[] }) {
return (
<div className="space-y-4 mt-6">
{comments.map((comment, index) => (
@@ -42,8 +41,8 @@ function Comments({ comments }: { comments: BoltProblemComment[] }) {
);
}
function ProblemViewer({ problem }: { problem: BoltProblem }) {
const { problemId, title, description, status = BoltProblemStatus.Pending, keywords = [], comments = [] } = problem;
function ProblemViewer({ problem }: { problem: NutProblem }) {
const { problemId, title, description, status = NutProblemStatus.Pending, keywords = [], comments = [] } = problem;
return (
<div className="benchmark">
@@ -122,7 +121,7 @@ function UpdateProblemForm(props: UpdateProblemFormProps) {
);
}
type DoUpdateCallback = (problem: BoltProblem) => BoltProblem;
type DoUpdateCallback = (problem: NutProblem) => NutProblem;
type UpdateProblemCallback = (doUpdate: DoUpdateCallback) => void;
type DeleteProblemCallback = () => void;
@@ -133,12 +132,10 @@ function UpdateProblemForms({
updateProblem: UpdateProblemCallback;
deleteProblem: DeleteProblemCallback;
}) {
const username = useStore(usernameStore);
const handleAddComment = (content: string) => {
const newComment: BoltProblemComment = {
const newComment: NutProblemComment = {
timestamp: Date.now(),
username,
username: 'Anonymous',
content,
};
updateProblem((problem) => {
@@ -165,7 +162,7 @@ function UpdateProblemForms({
};
const handleSetStatus = (status: string) => {
const statusEnum = BoltProblemStatus[status as keyof typeof BoltProblemStatus];
const statusEnum = NutProblemStatus[status as keyof typeof NutProblemStatus];
if (!statusEnum) {
toast.error('Invalid status');
@@ -189,8 +186,8 @@ function UpdateProblemForms({
}));
};
// Convert BoltProblemStatus enum to options array for select
const statusOptions = Object.entries(BoltProblemStatus).map(([key, _value]) => ({
// Convert NutProblemStatus enum to options array for select
const statusOptions = Object.entries(NutProblemStatus).map(([key, _value]) => ({
value: key,
label: key,
}));
@@ -244,7 +241,7 @@ function ViewProblemPage() {
throw new Error('Problem ID is required');
}
const [problemData, setProblemData] = useState<BoltProblem | null>(null);
const [problemData, setProblemData] = useState<NutProblem | null>(null);
const updateProblem = useCallback(
async (callback: DoUpdateCallback) => {

View File

@@ -5,8 +5,8 @@ import BackgroundRays from '~/components/ui/BackgroundRays';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { cssTransition, ToastContainer } from 'react-toastify';
import { Suspense, useEffect, useState } from 'react';
import { listAllProblems, BoltProblemStatus } from '~/lib/replay/Problems';
import type { BoltProblemDescription } from '~/lib/replay/Problems';
import { listAllProblems, NutProblemStatus } from '~/lib/replay/Problems';
import type { NutProblemDescription } from '~/lib/replay/Problems';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -45,15 +45,15 @@ export function ToastContainerWrapper() {
);
}
export function Status({ status }: { status: BoltProblemStatus | undefined }) {
export function Status({ status }: { status: NutProblemStatus | undefined }) {
if (!status) {
status = BoltProblemStatus.Pending;
status = NutProblemStatus.Pending;
}
const statusColors: Record<BoltProblemStatus, string> = {
[BoltProblemStatus.Pending]: 'bg-yellow-400 dark:text-yellow-400',
[BoltProblemStatus.Unsolved]: 'bg-orange-500 dark:text-orange-500',
[BoltProblemStatus.Solved]: 'bg-blue-500 dark:text-blue-500',
const statusColors: Record<NutProblemStatus, string> = {
[NutProblemStatus.Pending]: 'bg-yellow-400 dark:text-yellow-400',
[NutProblemStatus.Unsolved]: 'bg-orange-500 dark:text-orange-500',
[NutProblemStatus.Solved]: 'bg-blue-500 dark:text-blue-500',
};
return (
@@ -88,15 +88,15 @@ export function Keywords({ keywords }: { keywords: string[] | undefined }) {
);
}
function getProblemStatus(problem: BoltProblemDescription): BoltProblemStatus {
return problem.status ?? BoltProblemStatus.Pending;
function getProblemStatus(problem: NutProblemDescription): NutProblemStatus {
return problem.status ?? NutProblemStatus.Pending;
}
const Nothing = () => null;
function ProblemsPage() {
const [problems, setProblems] = useState<BoltProblemDescription[] | null>(null);
const [statusFilter, setStatusFilter] = useState<BoltProblemStatus | 'all'>(BoltProblemStatus.Solved);
const [problems, setProblems] = useState<NutProblemDescription[] | null>(null);
const [statusFilter, setStatusFilter] = useState<NutProblemStatus | 'all'>(NutProblemStatus.Solved);
useEffect(() => {
listAllProblems().then(setProblems);
@@ -119,7 +119,7 @@ function ProblemsPage() {
<div className="mb-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as BoltProblemStatus | 'all')}
onChange={(e) => setStatusFilter(e.target.value as NutProblemStatus | 'all')}
className="appearance-none w-48 px-4 py-2.5 rounded-lg bg-bolt-elements-background-depth-2 border border-bolt-elements-border text-bolt-content-primary hover:border-bolt-elements-border-hover focus:outline-none focus:ring-2 focus:ring-bolt-accent-primary/20 focus:border-bolt-accent-primary cursor-pointer relative pr-10"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
@@ -129,7 +129,7 @@ function ProblemsPage() {
}}
>
<option value="all">{`All Problems (${problems?.length ?? 0})`}</option>
{Object.values(BoltProblemStatus).map((status) => {
{Object.values(NutProblemStatus).map((status) => {
const count = problems?.filter((problem) => getProblemStatus(problem) === status).length ?? 0;
return (
<option key={status} value={status}>

View File

@@ -1,30 +0,0 @@
export type ActionType = 'file' | 'shell';
export interface BaseAction {
content: string;
}
export interface FileAction extends BaseAction {
type: 'file';
filePath: string;
}
export interface ShellAction extends BaseAction {
type: 'shell';
}
export interface StartAction extends BaseAction {
type: 'start';
}
export type BoltAction = FileAction | ShellAction | StartAction;
export type BoltActionData = BoltAction | BaseAction;
export interface ActionAlert {
type: string;
title: string;
description: string;
content: string;
source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors
}

View File

@@ -1,5 +0,0 @@
export interface BoltArtifactData {
id: string;
title: string;
type?: string | undefined;
}

View File

@@ -1,24 +0,0 @@
import type { ModelInfo } from '~/lib/modules/llm/types';
export type ProviderInfo = {
staticModels: ModelInfo[];
name: string;
getDynamicModels?: (
providerName: string,
apiKeys?: Record<string, string>,
providerSettings?: IProviderSetting,
serverEnv?: Record<string, string>,
) => Promise<ModelInfo[]>;
getApiKeyLink?: string;
labelForGetApiKey?: string;
icon?: string;
};
export interface IProviderSetting {
enabled?: boolean;
baseUrl?: string;
}
export type IProviderConfig = ProviderInfo & {
settings: IProviderSetting;
};

View File

@@ -1,8 +0,0 @@
export interface Template {
name: string;
label: string;
description: string;
githubRepo: string;
tags?: string[];
icon?: string;
}

View File

@@ -1,148 +0,0 @@
import type { IProviderSetting } from '~/types/model';
import { LLMManager } from '~/lib/modules/llm/manager';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { Template } from '~/types/template';
export const WORK_DIR_NAME = 'project';
export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/;
export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
export const PROMPT_COOKIE_KEY = 'cachedPrompt';
const llmManager = LLMManager.getInstance(import.meta.env);
export const PROVIDER_LIST = llmManager.getAllProviders();
export const DEFAULT_PROVIDER = llmManager.getDefaultProvider();
let MODEL_LIST = llmManager.getModelList();
const providerBaseUrlEnvKeys: Record<string, { baseUrlKey?: string; apiTokenKey?: string }> = {};
PROVIDER_LIST.forEach((provider) => {
providerBaseUrlEnvKeys[provider.name] = {
baseUrlKey: provider.config.baseUrlKey,
apiTokenKey: provider.config.apiTokenKey,
};
});
// Export the getModelList function using the manager
export async function getModelList(options: {
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
serverEnv?: Record<string, string>;
}) {
return await llmManager.updateModelList(options);
}
async function initializeModelList(options: {
env?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
apiKeys?: Record<string, string>;
}): Promise<ModelInfo[]> {
const { providerSettings, apiKeys, env } = options;
const list = await getModelList({
apiKeys,
providerSettings,
serverEnv: env,
});
MODEL_LIST = list || MODEL_LIST;
return list;
}
// initializeModelList({})
export { initializeModelList, providerBaseUrlEnvKeys, MODEL_LIST };
// starter Templates
export const STARTER_TEMPLATES: Template[] = [
{
name: 'bolt-astro-basic',
label: 'Astro Basic',
description: 'Lightweight Astro starter template for building fast static websites',
githubRepo: 'thecodacus/bolt-astro-basic-template',
tags: ['astro', 'blog', 'performance'],
icon: 'i-bolt:astro',
},
{
name: 'bolt-nextjs-shadcn',
label: 'Next.js with shadcn/ui',
description: 'Next.js starter fullstack template integrated with shadcn/ui components and styling system',
githubRepo: 'thecodacus/bolt-nextjs-shadcn-template',
tags: ['nextjs', 'react', 'typescript', 'shadcn', 'tailwind'],
icon: 'i-bolt:nextjs',
},
{
name: 'bolt-qwik-ts',
label: 'Qwik TypeScript',
description: 'Qwik framework starter with TypeScript for building resumable applications',
githubRepo: 'thecodacus/bolt-qwik-ts-template',
tags: ['qwik', 'typescript', 'performance', 'resumable'],
icon: 'i-bolt:qwik',
},
{
name: 'bolt-remix-ts',
label: 'Remix TypeScript',
description: 'Remix framework starter with TypeScript for full-stack web applications',
githubRepo: 'thecodacus/bolt-remix-ts-template',
tags: ['remix', 'typescript', 'fullstack', 'react'],
icon: 'i-bolt:remix',
},
{
name: 'bolt-slidev',
label: 'Slidev Presentation',
description: 'Slidev starter template for creating developer-friendly presentations using Markdown',
githubRepo: 'thecodacus/bolt-slidev-template',
tags: ['slidev', 'presentation', 'markdown'],
icon: 'i-bolt:slidev',
},
{
name: 'bolt-sveltekit',
label: 'SvelteKit',
description: 'SvelteKit starter template for building fast, efficient web applications',
githubRepo: 'bolt-sveltekit-template',
tags: ['svelte', 'sveltekit', 'typescript'],
icon: 'i-bolt:svelte',
},
{
name: 'vanilla-vite',
label: 'Vanilla + Vite',
description: 'Minimal Vite starter template for vanilla JavaScript projects',
githubRepo: 'thecodacus/vanilla-vite-template',
tags: ['vite', 'vanilla-js', 'minimal'],
icon: 'i-bolt:vite',
},
{
name: 'bolt-vite-react',
label: 'React + Vite + typescript',
description: 'React starter template powered by Vite for fast development experience',
githubRepo: 'thecodacus/bolt-vite-react-ts-template',
tags: ['react', 'vite', 'frontend'],
icon: 'i-bolt:react',
},
{
name: 'bolt-vite-ts',
label: 'Vite + TypeScript',
description: 'Vite starter template with TypeScript configuration for type-safe development',
githubRepo: 'thecodacus/bolt-vite-ts-template',
tags: ['vite', 'typescript', 'minimal'],
icon: 'i-bolt:typescript',
},
{
name: 'bolt-vue',
label: 'Vue.js',
description: 'Vue.js starter template with modern tooling and best practices',
githubRepo: 'thecodacus/bolt-vue-template',
tags: ['vue', 'typescript', 'frontend'],
icon: 'i-bolt:vue',
},
{
name: 'bolt-angular',
label: 'Angular Starter',
description: 'A modern Angular starter template with TypeScript support and best practices configuration',
githubRepo: 'thecodacus/bolt-angular-template',
tags: ['angular', 'typescript', 'frontend', 'spa'],
icon: 'i-bolt:angular',
},
];

View File

@@ -1,3 +1,2 @@
export const anthropicApiKeyCookieName = 'anthropicApiKey';
export const anthropicNumFreeUsesCookieName = 'anthropicNumFreeUses';
export const maxFreeUses = 5;

View File

@@ -61,7 +61,7 @@ const rehypeSanitizeOptions: RehypeSanitizeOptions = {
tagNames: allowedHTMLElements,
attributes: {
...defaultSchema.attributes,
div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className', '__boltArtifact__']],
div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className']],
},
strip: [],
};

View File

@@ -1,304 +0,0 @@
import ignore from 'ignore';
import type { ProviderInfo } from '~/types/model';
import type { Template } from '~/types/template';
import { STARTER_TEMPLATES } from './constants';
import Cookies from 'js-cookie';
const starterTemplateSelectionPrompt = (templates: Template[]) => `
You are an experienced developer who helps people choose the best starter template for their projects.
Available templates:
<template>
<name>blank</name>
<description>Empty starter for simple scripts and trivial tasks that don't require a full template setup</description>
<tags>basic, script</tags>
</template>
${templates
.map(
(template) => `
<template>
<name>${template.name}</name>
<description>${template.description}</description>
${template.tags ? `<tags>${template.tags.join(', ')}</tags>` : ''}
</template>
`,
)
.join('\n')}
Response Format:
<selection>
<templateName>{selected template name}</templateName>
<title>{a proper title for the project}</title>
</selection>
Examples:
<example>
User: I need to build a todo app
Response:
<selection>
<templateName>react-basic-starter</templateName>
<title>Simple React todo application</title>
</selection>
</example>
<example>
User: Write a script to generate numbers from 1 to 100
Response:
<selection>
<templateName>blank</templateName>
<title>script to generate numbers from 1 to 100</title>
</selection>
</example>
Instructions:
1. For trivial tasks and simple scripts, always recommend the blank template
2. For more complex projects, recommend templates from the provided list
3. Follow the exact XML format
4. Consider both technical requirements and tags
5. If no perfect match exists, recommend the closest option
Important: Provide only the selection tags in your response, no additional text.
`;
const templates: Template[] = STARTER_TEMPLATES.filter((t) => !t.name.includes('shadcn'));
const parseSelectedTemplate = (llmOutput: string): { template: string; title: string } | null => {
try {
// Extract content between <templateName> tags
const templateNameMatch = llmOutput.match(/<templateName>(.*?)<\/templateName>/);
const titleMatch = llmOutput.match(/<title>(.*?)<\/title>/);
if (!templateNameMatch) {
return null;
}
return { template: templateNameMatch[1].trim(), title: titleMatch?.[1].trim() || 'Untitled Project' };
} catch (error) {
console.error('Error parsing template selection:', error);
return null;
}
};
export const selectStarterTemplate = async (options: { message: string; model: string; provider: ProviderInfo }) => {
const { message, model, provider } = options;
const requestBody = {
message,
model,
provider,
system: starterTemplateSelectionPrompt(templates),
};
const response = await fetch('/api/llmcall', {
method: 'POST',
body: JSON.stringify(requestBody),
});
const respJson: { text: string } = await response.json();
console.log(respJson);
const { text } = respJson;
const selectedTemplate = parseSelectedTemplate(text);
if (selectedTemplate) {
return selectedTemplate;
} else {
console.log('No template selected, using blank template');
return {
template: 'blank',
title: '',
};
}
};
const getGitHubRepoContent = async (
repoName: string,
path: string = '',
): Promise<{ name: string; path: string; content: string }[]> => {
const baseUrl = 'https://api.github.com';
try {
const token = Cookies.get('githubToken') || import.meta.env.VITE_GITHUB_ACCESS_TOKEN;
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3+json',
};
// Add your GitHub token if needed
if (token) {
headers.Authorization = 'token ' + token;
}
// Fetch contents of the path
const response = await fetch(`${baseUrl}/repos/${repoName}/contents/${path}`, {
headers,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: any = await response.json();
// If it's a single file, return its content
if (!Array.isArray(data)) {
if (data.type === 'file') {
// If it's a file, get its content
const content = atob(data.content); // Decode base64 content
return [
{
name: data.name,
path: data.path,
content,
},
];
}
}
// Process directory contents recursively
const contents = await Promise.all(
data.map(async (item: any) => {
if (item.type === 'dir') {
// Recursively get contents of subdirectories
return await getGitHubRepoContent(repoName, item.path);
} else if (item.type === 'file') {
// Fetch file content
const fileResponse = await fetch(item.url, {
headers,
});
const fileData: any = await fileResponse.json();
const content = atob(fileData.content); // Decode base64 content
return [
{
name: item.name,
path: item.path,
content,
},
];
}
return [];
}),
);
// Flatten the array of contents
return contents.flat();
} catch (error) {
console.error('Error fetching repo contents:', error);
throw error;
}
};
export async function getTemplates(templateName: string, title?: string) {
const template = STARTER_TEMPLATES.find((t) => t.name == templateName);
if (!template) {
return null;
}
const githubRepo = template.githubRepo;
const files = await getGitHubRepoContent(githubRepo);
let filteredFiles = files;
/*
* ignoring common unwanted files
* exclude .git
*/
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.git') == false);
// exclude lock files
const comminLockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
filteredFiles = filteredFiles.filter((x) => comminLockFiles.includes(x.name) == false);
// exclude .bolt
filteredFiles = filteredFiles.filter((x) => x.path.startsWith('.bolt') == false);
// check for ignore file in .bolt folder
const templateIgnoreFile = files.find((x) => x.path.startsWith('.bolt') && x.name == 'ignore');
const filesToImport = {
files: filteredFiles,
ignoreFile: [] as typeof filteredFiles,
};
if (templateIgnoreFile) {
// redacting files specified in ignore file
const ignorepatterns = templateIgnoreFile.content.split('\n').map((x) => x.trim());
const ig = ignore().add(ignorepatterns);
// filteredFiles = filteredFiles.filter(x => !ig.ignores(x.path))
const ignoredFiles = filteredFiles.filter((x) => ig.ignores(x.path));
filesToImport.files = filteredFiles;
filesToImport.ignoreFile = ignoredFiles;
}
const assistantMessage = `
<boltArtifact id="imported-files" title="${title || 'Importing Starter Files'}" type="bundled">
${filesToImport.files
.map(
(file) =>
`<boltAction type="file" filePath="${file.path}">
${file.content}
</boltAction>`,
)
.join('\n')}
</boltArtifact>
`;
let userMessage = ``;
const templatePromptFile = files.filter((x) => x.path.startsWith('.bolt')).find((x) => x.name == 'prompt');
if (templatePromptFile) {
userMessage = `
TEMPLATE INSTRUCTIONS:
${templatePromptFile.content}
IMPORTANT: Dont Forget to install the dependencies before running the app
---
`;
}
if (filesToImport.ignoreFile.length > 0) {
userMessage =
userMessage +
`
STRICT FILE ACCESS RULES - READ CAREFULLY:
The following files are READ-ONLY and must never be modified:
${filesToImport.ignoreFile.map((file) => `- ${file.path}`).join('\n')}
Permitted actions:
✓ Import these files as dependencies
✓ Read from these files
✓ Reference these files
Strictly forbidden actions:
❌ Modify any content within these files
❌ Delete these files
❌ Rename these files
❌ Move these files
❌ Create new versions of these files
❌ Suggest changes to these files
Any attempt to modify these protected files will result in immediate termination of the operation.
If you need to make changes to functionality, create new files instead of modifying the protected ones listed above.
---
`;
}
userMessage += `
---
template import is done, and you can now use the imported files,
edit only the files that need to be changed, and you can create new files as needed.
NO NOT EDIT/WRITE ANY FILES THAT ALREADY EXIST IN THE PROJECT AND DOES NOT NEED TO BE MODIFIED
---
Now that the Template is imported please continue with my original request
`;
return {
assistantMessage,
userMessage,
};
}

View File

@@ -1,14 +0,0 @@
# 🚀 Release v0.0.5
## What's Changed 🌟
### 🔄 Changes since v0.0.4
### 🐛 Bug Fixes
* hotfix auto select starter template works without github token #release ([#959](https://github.com/stackblitz-labs/bolt.diy/pull/959)) by @thecodacus
## 📈 Stats
**Full Changelog**: [`v0.0.4..v0.0.5`](https://github.com/stackblitz-labs/bolt.diy/compare/v0.0.4...v0.0.5)

View File

@@ -1,73 +0,0 @@
services:
app-prod:
image: bolt-ai:production
build:
context: .
dockerfile: Dockerfile
target: bolt-ai-production
ports:
- "5173:5173"
env_file: ".env.local"
environment:
- NODE_ENV=production
- COMPOSE_PROFILES=production
# No strictly needed but serving as hints for Coolify
- PORT=5173
- GROQ_API_KEY=${GROQ_API_KEY}
- HuggingFace_API_KEY=${HuggingFace_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
- XAI_API_KEY=${XAI_API_KEY}
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
- RUNNING_IN_DOCKER=true
extra_hosts:
- "host.docker.internal:host-gateway"
command: pnpm run dockerstart
profiles:
- production
app-dev:
image: bolt-ai:development
build:
target: bolt-ai-development
environment:
- NODE_ENV=development
- VITE_HMR_PROTOCOL=ws
- VITE_HMR_HOST=localhost
- VITE_HMR_PORT=5173
- CHOKIDAR_USEPOLLING=true
- WATCHPACK_POLLING=true
- PORT=5173
- GROQ_API_KEY=${GROQ_API_KEY}
- HuggingFace_API_KEY=${HuggingFace_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPEN_ROUTER_API_KEY=${OPEN_ROUTER_API_KEY}
- XAI_API_KEY=${XAI_API_KEY}
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY}
- OLLAMA_API_BASE_URL=${OLLAMA_API_BASE_URL}
- TOGETHER_API_KEY=${TOGETHER_API_KEY}
- TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL}
- AWS_BEDROCK_CONFIG=${AWS_BEDROCK_CONFIG}
- VITE_LOG_LEVEL=${VITE_LOG_LEVEL:-debug}
- DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768}
- RUNNING_IN_DOCKER=true
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- type: bind
source: .
target: /app
consistency: cached
- /app/node_modules
ports:
- "5173:5173"
command: pnpm run dev --host 0.0.0.0
profiles: ["development", "default"]

3
docs/.gitignore vendored
View File

@@ -1,3 +0,0 @@
.venv
site/
.python-version

View File

@@ -1 +0,0 @@
3.12.0

View File

View File

@@ -1,149 +0,0 @@
# Contribution Guidelines
Welcome! This guide provides all the details you need to contribute effectively to the project. Thank you for helping us make **bolt.diy** a better tool for developers worldwide. 💡
---
## 📋 Table of Contents
1. [Code of Conduct](#code-of-conduct)
2. [How Can I Contribute?](#how-can-i-contribute)
3. [Pull Request Guidelines](#pull-request-guidelines)
4. [Coding Standards](#coding-standards)
5. [Development Setup](#development-setup)
6. [Testing](#testing)
7. [Deployment](#deployment)
8. [Docker Deployment](#docker-deployment)
9. [VS Code Dev Containers Integration](#vs-code-dev-containers-integration)
---
## 🛡️ Code of Conduct
This project is governed by our **Code of Conduct**. By participating, you agree to uphold this code. Report unacceptable behavior to the project maintainers.
---
## 🛠️ How Can I Contribute?
### 1⃣ Reporting Bugs or Feature Requests
- Check the [issue tracker](#) to avoid duplicates.
- Use issue templates (if available).
- Provide detailed, relevant information and steps to reproduce bugs.
### 2⃣ Code Contributions
1. Fork the repository.
2. Create a feature or fix branch.
3. Write and test your code.
4. Submit a pull request (PR).
### 3⃣ Join as a Core Contributor
Interested in maintaining and growing the project? Fill out our [Contributor Application Form](https://forms.gle/TBSteXSDCtBDwr5m7).
---
## ✅ Pull Request Guidelines
### PR Checklist
- Branch from the **main** branch.
- Update documentation, if needed.
- Test all functionality manually.
- Focus on one feature/bug per PR.
### Review Process
1. Manual testing by reviewers.
2. At least one maintainer review required.
3. Address review comments.
4. Maintain a clean commit history.
---
## 📏 Coding Standards
### General Guidelines
- Follow existing code style.
- Comment complex logic.
- Keep functions small and focused.
- Use meaningful variable names.
---
## 🖥️ Development Setup
### 1⃣ Initial Setup
- Clone the repository:
```bash
git clone https://github.com/stackblitz-labs/bolt.diy.git
```
- Install dependencies:
```bash
pnpm install
```
- Set up environment variables:
1. Rename `.env.example` to `.env.local`.
2. Add your API keys:
```bash
GROQ_API_KEY=XXX
HuggingFace_API_KEY=XXX
OPENAI_API_KEY=XXX
...
```
3. Optionally set:
- Debug level: `VITE_LOG_LEVEL=debug`
- Context size: `DEFAULT_NUM_CTX=32768`
**Note**: Never commit your `.env.local` file to version control. Its already in `.gitignore`.
### 2⃣ Run Development Server
```bash
pnpm run dev
```
**Tip**: Use **Google Chrome Canary** for local testing.
---
## 🧪 Testing
Run the test suite with:
```bash
pnpm test
```
---
### Coolify Deployment
For an easy deployment process, use [Coolify](https://github.com/coollabsio/coolify):
1. Import your Git repository into Coolify.
2. Choose **Docker Compose** as the build pack.
3. Configure environment variables (e.g., API keys).
4. Set the start command:
```bash
docker compose --profile production up
```
---
## 🛠️ VS Code Dev Containers Integration
The `docker-compose.yaml` configuration is compatible with **VS Code Dev Containers**, making it easy to set up a development environment directly in Visual Studio Code.
### Steps to Use Dev Containers
1. Open the command palette in VS Code (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS).
2. Select **Dev Containers: Reopen in Container**.
3. Choose the **development** profile when prompted.
4. VS Code will rebuild the container and open it with the pre-configured environment.
---
## 🔑 Environment Variables
Ensure `.env.local` is configured correctly with:
- API keys.
- Context-specific configurations.
Example for the `DEFAULT_NUM_CTX` variable:
```bash
DEFAULT_NUM_CTX=24576 # Uses 32GB VRAM
```

View File

@@ -1,95 +0,0 @@
# Frequently Asked Questions (FAQ)
## Models and Setup
??? question "What are the best models for bolt.diy?"
For the best experience with bolt.diy, we recommend using the following models:
- **Claude 3.5 Sonnet (old)**: Best overall coder, providing excellent results across all use cases
- **Gemini 2.0 Flash**: Exceptional speed while maintaining good performance
- **GPT-4o**: Strong alternative to Claude 3.5 Sonnet with comparable capabilities
- **DeepSeekCoder V3**: Best open source model (available through OpenRouter, DeepSeek API, or self-hosted)
- **DeepSeekCoder V2 236b**: available through OpenRouter, DeepSeek API, or self-hosted
- **Qwen 2.5 Coder 32b**: Best model for self-hosting with reasonable hardware requirements
!!! warning
Models with less than 7b parameters typically lack the capability to properly interact with bolt!
## Best Practices
??? question "How do I get the best results with bolt.diy?"
- **Be specific about your stack**:
Mention the frameworks or libraries you want to use (e.g., Astro, Tailwind, ShadCN) in your initial prompt. This ensures that bolt.diy scaffolds the project according to your preferences.
- **Use the enhance prompt icon**:
Before sending your prompt, click the *enhance* icon to let the AI refine your prompt. You can edit the suggested improvements before submitting.
- **Scaffold the basics first, then add features**:
Ensure the foundational structure of your application is in place before introducing advanced functionality. This helps bolt.diy establish a solid base to build on.
- **Batch simple instructions**:
Combine simple tasks into a single prompt to save time and reduce API credit consumption. For example:
*"Change the color scheme, add mobile responsiveness, and restart the dev server."*
## Project Information
??? question "How do I contribute to bolt.diy?"
Check out our [Contribution Guide](CONTRIBUTING.md) for more details on how to get involved!
??? question "What are the future plans for bolt.diy?"
Visit our [Roadmap](https://roadmap.sh/r/ottodev-roadmap-2ovzo) for the latest updates.
New features and improvements are on the way!
??? question "Why are there so many open issues/pull requests?"
bolt.diy began as a small showcase project on @ColeMedin's YouTube channel to explore editing open-source projects with local LLMs. However, it quickly grew into a massive community effort!
We're forming a team of maintainers to manage demand and streamline issue resolution. The maintainers are rockstars, and we're also exploring partnerships to help the project thrive.
## Model Comparisons
??? question "How do local LLMs compare to larger models like Claude 3.5 Sonnet for bolt.diy?"
While local LLMs are improving rapidly, larger models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b still offer the best results for complex applications. Our ongoing focus is to improve prompts, agents, and the platform to better support smaller local LLMs.
## Troubleshooting
??? error "There was an error processing this request"
This generic error message means something went wrong. Check both:
- The terminal (if you started the app with Docker or `pnpm`).
- The developer console in your browser (press `F12` or right-click > *Inspect*, then go to the *Console* tab).
??? error "x-api-key header missing"
This error is sometimes resolved by restarting the Docker container.
If that doesn't work, try switching from Docker to `pnpm` or vice versa. We're actively investigating this issue.
??? error "Blank preview when running the app"
A blank preview often occurs due to hallucinated bad code or incorrect commands.
To troubleshoot:
- Check the developer console for errors.
- Remember, previews are core functionality, so the app isn't broken! We're working on making these errors more transparent.
??? error "Everything works, but the results are bad"
Local LLMs like Qwen-2.5-Coder are powerful for small applications but still experimental for larger projects. For better results, consider using larger models like
- GPT-4o
- Claude 3.5 Sonnet
- DeepSeek Coder V2 236b
??? error "Received structured exception #0xc0000005: access violation"
If you are getting this, you are probably on Windows. The fix is generally to update the [Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)
??? error "Miniflare or Wrangler errors in Windows"
You will need to make sure you have the latest version of Visual Studio C++ installed (14.40.33816), more information here <a href="https://github.com/stackblitz-labs/bolt.diy/issues/19">Github Issues</a>
---
## Get Help & Support
!!! tip "Community Support"
[Join the bolt.diy Community](https://thinktank.ottomator.ai/c/bolt-diy/17){target=_blank} for discussions and help
!!! bug "Report Issues"
[Open an Issue](https://github.com/stackblitz-labs/bolt.diy/issues/19){target=_blank} in our GitHub Repository

View File

@@ -1,248 +0,0 @@
# Welcome to bolt diy
bolt.diy allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
## Table of Contents
- [Join the community!](#join-the-community)
- [Features](#features)
- [Setup](#setup)
- [Prerequisites](#prerequisites)
- [Clone the Repository](#clone-the-repository)
- [Entering API Keys](#entering-api-keys)
- [1. Set API Keys in the `.env.local` File](#1-set-api-keys-in-the-envlocal-file)
- [2. Configure API Keys Directly in the Application](#2-configure-api-keys-directly-in-the-application)
- [Run the Application](#run-the-application)
- [Option 1: Without Docker](#option-1-without-docker)
- [Option 2: With Docker](#option-2-with-docker)
- [Update Your Local Version to the Latest](#update-your-local-version-to-the-latest)
- [Adding New LLMs](#adding-new-llms)
- [Available Scripts](#available-scripts)
- [Development](#development)
- [Tips and Tricks](#tips-and-tricks)
---
## Join the community!
[Join the community!](https://thinktank.ottomator.ai)
Also [this pinned post in our community](https://thinktank.ottomator.ai/t/videos-tutorial-helpful-content/3243) has a bunch of incredible resources for running and deploying bolt.diy yourself!
---
## Features
- **AI-powered full-stack web development** directly in your browser.
- **Support for multiple LLMs** with an extensible architecture to integrate additional models.
- **Attach images to prompts** for better contextual understanding.
- **Integrated terminal** to view output of LLM-run commands.
- **Revert code to earlier versions** for easier debugging and quicker changes.
- **Download projects as ZIP** for easy portability.
- **Integration-ready Docker support** for a hassle-free setup.
---
## Setup
If you're new to installing software from GitHub, don't worry! If you encounter any issues, feel free to submit an "issue" using the provided links or improve this documentation by forking the repository, editing the instructions, and submitting a pull request. The following instruction will help you get the stable branch up and running on your local machine in no time.
### Prerequisites
1. **Install Git**: [Download Git](https://git-scm.com/downloads)
2. **Install Node.js**: [Download Node.js](https://nodejs.org/en/download/)
- After installation, the Node.js path is usually added to your system automatically. To verify:
- **Windows**: Search for "Edit the system environment variables," click "Environment Variables," and check if `Node.js` is in the `Path` variable.
- **Mac/Linux**: Open a terminal and run:
```bash
echo $PATH
```
Look for `/usr/local/bin` in the output.
### Clone the Repository
Alternatively, you can download the latest version of the project directly from the [Releases Page](https://github.com/stackblitz-labs/bolt.diy/releases/latest). Simply download the .zip file, extract it, and proceed with the setup instructions below. If you are comfertiable using git then run the command below.
Clone the repository using Git:
```bash
git clone -b stable https://github.com/stackblitz-labs/bolt.diy
```
---
### Entering API Keys
There are two ways to configure your API keys in bolt.diy:
#### 1. Set API Keys in the `.env.local` File
When setting up the application, you will need to add your API keys for the LLMs you wish to use. You can do this by renaming the `.env.example` file to `.env.local` and adding your API keys there.
- On **Mac**, you can find the file at `[your name]/bolt.diy/.env.example`.
- On **Windows/Linux**, the path will be similar.
If you can't see the file, it's likely because hidden files are not being shown. On **Mac**, open a Terminal window and enter the following command to show hidden files:
```bash
defaults write com.apple.finder AppleShowAllFiles YES
```
Make sure to add your API keys for each provider you want to use, for example:
```
GROQ_API_KEY=XXX
OPENAI_API_KEY=XXX
ANTHROPIC_API_KEY=XXX
```
Once you've set your keys, you can proceed with running the app. You will set these keys up during the initial setup, and you can revisit and update them later after the app is running.
**Note**: Never commit your `.env.local` file to version control. Its already included in the `.gitignore`.
#### 2. Configure API Keys Directly in the Application
Alternatively, you can configure your API keys directly in the application once it's running. To do this:
1. Launch the application and navigate to the provider selection dropdown.
2. Select the provider you wish to configure.
3. Click the pencil icon next to the selected provider.
4. Enter your API key in the provided field.
This method allows you to easily add or update your keys without needing to modify files directly.
Once you've configured your keys, the application will be ready to use the selected LLMs.
---
## Run the Application
### Option 1: Without Docker
1. **Install Dependencies**:
```bash
pnpm install
```
If `pnpm` is not installed, install it using:
```bash
sudo npm install -g pnpm
```
2. **Start the Application**:
```bash
pnpm run dev
```
This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
### Option 2: With Docker
#### Prerequisites
- Ensure Git, Node.js, and Docker are installed: [Download Docker](https://www.docker.com/)
#### Steps
1. **Build the Docker Image**:
Use the provided NPM scripts:
```bash
npm run dockerbuild
```
Alternatively, use Docker commands directly:
```bash
docker build . --target bolt-ai-development
```
2. **Run the Container**:
Use Docker Compose profiles to manage environments:
```bash
docker-compose --profile development up
```
- With the development profile, changes to your code will automatically reflect in the running container (hot reloading).
---
### Update Your Local Version to the Latest
To keep your local version of bolt.diy up to date with the latest changes, follow these steps for your operating system:
#### 1. **Navigate to your project folder**
Navigate to the directory where you cloned the repository and open a terminal:
#### 2. **Fetch the Latest Changes**
Use Git to pull the latest changes from the main repository:
```bash
git pull origin main
```
#### 3. **Update Dependencies**
After pulling the latest changes, update the project dependencies by running the following command:
```bash
pnpm install
```
#### 4. **Rebuild and Start the Application**
- **If using Docker**, ensure you rebuild the Docker image to avoid using a cached version:
```bash
docker-compose --profile development up --build
```
- **If not using Docker**, you can start the application as usual with:
```bash
pnpm run dev
```
This ensures that you're running the latest version of bolt.diy and can take advantage of all the newest features and bug fixes.
---
## Adding New LLMs:
To make new LLMs available to use in this version of bolt.diy, 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 label 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!
When you add a new model to the MODEL_LIST array, it will immediately be available to use when you run the app locally or reload it. For Ollama models, make sure you have the model installed already before trying to use it here!
---
## Available Scripts
- `pnpm run dev`: Starts the development server.
- `pnpm run build`: Builds the project.
- `pnpm run start`: Runs the built application locally using Wrangler Pages. This script uses `bindings.sh` to set up necessary bindings so you don't have to duplicate environment variables.
- `pnpm run preview`: Builds the project and then starts it locally, useful for testing the production build. Note, HTTP streaming currently doesn't work as expected with `wrangler pages dev`.
- `pnpm test`: Runs the test suite using Vitest.
- `pnpm run typecheck`: Runs TypeScript type checking.
- `pnpm run typegen`: Generates TypeScript types using Wrangler.
- `pnpm run deploy`: Builds the project and deploys it to Vercel.
---
## Development
To start the development server:
```bash
pnpm run dev
```
This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
---
## Tips and Tricks
Here are some tips to get the most out of bolt.diy:
- **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure Bolt scaffolds the project accordingly.
- **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting.
- **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps Bolt understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality.
- **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask Bolt to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

View File

@@ -1,76 +0,0 @@
site_name: bolt.diy Docs
site_dir: ../site
theme:
name: material
palette:
- scheme: default
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
- scheme: slate
toggle:
icon: material/toggle-switch
name: Switch to light mode
features:
- navigation.tabs
- navigation.sections
- toc.follow
- toc.integrate
- navigation.top
- search.suggest
- search.highlight
- content.tabs.link
- content.code.annotation
- content.code.copy
# - navigation.instant
# - navigation.tracking
# - navigation.tabs.sticky
# - navigation.expand
# - content.code.annotate
icon:
repo: fontawesome/brands/github
# logo: assets/logo.png
# favicon: assets/logo.png
repo_name: bolt.diy
repo_url: https://github.com/stackblitz-labs/bolt.diy
edit_uri: ""
extra:
generator: false
social:
- icon: fontawesome/brands/github
link: https://github.com/stackblitz-labs/bolt.diy
name: bolt.diy
- icon: fontawesome/brands/discourse
link: https://thinktank.ottomator.ai/
name: bolt.diy Discourse
- icon: fontawesome/brands/x-twitter
link: https://x.com/bolt_diy
name: bolt.diy on X
- icon: fontawesome/brands/bluesky
link: https://bsky.app/profile/bolt.diy
name: bolt.diy on Bluesky
markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.arithmatex:
generic: true
- footnotes
- pymdownx.details
- pymdownx.superfences
- pymdownx.mark
- attr_list
- md_in_html
- tables
- def_list
- admonition
- pymdownx.tasklist:
custom_checkbox: true
- toc:
permalink: true

798
docs/poetry.lock generated
View File

@@ -1,798 +0,0 @@
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "babel"
version = "2.16.0"
description = "Internationalization utilities"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"},
{file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"},
]
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "certifi"
version = "2024.8.30"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
{file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
category = "main"
optional = false
python-versions = "*"
files = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
[package.dependencies]
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "idna"
version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "jinja2"
version = "3.1.4"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markdown"
version = "3.7"
description = "Python implementation of John Gruber's Markdown."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
{file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
]
[package.extras]
docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.9"
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
{file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
{file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
{file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
{file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
{file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
{file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
{file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
{file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
{file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
{file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
]
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
[[package]]
name = "mkdocs"
version = "1.6.1"
description = "Project documentation with Markdown."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
{file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
]
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0"
jinja2 = ">=2.11.1"
markdown = ">=3.3.6"
markupsafe = ">=2.0.1"
mergedeep = ">=1.3.4"
mkdocs-get-deps = ">=0.2.0"
packaging = ">=20.5"
pathspec = ">=0.11.1"
pyyaml = ">=5.1"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
[[package]]
name = "mkdocs-get-deps"
version = "0.2.0"
description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
{file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
]
[package.dependencies]
mergedeep = ">=1.3.4"
platformdirs = ">=2.2.0"
pyyaml = ">=5.1"
[[package]]
name = "mkdocs-material"
version = "9.5.45"
description = "Documentation that simply works"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.5.45-py3-none-any.whl", hash = "sha256:a9be237cfd0be14be75f40f1726d83aa3a81ce44808dc3594d47a7a592f44547"},
{file = "mkdocs_material-9.5.45.tar.gz", hash = "sha256:286489cf0beca4a129d91d59d6417419c63bceed1ce5cd0ec1fc7e1ebffb8189"},
]
[package.dependencies]
babel = ">=2.10,<3.0"
colorama = ">=0.4,<1.0"
jinja2 = ">=3.0,<4.0"
markdown = ">=3.2,<4.0"
mkdocs = ">=1.6,<2.0"
mkdocs-material-extensions = ">=1.3,<2.0"
paginate = ">=0.5,<1.0"
pygments = ">=2.16,<3.0"
pymdown-extensions = ">=10.2,<11.0"
regex = ">=2022.4"
requests = ">=2.26,<3.0"
[package.extras]
git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"]
recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
{file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "paginate"
version = "0.5.7"
description = "Divides large result sets into pages for easier browsing"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
{file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
]
[package.extras]
dev = ["pytest", "tox"]
lint = ["black"]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.11.2)"]
[[package]]
name = "pygments"
version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pymdown-extensions"
version = "10.12"
description = "Extension pack for Python Markdown."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"},
{file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"},
]
[package.dependencies]
markdown = ">=3.6"
pyyaml = "*"
[package.extras]
extra = ["pygments (>=2.12)"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
[package.dependencies]
pyyaml = "*"
[[package]]
name = "regex"
version = "2024.11.6"
description = "Alternative regular expression module, to replace re."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
{file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"},
{file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"},
{file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"},
{file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"},
{file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"},
{file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"},
{file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"},
{file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"},
{file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"},
{file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"},
{file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"},
{file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"},
{file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"},
{file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"},
{file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"},
{file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"},
{file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"},
{file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"},
{file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
]
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "urllib3"
version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "watchdog"
version = "6.0.0"
description = "Filesystem events monitoring"
category = "main"
optional = false
python-versions = ">=3.9"
files = [
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
{file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"},
{file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"},
{file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"},
{file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"},
{file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"},
{file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"},
{file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"},
{file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"},
{file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"},
{file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"},
{file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"},
{file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"},
{file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"},
{file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"},
{file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"},
{file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"},
{file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"},
{file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"},
{file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"},
{file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"},
{file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
]
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "dee3864dbc26fd653b2a74c70243efffcbb63a0a0f23abe8311aad8fd5bf2eb0"

Some files were not shown because too many files have changed in this diff Show More