mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
feat: Ask Sourcebot (#392)
Co-authored-by: msukkari <michael.sukkarieh@mail.mcgill.ca>
This commit is contained in:
parent
eb20027210
commit
2b0dac4782
143 changed files with 16281 additions and 815 deletions
2
.github/workflows/_gcp-deploy.yml
vendored
2
.github/workflows/_gcp-deploy.yml
vendored
|
|
@ -60,6 +60,8 @@ jobs:
|
|||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
|
||||
NEXT_PUBLIC_SENTRY_WEBAPP_DSN=${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }}
|
||||
NEXT_PUBLIC_SENTRY_BACKEND_DSN=${{ vars.NEXT_PUBLIC_SENTRY_BACKEND_DSN }}
|
||||
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY=${{ vars.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_LANGFUSE_BASE_URL=${{ vars.NEXT_PUBLIC_LANGFUSE_BASE_URL }}
|
||||
SENTRY_SMUAT=${{ secrets.SENTRY_SMUAT }}
|
||||
SENTRY_ORG=${{ vars.SENTRY_ORG }}
|
||||
SENTRY_WEBAPP_PROJECT=${{ vars.SENTRY_WEBAPP_PROJECT }}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Introducing Ask Sourcebot - ask natural langauge about your codebase. Get back comprehensive Markdown responses with inline citations back to the code. Bring your own LLM api key. [#392](https://github.com/sourcebot-dev/sourcebot/pull/392)
|
||||
|
||||
## [4.5.3] - 2025-07-20
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -44,16 +44,20 @@
|
|||
|
||||
# About
|
||||
|
||||
Sourcebot lets you index all your repos and branches across multiple code hosts (GitHub, GitLab, Bitbucket, Gitea, or Gerrit) and search through them using a blazingly fast interface.
|
||||
Sourcebot is a self-hosted tool that helps you understand your codebase.
|
||||
|
||||
https://github.com/user-attachments/assets/ced355f3-967e-4f37-ae6e-74ab8c06b9ec
|
||||
- **Ask Sourcebot:** Ask questions about your codebase and have Sourcebot provide detailed answers grounded with inline citations.
|
||||
- **Code search:** Search and navigate across all your repos and branches, no matter where they’re hosted.
|
||||
|
||||
https://github.com/user-attachments/assets/286ad97a-a543-4eef-a2f1-4fa31bea1b32
|
||||
|
||||
|
||||
## Features
|
||||
- 💻 **One-command deployment**: Get started instantly using Docker on your own machine.
|
||||
- 🔍 **Multi-repo search**: Index and search through multiple public and private repositories and branches on GitHub, GitLab, Bitbucket, Gitea, or Gerrit.
|
||||
- ⚡**Lightning fast performance**: Built on top of the powerful [Zoekt](https://github.com/sourcegraph/zoekt) search engine.
|
||||
- 🎨 **Modern web app**: Enjoy a sleek interface with features like syntax highlighting, light/dark mode, and vim-style navigation
|
||||
- 🤖 **Bring your own model**: Connect Sourcebot to any of the reasoning models you're already using.
|
||||
- 🔍 **Multi-repo support**: Index and search through multiple public and private repositories and branches on GitHub, GitLab, Bitbucket, Gitea, or Gerrit.
|
||||
- ⚡ **Lightning fast performance**: Built on top of the powerful [Zoekt](https://github.com/sourcegraph/zoekt) search engine.
|
||||
- 🎨 **Modern web app**: Enjoy a sleek interface with features like syntax highlighting, light/dark mode, and vim-style navigation.
|
||||
- 📂 **Full file visualization**: Instantly view the entire file when selecting any search result.
|
||||
|
||||
You can try out our public hosted demo [here](https://demo.sourcebot.dev)!
|
||||
|
|
|
|||
|
|
@ -28,13 +28,21 @@
|
|||
"group": "Features",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Search",
|
||||
"group": "Code Search",
|
||||
"pages": [
|
||||
"docs/features/search/overview",
|
||||
"docs/features/search/syntax-reference",
|
||||
"docs/features/search/multi-branch-indexing",
|
||||
"docs/features/search/search-contexts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ask Sourcebot",
|
||||
"pages": [
|
||||
"docs/features/ask/overview",
|
||||
"docs/features/ask/add-model-providers"
|
||||
]
|
||||
},
|
||||
"docs/features/code-navigation",
|
||||
"docs/features/analytics",
|
||||
"docs/features/mcp-server",
|
||||
|
|
@ -51,6 +59,7 @@
|
|||
{
|
||||
"group": "Configuration",
|
||||
"pages": [
|
||||
"docs/configuration/config-file",
|
||||
{
|
||||
"group": "Indexing your code",
|
||||
"pages": [
|
||||
|
|
@ -66,8 +75,7 @@
|
|||
"docs/connections/request-new"
|
||||
]
|
||||
},
|
||||
"docs/license-key",
|
||||
"docs/configuration/environment-variables",
|
||||
"docs/configuration/language-model-providers",
|
||||
{
|
||||
"group": "Authentication",
|
||||
"pages": [
|
||||
|
|
@ -78,6 +86,8 @@
|
|||
"docs/configuration/auth/faq"
|
||||
]
|
||||
},
|
||||
"docs/configuration/environment-variables",
|
||||
"docs/license-key",
|
||||
"docs/configuration/transactional-emails",
|
||||
"docs/configuration/structured-logging",
|
||||
"docs/configuration/audit-logs"
|
||||
|
|
|
|||
49
docs/docs/configuration/config-file.mdx
Normal file
49
docs/docs/configuration/config-file.mdx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: Config File
|
||||
sidebarTitle: Config file
|
||||
---
|
||||
|
||||
When self-hosting Sourcebot, you **must** provide it a config file. This is done by defining a config file in a volume that's mounted to Sourcebot, and providing the path to this
|
||||
file in the `CONFIG_PATH` environment variable. For example:
|
||||
|
||||
```bash icon="terminal" Passing in a CONFIG_PATH to Sourcebot
|
||||
docker run \
|
||||
-v $(pwd)/config.json:/data/config.json \
|
||||
-e CONFIG_PATH=/data/config.json \
|
||||
... \ # other options
|
||||
ghcr.io/sourcebot-dev/sourcebot:latest
|
||||
```
|
||||
|
||||
The config file tells Sourcebot which repos to index, what language models to use, and various other settings as defined in the [schema](#config-file-schema).
|
||||
|
||||
# Config File Schema
|
||||
|
||||
The config file you provide Sourcebot must follow the [schema](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/index.json). This schema consists of the following properties:
|
||||
|
||||
- [Connections](/docs/connections/overview) (`connections`): Defines a set of connections that tell Sourcebot which repos to index and from where
|
||||
- [Language Models](/docs/configuration/language-model-providers) (`models`): Defines a set of language model providers for use with [Ask Sourcebot](/docs/features/ask)
|
||||
- [Settings](#settings) (`settings`): Additional settings to tweak your Sourcebot deployment
|
||||
- [Search Contexts](/docs/features/search/search-contexts) (`contexts`): Groupings of repos that you can search against
|
||||
|
||||
# Config File Syncing
|
||||
|
||||
Sourcebot syncs the config file on startup, and automatically whenever a change is detected.
|
||||
|
||||
# Settings
|
||||
|
||||
The following are settings that can be provided in your config file to modify Sourcebot's behavior
|
||||
|
||||
| Setting | Type | Default | Minimum | Description / Notes |
|
||||
|-------------------------------------------|---------|------------|---------|----------------------------------------------------------------------------------------|
|
||||
| `maxFileSize` | number | 2 MB | 1 | Maximum size (bytes) of a file to index. Files exceeding this are skipped. |
|
||||
| `maxTrigramCount` | number | 20 000 | 1 | Maximum trigrams per document. Larger files are skipped. |
|
||||
| `reindexIntervalMs` | number | 1 hour | 1 | Interval at which all repositories are re‑indexed. |
|
||||
| `resyncConnectionIntervalMs` | number | 24 hours | 1 | Interval for checking connections that need re‑syncing. |
|
||||
| `resyncConnectionPollingIntervalMs` | number | 1 second | 1 | DB polling rate for connections that need re‑syncing. |
|
||||
| `reindexRepoPollingIntervalMs` | number | 1 second | 1 | DB polling rate for repos that should be re‑indexed. |
|
||||
| `maxConnectionSyncJobConcurrency` | number | 8 | 1 | Concurrent connection‑sync jobs. |
|
||||
| `maxRepoIndexingJobConcurrency` | number | 8 | 1 | Concurrent repo‑indexing jobs. |
|
||||
| `maxRepoGarbageCollectionJobConcurrency` | number | 8 | 1 | Concurrent repo‑garbage‑collection jobs. |
|
||||
| `repoGarbageCollectionGracePeriodMs` | number | 10 seconds | 1 | Grace period to avoid deleting shards while loading. |
|
||||
| `repoIndexTimeoutMs` | number | 2 hours | 1 | Timeout for a single repo‑indexing run. |
|
||||
| `enablePublicAccess` **(deprecated)** | boolean | false | — | Use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead. |
|
||||
184
docs/docs/configuration/language-model-providers.mdx
Normal file
184
docs/docs/configuration/language-model-providers.mdx
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
---
|
||||
title: Language Model Providers
|
||||
sidebarTitle: Language model providers
|
||||
---
|
||||
|
||||
To use [Ask Sourcebot](/docs/features/ask) you must define at least one Language Model Provider. These providers are defined within the [config file](/docs/configuration/config-file) you
|
||||
provide Sourcebot.
|
||||
|
||||
|
||||
```json wrap icon="code" Example config with language model provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
// 1. Google Vertex config for Gemini 2.5 Pro
|
||||
{
|
||||
"provider": "google-vertex",
|
||||
"model": "gemini-2.5-pro",
|
||||
"displayName": "Gemini 2.5 Pro",
|
||||
"project": "sourcebot",
|
||||
"credentials": {
|
||||
"env": "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
}
|
||||
},
|
||||
// 2. OpenAI config for o3
|
||||
{
|
||||
"provider": "openai",
|
||||
"model": "o3",
|
||||
"displayName": "o3",
|
||||
"token": {
|
||||
"env": "OPENAI_API_KEY"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# Supported Providers
|
||||
|
||||
Sourcebot uses the [Vercel AI SDK](https://ai-sdk.dev/docs/introduction), so it can integrate with any provider the SDK supports. If you don't see your provider below please submit
|
||||
a [feature request](https://github.com/sourcebot-dev/sourcebot/discussions/categories/feature-requests).
|
||||
|
||||
For a detailed description of all the providers, please refer to the [schema](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/languageModel.json).
|
||||
|
||||
<Note>Any parameter defined using `env` will read the value from the corresponding environment variable you provide Sourcebot</Note>
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
[Vercel AI SDK Amazon Bedrock Docs](https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock)
|
||||
|
||||
```json wrap icon="code" Example config with Amazon Bedrock provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
{
|
||||
"provider": "amazon-bedrock",
|
||||
"model": "YOUR_MODEL_HERE",
|
||||
"displayName": "OPTIONAL_DISPLAY_NAME",
|
||||
"accessKeyId": {
|
||||
"env": "AWS_ACCESS_KEY_ID"
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"env": "AWS_SECRET_ACCESS_KEY"
|
||||
},
|
||||
"region": "YOUR_REGION_HERE", // defaults to the AWS_REGION env var if not set
|
||||
"baseUrl": "OPTIONAL_BASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Anthropic
|
||||
|
||||
[Vercel AI SDK Anthropic Docs](https://ai-sdk.dev/providers/ai-sdk-providers/anthropic)
|
||||
|
||||
```json wrap icon="code" Example config with Anthropic provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
{
|
||||
"provider": "anthropic",
|
||||
"model": "YOUR_MODEL_HERE",
|
||||
"displayName": "OPTIONAL_DISPLAY_NAME",
|
||||
"token": {
|
||||
"env": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
"baseUrl": "OPTIONAL_BASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Google Generative AI
|
||||
|
||||
[Vercel AI SDK Google Generative AI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai)
|
||||
|
||||
```json wrap icon="code" Example config with Google Generative AI provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
{
|
||||
"provider": "google-generative-ai",
|
||||
"model": "YOUR_MODEL_HERE",
|
||||
"displayName": "OPTIONAL_DISPLAY_NAME",
|
||||
"token": {
|
||||
"env": "GOOGLE_GENERATIVE_AI_API_KEY"
|
||||
},
|
||||
"baseUrl": "OPTIONAL_BASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Google Vertex
|
||||
|
||||
<Note>If you're using an Anthropic model on Google Vertex, you must define a [Google Vertex Anthropic](#google-vertex-anthropic) provider instead</Note>
|
||||
<Note>The `credentials` paramater here expects a **path** to a [credentials](https://console.cloud.google.com/apis/credentials) file. This file **must be in a volume mounted by Sourcebot** for it to be readable.</Note>
|
||||
|
||||
[Vercel AI SDK Google Vertex AI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/google-vertex)
|
||||
|
||||
```json wrap icon="code" Example config with Google Vertex provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
{
|
||||
"provider": "google-vertex",
|
||||
"model": "YOUR_MODEL_HERE", // e.g., "gemini-2.0-flash-exp", "gemini-1.5-pro", "gemini-1.5-flash"
|
||||
"displayName": "OPTIONAL_DISPLAY_NAME",
|
||||
"project": "YOUR_PROJECT_ID", // defaults to the GOOGLE_VERTEX_PROJECT env var if not set
|
||||
"region": "YOUR_REGION_HERE", // defaults to the GOOGLE_VERTEX_REGION env var if not set, e.g., "us-central1", "us-east1", "europe-west1"
|
||||
"credentials": {
|
||||
"env": "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
},
|
||||
"baseUrl": "OPTIONAL_BASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Google Vertex Anthropic
|
||||
|
||||
<Note>The `credentials` paramater here expects a **path** to a [credentials](https://console.cloud.google.com/apis/credentials) file. This file **must be in a volume mounted by Sourcebot** for it to be readable.</Note>
|
||||
|
||||
|
||||
[Vercel AI SDK Google Vertex Anthropic Docs](https://ai-sdk.dev/providers/ai-sdk-providers/google-vertex#google-vertex-anthropic-provider-usage)
|
||||
|
||||
```json wrap icon="code" Example config with Google Vertex Anthropic provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
{
|
||||
"provider": "google-vertex-anthropic",
|
||||
"model": "YOUR_MODEL_HERE", // e.g., "claude-sonnet-4"
|
||||
"displayName": "OPTIONAL_DISPLAY_NAME",
|
||||
"project": "YOUR_PROJECT_ID", // defaults to the GOOGLE_VERTEX_PROJECT env var if not set
|
||||
"region": "YOUR_REGION_HERE", // defaults to the GOOGLE_VERTEX_REGION env var if not set, e.g., "us-central1", "us-east1", "europe-west1"
|
||||
"credentials": {
|
||||
"env": "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
},
|
||||
"baseUrl": "OPTIONAL_BASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI
|
||||
|
||||
[Vercel AI SDK OpenAI Docs](https://ai-sdk.dev/providers/ai-sdk-providers/openai)
|
||||
|
||||
```json wrap icon="code" Example config with OpenAI provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"models": [
|
||||
{
|
||||
"provider": "openai",
|
||||
"model": "YOUR_MODEL_HERE", // e.g., "gpt-4.1", "o4-mini", "o3", "o3-deep-research"
|
||||
"displayName": "OPTIONAL_DISPLAY_NAME",
|
||||
"token": {
|
||||
"env": "OPENAI_API_KEY"
|
||||
},
|
||||
"baseUrl": "OPTIONAL_BASE_URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -12,6 +12,8 @@ import BitbucketSchema from '/snippets/schemas/v3/bitbucket.schema.mdx'
|
|||
Looking for docs on Bitbucket Data Center? See [this doc](/docs/connections/bitbucket-data-center).
|
||||
</Note>
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Examples
|
||||
|
||||
<AccordionGroup>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import BitbucketSchema from '/snippets/schemas/v3/bitbucket.schema.mdx'
|
|||
Looking for docs on Bitbucket Cloud? See [this doc](/docs/connections/bitbucket-cloud).
|
||||
</Note>
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Examples
|
||||
|
||||
<AccordionGroup>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import GenericGitHost from '/snippets/schemas/v3/genericGitHost.schema.mdx'
|
|||
|
||||
Sourcebot can sync code from any Git host (by clone url). This is helpful when you want to search code that not in a [supported code host](/docs/connections/overview#supported-code-hosts).
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To connect to a Git host, create a new [connection](/docs/connections/overview) with type `git` and specify the clone url in the `url` property. For example:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import GerritSchema from '/snippets/schemas/v3/gerrit.schema.mdx'
|
|||
|
||||
Sourcebot can sync code from self-hosted gerrit instances.
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Connecting to a Gerrit instance
|
||||
|
||||
To connect to a gerrit instance, provide the `url` property to your config:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import GiteaSchema from '/snippets/schemas/v3/gitea.schema.mdx'
|
|||
|
||||
Sourcebot can sync code from Gitea Cloud, and self-hosted.
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Examples
|
||||
|
||||
<AccordionGroup>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import GitHubSchema from '/snippets/schemas/v3/github.schema.mdx'
|
|||
|
||||
Sourcebot can sync code from GitHub.com, GitHub Enterprise Server, and GitHub Enterprise Cloud.
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Examples
|
||||
|
||||
<AccordionGroup>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import GitLabSchema from '/snippets/schemas/v3/gitlab.schema.mdx'
|
|||
|
||||
Sourcebot can sync code from GitLab.com, Self Managed (CE & EE), and Dedicated.
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import GenericGitHost from '/snippets/schemas/v3/genericGitHost.schema.mdx'
|
|||
|
||||
Sourcebot can sync code from generic git repositories stored in a local directory. This can be helpful in scenarios where you already have a large number of repos already checked out. Local repositories are treated as **read-only**, meaning Sourcebot will **not** `git fetch` new revisions.
|
||||
|
||||
If you're not familiar with Sourcebot [connections](/docs/connections/overview), please read that overview first.
|
||||
|
||||
## Getting Started
|
||||
|
||||
<Warning>
|
||||
|
|
|
|||
|
|
@ -6,20 +6,7 @@ sidebarTitle: Overview
|
|||
import SupportedPlatforms from '/snippets/platform-support.mdx'
|
||||
import ConfigSchema from '/snippets/schemas/v3/index.schema.mdx'
|
||||
|
||||
To index your code with Sourcebot, you must provide a configuration file. When running Sourcebot, this file must be mounted in a volume that is accessible to the container, with its
|
||||
path specified in the `CONFIG_PATH` environment variable. For example:
|
||||
|
||||
```bash icon="terminal" Passing in a CONFIG_PATH to Sourcebot
|
||||
docker run \
|
||||
-v $(pwd)/config.json:/data/config.json \
|
||||
-e CONFIG_PATH=/data/config.json \
|
||||
... \ # other config
|
||||
ghcr.io/sourcebot-dev/sourcebot:latest
|
||||
```
|
||||
|
||||
## Config Schema
|
||||
|
||||
The configuration file defines a set of **connections**. A connection in Sourcebot represents a link to a code host (such as GitHub, GitLab, Bitbucket, etc.).
|
||||
A **connection** represents Sourcebot's link to a code host platform (GitHub, GitLab, etc). Connections are defined within the [config file](/docs/configuration/config-file) you provide Sourcebot.
|
||||
|
||||
Each connection defines how Sourcebot should authenticate and interact with a particular host, and which repositories to sync and index from that host. Connections are uniquely identified by their name.
|
||||
|
||||
|
|
@ -55,10 +42,11 @@ Each connection defines how Sourcebot should authenticate and interact with a pa
|
|||
|
||||
Configuration files must conform to the [JSON schema](#schema-reference).
|
||||
|
||||
## Config Syncing
|
||||
Sourcebot performs syncing in the background. Syncing consists of two steps:
|
||||
1. Fetch the latest changes from `HEAD` (and any [additional branches](/docs/features/search/multi-branch-indexing)) from the code host.
|
||||
2. Re-indexes the repository.
|
||||
## Connection Syncing
|
||||
|
||||
When a connection is first discovered, or the `resyncConnectionIntervalMs` [setting](/docs/configuration/config-file#settings) has exceeded, the connection will be synced. This consists of:
|
||||
1. Fetching the latest changes from `HEAD` (and any [additional branches](/docs/features/search/multi-branch-indexing)) from the code host.
|
||||
2. Re-indexing the repository.
|
||||
|
||||
This is processed in a [job queue](/docs/overview#architecture), and is parallelized across multiple worker processes. Jobs will take longer to complete the first time a repository is synced, or when a diff is large.
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ The following guide will walk you through the steps to deploy Sourcebot on your
|
|||
</Step>
|
||||
|
||||
<Step title="Done">
|
||||
You're all set! You can now start searching - checkout the [syntax guide](/docs/features/search/syntax-reference) to learn more about how to search.
|
||||
You're all set! If you'd like to setup [Ask Sourcebot](/docs/features/ask/overview), configure a language model [provider](/docs/configuration/language-model-providers)
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
|
|
|||
5
docs/docs/features/ask/add-model-providers.mdx
Normal file
5
docs/docs/features/ask/add-model-providers.mdx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
sidebarTitle: Configure language models
|
||||
url: /docs/configuration/language-model-providers
|
||||
title: Configure Language Models
|
||||
---
|
||||
52
docs/docs/features/ask/overview.mdx
Normal file
52
docs/docs/features/ask/overview.mdx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
Ask Sourcebot gives you the ability to ask complex questions about your codebase in natural language.
|
||||
|
||||
It uses Sourcebot’s existing [code search](/docs/features/search/overview) and [navigation](/docs/features/code-navigation) tools to allow reasoning models to search your code,
|
||||
follow code nav references, and provide an answer that’s rich with inline citations and navigable code snippets.
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Configure language models" icon="robot" href="/docs/configuration/language-model-providers" horizontal="true">
|
||||
Learn how to connect your language model to Sourcebot
|
||||
</Card>
|
||||
<Card title="Index repos" icon="book" href="/docs/connections/overview" horizontal="true">
|
||||
Learn how to index your repos so you can ask questions about them
|
||||
</Card>
|
||||
<Card title="Deployment guide" icon="server" href="/docs/deployment-guide" horizontal="true">
|
||||
Learn how to self-host Sourcebot in a few simple steps.
|
||||
</Card>
|
||||
<Card title="Public demo" icon="globe" href="https://demo.sourcebot.dev/" horizontal="true">
|
||||
Try Ask Sourcebot on our public demo instance.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="w-full aspect-video"
|
||||
src="/images/ask_sourcebot_low_res.mp4"
|
||||
></video>
|
||||
|
||||
# Why do we need another AI dev tool?
|
||||
|
||||
Existing AI dev tools (Cursor, Claude Code, Copilot) are great at generating code. However, we believe one of the hardest parts of being
|
||||
a software engineer is **understanding code**.
|
||||
|
||||
In this domain, these tools fall short:
|
||||
- You can only ask questions about the code you have checked out locally
|
||||
- You get a wall of text that's difficult to parse, requiring you to go back and forth through different code snippets in the response
|
||||
- The richness of the explanation is limited by the fact that you're in your IDE
|
||||
|
||||
We built Ask Sourcebot to address these problems. With Ask Sourcebot, you can:
|
||||
- Ask questions about your teams entire codebase (even on repos you don't have locally)
|
||||
- Easily parse the response with side-by-side citations and code navigation
|
||||
- Share answers with your team to spread the knowledge
|
||||
|
||||
Being a web app is less convenient than being in your IDE, but it allows Sourcebot to provide responses in a richer UI that isn't constrained by the IDE.
|
||||
|
||||
We believe this experience of understanding your codebase is superior, and we hope you find it useful. We'd love to know what you think! Feel free to join the discussion on our
|
||||
[GitHub](https://github.com/sourcebot-dev/sourcebot/discussions).
|
||||
40
docs/docs/features/search/overview.mdx
Normal file
40
docs/docs/features/search/overview.mdx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
Search across all your repos/branches across any code host platform. Blazingly fast, and supports regular expressions, repo/language search filters, boolean logic, and more.
|
||||
|
||||
<Accordion title="Key benefits">
|
||||
- **Regex support:** Use regular expressions to find code with precision.
|
||||
- **Query language:** Scope searches to specific files, repos, languages, symbol definitions and more using a rich [query language](/docs/features/search/syntax-reference).
|
||||
- **Branch search:** Specify a list of branches to search across ([docs](/docs/features/search/multi-branch-indexing)).
|
||||
- **Fast & scalable:** Sourcebot uses [trigram indexing](https://en.wikipedia.org/wiki/Trigram_search), allowing it to scale to massive codebases.
|
||||
- **Syntax highlighting:** Syntax highlighting support for over [100+ languages](https://github.com/sourcebot-dev/sourcebot/blob/57724689303f351c279d37f45b6406f1d5d5d5ab/packages/web/src/lib/codemirrorLanguage.ts#L125).
|
||||
- **Multi-repository:** Search across all of your repositories in a single search.
|
||||
- **Search suggestions:** Get search suggestions as you craft your query.
|
||||
- **Filter panel:** Filter results by repository or by language.
|
||||
</Accordion>
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Index repos" icon="book" href="/docs/connections/overview" horizontal="true">
|
||||
Learn how to index your repos so you can ask questions about them
|
||||
</Card>
|
||||
<Card title="Branches" icon="split" href="/docs/features/search/multi-branch-indexing" horizontal="true">
|
||||
Learn how to index and search through your branches
|
||||
</Card>
|
||||
<Card title="Deployment guide" icon="server" href="/docs/deployment-guide" horizontal="true">
|
||||
Learn how to self-host Sourcebot in a few simple steps.
|
||||
</Card>
|
||||
<Card title="Public demo" icon="globe" href="https://demo.sourcebot.dev/" horizontal="true">
|
||||
Try Sourcebot's code search on our public demo instance.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="w-full aspect-video"
|
||||
src="https://framerusercontent.com/assets/cEqHNSLiMbNeG3bk5xheQWXmKqc.mp4"
|
||||
></video>
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
title: "Overview"
|
||||
---
|
||||
|
||||
[Sourcebot]((https://github.com/sourcebot-dev/sourcebot)) is an open-source, self-hosted code search tool. It allows you to search and navigate across millions of lines of code across several code host platforms.
|
||||
[Sourcebot](https://github.com/sourcebot-dev/sourcebot) is a self-hosted tool that helps you understand your codebase.
|
||||
|
||||
- [Code search](/docs/features/search/overview): Search and navigate across all your repos and branches, no matter where they’re hosted
|
||||
- [Ask Sourcebot](/docs/features/ask): Ask questions about your codebase and have Sourcebot provide detailed answers grounded with inline citations
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Deployment guide" icon="server" href="/docs/deployment-guide" horizontal="true">
|
||||
|
|
@ -30,9 +33,28 @@ title: "Overview"
|
|||
Find an overview of all Sourcebot features below. For details, see the individual documentation pages.
|
||||
</Info>
|
||||
|
||||
### Fast indexed based search
|
||||
### Ask Sourcebot
|
||||
|
||||
Search across millions of lines of code instantly using Sourcebot's blazingly fast indexed search. Find exactly what you are looking for with regular expressions, search filters, boolean logic, and more.
|
||||
[Ask Sourcebot](/docs/features/ask) gives you the ability to ask complex questions about your codebase, and have Sourcebot provide detailed answers with inline citations.
|
||||
|
||||
<Accordion title="Key benefits">
|
||||
- **Bring your own model:** [Configure](/docs/configuration/language-model-providers) to any language model you'd like
|
||||
- **Inline citations:** Every answer Sourcebot provides is grounded with inline citations directly into your codebase
|
||||
- **Mutli-repo:** Ask questions about any repository you have indexed on Sourcebot
|
||||
</Accordion>
|
||||
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="w-full aspect-video"
|
||||
src="/images/ask_sourcebot_low_res.mp4"
|
||||
></video>
|
||||
|
||||
### Code Search
|
||||
|
||||
Search across all your repos/branches across any code host platform. Blazingly fast, and supports regular expressions, repo/language search filters, boolean logic, and more.
|
||||
|
||||
<Accordion title="Key benefits">
|
||||
- **Regex support:** Use regular expressions to find code with precision.
|
||||
|
|
|
|||
BIN
docs/images/ask_sourcebot_low_res.mp4
Normal file
BIN
docs/images/ask_sourcebot_low_res.mp4
Normal file
Binary file not shown.
|
|
@ -1136,6 +1136,886 @@
|
|||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"description": "Defines a collection of language models that are available to Sourcebot.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "LanguageModel",
|
||||
"definitions": {
|
||||
"OpenAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AmazonBedrockLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleGenerativeAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexAnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
879
docs/snippets/schemas/v3/languageModel.schema.mdx
Normal file
879
docs/snippets/schemas/v3/languageModel.schema.mdx
Normal file
|
|
@ -0,0 +1,879 @@
|
|||
{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */}
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"title": "LanguageModel",
|
||||
"definitions": {
|
||||
"OpenAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AmazonBedrockLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleGenerativeAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexAnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
"dev:prisma:migrate:dev": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:dev",
|
||||
"dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio",
|
||||
"dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset",
|
||||
"dev:prisma:db:push": "yarn with-env yarn workspace @sourcebot/db prisma:db:push",
|
||||
"build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "ChatVisibility" AS ENUM ('PRIVATE', 'PUBLIC');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Chat" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"orgId" INTEGER NOT NULL,
|
||||
"visibility" "ChatVisibility" NOT NULL DEFAULT 'PRIVATE',
|
||||
"isReadonly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"messages" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Chat_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -35,6 +35,11 @@ enum StripeSubscriptionStatus {
|
|||
INACTIVE
|
||||
}
|
||||
|
||||
enum ChatVisibility {
|
||||
PRIVATE
|
||||
PUBLIC
|
||||
}
|
||||
|
||||
model Repo {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
|
|
@ -183,6 +188,8 @@ model Org {
|
|||
accountRequests AccountRequest[]
|
||||
|
||||
searchContexts SearchContext[]
|
||||
|
||||
chats Chat[]
|
||||
}
|
||||
|
||||
enum OrgRole {
|
||||
|
|
@ -276,6 +283,8 @@ model User {
|
|||
|
||||
apiKeys ApiKey[]
|
||||
|
||||
chats Chat[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
@ -311,3 +320,23 @@ model VerificationToken {
|
|||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
|
||||
name String?
|
||||
|
||||
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
||||
createdById String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
orgId Int
|
||||
|
||||
visibility ChatVisibility @default(PRIVATE)
|
||||
isReadonly Boolean @default(false)
|
||||
|
||||
messages Json // This is a JSON array of `Message` types from @ai-sdk/ui-utils.
|
||||
}
|
||||
|
|
@ -1135,6 +1135,886 @@ const schema = {
|
|||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"description": "Defines a collection of language models that are available to Sourcebot.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "LanguageModel",
|
||||
"definitions": {
|
||||
"OpenAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AmazonBedrockLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleGenerativeAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexAnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ export type ConnectionConfig =
|
|||
| GerritConnectionConfig
|
||||
| BitbucketConnectionConfig
|
||||
| GenericGitHostConnectionConfig;
|
||||
export type LanguageModel =
|
||||
| OpenAILanguageModel
|
||||
| AmazonBedrockLanguageModel
|
||||
| AnthropicLanguageModel
|
||||
| GoogleGenerativeAILanguageModel
|
||||
| GoogleVertexLanguageModel
|
||||
| GoogleVertexAnthropicLanguageModel;
|
||||
|
||||
export interface SourcebotConfig {
|
||||
$schema?: string;
|
||||
|
|
@ -27,6 +34,10 @@ export interface SourcebotConfig {
|
|||
connections?: {
|
||||
[k: string]: ConnectionConfig;
|
||||
};
|
||||
/**
|
||||
* Defines a collection of language models that are available to Sourcebot.
|
||||
*/
|
||||
models?: LanguageModel[];
|
||||
}
|
||||
/**
|
||||
* Defines the global settings for Sourcebot.
|
||||
|
|
@ -417,3 +428,243 @@ export interface GenericGitHostConnectionConfig {
|
|||
url: string;
|
||||
revisions?: GitRevisions;
|
||||
}
|
||||
export interface OpenAILanguageModel {
|
||||
/**
|
||||
* OpenAI Configuration
|
||||
*/
|
||||
provider: "openai";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface AmazonBedrockLanguageModel {
|
||||
/**
|
||||
* Amazon Bedrock Configuration
|
||||
*/
|
||||
provider: "amazon-bedrock";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.
|
||||
*/
|
||||
accessKeyId?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.
|
||||
*/
|
||||
accessKeySecret?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* The AWS region. Defaults to the `AWS_REGION` environment variable.
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface AnthropicLanguageModel {
|
||||
/**
|
||||
* Anthropic Configuration
|
||||
*/
|
||||
provider: "anthropic";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface GoogleGenerativeAILanguageModel {
|
||||
/**
|
||||
* Google Generative AI Configuration
|
||||
*/
|
||||
provider: "google-generative-ai";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface GoogleVertexLanguageModel {
|
||||
/**
|
||||
* Google Vertex AI Configuration
|
||||
*/
|
||||
provider: "google-vertex";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable.
|
||||
*/
|
||||
project?: string;
|
||||
/**
|
||||
* The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.
|
||||
*/
|
||||
credentials?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface GoogleVertexAnthropicLanguageModel {
|
||||
/**
|
||||
* Google Vertex AI Anthropic Configuration
|
||||
*/
|
||||
provider: "google-vertex-anthropic";
|
||||
/**
|
||||
* The name of the Anthropic language model running on Google Vertex.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable.
|
||||
*/
|
||||
project?: string;
|
||||
/**
|
||||
* The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.
|
||||
*/
|
||||
credentials?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
|
|
|||
878
packages/schemas/src/v3/languageModel.schema.ts
Normal file
878
packages/schemas/src/v3/languageModel.schema.ts
Normal file
|
|
@ -0,0 +1,878 @@
|
|||
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
|
||||
const schema = {
|
||||
"type": "object",
|
||||
"title": "LanguageModel",
|
||||
"definitions": {
|
||||
"OpenAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AmazonBedrockLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleGenerativeAILanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"GoogleVertexAnthropicLanguageModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"description": "OpenAI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gpt-4.1",
|
||||
"o4-mini",
|
||||
"o3",
|
||||
"o3-deep-research"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "amazon-bedrock",
|
||||
"description": "Amazon Bedrock Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"accessKeyId": {
|
||||
"description": "Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessKeySecret": {
|
||||
"description": "Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The AWS region. Defaults to the `AWS_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-east-1",
|
||||
"us-west-2",
|
||||
"eu-west-1"
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"description": "Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-generative-ai",
|
||||
"description": "Google Generative AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model."
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"token": {
|
||||
"description": "Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex",
|
||||
"description": "Google Vertex AI Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the language model.",
|
||||
"examples": [
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "google-vertex-anthropic",
|
||||
"description": "Google Vertex AI Anthropic Configuration"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "The name of the Anthropic language model running on Google Vertex.",
|
||||
"examples": [
|
||||
"claude-sonnet-4"
|
||||
]
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Optional display name."
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable."
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.",
|
||||
"examples": [
|
||||
"us-central1",
|
||||
"us-east1",
|
||||
"europe-west1"
|
||||
]
|
||||
},
|
||||
"credentials": {
|
||||
"description": "Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$",
|
||||
"description": "Optional base URL."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
export { schema as languageModelSchema };
|
||||
250
packages/schemas/src/v3/languageModel.type.ts
Normal file
250
packages/schemas/src/v3/languageModel.type.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
|
||||
|
||||
export type LanguageModel =
|
||||
| OpenAILanguageModel
|
||||
| AmazonBedrockLanguageModel
|
||||
| AnthropicLanguageModel
|
||||
| GoogleGenerativeAILanguageModel
|
||||
| GoogleVertexLanguageModel
|
||||
| GoogleVertexAnthropicLanguageModel;
|
||||
|
||||
export interface OpenAILanguageModel {
|
||||
/**
|
||||
* OpenAI Configuration
|
||||
*/
|
||||
provider: "openai";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional API key to use with the model. Defaults to the `OPENAI_API_KEY` environment variable.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface AmazonBedrockLanguageModel {
|
||||
/**
|
||||
* Amazon Bedrock Configuration
|
||||
*/
|
||||
provider: "amazon-bedrock";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional access key ID to use with the model. Defaults to the `AWS_ACCESS_KEY_ID` environment variable.
|
||||
*/
|
||||
accessKeyId?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional secret access key to use with the model. Defaults to the `AWS_SECRET_ACCESS_KEY` environment variable.
|
||||
*/
|
||||
accessKeySecret?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* The AWS region. Defaults to the `AWS_REGION` environment variable.
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface AnthropicLanguageModel {
|
||||
/**
|
||||
* Anthropic Configuration
|
||||
*/
|
||||
provider: "anthropic";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional API key to use with the model. Defaults to the `ANTHROPIC_API_KEY` environment variable.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface GoogleGenerativeAILanguageModel {
|
||||
/**
|
||||
* Google Generative AI Configuration
|
||||
*/
|
||||
provider: "google-generative-ai";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Optional API key to use with the model. Defaults to the `GOOGLE_GENERATIVE_AI_API_KEY` environment variable.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface GoogleVertexLanguageModel {
|
||||
/**
|
||||
* Google Vertex AI Configuration
|
||||
*/
|
||||
provider: "google-vertex";
|
||||
/**
|
||||
* The name of the language model.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable.
|
||||
*/
|
||||
project?: string;
|
||||
/**
|
||||
* The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.
|
||||
*/
|
||||
credentials?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
export interface GoogleVertexAnthropicLanguageModel {
|
||||
/**
|
||||
* Google Vertex AI Anthropic Configuration
|
||||
*/
|
||||
provider: "google-vertex-anthropic";
|
||||
/**
|
||||
* The name of the Anthropic language model running on Google Vertex.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* Optional display name.
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The Google Cloud project ID. Defaults to the `GOOGLE_VERTEX_PROJECT` environment variable.
|
||||
*/
|
||||
project?: string;
|
||||
/**
|
||||
* The Google Cloud region. Defaults to the `GOOGLE_VERTEX_REGION` environment variable.
|
||||
*/
|
||||
region?: string;
|
||||
/**
|
||||
* Optional file path to service account credentials JSON. Defaults to the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or application default credentials.
|
||||
*/
|
||||
credentials?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Optional base URL.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
|
@ -12,6 +12,12 @@
|
|||
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "3.0.0-beta.7",
|
||||
"@ai-sdk/anthropic": "2.0.0-beta.8",
|
||||
"@ai-sdk/google": "2.0.0-beta.11",
|
||||
"@ai-sdk/google-vertex": "3.0.0-beta.15",
|
||||
"@ai-sdk/openai": "2.0.0-beta.9",
|
||||
"@ai-sdk/react": "2.0.0-beta.21",
|
||||
"@auth/prisma-adapter": "^2.7.4",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/lang-cpp": "^6.0.2",
|
||||
|
|
@ -43,9 +49,14 @@
|
|||
"@hookform/resolvers": "^3.9.0",
|
||||
"@iconify/react": "^5.1.0",
|
||||
"@iizukak/codemirror-lang-wgsl": "^0.3.0",
|
||||
"@opentelemetry/api-logs": "^0.203.0",
|
||||
"@opentelemetry/instrumentation": "^0.203.0",
|
||||
"@opentelemetry/sdk-logs": "^0.203.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.5",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
|
|
@ -82,14 +93,17 @@
|
|||
"@stripe/react-stripe-js": "^3.1.1",
|
||||
"@stripe/stripe-js": "^5.6.0",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.53.3",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@tanstack/react-virtual": "^3.10.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/codemirror-themes": "^4.23.6",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@vercel/otel": "^1.13.0",
|
||||
"@viz-js/lang-dot": "^1.0.4",
|
||||
"@xiechao/codemirror-lang-handlebars": "^1.0.4",
|
||||
"ai": "5.0.0-beta.21",
|
||||
"ajv": "^8.17.1",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
|
@ -117,14 +131,17 @@
|
|||
"embla-carousel-react": "^8.3.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"google-auth-library": "^10.1.0",
|
||||
"graphql": "^16.9.0",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"langfuse": "^3.38.4",
|
||||
"langfuse-vercel": "^3.38.4",
|
||||
"lucide-react": "^0.517.0",
|
||||
"micromatch": "^4.0.8",
|
||||
"next": "14.2.26",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-navigation-guard": "^0.2.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"nodemailer": "^6.10.0",
|
||||
"octokit": "^4.1.3",
|
||||
|
|
@ -139,23 +156,32 @@
|
|||
"react-hook-form": "^7.53.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^2.1.1",
|
||||
"recharts": "^2.15.3",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"simple-git": "^3.27.0",
|
||||
"slate": "^0.117.0",
|
||||
"slate-dom": "^0.116.0",
|
||||
"slate-history": "^0.113.1",
|
||||
"slate-react": "^0.117.1",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"stripe": "^17.6.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"vscode-icons-js": "^11.6.1",
|
||||
"zod": "^3.24.3",
|
||||
"zod": "^3.25.74",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.74.7",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
|
|
|
|||
7
packages/web/public/anthropic.svg
Normal file
7
packages/web/public/anthropic.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Anthropic-Icon--Streamline-Svg-Logos" height="24" width="24">
|
||||
<desc>
|
||||
Anthropic Icon Streamline Icon: https://streamlinehq.com
|
||||
</desc>
|
||||
<path fill="#181818" d="m13.788825 3.932 6.43325 16.136075h3.5279L17.316725 3.932H13.788825Z" stroke-width="0.25"></path>
|
||||
<path fill="#181818" d="m6.325375 13.682775 2.20125 -5.67065 2.201275 5.67065H6.325375ZM6.68225 3.932 0.25 20.068075h3.596525l1.3155 -3.3886h6.729425l1.315275 3.3886h3.59655L10.371 3.932H6.68225Z" stroke-width="0.25"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 575 B |
1
packages/web/public/bedrock.svg
Normal file
1
packages/web/public/bedrock.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z"></path></svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
1
packages/web/public/gemini.svg
Normal file
1
packages/web/public/gemini.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 28 28"><defs><style>.cls-1{fill:url(#radial-gradient);}</style><radialGradient id="radial-gradient" cx="-576.08" cy="491.7" fx="-576.08" fy="491.7" r="1" gradientTransform="translate(-21336.18 116711.38) rotate(18.68) scale(29.8 -238.74)" gradientUnits="userSpaceOnUse"><stop offset=".07" stop-color="#9168c0"/><stop offset=".34" stop-color="#5684d1"/><stop offset=".67" stop-color="#1ba1e3"/></radialGradient></defs><path class="cls-1" d="M14,28c0-1.94-.37-3.76-1.12-5.46-.72-1.7-1.72-3.19-2.98-4.45-1.26-1.26-2.74-2.25-4.44-2.97-1.7-.75-3.52-1.12-5.46-1.12,1.94,0,3.76-.36,5.46-1.09,1.7-.75,3.19-1.75,4.44-3.01,1.26-1.26,2.25-2.74,2.98-4.44.75-1.7,1.12-3.52,1.12-5.46,0,1.94.36,3.76,1.09,5.46.75,1.7,1.75,3.19,3.01,4.44,1.26,1.26,2.74,2.26,4.45,3.01,1.7.72,3.52,1.09,5.46,1.09-1.94,0-3.76.37-5.46,1.12-1.7.72-3.19,1.71-4.45,2.97s-2.26,2.74-3.01,4.45c-.72,1.7-1.09,3.52-1.09,5.46Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
2
packages/web/public/openai.svg
Normal file
2
packages/web/public/openai.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -9,7 +9,7 @@ const markDecoration = Decoration.mark({
|
|||
});
|
||||
|
||||
const lineDecoration = Decoration.line({
|
||||
attributes: { class: "lineHighlight" },
|
||||
attributes: { class: "cm-range-border-radius lineHighlight" },
|
||||
});
|
||||
|
||||
export const rangeHighlightingExtension = (range: BrowseHighlightRange) => StateField.define<DecorationSet>({
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }:
|
|||
webUrl: repoInfoResponse.webUrl,
|
||||
}}
|
||||
pathType="tree"
|
||||
isFileIconVisible={false}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { useCallback } from "react";
|
||||
|
|
@ -13,15 +15,47 @@ export type BrowseHighlightRange = {
|
|||
|
||||
export const HIGHLIGHT_RANGE_QUERY_PARAM = 'highlightRange';
|
||||
|
||||
interface NavigateToPathOptions {
|
||||
export interface GetBrowsePathProps {
|
||||
repoName: string;
|
||||
revisionName?: string;
|
||||
path: string;
|
||||
pathType: 'blob' | 'tree';
|
||||
highlightRange?: BrowseHighlightRange;
|
||||
setBrowseState?: Partial<BrowseState>;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export const getBrowsePath = ({
|
||||
repoName,
|
||||
revisionName = 'HEAD',
|
||||
path,
|
||||
pathType,
|
||||
highlightRange,
|
||||
setBrowseState,
|
||||
domain,
|
||||
}: GetBrowsePathProps) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (highlightRange) {
|
||||
const { start, end } = highlightRange;
|
||||
|
||||
if ('column' in start && 'column' in end) {
|
||||
params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`);
|
||||
} else {
|
||||
params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (setBrowseState) {
|
||||
params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState));
|
||||
}
|
||||
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const browsePath = `/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${encodedPath}${params.size > 0 ? `?${params.toString()}` : ''}`;
|
||||
return browsePath;
|
||||
}
|
||||
|
||||
|
||||
export const useBrowseNavigation = () => {
|
||||
const router = useRouter();
|
||||
const domain = useDomain();
|
||||
|
|
@ -33,24 +67,18 @@ export const useBrowseNavigation = () => {
|
|||
pathType,
|
||||
highlightRange,
|
||||
setBrowseState,
|
||||
}: NavigateToPathOptions) => {
|
||||
const params = new URLSearchParams();
|
||||
}: Omit<GetBrowsePathProps, 'domain'>) => {
|
||||
const browsePath = getBrowsePath({
|
||||
repoName,
|
||||
revisionName,
|
||||
path,
|
||||
pathType,
|
||||
highlightRange,
|
||||
setBrowseState,
|
||||
domain,
|
||||
});
|
||||
|
||||
if (highlightRange) {
|
||||
const { start, end } = highlightRange;
|
||||
|
||||
if ('column' in start && 'column' in end) {
|
||||
params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`);
|
||||
} else {
|
||||
params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (setBrowseState) {
|
||||
params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState));
|
||||
}
|
||||
|
||||
router.push(`/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${path}?${params.toString()}`);
|
||||
router.push(browsePath);
|
||||
}, [domain, router]);
|
||||
|
||||
return {
|
||||
|
|
|
|||
32
packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts
Normal file
32
packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { getBrowsePath, GetBrowsePathProps } from "./useBrowseNavigation";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
||||
export const useBrowsePath = ({
|
||||
repoName,
|
||||
revisionName,
|
||||
path,
|
||||
pathType,
|
||||
highlightRange,
|
||||
setBrowseState,
|
||||
}: Omit<GetBrowsePathProps, 'domain'>) => {
|
||||
const domain = useDomain();
|
||||
|
||||
const browsePath = useMemo(() => {
|
||||
return getBrowsePath({
|
||||
repoName,
|
||||
revisionName,
|
||||
path,
|
||||
pathType,
|
||||
highlightRange,
|
||||
setBrowseState,
|
||||
domain,
|
||||
});
|
||||
}, [repoName, revisionName, path, pathType, highlightRange, setBrowseState, domain]);
|
||||
|
||||
return {
|
||||
path: browsePath,
|
||||
}
|
||||
}
|
||||
|
|
@ -6,33 +6,33 @@ import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle
|
|||
import { BrowseStateProvider } from "./browseStateProvider";
|
||||
import { FileTreePanel } from "@/features/fileTree/components/fileTreePanel";
|
||||
import { TopBar } from "@/app/[domain]/components/topBar";
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useBrowseParams } from "./hooks/useBrowseParams";
|
||||
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { SearchBar } from "../components/searchBar";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: {
|
||||
domain: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Layout({
|
||||
children: codePreviewPanel,
|
||||
params,
|
||||
children,
|
||||
}: LayoutProps) {
|
||||
const { repoName, revisionName } = useBrowseParams();
|
||||
const domain = useDomain();
|
||||
|
||||
return (
|
||||
<BrowseStateProvider>
|
||||
<div className="flex flex-col h-screen">
|
||||
<div className='sticky top-0 left-0 right-0 z-10'>
|
||||
<TopBar
|
||||
defaultSearchQuery={`repo:${repoName}${revisionName ? ` rev:${revisionName}` : ''} `}
|
||||
domain={params.domain}
|
||||
<TopBar
|
||||
domain={domain}
|
||||
>
|
||||
<SearchBar
|
||||
size="sm"
|
||||
defaultQuery={`repo:${repoName}${revisionName ? ` rev:${revisionName}` : ''} `}
|
||||
className="w-full"
|
||||
/>
|
||||
<Separator />
|
||||
</div>
|
||||
</TopBar>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
>
|
||||
|
|
@ -53,7 +53,7 @@ export default function Layout({
|
|||
order={1}
|
||||
id="code-preview-panel"
|
||||
>
|
||||
{codePreviewPanel}
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
<AnimatedResizableHandle />
|
||||
<BottomPanel
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
'use client';
|
||||
|
||||
import { ResizablePanel } from '@/components/ui/resizable';
|
||||
import { ChatThread } from '@/features/chat/components/chatThread';
|
||||
import { LanguageModelInfo, SBChatMessage, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types';
|
||||
import { RepositoryQuery } from '@/lib/types';
|
||||
import { CreateUIMessage } from 'ai';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useChatId } from '../../useChatId';
|
||||
|
||||
interface ChatThreadPanelProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
order: number;
|
||||
messages: SBChatMessage[];
|
||||
isChatReadonly: boolean;
|
||||
}
|
||||
|
||||
export const ChatThreadPanel = ({
|
||||
languageModels,
|
||||
repos,
|
||||
order,
|
||||
messages,
|
||||
isChatReadonly,
|
||||
}: ChatThreadPanelProps) => {
|
||||
// @note: we are guaranteed to have a chatId because this component will only be
|
||||
// mounted when on a /chat/[id] route.
|
||||
const chatId = useChatId()!;
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [inputMessage, setInputMessage] = useState<CreateUIMessage<SBChatMessage> | undefined>(undefined);
|
||||
|
||||
// Use the last user's last message to determine what repos we should select by default.
|
||||
const [selectedRepos, setSelectedRepos] = useState<string[]>(messages.findLast((message) => message.role === "user")?.metadata?.selectedRepos ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
const setChatState = searchParams.get(SET_CHAT_STATE_QUERY_PARAM);
|
||||
if (!setChatState) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { inputMessage, selectedRepos } = JSON.parse(setChatState) as SetChatStatePayload;
|
||||
setInputMessage(inputMessage);
|
||||
setSelectedRepos(selectedRepos);
|
||||
} catch {
|
||||
console.error('Invalid message in URL');
|
||||
}
|
||||
|
||||
// Remove the message from the URL
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||
newSearchParams.delete(SET_CHAT_STATE_QUERY_PARAM);
|
||||
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
|
||||
}, [searchParams, router]);
|
||||
|
||||
return (
|
||||
<ResizablePanel
|
||||
order={order}
|
||||
id="chat-thread-panel"
|
||||
defaultSize={85}
|
||||
>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<ChatThread
|
||||
id={chatId}
|
||||
initialMessages={messages}
|
||||
inputMessage={inputMessage}
|
||||
languageModels={languageModels}
|
||||
repos={repos}
|
||||
selectedRepos={selectedRepos}
|
||||
onSelectedReposChange={setSelectedRepos}
|
||||
isChatReadonly={isChatReadonly}
|
||||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
)
|
||||
}
|
||||
84
packages/web/src/app/[domain]/chat/[id]/page.tsx
Normal file
84
packages/web/src/app/[domain]/chat/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { getRepos } from '@/actions';
|
||||
import { getUserChatHistory, getConfiguredLanguageModelsInfo, getChatInfo } from '@/features/chat/actions';
|
||||
import { ServiceErrorException } from '@/lib/serviceError';
|
||||
import { isServiceError } from '@/lib/utils';
|
||||
import { ChatThreadPanel } from './components/chatThreadPanel';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { TopBar } from '../../components/topBar';
|
||||
import { ChatName } from '../components/chatName';
|
||||
import { auth } from '@/auth';
|
||||
import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle';
|
||||
import { ChatSidePanel } from '../components/chatSidePanel';
|
||||
import { ResizablePanelGroup } from '@/components/ui/resizable';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
domain: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const languageModels = await getConfiguredLanguageModelsInfo();
|
||||
const repos = await getRepos(params.domain);
|
||||
const chatInfo = await getChatInfo({ chatId: params.id }, params.domain);
|
||||
const session = await auth();
|
||||
const chatHistory = session ? await getUserChatHistory(params.domain) : [];
|
||||
|
||||
if (isServiceError(chatHistory)) {
|
||||
throw new ServiceErrorException(chatHistory);
|
||||
}
|
||||
|
||||
if (isServiceError(repos)) {
|
||||
throw new ServiceErrorException(repos);
|
||||
}
|
||||
|
||||
if (isServiceError(chatInfo)) {
|
||||
if (chatInfo.statusCode === StatusCodes.NOT_FOUND) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
throw new ServiceErrorException(chatInfo);
|
||||
}
|
||||
|
||||
const { messages, name, visibility, isReadonly } = chatInfo;
|
||||
|
||||
const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBar
|
||||
domain={params.domain}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="text-muted mx-2 select-none">/</span>
|
||||
<ChatName
|
||||
name={name}
|
||||
visibility={visibility}
|
||||
id={params.id}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
</div>
|
||||
</TopBar>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
>
|
||||
<ChatSidePanel
|
||||
order={1}
|
||||
chatHistory={chatHistory}
|
||||
isAuthenticated={!!session}
|
||||
isCollapsedInitially={true}
|
||||
/>
|
||||
<AnimatedResizableHandle />
|
||||
<ChatThreadPanel
|
||||
languageModels={languageModels}
|
||||
repos={indexedRepos}
|
||||
messages={messages}
|
||||
order={2}
|
||||
isChatReadonly={isReadonly}
|
||||
/>
|
||||
</ResizablePanelGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
94
packages/web/src/app/[domain]/chat/components/chatName.tsx
Normal file
94
packages/web/src/app/[domain]/chat/components/chatName.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
'use client';
|
||||
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { updateChatName } from "@/features/chat/actions";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { GlobeIcon } from "@radix-ui/react-icons";
|
||||
import { ChatVisibility } from "@sourcebot/db";
|
||||
import { LockIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RenameChatDialog } from "./renameChatDialog";
|
||||
|
||||
interface ChatNameProps {
|
||||
name: string | null;
|
||||
visibility: ChatVisibility;
|
||||
id: string;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
|
||||
export const ChatName = ({ name, visibility, id, isReadonly }: ChatNameProps) => {
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const domain = useDomain();
|
||||
const router = useRouter();
|
||||
|
||||
const onRenameChat = useCallback(async (name: string) => {
|
||||
|
||||
const response = await updateChatName({
|
||||
chatId: id,
|
||||
name: name,
|
||||
}, domain);
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
description: `❌ Failed to rename chat. Reason: ${response.message}`
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Chat renamed successfully`
|
||||
});
|
||||
router.refresh();
|
||||
}
|
||||
}, [id, domain, toast, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p
|
||||
className="text-sm font-medium hover:underline cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsRenameDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
{name ?? 'Untitled chat'}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Rename chat
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{visibility && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Badge variant="outline" className="cursor-default">
|
||||
{visibility === ChatVisibility.PUBLIC ? (
|
||||
<GlobeIcon className="w-3 h-3 mr-1" />
|
||||
) : (
|
||||
<LockIcon className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
{visibility === ChatVisibility.PUBLIC ? (isReadonly ? 'Public (Read-only)' : 'Public') : 'Private'}
|
||||
</Badge>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{visibility === ChatVisibility.PUBLIC ? `Anyone with the link can view this chat${!isReadonly ? ' and ask follow-up questions' : ''}.` : 'Only you can view and edit this chat.'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<RenameChatDialog
|
||||
isOpen={isRenameDialogOpen}
|
||||
onOpenChange={setIsRenameDialogOpen}
|
||||
onRename={onRenameChat}
|
||||
currentName={name ?? ""}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
264
packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx
Normal file
264
packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
'use client';
|
||||
|
||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { ResizablePanel } from "@/components/ui/resizable";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { deleteChat, updateChatName } from "@/features/chat/actions";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { cn, isServiceError } from "@/lib/utils";
|
||||
import { CirclePlusIcon, EllipsisIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import {
|
||||
GoSidebarCollapse as ExpandIcon,
|
||||
} from "react-icons/go";
|
||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||
import { useChatId } from "../useChatId";
|
||||
import { RenameChatDialog } from "./renameChatDialog";
|
||||
import { DeleteChatDialog } from "./deleteChatDialog";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ChatSidePanelProps {
|
||||
order: number;
|
||||
chatHistory: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
createdAt: Date;
|
||||
}[];
|
||||
isAuthenticated: boolean;
|
||||
isCollapsedInitially: boolean;
|
||||
}
|
||||
|
||||
export const ChatSidePanel = ({
|
||||
order,
|
||||
chatHistory,
|
||||
isAuthenticated,
|
||||
isCollapsedInitially,
|
||||
}: ChatSidePanelProps) => {
|
||||
const domain = useDomain();
|
||||
const [isCollapsed, setIsCollapsed] = useState(isCollapsedInitially);
|
||||
const sidePanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const chatId = useChatId();
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const [chatIdToRename, setChatIdToRename] = useState<string | null>(null);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [chatIdToDelete, setChatIdToDelete] = useState<string | null>(null);
|
||||
|
||||
useHotkeys("mod+b", () => {
|
||||
if (isCollapsed) {
|
||||
sidePanelRef.current?.expand();
|
||||
} else {
|
||||
sidePanelRef.current?.collapse();
|
||||
}
|
||||
}, {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
description: "Toggle side panel",
|
||||
});
|
||||
|
||||
const onRenameChat = useCallback(async (name: string, chatId: string) => {
|
||||
if (!chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await updateChatName({
|
||||
chatId,
|
||||
name: name,
|
||||
}, domain);
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
description: `❌ Failed to rename chat. Reason: ${response.message}`
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Chat renamed successfully`
|
||||
});
|
||||
router.refresh();
|
||||
}
|
||||
}, [router, toast, domain]);
|
||||
|
||||
const onDeleteChat = useCallback(async (chatIdToDelete: string) => {
|
||||
if (!chatIdToDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteChat({ chatId: chatIdToDelete }, domain);
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
description: `❌ Failed to delete chat. Reason: ${response.message}`
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Chat deleted successfully`
|
||||
});
|
||||
|
||||
// If we just deleted the current chat, navigate to new chat
|
||||
if (chatIdToDelete === chatId) {
|
||||
router.push(`/${domain}/chat`);
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
}
|
||||
}, [chatId, router, toast, domain]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanel
|
||||
ref={sidePanelRef}
|
||||
order={order}
|
||||
minSize={10}
|
||||
maxSize={15}
|
||||
defaultSize={isCollapsed ? 0 : 15}
|
||||
collapsible={true}
|
||||
id="chat-side-panel"
|
||||
onCollapse={() => setIsCollapsed(true)}
|
||||
onExpand={() => setIsCollapsed(false)}
|
||||
>
|
||||
<div className="flex flex-col h-full py-4">
|
||||
<div className="px-2.5 mb-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
router.push(`/${domain}/chat`);
|
||||
}}
|
||||
>
|
||||
<CirclePlusIcon className="w-4 h-4 mr-1" />
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="flex flex-col h-full px-2.5">
|
||||
<p className="text-sm font-medium mb-4">Recent Chats</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{!isAuthenticated ? (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
<Link
|
||||
href={`/login?callbackUrl=${encodeURIComponent(`/${domain}/chat`)}`}
|
||||
className="text-sm text-link hover:underline cursor-pointer"
|
||||
>
|
||||
Sign in
|
||||
</Link> to access your chat history.
|
||||
</p>
|
||||
</div>
|
||||
) : chatHistory.length === 0 ? (
|
||||
<div className="mx-auto w-full h-52 border border-dashed border-muted-foreground rounded-md flex items-center justify-center p-6">
|
||||
<p className="text-sm text-muted-foreground text-center">Recent chats will appear here.</p>
|
||||
</div>
|
||||
) : chatHistory.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className={cn("group flex flex-row items-center justify-between hover:bg-muted rounded-md px-2 py-1.5 cursor-pointer",
|
||||
chat.id === chatId && "bg-muted"
|
||||
)}
|
||||
onClick={() => {
|
||||
router.push(`/${domain}/chat/${chat.id}`);
|
||||
}}
|
||||
>
|
||||
<span className="text-sm truncate">{chat.name ?? 'Untitled chat'}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 z-10 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-muted-accent"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<EllipsisIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="z-20"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setChatIdToRename(chat.id);
|
||||
setIsRenameDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="w-4 h-4 mr-2" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setChatIdToDelete(chat.id);
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
</ResizablePanel >
|
||||
{isCollapsed && (
|
||||
<div className="flex flex-col items-center h-full p-2">
|
||||
<Tooltip
|
||||
delayDuration={100}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => {
|
||||
sidePanelRef.current?.expand();
|
||||
}}
|
||||
>
|
||||
<ExpandIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="flex flex-row items-center gap-2">
|
||||
<KeyboardShortcutHint shortcut="⌘ B" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<span>Open side panel</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<RenameChatDialog
|
||||
isOpen={isRenameDialogOpen}
|
||||
onOpenChange={setIsRenameDialogOpen}
|
||||
onRename={(name) => {
|
||||
if (chatIdToRename) {
|
||||
onRenameChat(name, chatIdToRename);
|
||||
}
|
||||
}}
|
||||
currentName={chatHistory?.find((chat) => chat.id === chatIdToRename)?.name ?? ""}
|
||||
/>
|
||||
<DeleteChatDialog
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
onDelete={() => {
|
||||
if (chatIdToDelete) {
|
||||
onDeleteChat(chatIdToDelete);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useCallback } from "react";
|
||||
|
||||
interface DeleteChatDialogProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const DeleteChatDialog = ({ isOpen, onOpenChange, onDelete }: DeleteChatDialogProps) => {
|
||||
const handleDelete = useCallback(() => {
|
||||
onDelete();
|
||||
onOpenChange(false);
|
||||
}, [onDelete, onOpenChange]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete chat?</DialogTitle>
|
||||
<DialogDescription>
|
||||
The chat will be deleted and removed from your chat history. This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
'use client';
|
||||
|
||||
import { ResizablePanel } from "@/components/ui/resizable";
|
||||
import { ChatBox } from "@/features/chat/components/chatBox";
|
||||
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
||||
import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
|
||||
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
||||
import { LanguageModelInfo } from "@/features/chat/types";
|
||||
import { RepositoryQuery } from "@/lib/types";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Descendant } from "slate";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
|
||||
interface NewChatPanelProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
order: number;
|
||||
}
|
||||
|
||||
export const NewChatPanel = ({
|
||||
languageModels,
|
||||
repos,
|
||||
order,
|
||||
}: NewChatPanelProps) => {
|
||||
const [selectedRepos, setSelectedRepos] = useLocalStorage<string[]>("selectedRepos", []);
|
||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||
const [isRepoSelectorOpen, setIsRepoSelectorOpen] = useState(false);
|
||||
|
||||
const onSubmit = useCallback((children: Descendant[]) => {
|
||||
createNewChatThread(children, selectedRepos);
|
||||
}, [createNewChatThread, selectedRepos]);
|
||||
|
||||
|
||||
return (
|
||||
<ResizablePanel
|
||||
order={order}
|
||||
id="new-chat-panel"
|
||||
defaultSize={85}
|
||||
>
|
||||
<div className="flex flex-col h-full w-full items-center justify-start pt-[20vh]">
|
||||
<h2 className="text-4xl font-bold mb-8">What can I help you understand?</h2>
|
||||
<div className="border rounded-md w-full max-w-3xl mx-auto mb-8 shadow-sm">
|
||||
<CustomSlateEditor>
|
||||
<ChatBox
|
||||
onSubmit={onSubmit}
|
||||
className="min-h-[80px]"
|
||||
preferredSuggestionsBoxPlacement="bottom-start"
|
||||
isRedirecting={isLoading}
|
||||
languageModels={languageModels}
|
||||
selectedRepos={selectedRepos}
|
||||
onRepoSelectorOpenChanged={setIsRepoSelectorOpen}
|
||||
/>
|
||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||
<ChatBoxToolbar
|
||||
languageModels={languageModels}
|
||||
repos={repos}
|
||||
selectedRepos={selectedRepos}
|
||||
onSelectedReposChange={setSelectedRepos}
|
||||
isRepoSelectorOpen={isRepoSelectorOpen}
|
||||
onRepoSelectorOpenChanged={setIsRepoSelectorOpen}
|
||||
/>
|
||||
</div>
|
||||
</CustomSlateEditor>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
interface RenameChatDialogProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onRename: (name: string) => void;
|
||||
currentName: string;
|
||||
}
|
||||
|
||||
export const RenameChatDialog = ({ isOpen, onOpenChange, onRename, currentName }: RenameChatDialogProps) => {
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
name: currentName,
|
||||
});
|
||||
}, [currentName, form]);
|
||||
|
||||
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
||||
onRename(data.name);
|
||||
form.reset();
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename Chat</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{`Rename "${currentName ?? 'untitled chat'}" to a new name.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form
|
||||
{...form}
|
||||
>
|
||||
<form
|
||||
className="space-y-4 flex flex-col w-full"
|
||||
onSubmit={(event) => {
|
||||
event.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(event);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<FormControl
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Enter chat name"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
18
packages/web/src/app/[domain]/chat/layout.tsx
Normal file
18
packages/web/src/app/[domain]/chat/layout.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { NavigationGuardProvider } from 'next-navigation-guard';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: LayoutProps) {
|
||||
|
||||
return (
|
||||
// @note: we use a navigation guard here since we don't support resuming streams yet.
|
||||
// @see: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence#resuming-ongoing-streams
|
||||
<NavigationGuardProvider>
|
||||
<div className="flex flex-col h-screen w-screen">
|
||||
{children}
|
||||
</div>
|
||||
</NavigationGuardProvider>
|
||||
)
|
||||
}
|
||||
57
packages/web/src/app/[domain]/chat/page.tsx
Normal file
57
packages/web/src/app/[domain]/chat/page.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { getRepos } from "@/actions";
|
||||
import { getUserChatHistory, getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
|
||||
import { ServiceErrorException } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { NewChatPanel } from "./components/newChatPanel";
|
||||
import { TopBar } from "../components/topBar";
|
||||
import { ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import { ChatSidePanel } from "./components/chatSidePanel";
|
||||
import { auth } from "@/auth";
|
||||
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
domain: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const languageModels = await getConfiguredLanguageModelsInfo();
|
||||
const repos = await getRepos(params.domain);
|
||||
const session = await auth();
|
||||
const chatHistory = session ? await getUserChatHistory(params.domain) : [];
|
||||
|
||||
if (isServiceError(chatHistory)) {
|
||||
throw new ServiceErrorException(chatHistory);
|
||||
}
|
||||
|
||||
if (isServiceError(repos)) {
|
||||
throw new ServiceErrorException(repos);
|
||||
}
|
||||
|
||||
const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBar
|
||||
domain={params.domain}
|
||||
/>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
>
|
||||
<ChatSidePanel
|
||||
order={1}
|
||||
chatHistory={chatHistory}
|
||||
isAuthenticated={!!session}
|
||||
isCollapsedInitially={false}
|
||||
/>
|
||||
<AnimatedResizableHandle />
|
||||
<NewChatPanel
|
||||
languageModels={languageModels}
|
||||
repos={indexedRepos}
|
||||
order={2}
|
||||
/>
|
||||
</ResizablePanelGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
packages/web/src/app/[domain]/chat/useChatId.ts
Normal file
8
packages/web/src/app/[domain]/chat/useChatId.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
export const useChatId = (): string | undefined => {
|
||||
const { id: chatId } = useParams<{ id: string }>();
|
||||
return chatId;
|
||||
}
|
||||
39
packages/web/src/app/[domain]/components/copyIconButton.tsx
Normal file
39
packages/web/src/app/[domain]/components/copyIconButton.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle2, Copy } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
interface CopyIconButtonProps {
|
||||
onCopy: () => boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CopyIconButton = ({ onCopy, className }: CopyIconButtonProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
const success = onCopy();
|
||||
if (success) {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [onCopy]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn("h-6 w-6 text-muted-foreground", className)}
|
||||
onClick={onClick}
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ChatBox } from "@/features/chat/components/chatBox";
|
||||
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
||||
import { LanguageModelInfo } from "@/features/chat/types";
|
||||
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
||||
import { resetEditor } from "@/features/chat/utils";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { RepositoryQuery } from "@/lib/types";
|
||||
import { getDisplayTime } from "@/lib/utils";
|
||||
import { BrainIcon, FileIcon, LucideIcon, SearchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ReactEditor, useSlate } from "slate-react";
|
||||
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
|
||||
// @todo: we should probably rename this to a different type since it sort-of clashes
|
||||
// with the Suggestion system we have built into the chat box.
|
||||
type SuggestionType = "understand" | "find" | "summarize";
|
||||
|
||||
const suggestionTypes: Record<SuggestionType, {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}> = {
|
||||
understand: {
|
||||
icon: BrainIcon,
|
||||
title: "Understand",
|
||||
description: "Understand the codebase",
|
||||
},
|
||||
find: {
|
||||
icon: SearchIcon,
|
||||
title: "Find",
|
||||
description: "Find the codebase",
|
||||
},
|
||||
summarize: {
|
||||
icon: FileIcon,
|
||||
title: "Summarize",
|
||||
description: "Summarize the codebase",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-highlight">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const suggestions: Record<SuggestionType, {
|
||||
queryText: string;
|
||||
queryNode?: ReactNode;
|
||||
openRepoSelector?: boolean;
|
||||
}[]> = {
|
||||
understand: [
|
||||
{
|
||||
queryText: "How does authentication work in this codebase?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "How are API endpoints structured and organized?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "How does the build and deployment process work?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "How is error handling implemented across the application?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
],
|
||||
find: [
|
||||
{
|
||||
queryText: "Find examples of different logging libraries used throughout the codebase.",
|
||||
},
|
||||
{
|
||||
queryText: "Find examples of potential security vulnerabilities or authentication issues.",
|
||||
},
|
||||
{
|
||||
queryText: "Find examples of API endpoints and route handlers.",
|
||||
}
|
||||
],
|
||||
summarize: [
|
||||
{
|
||||
queryText: "Summarize the purpose of this file @file:",
|
||||
queryNode: <span>Summarize the purpose of this file <Highlight>@file:</Highlight></span>
|
||||
},
|
||||
{
|
||||
queryText: "Summarize the project structure and architecture.",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "Provide a quick start guide for ramping up on this codebase.",
|
||||
openRepoSelector: true,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
const MAX_RECENT_CHAT_HISTORY_COUNT = 10;
|
||||
|
||||
|
||||
interface AgenticSearchProps {
|
||||
searchModeSelectorProps: SearchModeSelectorProps;
|
||||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
chatHistory: {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
name: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const AgenticSearch = ({
|
||||
searchModeSelectorProps,
|
||||
languageModels,
|
||||
repos,
|
||||
chatHistory,
|
||||
}: AgenticSearchProps) => {
|
||||
const [selectedSuggestionType, _setSelectedSuggestionType] = useState<SuggestionType | undefined>(undefined);
|
||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const editor = useSlate();
|
||||
const [selectedRepos, setSelectedRepos] = useLocalStorage<string[]>("selectedRepos", []);
|
||||
const domain = useDomain();
|
||||
const [isRepoSelectorOpen, setIsRepoSelectorOpen] = useState(false);
|
||||
|
||||
const setSelectedSuggestionType = useCallback((type: SuggestionType | undefined) => {
|
||||
_setSelectedSuggestionType(type);
|
||||
if (type) {
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
}, [editor, _setSelectedSuggestionType]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
!dropdownRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
setSelectedSuggestionType(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||
}, [setSelectedSuggestionType]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full max-w-[800px]">
|
||||
<div
|
||||
className="mt-4 w-full border rounded-md shadow-sm"
|
||||
>
|
||||
<ChatBox
|
||||
onSubmit={(children) => {
|
||||
createNewChatThread(children, selectedRepos);
|
||||
}}
|
||||
className="min-h-[50px]"
|
||||
isRedirecting={isLoading}
|
||||
languageModels={languageModels}
|
||||
selectedRepos={selectedRepos}
|
||||
onRepoSelectorOpenChanged={setIsRepoSelectorOpen}
|
||||
/>
|
||||
<Separator />
|
||||
<div className="relative">
|
||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||
<ChatBoxToolbar
|
||||
languageModels={languageModels}
|
||||
repos={repos}
|
||||
selectedRepos={selectedRepos}
|
||||
onSelectedReposChange={setSelectedRepos}
|
||||
isRepoSelectorOpen={isRepoSelectorOpen}
|
||||
onRepoSelectorOpenChanged={setIsRepoSelectorOpen}
|
||||
/>
|
||||
<SearchModeSelector
|
||||
{...searchModeSelectorProps}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedSuggestionType && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="w-full absolute top-10 z-10 drop-shadow-2xl bg-background border rounded-md p-2"
|
||||
>
|
||||
<p className="text-muted-foreground text-sm mb-2">
|
||||
{suggestionTypes[selectedSuggestionType].title}
|
||||
</p>
|
||||
{suggestions[selectedSuggestionType].map(({ queryText, queryNode, openRepoSelector }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-row items-center gap-2 cursor-pointer hover:bg-muted rounded-md px-1 py-0.5"
|
||||
onClick={() => {
|
||||
resetEditor(editor);
|
||||
editor.insertText(queryText);
|
||||
setSelectedSuggestionType(undefined);
|
||||
|
||||
if (openRepoSelector) {
|
||||
setIsRepoSelectorOpen(true);
|
||||
} else {
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchIcon className="w-4 h-4" />
|
||||
{queryNode ?? queryText}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center w-fit gap-6 mt-8 relative">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{Object.entries(suggestionTypes).map(([type, suggestion], index) => (
|
||||
<ExampleButton
|
||||
key={index}
|
||||
Icon={suggestion.icon}
|
||||
title={suggestion.title}
|
||||
onClick={() => {
|
||||
setSelectedSuggestionType(type as SuggestionType);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{chatHistory.length > 0 && (
|
||||
<div className="flex flex-col items-center w-[80%]">
|
||||
<Separator className="my-6" />
|
||||
<span className="font-semibold mb-2">Recent conversations</span>
|
||||
<div
|
||||
className="flex flex-col gap-1 w-full"
|
||||
>
|
||||
{chatHistory
|
||||
.slice(0, MAX_RECENT_CHAT_HISTORY_COUNT)
|
||||
.map((chat) => (
|
||||
<Link
|
||||
key={chat.id}
|
||||
className="flex flex-row items-center justify-between gap-1 w-full rounded-md hover:bg-muted px-2 py-0.5 cursor-pointer group"
|
||||
href={`/${domain}/chat/${chat.id}`}
|
||||
>
|
||||
<span className="text-sm text-muted-foreground group-hover:text-foreground">
|
||||
{chat.name ?? "Untitled Chat"}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground group-hover:text-foreground">
|
||||
{getDisplayTime(chat.createdAt)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{chatHistory.length > MAX_RECENT_CHAT_HISTORY_COUNT && (
|
||||
<Link
|
||||
href={`/${domain}/chat`}
|
||||
className="text-sm text-link hover:underline mt-6"
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
interface ExampleButtonProps {
|
||||
Icon: LucideIcon;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const ExampleButton = ({
|
||||
Icon,
|
||||
title,
|
||||
onClick,
|
||||
}: ExampleButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
className="h-9"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{title}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
84
packages/web/src/app/[domain]/components/homepage/index.tsx
Normal file
84
packages/web/src/app/[domain]/components/homepage/index.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
'use client';
|
||||
|
||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||
import { LanguageModelInfo } from "@/features/chat/types";
|
||||
import { RepositoryQuery } from "@/lib/types";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { AgenticSearch } from "./agenticSearch";
|
||||
import { PreciseSearch } from "./preciseSearch";
|
||||
import { SearchMode } from "./toolbar";
|
||||
import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
|
||||
|
||||
interface HomepageProps {
|
||||
initialRepos: RepositoryQuery[];
|
||||
languageModels: LanguageModelInfo[];
|
||||
chatHistory: {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
name: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
||||
export const Homepage = ({
|
||||
initialRepos,
|
||||
languageModels,
|
||||
chatHistory,
|
||||
}: HomepageProps) => {
|
||||
const [searchMode, setSearchMode] = useLocalStorage<SearchMode>("search-mode", "precise", { initializeWithValue: false });
|
||||
const isAgenticSearchEnabled = languageModels.length > 0;
|
||||
|
||||
useHotkeys("mod+i", (e) => {
|
||||
e.preventDefault();
|
||||
setSearchMode("agentic");
|
||||
}, {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
description: "Switch to agentic search",
|
||||
});
|
||||
|
||||
useHotkeys("mod+p", (e) => {
|
||||
e.preventDefault();
|
||||
setSearchMode("precise");
|
||||
}, {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
description: "Switch to precise search",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||
<div className="max-h-44 w-auto">
|
||||
<SourcebotLogo
|
||||
className="h-18 md:h-40 w-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{searchMode === "precise" ? (
|
||||
<PreciseSearch
|
||||
initialRepos={initialRepos}
|
||||
searchModeSelectorProps={{
|
||||
searchMode: "precise",
|
||||
isAgenticSearchEnabled,
|
||||
onSearchModeChange: setSearchMode,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CustomSlateEditor>
|
||||
<AgenticSearch
|
||||
searchModeSelectorProps={{
|
||||
searchMode: "agentic",
|
||||
isAgenticSearchEnabled,
|
||||
onSearchModeChange: setSearchMode,
|
||||
}}
|
||||
languageModels={languageModels}
|
||||
repos={initialRepos}
|
||||
chatHistory={chatHistory}
|
||||
/>
|
||||
</CustomSlateEditor>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
'use client';
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SyntaxReferenceGuideHint } from "../syntaxReferenceGuideHint";
|
||||
import { RepositorySnapshot } from "./repositorySnapshot";
|
||||
import { RepositoryQuery } from "@/lib/types";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import Link from "next/link";
|
||||
import { SearchBar } from "../searchBar/searchBar";
|
||||
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
|
||||
|
||||
interface PreciseSearchProps {
|
||||
initialRepos: RepositoryQuery[];
|
||||
searchModeSelectorProps: SearchModeSelectorProps;
|
||||
}
|
||||
|
||||
export const PreciseSearch = ({
|
||||
initialRepos,
|
||||
searchModeSelectorProps,
|
||||
}: PreciseSearchProps) => {
|
||||
const domain = useDomain();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-4 w-full max-w-[800px] border rounded-md shadow-sm">
|
||||
<SearchBar
|
||||
autoFocus={true}
|
||||
className="border-none pt-0.5 pb-0"
|
||||
/>
|
||||
<Separator />
|
||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||
<SearchModeSelector
|
||||
{...searchModeSelectorProps}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<RepositorySnapshot
|
||||
repos={initialRepos}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center w-fit gap-6">
|
||||
<Separator className="mt-5" />
|
||||
<span className="font-semibold">How to search</span>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<HowToSection
|
||||
title="Search in files or paths"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="test todo" domain={domain}>test todo</Query> <QueryExplanation>(both test and todo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="test or todo" domain={domain}>test <Highlight>or</Highlight> todo</Query> <QueryExplanation>(either test or todo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query={`"exit boot"`} domain={domain}>{`"exit boot"`}</Query> <QueryExplanation>(exact match)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="TODO case:yes" domain={domain}>TODO <Highlight>case:</Highlight>yes</Query> <QueryExplanation>(case sensitive)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
<HowToSection
|
||||
title="Filter results"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="file:README setup" domain={domain}><Highlight>file:</Highlight>README setup</Query> <QueryExplanation>(by filename)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="repo:torvalds/linux test" domain={domain}><Highlight>repo:</Highlight>torvalds/linux test</Query> <QueryExplanation>(by repo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="lang:typescript" domain={domain}><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="rev:HEAD" domain={domain}><Highlight>rev:</Highlight>HEAD</Query> <QueryExplanation>(by branch or tag)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
<HowToSection
|
||||
title="Advanced"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="file:\.py$" domain={domain}><Highlight>file:</Highlight>{`\\.py$`}</Query> <QueryExplanation>{`(files that end in ".py")`}</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="sym:main" domain={domain}><Highlight>sym:</Highlight>main</Query> <QueryExplanation>{`(symbols named "main")`}</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="todo -lang:c" domain={domain}>todo <Highlight>-lang:c</Highlight></Query> <QueryExplanation>(negate filter)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="content:README" domain={domain}><Highlight>content:</Highlight>README</Query> <QueryExplanation>(search content only)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
</div>
|
||||
<SyntaxReferenceGuideHint />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const HowToSection = ({ title, children }: { title: string, children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="dark:text-gray-300 text-sm mb-2 underline">{title}</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-highlight">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryExample = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-sm font-mono">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryExplanation = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-3">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Query = ({ query, domain, children }: { query: string, domain: string, children: React.ReactNode }) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/${domain}/search?query=${query}`}
|
||||
className="cursor-pointer hover:underline"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export function RepositorySnapshot({
|
|||
{`Search ${indexedRepos.length} `}
|
||||
<Link
|
||||
href={`${domain}/repos`}
|
||||
className="text-blue-500"
|
||||
className="text-link hover:underline"
|
||||
>
|
||||
{indexedRepos.length > 1 ? 'repositories' : 'repository'}
|
||||
</Link>
|
||||
157
packages/web/src/app/[domain]/components/homepage/toolbar.tsx
Normal file
157
packages/web/src/app/[domain]/components/homepage/toolbar.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
'use client';
|
||||
|
||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MessageCircleIcon, SearchIcon, TriangleAlert } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
export type SearchMode = "precise" | "agentic";
|
||||
|
||||
const PRECISE_SEARCH_DOCS_URL = "https://docs.sourcebot.dev/docs/features/search/overview";
|
||||
// @tood: point this to the actual docs page
|
||||
const AGENTIC_SEARCH_DOCS_URL = "https://docs.sourcebot.dev/docs/features/ask/overview";
|
||||
|
||||
export interface SearchModeSelectorProps {
|
||||
searchMode: SearchMode;
|
||||
isAgenticSearchEnabled: boolean;
|
||||
onSearchModeChange: (searchMode: SearchMode) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SearchModeSelector = ({
|
||||
searchMode,
|
||||
isAgenticSearchEnabled,
|
||||
onSearchModeChange,
|
||||
className,
|
||||
}: SearchModeSelectorProps) => {
|
||||
const [focusedSearchMode, setFocusedSearchMode] = useState<SearchMode>(searchMode);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-row items-center", className)}>
|
||||
<Select
|
||||
value={searchMode}
|
||||
onValueChange={(value) => onSearchModeChange(value as "precise" | "agentic")}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="flex flex-row items-center h-6 mt-0.5 font-mono font-semibold text-xs p-0 w-fit border-none bg-inherit rounded-md"
|
||||
>
|
||||
{searchMode === "precise" ? (
|
||||
<SearchIcon className="w-4 h-4 text-muted-foreground mr-1.5" />
|
||||
) : (
|
||||
<MessageCircleIcon className="w-4 h-4 text-muted-foreground mr-1.5" />
|
||||
)}
|
||||
<SelectValue>
|
||||
{searchMode === "precise" ? "Code Search" : "Ask"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent
|
||||
className="overflow-visible relative"
|
||||
>
|
||||
<Tooltip
|
||||
delayDuration={100}
|
||||
open={focusedSearchMode === "precise"}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseEnter={() => setFocusedSearchMode("precise")}
|
||||
onFocus={() => setFocusedSearchMode("precise")}
|
||||
>
|
||||
<SelectItem
|
||||
value="precise"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-1.5">
|
||||
<span>Search</span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<KeyboardShortcutHint shortcut="⌘ P" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</SelectItem>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="w-64 z-50"
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-semibold">Code Search</p>
|
||||
<Separator orientation="horizontal" className="w-full my-0.5" />
|
||||
<p>Search for exact matches using regular expressions and filters.</p>
|
||||
<Link
|
||||
href={PRECISE_SEARCH_DOCS_URL}
|
||||
className="text-link hover:underline"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
<Tooltip delayDuration={100} open={focusedSearchMode === "agentic"}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseEnter={() => setFocusedSearchMode("agentic")}
|
||||
onFocus={() => setFocusedSearchMode("agentic")}
|
||||
className={cn({
|
||||
"cursor-not-allowed": !isAgenticSearchEnabled,
|
||||
})}
|
||||
>
|
||||
<SelectItem
|
||||
value="agentic"
|
||||
disabled={!isAgenticSearchEnabled}
|
||||
className={cn({
|
||||
"cursor-pointer": isAgenticSearchEnabled,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-1.5">
|
||||
<span>Ask</span>
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<KeyboardShortcutHint shortcut="⌘ I" />
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="w-64 z-50"
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{!isAgenticSearchEnabled && (
|
||||
<TriangleAlert className="w-4 h-4 flex-shrink-0 text-warning" />
|
||||
)}
|
||||
<p className="font-semibold">Ask Sourcebot</p>
|
||||
</div>
|
||||
{!isAgenticSearchEnabled && (
|
||||
<p className="text-destructive">Language model not configured. <Link href={AGENTIC_SEARCH_DOCS_URL} className="text-link hover:underline">See setup instructions.</Link></p>
|
||||
)}
|
||||
<Separator orientation="horizontal" className="w-full my-0.5" />
|
||||
<p>Use natural language to search, summarize and understand your codebase using a reasoning agent.</p>
|
||||
<Link
|
||||
href={AGENTIC_SEARCH_DOCS_URL}
|
||||
className="text-link hover:underline"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -153,7 +153,6 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
|
|||
</span>
|
||||
)}
|
||||
<span
|
||||
className="cm-line"
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingLeft: '6px',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { getCodeHostInfoForRepo } from "@/lib/utils";
|
||||
import { cn, getCodeHostInfoForRepo } from "@/lib/utils";
|
||||
import { LaptopIcon } from "@radix-ui/react-icons";
|
||||
import clsx from "clsx";
|
||||
import Image from "next/image";
|
||||
import { useBrowseNavigation } from "../browse/hooks/useBrowseNavigation";
|
||||
import { Copy, CheckCircle2, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import { useCallback, useState, useMemo, useRef, useEffect } from "react";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import {
|
||||
|
|
@ -14,6 +13,8 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||
import { CopyIconButton } from "./copyIconButton";
|
||||
|
||||
interface FileHeaderProps {
|
||||
path: string;
|
||||
|
|
@ -30,6 +31,9 @@ interface FileHeaderProps {
|
|||
},
|
||||
branchDisplayName?: string;
|
||||
branchDisplayTitle?: string;
|
||||
isCodeHostIconVisible?: boolean;
|
||||
isFileIconVisible?: boolean;
|
||||
repoNameClassName?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbSegment {
|
||||
|
|
@ -49,6 +53,9 @@ export const PathHeader = ({
|
|||
branchDisplayName,
|
||||
branchDisplayTitle,
|
||||
pathType = 'blob',
|
||||
isCodeHostIconVisible = true,
|
||||
isFileIconVisible = true,
|
||||
repoNameClassName,
|
||||
}: FileHeaderProps) => {
|
||||
const info = getCodeHostInfoForRepo({
|
||||
name: repo.name,
|
||||
|
|
@ -59,28 +66,26 @@ export const PathHeader = ({
|
|||
|
||||
const { navigateToPath } = useBrowseNavigation();
|
||||
const { toast } = useToast();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const breadcrumbsRef = useRef<HTMLDivElement>(null);
|
||||
const [visibleSegmentCount, setVisibleSegmentCount] = useState<number | null>(null);
|
||||
|
||||
|
||||
// Create breadcrumb segments from file path
|
||||
const breadcrumbSegments = useMemo(() => {
|
||||
const pathParts = path.split('/').filter(Boolean);
|
||||
const segments: BreadcrumbSegment[] = [];
|
||||
|
||||
|
||||
let currentPath = '';
|
||||
pathParts.forEach((part, index) => {
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
const isLastSegment = index === pathParts.length - 1;
|
||||
|
||||
|
||||
// Calculate highlight range for this segment if it exists
|
||||
let segmentHighlight: { from: number; to: number } | undefined;
|
||||
if (pathHighlightRange) {
|
||||
const segmentStart = path.indexOf(part, currentPath.length - part.length);
|
||||
const segmentEnd = segmentStart + part.length;
|
||||
|
||||
|
||||
// Check if highlight overlaps with this segment
|
||||
if (pathHighlightRange.from < segmentEnd && pathHighlightRange.to > segmentStart) {
|
||||
segmentHighlight = {
|
||||
|
|
@ -89,7 +94,7 @@ export const PathHeader = ({
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
segments.push({
|
||||
name: part,
|
||||
fullPath: currentPath,
|
||||
|
|
@ -97,7 +102,7 @@ export const PathHeader = ({
|
|||
highlightRange: segmentHighlight
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return segments;
|
||||
}, [path, pathHighlightRange]);
|
||||
|
||||
|
|
@ -105,10 +110,10 @@ export const PathHeader = ({
|
|||
useEffect(() => {
|
||||
const measureSegments = () => {
|
||||
if (!containerRef.current || !breadcrumbsRef.current) return;
|
||||
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const availableWidth = containerWidth - 175; // Reserve space for copy button and padding
|
||||
|
||||
|
||||
// Create a temporary element to measure segment widths
|
||||
const tempElement = document.createElement('div');
|
||||
tempElement.style.position = 'absolute';
|
||||
|
|
@ -116,17 +121,17 @@ export const PathHeader = ({
|
|||
tempElement.style.whiteSpace = 'nowrap';
|
||||
tempElement.className = 'font-mono text-sm';
|
||||
document.body.appendChild(tempElement);
|
||||
|
||||
|
||||
let totalWidth = 0;
|
||||
let visibleCount = breadcrumbSegments.length;
|
||||
|
||||
|
||||
// Start from the end (most important segments) and work backwards
|
||||
for (let i = breadcrumbSegments.length - 1; i >= 0; i--) {
|
||||
const segment = breadcrumbSegments[i];
|
||||
tempElement.textContent = segment.name;
|
||||
const segmentWidth = tempElement.offsetWidth;
|
||||
const separatorWidth = i < breadcrumbSegments.length - 1 ? 16 : 0; // ChevronRight width
|
||||
|
||||
|
||||
if (totalWidth + segmentWidth + separatorWidth > availableWidth && i > 0) {
|
||||
// If adding this segment would overflow and it's not the last segment
|
||||
visibleCount = breadcrumbSegments.length - i;
|
||||
|
|
@ -136,21 +141,21 @@ export const PathHeader = ({
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
totalWidth += segmentWidth + separatorWidth;
|
||||
}
|
||||
|
||||
|
||||
document.body.removeChild(tempElement);
|
||||
setVisibleSegmentCount(visibleCount);
|
||||
};
|
||||
|
||||
measureSegments();
|
||||
|
||||
|
||||
const resizeObserver = new ResizeObserver(measureSegments);
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [breadcrumbSegments]);
|
||||
|
||||
|
|
@ -170,9 +175,8 @@ export const PathHeader = ({
|
|||
|
||||
const onCopyPath = useCallback(() => {
|
||||
navigator.clipboard.writeText(path);
|
||||
setCopied(true);
|
||||
toast({ description: "✅ Copied to clipboard" });
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
return true;
|
||||
}, [path, toast]);
|
||||
|
||||
const onBreadcrumbClick = useCallback((segment: BreadcrumbSegment) => {
|
||||
|
|
@ -204,19 +208,24 @@ export const PathHeader = ({
|
|||
|
||||
return (
|
||||
<div className="flex flex-row gap-2 items-center w-full overflow-hidden">
|
||||
{info?.icon ? (
|
||||
<a href={info.repoLink} target="_blank" rel="noopener noreferrer">
|
||||
<Image
|
||||
src={info.icon}
|
||||
alt={info.codeHostName}
|
||||
className={`w-4 h-4 ${info.iconClassName}`}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<LaptopIcon className="w-4 h-4" />
|
||||
{isCodeHostIconVisible && (
|
||||
<>
|
||||
{info?.icon ? (
|
||||
<a href={info.repoLink} target="_blank" rel="noopener noreferrer">
|
||||
<Image
|
||||
src={info.icon}
|
||||
alt={info.codeHostName}
|
||||
className={`w-4 h-4 ${info.iconClassName}`}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<LaptopIcon className="w-4 h-4" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="font-medium cursor-pointer hover:underline"
|
||||
className={cn("font-medium cursor-pointer hover:underline", repoNameClassName)}
|
||||
onClick={() => navigateToPath({
|
||||
repoName: repo.name,
|
||||
path: '',
|
||||
|
|
@ -269,8 +278,11 @@ export const PathHeader = ({
|
|||
)}
|
||||
{visibleSegments.map((segment, index) => (
|
||||
<div key={segment.fullPath} className="flex items-center">
|
||||
{(isFileIconVisible && index === visibleSegments.length - 1) && (
|
||||
<VscodeFileIcon fileName={segment.name} className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
<span
|
||||
className={clsx(
|
||||
className={cn(
|
||||
"font-mono text-sm truncate cursor-pointer hover:underline",
|
||||
)}
|
||||
onClick={() => onBreadcrumbClick(segment)}
|
||||
|
|
@ -283,18 +295,10 @@ export const PathHeader = ({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="ml-2 p-1 rounded transition-colors flex-shrink-0"
|
||||
onClick={onCopyPath}
|
||||
aria-label="Copy file path"
|
||||
type="button"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<CopyIconButton
|
||||
onCopy={onCopyPath}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export const SettingsDropdown = ({
|
|||
|
||||
const { theme: _theme, setTheme } = useTheme();
|
||||
const [keymapType, setKeymapType] = useKeymapType();
|
||||
const { data: session, update } = useSession();
|
||||
const { data: session } = useSession();
|
||||
const domain = useDomain();
|
||||
|
||||
const theme = useMemo(() => {
|
||||
|
|
@ -67,14 +67,7 @@ export const SettingsDropdown = ({
|
|||
}, [theme]);
|
||||
|
||||
return (
|
||||
// Was hitting a bug with invite code login where the first time the user signs in, the settingsDropdown doesn't have a valid session. To fix this
|
||||
// we can simply update the session everytime the settingsDropdown is opened. This isn't a super frequent operation and updating the session is low cost,
|
||||
// so this is a simple solution to the problem.
|
||||
<DropdownMenu onOpenChange={(isOpen) => {
|
||||
if (isOpen) {
|
||||
update();
|
||||
}
|
||||
}}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className={cn(menuButtonClassName)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useSyntaxGuide } from "./syntaxGuideProvider";
|
||||
import { CodeSnippet } from "@/app/components/codeSnippet";
|
||||
|
||||
const LINGUIST_LINK = "https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml";
|
||||
const CTAGS_LINK = "https://ctags.io/";
|
||||
|
|
@ -66,7 +66,7 @@ export const SyntaxReferenceGuide = () => {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Syntax Reference Guide</DialogTitle>
|
||||
<DialogDescription className="text-sm text-foreground">
|
||||
Queries consist of space-separated regular expressions. Wrapping expressions in <Code>{`""`}</Code> combines them. By default, a file must have at least one match for each expression to be included.
|
||||
Queries consist of space-seperated regular expressions. Wrapping expressions in <CodeSnippet>{`""`}</CodeSnippet> combines them. By default, a file must have at least one match for each expression to be included.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Table>
|
||||
|
|
@ -78,23 +78,23 @@ export const SyntaxReferenceGuide = () => {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet>foo</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo bar</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>and</b> <Code>/bar/</Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet>foo bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> <b>and</b> <CodeSnippet>/bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>{`"foo bar"`}</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo bar/</Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet>{`"foo bar"`}</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Separator className="my-2"/>
|
||||
<p className="text-sm">
|
||||
{`Multiple expressions can be or'd together with `}<Code>or</Code>, negated with <Code>-</Code>, or grouped with <Code>()</Code>.
|
||||
{`Multiple expressions can be or'd together with `}<CodeSnippet>or</CodeSnippet>, negated with <CodeSnippet>-</CodeSnippet>, or grouped with <CodeSnippet>()</CodeSnippet>.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
|
@ -105,23 +105,23 @@ export const SyntaxReferenceGuide = () => {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo <Highlight>or</Highlight> bar</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>or</b> <Code>/bar/</Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet>foo <Highlight>or</Highlight> bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> <b>or</b> <CodeSnippet>/bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo -bar</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> but <b>not</b> <Code>/bar/</Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet>foo -bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> but <b>not</b> <CodeSnippet>/bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo (bar <Highlight>or</Highlight> baz)</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>and</b> either <Code>/bar/</Code> <b>or</b> <Code>/baz/</Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet>foo (bar <Highlight>or</Highlight> baz)</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> <b>and</b> either <CodeSnippet>/bar/</CodeSnippet> <b>or</b> <CodeSnippet>/baz/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Separator className="my-2"/>
|
||||
<p className="text-sm">
|
||||
Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the <Code>-</Code> prefix.
|
||||
Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the <CodeSnippet>-</CodeSnippet> prefix.
|
||||
</p>
|
||||
|
||||
<Table>
|
||||
|
|
@ -134,87 +134,87 @@ export const SyntaxReferenceGuide = () => {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>file:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet><Highlight>file:</Highlight></CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Filter results from filepaths that match the regex. By default all files are searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
<CodeSnippet
|
||||
title="Filter results to filepaths that match regex /README/"
|
||||
>
|
||||
<Highlight>file:</Highlight>README
|
||||
</Code>
|
||||
<Code
|
||||
</CodeSnippet>
|
||||
<CodeSnippet
|
||||
title="Filter results to filepaths that match regex /my file/"
|
||||
>
|
||||
<Highlight>file:</Highlight>{`"my file"`}
|
||||
</Code>
|
||||
<Code
|
||||
</CodeSnippet>
|
||||
<CodeSnippet
|
||||
title="Ignore results from filepaths match regex /test\.ts$/"
|
||||
>
|
||||
<Highlight>-file:</Highlight>test\.ts$
|
||||
</Code>
|
||||
</CodeSnippet>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>repo:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet><Highlight>repo:</Highlight></CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Filter results from repos that match the regex. By default all repos are searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
<CodeSnippet
|
||||
title="Filter results to repos that match regex /linux/"
|
||||
>
|
||||
<Highlight>repo:</Highlight>linux
|
||||
</Code>
|
||||
<Code
|
||||
</CodeSnippet>
|
||||
<CodeSnippet
|
||||
title="Ignore results from repos that match regex /^web\/.*/"
|
||||
>
|
||||
<Highlight>-repo:</Highlight>^web/.*
|
||||
</Code>
|
||||
</CodeSnippet>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>rev:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet><Highlight>rev:</Highlight></CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Filter results from a specific branch or tag. By default <b>only</b> the default branch is searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
<CodeSnippet
|
||||
title="Filter results to branches that match regex /beta/"
|
||||
>
|
||||
<Highlight>rev:</Highlight>beta
|
||||
</Code>
|
||||
</CodeSnippet>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>lang:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet><Highlight>lang:</Highlight></CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Filter results by language (as defined by <Link className="text-blue-500" href={LINGUIST_LINK}>linguist</Link>). By default all languages are searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
<CodeSnippet
|
||||
title="Filter results to TypeScript files"
|
||||
>
|
||||
<Highlight>lang:</Highlight>TypeScript
|
||||
</Code>
|
||||
<Code
|
||||
</CodeSnippet>
|
||||
<CodeSnippet
|
||||
title="Ignore results from YAML files"
|
||||
>
|
||||
<Highlight>-lang:</Highlight>YAML
|
||||
</Code>
|
||||
</CodeSnippet>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>sym:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2"><CodeSnippet><Highlight>sym:</Highlight></CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match symbol definitions created by <Link className="text-blue-500" href={CTAGS_LINK}>universal ctags</Link> at index time.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
<CodeSnippet
|
||||
title="Filter results to symbols that match regex /\bmain\b/"
|
||||
>
|
||||
<Highlight>sym:</Highlight>\bmain\b
|
||||
</Code>
|
||||
</CodeSnippet>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -225,17 +225,6 @@ export const SyntaxReferenceGuide = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const Code = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
|
||||
return (
|
||||
<code
|
||||
className={clsx("bg-gray-100 dark:bg-gray-700 w-fit rounded-md font-mono px-2 py-0.5", className)}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-highlight">
|
||||
|
|
|
|||
|
|
@ -2,45 +2,44 @@ import Link from "next/link";
|
|||
import Image from "next/image";
|
||||
import logoLight from "@/public/sb_logo_light.png";
|
||||
import logoDark from "@/public/sb_logo_dark.png";
|
||||
import { SearchBar } from "./searchBar";
|
||||
import { SettingsDropdown } from "./settingsDropdown";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface TopBarProps {
|
||||
defaultSearchQuery?: string;
|
||||
domain: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TopBar = ({
|
||||
defaultSearchQuery,
|
||||
domain,
|
||||
children,
|
||||
}: TopBarProps) => {
|
||||
return (
|
||||
<div className="flex flex-row justify-between items-center py-1.5 px-3 gap-4 bg-background">
|
||||
<div className="grow flex flex-row gap-4 items-center">
|
||||
<Link
|
||||
href={`/${domain}`}
|
||||
className="shrink-0 cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="h-4 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-4 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
/>
|
||||
</Link>
|
||||
<SearchBar
|
||||
size="sm"
|
||||
defaultQuery={defaultSearchQuery}
|
||||
className="w-full"
|
||||
<div className='sticky top-0 left-0 right-0 z-10'>
|
||||
<div className="flex flex-row justify-between items-center py-1.5 px-3 gap-4 bg-background">
|
||||
<div className="grow flex flex-row gap-4 items-center">
|
||||
<Link
|
||||
href={`/${domain}`}
|
||||
className="shrink-0 cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="h-4 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-4 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
/>
|
||||
</Link>
|
||||
{children}
|
||||
</div>
|
||||
<SettingsDropdown
|
||||
menuButtonClassName="w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
<SettingsDropdown
|
||||
menuButtonClassName="w-8 h-8"
|
||||
/>
|
||||
<Separator />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,18 +4,7 @@ import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type"
|
|||
import { QuickAction } from "../components/configEditor";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Code = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
|
||||
return (
|
||||
<code
|
||||
className={cn("bg-gray-100 dark:bg-gray-700 w-fit rounded-md font-mono px-2 py-0.5", className)}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
import { CodeSnippet } from "@/app/components/codeSnippet";
|
||||
|
||||
export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
||||
{
|
||||
|
|
@ -30,7 +19,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
selectionText: "<owner>/<repo name>",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a individual repository to sync with. Ensure the repository is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add a individual repository to sync with. Ensure the repository is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
|
|
@ -38,7 +27,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
"vercel/next.js",
|
||||
"torvalds/linux"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -56,7 +45,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
selectionText: "<organization name>",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add an organization to sync with. All repositories in the organization visible to the provided <Code>token</Code> (if any) will be synced.</span>
|
||||
<span>Add an organization to sync with. All repositories in the organization visible to the provided <CodeSnippet>token</CodeSnippet> (if any) will be synced.</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
{[
|
||||
|
|
@ -64,7 +53,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
"sourcebot",
|
||||
"vercel"
|
||||
].map((org) => (
|
||||
<Code key={org}>{org}</Code>
|
||||
<CodeSnippet key={org}>{org}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +71,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
selectionText: "<username>",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a user to sync with. All repositories that the user owns visible to the provided <Code>token</Code> (if any) will be synced.</span>
|
||||
<span>Add a user to sync with. All repositories that the user owns visible to the provided <CodeSnippet>token</CodeSnippet> (if any) will be synced.</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
{[
|
||||
|
|
@ -90,7 +79,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
"torvalds",
|
||||
"octocat"
|
||||
].map((org) => (
|
||||
<Code key={org}>{org}</Code>
|
||||
<CodeSnippet key={org}>{org}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,7 +92,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
}),
|
||||
name: "Set url to GitHub instance",
|
||||
selectionText: "https://github.example.com",
|
||||
description: <span>Set a custom GitHub host. Defaults to <Code>https://github.com</Code>.</span>
|
||||
description: <span>Set a custom GitHub host. Defaults to <CodeSnippet>https://github.com</CodeSnippet>.</span>
|
||||
},
|
||||
{
|
||||
fn: (previous: GithubConnectionConfig) => ({
|
||||
|
|
@ -127,7 +116,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
"my-org/docs*",
|
||||
"my-org/test*"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,7 +144,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
"docs",
|
||||
"ci"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -180,7 +169,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
"docs",
|
||||
"ci"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -223,14 +212,14 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
selectionText: "<project name>",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a individual project to sync with. Ensure the project is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add a individual project to sync with. Ensure the project is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
"gitlab-org/gitlab",
|
||||
"corp/team-project",
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -248,14 +237,14 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
selectionText: "<username>",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a user to sync with. All projects that the user owns visible to the provided <Code>token</Code> (if any) will be synced.</span>
|
||||
<span>Add a user to sync with. All projects that the user owns visible to the provided <CodeSnippet>token</CodeSnippet> (if any) will be synced.</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
{[
|
||||
"jane-doe",
|
||||
"torvalds"
|
||||
].map((org) => (
|
||||
<Code key={org}>{org}</Code>
|
||||
<CodeSnippet key={org}>{org}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -273,14 +262,14 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
selectionText: "<group name>",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a group to sync with. All projects in the group (and recursive subgroups) visible to the provided <Code>token</Code> (if any) will be synced.</span>
|
||||
<span>Add a group to sync with. All projects in the group (and recursive subgroups) visible to the provided <CodeSnippet>token</CodeSnippet> (if any) will be synced.</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
"my-group",
|
||||
"path/to/subgroup"
|
||||
].map((org) => (
|
||||
<Code key={org}>{org}</Code>
|
||||
<CodeSnippet key={org}>{org}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -293,7 +282,7 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
}),
|
||||
name: "Set url to GitLab instance",
|
||||
selectionText: "https://gitlab.example.com",
|
||||
description: <span>Set a custom GitLab host. Defaults to <Code>https://gitlab.com</Code>.</span>
|
||||
description: <span>Set a custom GitLab host. Defaults to <CodeSnippet>https://gitlab.com</CodeSnippet>.</span>
|
||||
},
|
||||
{
|
||||
fn: (previous: GitlabConnectionConfig) => ({
|
||||
|
|
@ -301,7 +290,7 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
all: true,
|
||||
}),
|
||||
name: "Sync all projects",
|
||||
description: <span>Sync all projects visible to the provided <Code>token</Code> (if any). Only available when using a self-hosted GitLab instance.</span>
|
||||
description: <span>Sync all projects visible to the provided <CodeSnippet>token</CodeSnippet> (if any). Only available when using a self-hosted GitLab instance.</span>
|
||||
},
|
||||
{
|
||||
fn: (previous: GitlabConnectionConfig) => ({
|
||||
|
|
@ -325,7 +314,7 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
"docs/**",
|
||||
"**/tests/**",
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -403,7 +392,7 @@ export const bitbucketCloudQuickActions: QuickAction<BitbucketConnectionConfig>[
|
|||
selectionText: "username",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Username to use for authentication. This is only required if you're using an App Password (stored in <Code>token</Code>) for authentication.</span>
|
||||
<span>Username to use for authentication. This is only required if you're using an App Password (stored in <CodeSnippet>token</CodeSnippet>) for authentication.</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -419,7 +408,7 @@ export const bitbucketCloudQuickActions: QuickAction<BitbucketConnectionConfig>[
|
|||
selectionText: "myWorkspace",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a workspace to sync with. Ensure the workspace is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add a workspace to sync with. Ensure the workspace is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -435,7 +424,7 @@ export const bitbucketCloudQuickActions: QuickAction<BitbucketConnectionConfig>[
|
|||
selectionText: "myWorkspace/myRepo",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add an individual repository to sync with. Ensure the repository is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add an individual repository to sync with. Ensure the repository is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -451,7 +440,7 @@ export const bitbucketCloudQuickActions: QuickAction<BitbucketConnectionConfig>[
|
|||
selectionText: "myProject",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a project to sync with. Ensure the project is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add a project to sync with. Ensure the project is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -506,14 +495,14 @@ export const bitbucketDataCenterQuickActions: QuickAction<BitbucketConnectionCon
|
|||
selectionText: "myProject/myRepo",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a individual repository to sync with. Ensure the repository is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add a individual repository to sync with. Ensure the repository is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
"PROJ/repo-name",
|
||||
"MYPROJ/api"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -531,7 +520,7 @@ export const bitbucketDataCenterQuickActions: QuickAction<BitbucketConnectionCon
|
|||
selectionText: "myProject",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a project to sync with. Ensure the project is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span>Add a project to sync with. Ensure the project is visible to the provided <CodeSnippet>token</CodeSnippet> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -554,7 +543,7 @@ export const bitbucketDataCenterQuickActions: QuickAction<BitbucketConnectionCon
|
|||
"myProject/myExcludedRepo",
|
||||
"myProject2/*"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
<CodeSnippet key={repo}>{repo}</CodeSnippet>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { NavigationMenu } from "./components/navigationMenu";
|
||||
import { SearchBar } from "./components/searchBar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { UpgradeToast } from "./components/upgradeToast";
|
||||
import Link from "next/link";
|
||||
import { getOrgFromDomain } from "@/data/org";
|
||||
import { PageNotFound } from "./components/pageNotFound";
|
||||
import { Footer } from "@/app/components/footer";
|
||||
import { SourcebotLogo } from "../components/sourcebotLogo";
|
||||
import { RepositorySnapshot } from "./components/repositorySnapshot";
|
||||
import { SyntaxReferenceGuideHint } from "./components/syntaxReferenceGuideHint";
|
||||
import { getRepos } from "@/actions";
|
||||
import { Footer } from "@/app/components/footer";
|
||||
import { getOrgFromDomain } from "@/data/org";
|
||||
import { getConfiguredLanguageModelsInfo, getUserChatHistory } from "@/features/chat/actions";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { Homepage } from "./components/homepage";
|
||||
import { NavigationMenu } from "./components/navigationMenu";
|
||||
import { PageNotFound } from "./components/pageNotFound";
|
||||
import { UpgradeToast } from "./components/upgradeToast";
|
||||
import { ServiceErrorException } from "@/lib/serviceError";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
export default async function Home({ params: { domain } }: { params: { domain: string } }) {
|
||||
const org = await getOrgFromDomain(domain);
|
||||
|
|
@ -18,7 +16,21 @@ export default async function Home({ params: { domain } }: { params: { domain: s
|
|||
return <PageNotFound />
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
|
||||
const models = await getConfiguredLanguageModelsInfo();
|
||||
const repos = await getRepos(domain);
|
||||
const chatHistory = session ? await getUserChatHistory(domain) : [];
|
||||
|
||||
if (isServiceError(repos)) {
|
||||
throw new ServiceErrorException(repos);
|
||||
}
|
||||
|
||||
if (isServiceError(chatHistory)) {
|
||||
throw new ServiceErrorException(chatHistory);
|
||||
}
|
||||
|
||||
const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center overflow-hidden min-h-screen">
|
||||
|
|
@ -26,123 +38,13 @@ export default async function Home({ params: { domain } }: { params: { domain: s
|
|||
domain={domain}
|
||||
/>
|
||||
<UpgradeToast />
|
||||
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||
<div className="max-h-44 w-auto">
|
||||
<SourcebotLogo
|
||||
className="h-18 md:h-40 w-auto"
|
||||
/>
|
||||
</div>
|
||||
<SearchBar
|
||||
autoFocus={true}
|
||||
className="mt-4 w-full max-w-[800px]"
|
||||
/>
|
||||
<div className="mt-8">
|
||||
<RepositorySnapshot
|
||||
repos={isServiceError(repos) ? [] : repos}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center w-fit gap-6">
|
||||
<Separator className="mt-5" />
|
||||
<span className="font-semibold">How to search</span>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<HowToSection
|
||||
title="Search in files or paths"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="test todo" domain={domain}>test todo</Query> <QueryExplanation>(both test and todo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="test or todo" domain={domain}>test <Highlight>or</Highlight> todo</Query> <QueryExplanation>(either test or todo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query={`"exit boot"`} domain={domain}>{`"exit boot"`}</Query> <QueryExplanation>(exact match)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="TODO case:yes" domain={domain}>TODO <Highlight>case:</Highlight>yes</Query> <QueryExplanation>(case sensitive)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
<HowToSection
|
||||
title="Filter results"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="file:README setup" domain={domain}><Highlight>file:</Highlight>README setup</Query> <QueryExplanation>(by filename)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="repo:torvalds/linux test" domain={domain}><Highlight>repo:</Highlight>torvalds/linux test</Query> <QueryExplanation>(by repo)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="lang:typescript" domain={domain}><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="rev:HEAD" domain={domain}><Highlight>rev:</Highlight>HEAD</Query> <QueryExplanation>(by branch or tag)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
<HowToSection
|
||||
title="Advanced"
|
||||
>
|
||||
<QueryExample>
|
||||
<Query query="file:\.py$" domain={domain}><Highlight>file:</Highlight>{`\\.py$`}</Query> <QueryExplanation>{`(files that end in ".py")`}</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="sym:main" domain={domain}><Highlight>sym:</Highlight>main</Query> <QueryExplanation>{`(symbols named "main")`}</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="todo -lang:c" domain={domain}>todo <Highlight>-lang:c</Highlight></Query> <QueryExplanation>(negate filter)</QueryExplanation>
|
||||
</QueryExample>
|
||||
<QueryExample>
|
||||
<Query query="content:README" domain={domain}><Highlight>content:</Highlight>README</Query> <QueryExplanation>(search content only)</QueryExplanation>
|
||||
</QueryExample>
|
||||
</HowToSection>
|
||||
</div>
|
||||
<SyntaxReferenceGuideHint />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Homepage
|
||||
initialRepos={indexedRepos}
|
||||
languageModels={models}
|
||||
chatHistory={chatHistory}
|
||||
/>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HowToSection = ({ title, children }: { title: string, children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="dark:text-gray-300 text-sm mb-2 underline">{title}</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-highlight">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryExample = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-sm font-mono">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryExplanation = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-3">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Query = ({ query, domain, children }: { query: string, domain: string, children: React.ReactNode }) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/${domain}/search?query=${query}`}
|
||||
className="cursor-pointer hover:underline"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { useHotkeys } from "react-hotkeys-hook";
|
|||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||
import { SearchBar } from "../components/searchBar";
|
||||
|
||||
const DEFAULT_MAX_MATCH_COUNT = 10000;
|
||||
|
||||
|
|
@ -172,13 +173,15 @@ const SearchPageInternal = () => {
|
|||
return (
|
||||
<div className="flex flex-col h-screen overflow-clip">
|
||||
{/* TopBar */}
|
||||
<div className="sticky top-0 left-0 right-0 z-10">
|
||||
<TopBar
|
||||
defaultSearchQuery={searchQuery}
|
||||
domain={domain}
|
||||
<TopBar
|
||||
domain={domain}
|
||||
>
|
||||
<SearchBar
|
||||
size="sm"
|
||||
defaultQuery={searchQuery}
|
||||
className="w-full"
|
||||
/>
|
||||
<Separator />
|
||||
</div>
|
||||
</TopBar>
|
||||
|
||||
{(isSearchLoading) ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-2">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { LucideKeyRound, MoreVertical, Search, LucideTrash } from "lucide-react";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { cn, getDisplayTime, isServiceError } from "@/lib/utils";
|
||||
import { getDisplayTime, isServiceError } from "@/lib/utils";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
|
|
@ -12,6 +12,7 @@ import { deleteSecret } from "@/actions";
|
|||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CodeSnippet } from "@/app/components/codeSnippet";
|
||||
|
||||
interface Secret {
|
||||
key: string;
|
||||
|
|
@ -138,7 +139,7 @@ export const SecretsList = ({ secrets }: SecretsListProps) => {
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Secret</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the secret <Code>{secretToDelete?.key}</Code>? Any connections that use this secret will <strong>fail to sync.</strong>
|
||||
Are you sure you want to delete the secret <CodeSnippet>{secretToDelete?.key}</CodeSnippet>? Any connections that use this secret will <strong>fail to sync.</strong>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
|
@ -155,14 +156,3 @@ export const SecretsList = ({ secrets }: SecretsListProps) => {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Code = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
|
||||
return (
|
||||
<code
|
||||
className={cn("bg-gray-100 dark:bg-gray-700 w-fit rounded-md font-mono px-2 py-0.5", className)}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
426
packages/web/src/app/api/(server)/chat/route.ts
Normal file
426
packages/web/src/app/api/(server)/chat/route.ts
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { _getConfiguredLanguageModelsFull, updateChatMessages, updateChatName } from "@/features/chat/actions";
|
||||
import { createAgentStream } from "@/features/chat/agent";
|
||||
import { additionalChatRequestParamsSchema, SBChatMessage } from "@/features/chat/types";
|
||||
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
|
||||
import { ErrorCode } from "@/lib/errorCodes";
|
||||
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { prisma } from "@/prisma";
|
||||
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
||||
import { AnthropicProviderOptions, createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { createVertex } from '@ai-sdk/google-vertex';
|
||||
import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
|
||||
import { createOpenAI, OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
|
||||
import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
import { OrgRole } from "@sourcebot/db";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { LanguageModel } from "@sourcebot/schemas/v3/index.type";
|
||||
import {
|
||||
createUIMessageStream,
|
||||
createUIMessageStreamResponse,
|
||||
generateText,
|
||||
JSONValue,
|
||||
ModelMessage,
|
||||
StreamTextResult,
|
||||
UIMessageStreamOptions,
|
||||
UIMessageStreamWriter,
|
||||
} from "ai";
|
||||
import { randomUUID } from "crypto";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { z } from "zod";
|
||||
|
||||
const logger = createLogger('chat-api');
|
||||
|
||||
const chatRequestSchema = z.object({
|
||||
// These paramt
|
||||
messages: z.array(z.any()),
|
||||
id: z.string(),
|
||||
...additionalChatRequestParamsSchema.shape,
|
||||
})
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const domain = req.headers.get("X-Org-Domain");
|
||||
if (!domain) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
|
||||
message: "Missing X-Org-Domain header",
|
||||
});
|
||||
}
|
||||
|
||||
const requestBody = await req.json();
|
||||
const parsed = await chatRequestSchema.safeParseAsync(requestBody);
|
||||
if (!parsed.success) {
|
||||
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||
}
|
||||
|
||||
const { messages, id, selectedRepos, languageModelId } = parsed.data;
|
||||
const response = await chatHandler({
|
||||
messages,
|
||||
id,
|
||||
selectedRepos,
|
||||
languageModelId,
|
||||
}, domain);
|
||||
|
||||
if (isServiceError(response)) {
|
||||
return serviceErrorResponse(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mergeStreamAsync = async (stream: StreamTextResult<any, any>, writer: UIMessageStreamWriter<SBChatMessage>, options: UIMessageStreamOptions<SBChatMessage> = {}) => {
|
||||
await new Promise<void>((resolve) => writer.merge(stream.toUIMessageStream({
|
||||
...options,
|
||||
onFinish: async () => {
|
||||
resolve();
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
interface ChatHandlerProps {
|
||||
messages: SBChatMessage[];
|
||||
id: string;
|
||||
selectedRepos: string[];
|
||||
languageModelId: string;
|
||||
}
|
||||
|
||||
const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.isReadonly) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||
message: "Chat is readonly and cannot be edited.",
|
||||
});
|
||||
}
|
||||
|
||||
const latestMessage = messages[messages.length - 1];
|
||||
const sources = latestMessage.parts
|
||||
.filter((part) => part.type === 'data-source')
|
||||
.map((part) => part.data);
|
||||
|
||||
// From the language model ID, attempt to find the
|
||||
// corresponding config in `config.json`.
|
||||
const languageModelConfig =
|
||||
(await _getConfiguredLanguageModelsFull())
|
||||
.find((model) => model.model === languageModelId);
|
||||
|
||||
if (!languageModelConfig) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||
message: `Language model ${languageModelId} is not configured.`,
|
||||
});
|
||||
}
|
||||
|
||||
const { model, providerOptions, headers } = await getAISDKLanguageModelAndOptions(languageModelConfig, org.id);
|
||||
|
||||
// @todo: refactor this
|
||||
if (
|
||||
messages.length === 1 &&
|
||||
messages[0].role === "user" &&
|
||||
messages[0].parts.length >= 1 &&
|
||||
messages[0].parts[0].type === 'text'
|
||||
) {
|
||||
const content = messages[0].parts[0].text;
|
||||
|
||||
const title = await generateChatTitle(content, model);
|
||||
if (title) {
|
||||
updateChatName({
|
||||
chatId: id,
|
||||
name: title,
|
||||
}, domain);
|
||||
}
|
||||
else {
|
||||
logger.error("Failed to generate chat title.");
|
||||
}
|
||||
}
|
||||
|
||||
const traceId = randomUUID();
|
||||
|
||||
// Extract user messages and assistant answers.
|
||||
// We will use this as the context we carry between messages.
|
||||
const messageHistory =
|
||||
messages.map((message): ModelMessage | undefined => {
|
||||
if (message.role === 'user') {
|
||||
return {
|
||||
role: 'user',
|
||||
content: message.parts[0].type === 'text' ? message.parts[0].text : '',
|
||||
};
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
const answerPart = getAnswerPartFromAssistantMessage(message, false);
|
||||
if (answerPart) {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: [answerPart]
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter(message => message !== undefined);
|
||||
|
||||
try {
|
||||
const stream = createUIMessageStream<SBChatMessage>({
|
||||
execute: async ({ writer }) => {
|
||||
writer.write({
|
||||
type: 'start',
|
||||
});
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
const researchStream = await createAgentStream({
|
||||
model,
|
||||
providerOptions,
|
||||
headers,
|
||||
inputMessages: messageHistory,
|
||||
inputSources: sources,
|
||||
selectedRepos,
|
||||
onWriteSource: (source) => {
|
||||
writer.write({
|
||||
type: 'data-source',
|
||||
data: source,
|
||||
});
|
||||
},
|
||||
traceId,
|
||||
});
|
||||
|
||||
await mergeStreamAsync(researchStream, writer, {
|
||||
sendReasoning: true,
|
||||
sendStart: false,
|
||||
sendFinish: false,
|
||||
});
|
||||
|
||||
const totalUsage = await researchStream.totalUsage;
|
||||
|
||||
writer.write({
|
||||
type: 'message-metadata',
|
||||
messageMetadata: {
|
||||
totalTokens: totalUsage.totalTokens,
|
||||
totalInputTokens: totalUsage.inputTokens,
|
||||
totalOutputTokens: totalUsage.outputTokens,
|
||||
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
|
||||
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
|
||||
traceId,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
writer.write({
|
||||
type: 'finish',
|
||||
});
|
||||
},
|
||||
onError: errorHandler,
|
||||
originalMessages: messages,
|
||||
onFinish: async ({ messages }) => {
|
||||
await updateChatMessages({
|
||||
chatId: id,
|
||||
messages
|
||||
}, domain);
|
||||
},
|
||||
});
|
||||
|
||||
return createUIMessageStreamResponse({
|
||||
stream,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error:", error)
|
||||
logger.error("Error stack:", error instanceof Error ? error.stack : "No stack trace")
|
||||
Sentry.captureException(error);
|
||||
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
errorCode: ErrorCode.UNEXPECTED_ERROR,
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
|
||||
));
|
||||
|
||||
const generateChatTitle = async (message: string, model: AISDKLanguageModelV2) => {
|
||||
try {
|
||||
const prompt = `Convert this question into a short topic title (max 50 characters).
|
||||
|
||||
Rules:
|
||||
- Do NOT include question words (what, where, how, why, when, which)
|
||||
- Do NOT end with a question mark
|
||||
- Capitalize the first letter of the title
|
||||
- Focus on the subject/topic being discussed
|
||||
- Make it sound like a file name or category
|
||||
|
||||
Examples:
|
||||
"Where is the authentication code?" → "Authentication Code"
|
||||
"How to setup the database?" → "Database Setup"
|
||||
"What are the API endpoints?" → "API Endpoints"
|
||||
|
||||
User question: ${message}`;
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
prompt,
|
||||
maxOutputTokens: 20,
|
||||
});
|
||||
|
||||
return result.text;
|
||||
} catch (error) {
|
||||
logger.error("Error generating summary:", error)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const getAISDKLanguageModelAndOptions = async (config: LanguageModel, orgId: number): Promise<{
|
||||
model: AISDKLanguageModelV2,
|
||||
providerOptions?: Record<string, Record<string, JSONValue>>,
|
||||
headers?: Record<string, string>,
|
||||
}> => {
|
||||
|
||||
const { provider, model: modelId } = config;
|
||||
|
||||
switch (provider) {
|
||||
case 'anthropic': {
|
||||
const anthropic = createAnthropic({
|
||||
baseURL: config.baseUrl,
|
||||
...(config.token ? {
|
||||
apiKey: (await getTokenFromConfig(config.token, orgId, prisma)),
|
||||
} : {
|
||||
apiKey: env.ANTHROPIC_API_KEY,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
model: anthropic(modelId),
|
||||
providerOptions: {
|
||||
anthropic: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: env.ANTHROPIC_THINKING_BUDGET_TOKENS,
|
||||
}
|
||||
} satisfies AnthropicProviderOptions,
|
||||
},
|
||||
headers: {
|
||||
// @see: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
|
||||
'anthropic-beta': 'interleaved-thinking-2025-05-14',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'openai': {
|
||||
const openai = createOpenAI({
|
||||
baseURL: config.baseUrl,
|
||||
...(config.token ? {
|
||||
apiKey: (await getTokenFromConfig(config.token, orgId, prisma)),
|
||||
} : {
|
||||
apiKey: env.OPENAI_API_KEY,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
model: openai(modelId),
|
||||
providerOptions: {
|
||||
openai: {
|
||||
reasoningEffort: 'high'
|
||||
} satisfies OpenAIResponsesProviderOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'google-generative-ai': {
|
||||
const google = createGoogleGenerativeAI({
|
||||
baseURL: config.baseUrl,
|
||||
...(config.token ? {
|
||||
apiKey: (await getTokenFromConfig(config.token, orgId, prisma)),
|
||||
} : {
|
||||
apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
model: google(modelId),
|
||||
};
|
||||
}
|
||||
case 'amazon-bedrock': {
|
||||
const aws = createAmazonBedrock({
|
||||
baseURL: config.baseUrl,
|
||||
region: config.region ?? env.AWS_REGION,
|
||||
...(config.accessKeyId ? {
|
||||
accessKeyId: (await getTokenFromConfig(config.accessKeyId, orgId, prisma)),
|
||||
} : {
|
||||
accessKeyId: env.AWS_ACCESS_KEY_ID,
|
||||
}),
|
||||
...(config.accessKeySecret ? {
|
||||
secretAccessKey: (await getTokenFromConfig(config.accessKeySecret, orgId, prisma)),
|
||||
} : {
|
||||
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
model: aws(modelId),
|
||||
};
|
||||
}
|
||||
case 'google-vertex': {
|
||||
const vertex = createVertex({
|
||||
project: config.project ?? env.GOOGLE_VERTEX_PROJECT,
|
||||
location: config.region ?? env.GOOGLE_VERTEX_REGION,
|
||||
...(config.credentials ? {
|
||||
googleAuthOptions: {
|
||||
keyFilename: await getTokenFromConfig(config.credentials, orgId, prisma),
|
||||
}
|
||||
} : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
model: vertex(modelId),
|
||||
};
|
||||
}
|
||||
case 'google-vertex-anthropic': {
|
||||
const vertexAnthropic = createVertexAnthropic({
|
||||
project: config.project ?? env.GOOGLE_VERTEX_PROJECT,
|
||||
location: config.region ?? env.GOOGLE_VERTEX_REGION,
|
||||
...(config.credentials ? {
|
||||
googleAuthOptions: {
|
||||
keyFilename: await getTokenFromConfig(config.credentials, orgId, prisma),
|
||||
}
|
||||
} : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
model: vertexAnthropic(modelId),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandler = (error: unknown) => {
|
||||
if (error == null) {
|
||||
return 'unknown error';
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
|
||||
52
packages/web/src/app/codemirror-styles.css
Normal file
52
packages/web/src/app/codemirror-styles.css
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/* Generic range border radius - applies 2px border radius along the perimeter */
|
||||
.cm-range-border-radius {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* First line in a range: rounded top corners only */
|
||||
.cm-range-border-radius:has(+ .cm-range-border-radius):not(.cm-range-border-radius + .cm-range-border-radius) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Middle lines: no rounded corners */
|
||||
.cm-range-border-radius+.cm-range-border-radius:has(+ .cm-range-border-radius) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Last line in a range: rounded bottom corners only */
|
||||
.cm-range-border-radius+.cm-range-border-radius:not(:has(+ .cm-range-border-radius)) {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Generic range border shadow - applies 1px box shadow along the perimeter */
|
||||
.cm-range-border-shadow {
|
||||
/* Default to transparent if no color is set */
|
||||
--cm-range-border-shadow-color: transparent;
|
||||
}
|
||||
|
||||
/* Single line (not adjacent to other range border shadow elements) */
|
||||
.cm-range-border-shadow:not(.cm-range-border-shadow + .cm-range-border-shadow):not(:has(+ .cm-range-border-shadow)) {
|
||||
box-shadow: inset 0 0 0 1px var(--cm-range-border-shadow-color);
|
||||
}
|
||||
|
||||
/* First line in a range: top and sides only */
|
||||
.cm-range-border-shadow:has(+ .cm-range-border-shadow):not(.cm-range-border-shadow + .cm-range-border-shadow) {
|
||||
box-shadow: inset 1px 0 0 0 var(--cm-range-border-shadow-color),
|
||||
inset -1px 0 0 0 var(--cm-range-border-shadow-color),
|
||||
inset 0 1px 0 0 var(--cm-range-border-shadow-color);
|
||||
}
|
||||
|
||||
/* Middle lines: sides only */
|
||||
.cm-range-border-shadow+.cm-range-border-shadow:has(+ .cm-range-border-shadow) {
|
||||
box-shadow: inset 1px 0 0 0 var(--cm-range-border-shadow-color),
|
||||
inset -1px 0 0 0 var(--cm-range-border-shadow-color);
|
||||
}
|
||||
|
||||
/* Last line in a range: bottom and sides only */
|
||||
.cm-range-border-shadow+.cm-range-border-shadow:not(:has(+ .cm-range-border-shadow)) {
|
||||
box-shadow: inset 1px 0 0 0 var(--cm-range-border-shadow-color),
|
||||
inset -1px 0 0 0 var(--cm-range-border-shadow-color),
|
||||
inset 0 -1px 0 0 var(--cm-range-border-shadow-color);
|
||||
}
|
||||
12
packages/web/src/app/components/codeSnippet.tsx
Normal file
12
packages/web/src/app/components/codeSnippet.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
export const CodeSnippet = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
|
||||
return (
|
||||
<code
|
||||
className={cn("bg-gray-100 dark:bg-gray-700 w-fit rounded-md px-2 py-0.5 font-medium font-mono", className)}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useSyntaxGuide } from "../[domain]/components/syntaxGuideProvider";
|
||||
|
||||
const LINGUIST_LINK = "https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml";
|
||||
const CTAGS_LINK = "https://ctags.io/";
|
||||
|
||||
export const SyntaxReferenceGuide = () => {
|
||||
const { isOpen, onOpenChanged } = useSyntaxGuide();
|
||||
const previousFocusedElement = useRef<HTMLElement | null>(null);
|
||||
|
||||
const openDialog = useCallback(() => {
|
||||
previousFocusedElement.current = document.activeElement as HTMLElement;
|
||||
onOpenChanged(true);
|
||||
}, [onOpenChanged]);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
onOpenChanged(false);
|
||||
|
||||
// @note: Without requestAnimationFrame, focus was not being returned
|
||||
// to codemirror elements for some reason.
|
||||
requestAnimationFrame(() => {
|
||||
previousFocusedElement.current?.focus();
|
||||
});
|
||||
}, [onOpenChanged]);
|
||||
|
||||
const handleOpenChange = useCallback((isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialog();
|
||||
} else {
|
||||
closeDialog();
|
||||
}
|
||||
}, [closeDialog, openDialog]);
|
||||
|
||||
useHotkeys("mod+/", (event) => {
|
||||
event.preventDefault();
|
||||
handleOpenChange(!isOpen);
|
||||
}, {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
description: "Open Syntax Reference Guide",
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-h-[80vh] max-w-[700px] overflow-scroll"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Syntax Reference Guide</DialogTitle>
|
||||
<DialogDescription className="text-sm text-foreground">
|
||||
Queries consist of space-separated regular expressions. Wrapping expressions in <Code>{`""`}</Code> combines them. By default, a file must have at least one match for each expression to be included.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo bar</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>and</b> <Code>/bar/</Code></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>{`"foo bar"`}</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo bar/</Code></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Separator className="my-2"/>
|
||||
<p className="text-sm">
|
||||
{`Multiple expressions can be or'd together with `}<Code>or</Code>, negated with <Code>-</Code>, or grouped with <Code>()</Code>.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo <Highlight>or</Highlight> bar</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>or</b> <Code>/bar/</Code></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo -bar</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> but <b>not</b> <Code>/bar/</Code></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code>foo (bar <Highlight>or</Highlight> baz)</Code></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>and</b> either <Code>/bar/</Code> <b>or</b> <Code>/baz/</Code></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Separator className="my-2"/>
|
||||
<p className="text-sm">
|
||||
Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the <Code>-</Code> prefix.
|
||||
</p>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Prefix</TableHead>
|
||||
<TableHead className="py-2">Description</TableHead>
|
||||
<TableHead className="py-2 w-[175px]">Example</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>file:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2">Filter results from filepaths that match the regex. By default all files are searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
title="Filter results to filepaths that match regex /README/"
|
||||
>
|
||||
<Highlight>file:</Highlight>README
|
||||
</Code>
|
||||
<Code
|
||||
title="Filter results to filepaths that match regex /my file/"
|
||||
>
|
||||
<Highlight>file:</Highlight>{`"my file"`}
|
||||
</Code>
|
||||
<Code
|
||||
title="Ignore results from filepaths match regex /test\.ts$/"
|
||||
>
|
||||
<Highlight>-file:</Highlight>test\.ts$
|
||||
</Code>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>repo:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2">Filter results from repos that match the regex. By default all repos are searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
title="Filter results to repos that match regex /linux/"
|
||||
>
|
||||
<Highlight>repo:</Highlight>linux
|
||||
</Code>
|
||||
<Code
|
||||
title="Ignore results from repos that match regex /^web\/.*/"
|
||||
>
|
||||
<Highlight>-repo:</Highlight>^web/.*
|
||||
</Code>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>rev:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2">Filter results from a specific branch or tag. By default <b>only</b> the default branch is searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
title="Filter results to branches that match regex /beta/"
|
||||
>
|
||||
<Highlight>rev:</Highlight>beta
|
||||
</Code>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>lang:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2">Filter results by language (as defined by <Link className="text-blue-500" href={LINGUIST_LINK}>linguist</Link>). By default all languages are searched.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
title="Filter results to TypeScript files"
|
||||
>
|
||||
<Highlight>lang:</Highlight>TypeScript
|
||||
</Code>
|
||||
<Code
|
||||
title="Ignore results from YAML files"
|
||||
>
|
||||
<Highlight>-lang:</Highlight>YAML
|
||||
</Code>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><Code><Highlight>sym:</Highlight></Code></TableCell>
|
||||
<TableCell className="py-2">Match symbol definitions created by <Link className="text-blue-500" href={CTAGS_LINK}>universal ctags</Link> at index time.</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Code
|
||||
title="Filter results to symbols that match regex /\bmain\b/"
|
||||
>
|
||||
<Highlight>sym:</Highlight>\bmain\b
|
||||
</Code>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const Code = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
|
||||
return (
|
||||
<code
|
||||
className={clsx("bg-gray-100 dark:bg-gray-700 w-fit rounded-md font-mono px-2 py-0.5", className)}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-highlight">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
25
packages/web/src/app/components/vscodeFileIcon.tsx
Normal file
25
packages/web/src/app/components/vscodeFileIcon.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use client';
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
import { getIconForFile } from "vscode-icons-js";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface VscodeFileIconProps {
|
||||
fileName: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VscodeFileIcon = ({ fileName, className }: VscodeFileIconProps) => {
|
||||
const iconName = useMemo(() => {
|
||||
const icon = getIconForFile(fileName);
|
||||
if (icon) {
|
||||
const iconName = `vscode-icons:${icon.substring(0, icon.indexOf('.')).replaceAll('_', '-')}`;
|
||||
return iconName;
|
||||
}
|
||||
|
||||
return "vscode-icons:file-type-unknown";
|
||||
}, [fileName]);
|
||||
|
||||
return <Icon icon={iconName} className={cn("w-4 h-4 flex-shrink-0", className)} />;
|
||||
}
|
||||
25
packages/web/src/app/components/vscodeFolderIcon.tsx
Normal file
25
packages/web/src/app/components/vscodeFolderIcon.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use client';
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
import { getIconForFolder } from "vscode-icons-js";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface VscodeFolderIconProps {
|
||||
folderName: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VscodeFolderIcon = ({ folderName, className }: VscodeFolderIconProps) => {
|
||||
const iconName = useMemo(() => {
|
||||
const icon = getIconForFolder(folderName);
|
||||
if (icon) {
|
||||
const iconName = `vscode-icons:${icon.substring(0, icon.indexOf('.')).replaceAll('_', '-')}`;
|
||||
return iconName;
|
||||
}
|
||||
|
||||
return "vscode-icons:folder";
|
||||
}, [folderName]);
|
||||
|
||||
return <Icon icon={iconName} className={cn("w-4 h-4 flex-shrink-0", className)} />;
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "./codemirror-styles.css";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: hsl(0 0% 100%);
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
--secondary-foreground: hsl(222.2 47.4% 11.2%);
|
||||
--muted: hsl(210 40% 96.1%);
|
||||
--muted-foreground: hsl(215.4 16.3% 46.9%);
|
||||
--muted-accent: hsl(210, 12%, 87%);
|
||||
--accent: hsl(210 40% 96.1%);
|
||||
--accent-foreground: hsl(222.2 47.4% 11.2%);
|
||||
--destructive: hsl(0 84.2% 60.2%);
|
||||
|
|
@ -39,6 +42,7 @@
|
|||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||
--sidebar-border: hsl(220 13% 91%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
--link: hsl(217, 91%, 60%);
|
||||
|
||||
--editor-font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
--editor-font-size: 13px;
|
||||
|
|
@ -83,6 +87,13 @@
|
|||
--editor-tag-number: #219;
|
||||
--editor-tag-regexp: #e40;
|
||||
--editor-tag-variable-local: #30a;
|
||||
|
||||
--chat-reference: #02255f11;
|
||||
--chat-reference-hover: #02225f22;
|
||||
--chat-reference-selected: #3b83f640;
|
||||
--chat-reference-selected-border: #e052b8;
|
||||
|
||||
--warning: #ca8a04;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -99,9 +110,10 @@
|
|||
--secondary-foreground: hsl(210 40% 98%);
|
||||
--muted: hsl(217.2 32.6% 17.5%);
|
||||
--muted-foreground: hsl(215 20.2% 65.1%);
|
||||
--muted-accent: hsl(218, 13%, 29%);
|
||||
--accent: hsl(217.2 32.6% 17.5%);
|
||||
--accent-foreground: hsl(210 40% 98%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive: hsl(0, 78%, 57%);
|
||||
--destructive-foreground: hsl(210 40% 98%);
|
||||
--border: hsl(217.2 32.6% 17.5%);
|
||||
--input: hsl(217.2 32.6% 17.5%);
|
||||
|
|
@ -120,6 +132,7 @@
|
|||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
--link: hsl(217, 91%, 60%);
|
||||
|
||||
--editor-background: var(--background);
|
||||
--editor-foreground: #abb2bf;
|
||||
|
|
@ -161,6 +174,13 @@
|
|||
--editor-tag-number: #e5c07b;
|
||||
--editor-tag-regexp: #56b6c2;
|
||||
--editor-tag-variable-local: #61afef;
|
||||
|
||||
--chat-reference: #2c313aad;
|
||||
--chat-reference-hover: #374151;
|
||||
--chat-reference-selected: #1e3b8a87;
|
||||
--chat-reference-selected-border: #60a5fa;
|
||||
|
||||
--warning: #fde047;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +223,34 @@
|
|||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Chat-specific styling classes */
|
||||
.chat-lineHighlight {
|
||||
background: var(--chat-reference);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-lineHighlight-hover {
|
||||
background: var(--chat-reference-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-lineHighlight-selected {
|
||||
background: var(--chat-reference-selected);
|
||||
cursor: pointer;
|
||||
--cm-range-border-shadow-color: var(--chat-reference-selected-border);
|
||||
}
|
||||
|
||||
/* Reference states for markdown reference spans */
|
||||
.chat-reference--selected {
|
||||
background-color: var(--chat-reference-selected) !important;
|
||||
border-color: var(--chat-reference-selected-border) !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-reference--hover {
|
||||
background-color: var(--chat-reference-hover) !important;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
|
|
@ -215,8 +263,6 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
|
|
@ -257,14 +303,14 @@
|
|||
.cm-editor .cm-line::selection,
|
||||
.cm-editor .cm-selectionLayer .cm-selectionBackground,
|
||||
.cm-content ::selection {
|
||||
background: var(--editor-selection-highlight, #ffe066) !important;
|
||||
color: var(--editor-selection-highlight-foreground, #222) !important;
|
||||
background: var(--editor-selection-highlight, #ffe066) !important;
|
||||
color: var(--editor-selection-highlight-foreground, #222) !important;
|
||||
}
|
||||
|
||||
.dark .cm-editor .cm-selectionBackground,
|
||||
.dark .cm-editor .cm-line::selection,
|
||||
.dark .cm-editor .cm-selectionLayer .cm-selectionBackground,
|
||||
.dark .cm-content ::selection {
|
||||
background: var(--editor-selection-highlight, #2563eb) !important;
|
||||
color: var(--editor-selection-highlight-foreground, #fff) !important;
|
||||
background: var(--editor-selection-highlight, #2563eb) !important;
|
||||
color: var(--editor-selection-highlight-foreground, #fff) !important;
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import { getEntitlements } from "@sourcebot/shared";
|
|||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sourcebot",
|
||||
description: "Sourcebot",
|
||||
description: "Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.",
|
||||
manifest: "/manifest.json",
|
||||
};
|
||||
|
||||
|
|
|
|||
58
packages/web/src/components/ui/accordion.tsx
Normal file
58
packages/web/src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ResizableHandle } from "./resizable";
|
||||
|
||||
export const AnimatedResizableHandle = () => {
|
||||
interface AnimatedResizableHandleProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AnimatedResizableHandle = ({ className }: AnimatedResizableHandleProps) => {
|
||||
return (
|
||||
<ResizableHandle
|
||||
className="w-[1px] bg-accent transition-colors delay-50 data-[resize-handle-state=drag]:bg-accent-foreground data-[resize-handle-state=hover]:bg-accent-foreground"
|
||||
className={cn("w-[1px] bg-accent transition-colors delay-50 data-[resize-handle-state=drag]:bg-accent-foreground data-[resize-handle-state=hover]:bg-accent-foreground", className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
11
packages/web/src/components/ui/collapsible.tsx
Normal file
11
packages/web/src/components/ui/collapsible.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
|
|
@ -14,7 +14,8 @@ const ScrollArea = React.forwardRef<
|
|||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{/* @see: https://github.com/radix-ui/primitives/issues/926#issuecomment-1447283516 */}
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full [&>div]:!block rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ const SelectItem = React.forwardRef<
|
|||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
{children}
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
|
|
|||
22
packages/web/src/components/ui/textarea.tsx
Normal file
22
packages/web/src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
|
|
@ -101,7 +101,6 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
|||
|
||||
useHotkeys('alt+shift+f12', () => {
|
||||
if (symbolInfo?.symbolName) {
|
||||
console.log('here!');
|
||||
onFindReferences(symbolInfo.symbolName);
|
||||
}
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -91,11 +91,30 @@ export const env = createEnv({
|
|||
GITHUB_APP_ID: z.string().optional(),
|
||||
GITHUB_APP_WEBHOOK_SECRET: z.string().optional(),
|
||||
GITHUB_APP_PRIVATE_KEY_PATH: z.string().optional(),
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
REVIEW_AGENT_API_KEY: z.string().optional(),
|
||||
REVIEW_AGENT_LOGGING_ENABLED: booleanSchema.default('true'),
|
||||
REVIEW_AGENT_AUTO_REVIEW_ENABLED: booleanSchema.default('false'),
|
||||
REVIEW_AGENT_REVIEW_COMMAND: z.string().default('review'),
|
||||
|
||||
ANTHROPIC_API_KEY: z.string().optional(),
|
||||
ANTHROPIC_THINKING_BUDGET_TOKENS: numberSchema.default(12000),
|
||||
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: z.string().optional(),
|
||||
GOOGLE_VERTEX_PROJECT: z.string().optional(),
|
||||
GOOGLE_VERTEX_REGION: z.string().default('us-central1'),
|
||||
GOOGLE_APPLICATION_CREDENTIALS: z.string().optional(),
|
||||
|
||||
AWS_ACCESS_KEY_ID: z.string().optional(),
|
||||
AWS_SECRET_ACCESS_KEY: z.string().optional(),
|
||||
AWS_REGION: z.string().optional(),
|
||||
|
||||
SOURCEBOT_CHAT_MODEL_TEMPERATURE: numberSchema.default(0.3),
|
||||
SOURCEBOT_CHAT_MAX_STEP_COUNT: numberSchema.default(20),
|
||||
|
||||
DEBUG_WRITE_CHAT_MESSAGES_TO_FILE: booleanSchema.default('false'),
|
||||
|
||||
LANGFUSE_SECRET_KEY: z.string().optional(),
|
||||
},
|
||||
// @NOTE: Please make sure of the following:
|
||||
// - Make sure you destructure all client variables in
|
||||
|
|
@ -110,6 +129,9 @@ export const env = createEnv({
|
|||
NEXT_PUBLIC_POLLING_INTERVAL_MS: numberSchema.default(5000),
|
||||
|
||||
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: z.enum(SOURCEBOT_CLOUD_ENVIRONMENT).optional(),
|
||||
|
||||
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_LANGFUSE_BASE_URL: z.string().optional()
|
||||
},
|
||||
// For Next.js >= 13.4.4, you only need to destructure client variables:
|
||||
experimental__runtimeEnv: {
|
||||
|
|
@ -117,6 +139,8 @@ export const env = createEnv({
|
|||
NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION,
|
||||
NEXT_PUBLIC_POLLING_INTERVAL_MS: process.env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
|
||||
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT,
|
||||
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: process.env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY,
|
||||
NEXT_PUBLIC_LANGFUSE_BASE_URL: process.env.NEXT_PUBLIC_LANGFUSE_BASE_URL,
|
||||
},
|
||||
skipValidation: process.env.SKIP_ENV_VALIDATION === "1",
|
||||
emptyStringAsUndefined: true,
|
||||
|
|
|
|||
15
packages/web/src/features/chat/README.md
Normal file
15
packages/web/src/features/chat/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
## Sources and References
|
||||
|
||||
We have the concept of "sources" and "references" in sb:
|
||||
1. **source** - A source is some artifact that exists in the codebase (e.g., file, commit, etc.) that helps the LLM ground its answer in reality.
|
||||
2. **references** - A reference (or citation) is a _pointer_ to a source that the LLM can output in it's response so that the developer can understand why the LLM got to the conclusion it got to.
|
||||
|
||||
Sources can be attached to a chat thread in two ways:
|
||||
1. The developer @ mentions a source (e.g., _"what does `@auth.ts` do?"_) in their request.
|
||||
2. The LLM makes a tool call (e.g., `readFile`) in its response.
|
||||
|
||||
Sources are included in the chat thread using a [custom data part](https://v5.ai-sdk.dev/docs/ai-sdk-ui/streaming-data#streaming-custom-data) as a JSON payload with the necessary data to allow us to retrieve the source at a later point (e.g., in `ReferencedSourcesListView.tsx`).
|
||||
|
||||
References are included in a LLMs response by embedding a known pattern (e.g., `@file:{auth.ts:12-24}`) that can be grepped and rendered with a custom component using a [remark plugin](https://github.com/remarkjs/remark). The LLM is instructed to use this pattern in the system prompt.
|
||||
|
||||
The process of resolving a reference to a source is inherently fuzzy since we are not guaranteed any determinism with LLMs (e.g., the LLM could hallucinate a source that doesn't exist). We perform reference resolution on a best-effort basis.
|
||||
302
packages/web/src/features/chat/actions.ts
Normal file
302
packages/web/src/features/chat/actions.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
'use server';
|
||||
|
||||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { chatIsReadonly, notFound, ServiceError } from "@/lib/serviceError";
|
||||
import { prisma } from "@/prisma";
|
||||
import { ChatVisibility, OrgRole, Prisma } from "@sourcebot/db";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { LanguageModelInfo, SBChatMessage } from "./types";
|
||||
import { loadConfig } from "@sourcebot/shared";
|
||||
import { LanguageModel } from "@sourcebot/schemas/v3/languageModel.type";
|
||||
import { SOURCEBOT_GUEST_USER_ID } from "@/lib/constants";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { ErrorCode } from "@/lib/errorCodes";
|
||||
|
||||
export const createChat = async (domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
|
||||
const isGuestUser = userId === SOURCEBOT_GUEST_USER_ID;
|
||||
|
||||
const chat = await prisma.chat.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
messages: [] as unknown as Prisma.InputJsonValue,
|
||||
createdById: userId,
|
||||
visibility: isGuestUser ? ChatVisibility.PUBLIC : ChatVisibility.PRIVATE,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: chat.id,
|
||||
}
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
|
||||
);
|
||||
|
||||
export const getChatInfo = async ({ chatId }: { chatId: string }, domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.visibility === ChatVisibility.PRIVATE && chat.createdById !== userId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
messages: chat.messages as unknown as SBChatMessage[],
|
||||
visibility: chat.visibility,
|
||||
name: chat.name,
|
||||
isReadonly: chat.isReadonly,
|
||||
};
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
|
||||
);
|
||||
|
||||
export const updateChatMessages = async ({ chatId, messages }: { chatId: string, messages: SBChatMessage[] }, domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.visibility === ChatVisibility.PRIVATE && chat.createdById !== userId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.isReadonly) {
|
||||
return chatIsReadonly();
|
||||
}
|
||||
|
||||
await prisma.chat.update({
|
||||
where: {
|
||||
id: chatId,
|
||||
},
|
||||
data: {
|
||||
messages: messages as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
if (env.DEBUG_WRITE_CHAT_MESSAGES_TO_FILE) {
|
||||
const chatDir = path.join(env.DATA_CACHE_DIR, 'chats');
|
||||
if (!fs.existsSync(chatDir)) {
|
||||
fs.mkdirSync(chatDir, { recursive: true });
|
||||
}
|
||||
|
||||
const chatFile = path.join(chatDir, `${chatId}.json`);
|
||||
fs.writeFileSync(chatFile, JSON.stringify(messages, null, 2));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
|
||||
);
|
||||
|
||||
export const getUserChatHistory = async (domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chats = await prisma.chat.findMany({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
createdById: userId,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return chats.map((chat) => ({
|
||||
id: chat.id,
|
||||
createdAt: chat.createdAt,
|
||||
name: chat.name,
|
||||
visibility: chat.visibility,
|
||||
}))
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export const updateChatName = async ({ chatId, name }: { chatId: string, name: string }, domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.visibility === ChatVisibility.PRIVATE && chat.createdById !== userId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.isReadonly) {
|
||||
return chatIsReadonly();
|
||||
}
|
||||
|
||||
await prisma.chat.update({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
|
||||
);
|
||||
|
||||
export const deleteChat = async ({ chatId }: { chatId: string }, domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// Public chats cannot be deleted.
|
||||
if (chat.visibility === ChatVisibility.PUBLIC) {
|
||||
return {
|
||||
statusCode: StatusCodes.FORBIDDEN,
|
||||
errorCode: ErrorCode.UNEXPECTED_ERROR,
|
||||
message: 'You are not allowed to delete this chat.',
|
||||
} satisfies ServiceError;
|
||||
}
|
||||
|
||||
// Only the creator of a chat can delete it.
|
||||
if (chat.createdById !== userId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
await prisma.chat.delete({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export const submitFeedback = async ({
|
||||
chatId,
|
||||
messageId,
|
||||
feedbackType
|
||||
}: {
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
feedbackType: 'like' | 'dislike'
|
||||
}, domain: string) => sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
id: chatId,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// When a chat is private, only the creator can submit feedback.
|
||||
if (chat.visibility === ChatVisibility.PRIVATE && chat.createdById !== userId) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const messages = chat.messages as unknown as SBChatMessage[];
|
||||
const updatedMessages = messages.map(message => {
|
||||
if (message.id === messageId && message.role === 'assistant') {
|
||||
return {
|
||||
...message,
|
||||
metadata: {
|
||||
...message.metadata,
|
||||
feedback: {
|
||||
type: feedbackType,
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: userId,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
await prisma.chat.update({
|
||||
where: { id: chatId },
|
||||
data: {
|
||||
messages: updatedMessages as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the subset of information about the configured language models
|
||||
* that we can safely send to the client.
|
||||
*/
|
||||
export const getConfiguredLanguageModelsInfo = async (): Promise<LanguageModelInfo[]> => {
|
||||
const models = await _getConfiguredLanguageModelsFull();
|
||||
return models.map((model): LanguageModelInfo => ({
|
||||
provider: model.provider,
|
||||
model: model.model,
|
||||
displayName: model.displayName,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full configuration of the language models.
|
||||
*
|
||||
* @warning Do NOT call this function from the client,
|
||||
* or pass the result of calling this function to the client.
|
||||
*/
|
||||
export const _getConfiguredLanguageModelsFull = async (): Promise<LanguageModel[]> => {
|
||||
if (!env.CONFIG_PATH) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await loadConfig(env.CONFIG_PATH);
|
||||
return config.models ?? [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config file ${env.CONFIG_PATH}: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
262
packages/web/src/features/chat/agent.ts
Normal file
262
packages/web/src/features/chat/agent.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { env } from "@/env.mjs";
|
||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { ProviderOptions } from "@ai-sdk/provider-utils";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { LanguageModel, ModelMessage, StopCondition, streamText } from "ai";
|
||||
import { ANSWER_TAG, FILE_REFERENCE_PREFIX, toolNames } from "./constants";
|
||||
import { createCodeSearchTool, findSymbolDefinitionsTool, findSymbolReferencesTool, readFilesTool } from "./tools";
|
||||
import { FileSource, Source } from "./types";
|
||||
import { addLineNumbers, fileReferenceToString } from "./utils";
|
||||
|
||||
const logger = createLogger('chat-agent');
|
||||
|
||||
interface AgentOptions {
|
||||
model: LanguageModel;
|
||||
providerOptions?: ProviderOptions;
|
||||
headers?: Record<string, string>;
|
||||
selectedRepos: string[];
|
||||
inputMessages: ModelMessage[];
|
||||
inputSources: Source[];
|
||||
onWriteSource: (source: Source) => void;
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
// If the agent exceeds the step count, then we will stop.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const stepCountIsGTE = (stepCount: number): StopCondition<any> => {
|
||||
return ({ steps }) => steps.length >= stepCount;
|
||||
}
|
||||
|
||||
export const createAgentStream = async ({
|
||||
model,
|
||||
providerOptions,
|
||||
headers,
|
||||
inputMessages,
|
||||
inputSources,
|
||||
selectedRepos,
|
||||
onWriteSource,
|
||||
traceId,
|
||||
}: AgentOptions) => {
|
||||
const baseSystemPrompt = createBaseSystemPrompt({
|
||||
selectedRepos,
|
||||
});
|
||||
|
||||
const stream = streamText({
|
||||
model,
|
||||
providerOptions,
|
||||
headers,
|
||||
system: baseSystemPrompt,
|
||||
messages: inputMessages,
|
||||
tools: {
|
||||
[toolNames.searchCode]: createCodeSearchTool(selectedRepos),
|
||||
[toolNames.readFiles]: readFilesTool,
|
||||
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
||||
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
||||
},
|
||||
prepareStep: async ({ stepNumber }) => {
|
||||
// The first step attaches any mentioned sources to the system prompt.
|
||||
if (stepNumber === 0 && inputSources.length > 0) {
|
||||
const fileSources = inputSources.filter((source) => source.type === 'file');
|
||||
|
||||
const resolvedFileSources = (
|
||||
await Promise.all(fileSources.map(resolveFileSource)))
|
||||
.filter((source) => source !== undefined)
|
||||
|
||||
const fileSourcesSystemPrompt = await createFileSourcesSystemPrompt({
|
||||
files: resolvedFileSources
|
||||
});
|
||||
|
||||
return {
|
||||
system: `${baseSystemPrompt}\n\n${fileSourcesSystemPrompt}`
|
||||
}
|
||||
}
|
||||
|
||||
if (stepNumber === env.SOURCEBOT_CHAT_MAX_STEP_COUNT - 1) {
|
||||
return {
|
||||
system: `**CRITICAL**: You have reached the maximum number of steps!! YOU MUST PROVIDE YOUR FINAL ANSWER NOW. DO NOT KEEP RESEARCHING.\n\n${answerInstructions}`,
|
||||
activeTools: [],
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
temperature: env.SOURCEBOT_CHAT_MODEL_TEMPERATURE,
|
||||
stopWhen: [
|
||||
stepCountIsGTE(env.SOURCEBOT_CHAT_MAX_STEP_COUNT),
|
||||
],
|
||||
toolChoice: "auto", // Let the model decide when to use tools
|
||||
onStepFinish: ({ toolResults }) => {
|
||||
// This takes care of extracting any sources that the LLM has seen as part of
|
||||
// the tool calls it made.
|
||||
toolResults.forEach(({ output, toolName }) => {
|
||||
if (isServiceError(output)) {
|
||||
// is there something we want to do here?
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolName === toolNames.readFiles) {
|
||||
output.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.path,
|
||||
revision: file.revision,
|
||||
name: file.path.split('/').pop() ?? file.path,
|
||||
})
|
||||
})
|
||||
}
|
||||
else if (toolName === toolNames.searchCode) {
|
||||
output.files.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.fileName,
|
||||
revision: file.revision,
|
||||
name: file.fileName.split('/').pop() ?? file.fileName,
|
||||
})
|
||||
})
|
||||
}
|
||||
else if (toolName === toolNames.findSymbolDefinitions || toolName === toolNames.findSymbolReferences) {
|
||||
output.forEach((file) => {
|
||||
onWriteSource({
|
||||
type: 'file',
|
||||
language: file.language,
|
||||
repo: file.repository,
|
||||
path: file.fileName,
|
||||
revision: file.revision,
|
||||
name: file.fileName.split('/').pop() ?? file.fileName,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
// Only enable langfuse traces in cloud environments.
|
||||
experimental_telemetry: {
|
||||
isEnabled: env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined,
|
||||
metadata: {
|
||||
langfuseTraceId: traceId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
interface BaseSystemPromptOptions {
|
||||
selectedRepos: string[];
|
||||
}
|
||||
|
||||
export const createBaseSystemPrompt = ({
|
||||
selectedRepos,
|
||||
}: BaseSystemPromptOptions) => {
|
||||
return `
|
||||
You are a powerful agentic AI code assistant built into Sourcebot, the world's best code-intelligence platform. Your job is to help developers understand and navigate their large codebases.
|
||||
|
||||
<workflow>
|
||||
Your workflow has two distinct phases:
|
||||
|
||||
**Phase 1: Research & Analysis**
|
||||
- Analyze the user's question and determine what context you need
|
||||
- Use available tools to gather code, search repositories, find references, etc.
|
||||
- Think through the problem and collect all relevant information
|
||||
- Do NOT provide partial answers or explanations during this phase
|
||||
|
||||
**Phase 2: Structured Response**
|
||||
- **MANDATORY**: You MUST always enter this phase and provide a structured markdown response, regardless of whether phase 1 was completed or interrupted
|
||||
- Provide your final response based on whatever context you have available
|
||||
- Always format your response according to the required response format below
|
||||
</workflow>
|
||||
|
||||
<available_repositories>
|
||||
The user has selected the following repositories for analysis:
|
||||
${selectedRepos.map(repo => `- ${repo}`).join('\n')}
|
||||
</available_repositories>
|
||||
|
||||
<research_phase_instructions>
|
||||
During the research phase, you have these tools available:
|
||||
- \`${toolNames.searchCode}\`: Search for code patterns, functions, or text across repositories
|
||||
- \`${toolNames.readFiles}\`: Read the contents of specific files
|
||||
- \`${toolNames.findSymbolReferences}\`: Find where symbols are referenced
|
||||
- \`${toolNames.findSymbolDefinitions}\`: Find where symbols are defined
|
||||
|
||||
Use these tools to gather comprehensive context before answering. Always explain why you're using each tool.
|
||||
</research_phase_instructions>
|
||||
|
||||
${answerInstructions}
|
||||
`;
|
||||
}
|
||||
|
||||
const answerInstructions = `
|
||||
<answer_instructions>
|
||||
When you have sufficient context, output your answer as a structured markdown response.
|
||||
|
||||
**Required Response Format:**
|
||||
- **CRITICAL**: You MUST always prefix your answer with a \`${ANSWER_TAG}\` tag at the very top of your response
|
||||
- **CRITICAL**: You MUST provide your complete response in markdown format with embedded code references
|
||||
- **CODE REFERENCE REQUIREMENT**: Whenever you mention, discuss, or refer to ANY specific part of the code (files, functions, variables, methods, classes, imports, etc.), you MUST immediately follow with a code reference using the format \`${fileReferenceToString({ fileName: 'filename'})}\` or \`${fileReferenceToString({ fileName: 'filename', range: { startLine: 1, endLine: 10 } })}\` (where the numbers are the start and end line numbers of the code snippet). This includes:
|
||||
- Files (e.g., "The \`auth.ts\` file" → must include \`${fileReferenceToString({ fileName: 'auth.ts' })}\`)
|
||||
- Function names (e.g., "The \`getRepos()\` function" → must include \`${fileReferenceToString({ fileName: 'auth.ts', range: { startLine: 15, endLine: 20 } })}\`)
|
||||
- Variable names (e.g., "The \`suggestionQuery\` variable" → must include \`${fileReferenceToString({ fileName: 'search.ts', range: { startLine: 42, endLine: 42 } })}\`)
|
||||
- Code patterns (e.g., "using \`file:\${suggestionQuery}\` pattern" → must include \`${fileReferenceToString({ fileName: 'search.ts', range: { startLine: 10, endLine: 15 } })}\`)
|
||||
- Any code snippet or line you're explaining
|
||||
- Class names, method calls, imports, etc.
|
||||
- Be clear and very concise. Use bullet points where appropriate
|
||||
- Do NOT explain code without providing the exact location reference. Every code mention requires a corresponding \`${FILE_REFERENCE_PREFIX}\` reference
|
||||
- If you cannot provide a code reference for something you're discussing, do not mention that specific code element
|
||||
- Always prefer to use \`${FILE_REFERENCE_PREFIX}\` over \`\`\`code\`\`\` blocks.
|
||||
|
||||
**Example answer structure:**
|
||||
\`\`\`markdown
|
||||
${ANSWER_TAG}
|
||||
Authentication in Sourcebot is built on NextAuth.js with a session-based approach using JWT tokens and Prisma as the database adapter ${fileReferenceToString({ fileName: 'auth.ts', range: { startLine: 135, endLine: 140 } })}. The system supports multiple authentication providers and implements organization-based authorization with role-defined permissions.
|
||||
\`\`\`
|
||||
|
||||
</answer_instructions>
|
||||
`;
|
||||
|
||||
interface FileSourcesSystemPromptOptions {
|
||||
files: {
|
||||
path: string;
|
||||
source: string;
|
||||
repo: string;
|
||||
language: string;
|
||||
revision: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const createFileSourcesSystemPrompt = async ({ files }: FileSourcesSystemPromptOptions) => {
|
||||
return `
|
||||
The user has mentioned the following files, which are automatically included for analysis.
|
||||
|
||||
${files.map(file => `<file path="${file.path}" repository="${file.repo}" language="${file.language}" revision="${file.revision}">
|
||||
${addLineNumbers(file.source)}
|
||||
</file>`).join('\n\n')}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
const resolveFileSource = async ({ path, repo, revision }: FileSource) => {
|
||||
const fileSource = await getFileSource({
|
||||
fileName: path,
|
||||
repository: repo,
|
||||
branch: revision,
|
||||
// @todo: handle multi-tenancy.
|
||||
}, SINGLE_TENANT_ORG_DOMAIN);
|
||||
|
||||
if (isServiceError(fileSource)) {
|
||||
// @todo: handle this
|
||||
logger.error("Error fetching file source:", fileSource)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
source: fileSource.source,
|
||||
repo,
|
||||
language: fileSource.language,
|
||||
revision,
|
||||
}
|
||||
}
|
||||
407
packages/web/src/features/chat/components/chatBox/chatBox.tsx
Normal file
407
packages/web/src/features/chat/components/chatBox/chatBox.tsx
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
'use client';
|
||||
|
||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor } from "@/features/chat/types";
|
||||
import { insertMention, slateContentToString } from "@/features/chat/utils";
|
||||
import { cn, IS_MAC } from "@/lib/utils";
|
||||
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
||||
import { ArrowUp, Loader2, StopCircleIcon, TriangleAlertIcon } from "lucide-react";
|
||||
import { Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Descendant, insertText } from "slate";
|
||||
import { Editable, ReactEditor, RenderElementProps, RenderLeafProps, useFocused, useSelected, useSlate } from "slate-react";
|
||||
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
||||
import { SuggestionBox } from "./suggestionsBox";
|
||||
import { Suggestion } from "./types";
|
||||
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
|
||||
import { useSuggestionsData } from "./useSuggestionsData";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
|
||||
interface ChatBoxProps {
|
||||
onSubmit: (children: Descendant[], editor: CustomEditor) => void;
|
||||
onStop?: () => void;
|
||||
preferredSuggestionsBoxPlacement?: "top-start" | "bottom-start";
|
||||
className?: string;
|
||||
isRedirecting?: boolean;
|
||||
isGenerating?: boolean;
|
||||
languageModels: LanguageModelInfo[];
|
||||
selectedRepos: string[];
|
||||
onRepoSelectorOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const ChatBox = ({
|
||||
onSubmit: _onSubmit,
|
||||
onStop,
|
||||
preferredSuggestionsBoxPlacement = "bottom-start",
|
||||
className,
|
||||
isRedirecting,
|
||||
isGenerating,
|
||||
languageModels,
|
||||
selectedRepos,
|
||||
onRepoSelectorOpenChanged,
|
||||
}: ChatBoxProps) => {
|
||||
const suggestionsBoxRef = useRef<HTMLDivElement>(null);
|
||||
const [index, setIndex] = useState(0);
|
||||
const editor = useSlate();
|
||||
const { suggestionQuery, suggestionMode, range } = useSuggestionModeAndQuery();
|
||||
const { suggestions, isLoading } = useSuggestionsData({
|
||||
suggestionMode,
|
||||
suggestionQuery,
|
||||
selectedRepos,
|
||||
});
|
||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
|
||||
// Reset the index when the suggestion mode changes.
|
||||
useEffect(() => {
|
||||
setIndex(0);
|
||||
}, [suggestionMode]);
|
||||
|
||||
// Hotkey to focus the chat box.
|
||||
useHotkeys("/", (e) => {
|
||||
e.preventDefault();
|
||||
ReactEditor.focus(editor);
|
||||
});
|
||||
|
||||
// Auto-focus chat box when the component mounts.
|
||||
useEffect(() => {
|
||||
ReactEditor.focus(editor);
|
||||
}, [editor]);
|
||||
|
||||
const renderElement = useCallback((props: RenderElementProps) => {
|
||||
switch (props.element.type) {
|
||||
case 'mention':
|
||||
return <MentionComponent {...props as RenderElementPropsFor<MentionElement>} />
|
||||
default:
|
||||
return <DefaultElement {...props} />
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderLeaf = useCallback((props: RenderLeafProps) => {
|
||||
return <Leaf {...props} />
|
||||
}, []);
|
||||
|
||||
const { isSubmitDisabled, isSubmitDisabledReason } = useMemo((): {
|
||||
isSubmitDisabled: true,
|
||||
isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-repos-selected" | "no-language-model-selected"
|
||||
} | {
|
||||
isSubmitDisabled: false,
|
||||
isSubmitDisabledReason: undefined,
|
||||
} => {
|
||||
if (slateContentToString(editor.children).trim().length === 0) {
|
||||
return {
|
||||
isSubmitDisabled: true,
|
||||
isSubmitDisabledReason: "empty",
|
||||
}
|
||||
}
|
||||
|
||||
if (isRedirecting) {
|
||||
return {
|
||||
isSubmitDisabled: true,
|
||||
isSubmitDisabledReason: "redirecting",
|
||||
}
|
||||
}
|
||||
|
||||
if (isGenerating) {
|
||||
return {
|
||||
isSubmitDisabled: true,
|
||||
isSubmitDisabledReason: "generating",
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedRepos.length === 0) {
|
||||
return {
|
||||
isSubmitDisabled: true,
|
||||
isSubmitDisabledReason: "no-repos-selected",
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedLanguageModel === undefined) {
|
||||
|
||||
return {
|
||||
isSubmitDisabled: true,
|
||||
isSubmitDisabledReason: "no-language-model-selected",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isSubmitDisabled: false,
|
||||
isSubmitDisabledReason: undefined,
|
||||
}
|
||||
|
||||
}, [
|
||||
editor.children,
|
||||
isRedirecting,
|
||||
isGenerating,
|
||||
selectedRepos.length,
|
||||
selectedLanguageModel,
|
||||
])
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isSubmitDisabled) {
|
||||
if (isSubmitDisabledReason === "no-repos-selected") {
|
||||
toast({
|
||||
description: "⚠️ One or more repositories must be selected.",
|
||||
variant: "destructive",
|
||||
});
|
||||
onRepoSelectorOpenChanged(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_onSubmit(editor.children, editor);
|
||||
}, [_onSubmit, editor, isSubmitDisabled, isSubmitDisabledReason, toast, onRepoSelectorOpenChanged]);
|
||||
|
||||
const onInsertSuggestion = useCallback((suggestion: Suggestion) => {
|
||||
switch (suggestion.type) {
|
||||
case 'file':
|
||||
insertMention(editor, {
|
||||
type: 'file',
|
||||
path: suggestion.path,
|
||||
repo: suggestion.repo,
|
||||
name: suggestion.name,
|
||||
language: suggestion.language,
|
||||
revision: suggestion.revision,
|
||||
}, range);
|
||||
break;
|
||||
case 'refine': {
|
||||
switch (suggestion.targetSuggestionMode) {
|
||||
case 'file':
|
||||
insertText(editor, 'file:');
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
ReactEditor.focus(editor);
|
||||
}, [editor, range]);
|
||||
|
||||
const onKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (suggestionMode === "none") {
|
||||
switch (event.key) {
|
||||
case 'Enter': {
|
||||
if (event.shiftKey) {
|
||||
break;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (suggestions.length > 0) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown': {
|
||||
event.preventDefault();
|
||||
const prevIndex = index >= suggestions.length - 1 ? 0 : index + 1
|
||||
setIndex(prevIndex)
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
event.preventDefault();
|
||||
const nextIndex = index <= 0 ? suggestions.length - 1 : index - 1
|
||||
setIndex(nextIndex)
|
||||
break;
|
||||
}
|
||||
case 'Tab':
|
||||
case 'Enter': {
|
||||
event.preventDefault();
|
||||
const suggestion = suggestions[index];
|
||||
onInsertSuggestion(suggestion);
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [suggestionMode, suggestions, onSubmit, index, onInsertSuggestion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!range || !suggestionsBoxRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const virtualElement: VirtualElement = {
|
||||
getBoundingClientRect: () => {
|
||||
if (!range) {
|
||||
return new DOMRect();
|
||||
}
|
||||
|
||||
return ReactEditor.toDOMRange(editor, range).getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
computePosition(virtualElement, suggestionsBoxRef.current, {
|
||||
placement: preferredSuggestionsBoxPlacement,
|
||||
middleware: [
|
||||
offset(2),
|
||||
flip({
|
||||
mainAxis: true,
|
||||
crossAxis: false,
|
||||
fallbackPlacements: ['top-start', 'bottom-start'],
|
||||
padding: 20,
|
||||
}),
|
||||
shift({
|
||||
padding: 5,
|
||||
})
|
||||
]
|
||||
}).then(({ x, y }) => {
|
||||
if (suggestionsBoxRef.current) {
|
||||
suggestionsBoxRef.current.style.left = `${x}px`;
|
||||
suggestionsBoxRef.current.style.top = `${y}px`;
|
||||
}
|
||||
})
|
||||
}, [editor, index, range, preferredSuggestionsBoxPlacement]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col justify-between gap-0.5 w-full px-3 py-2", className)}
|
||||
>
|
||||
<Editable
|
||||
className="w-full focus-visible:outline-none focus-visible:ring-0 bg-background text-base disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
placeholder="Ask, plan, or search your codebase. @mention files to refine your query."
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<div className="ml-auto z-10">
|
||||
{isRedirecting ? (
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={true}
|
||||
size="icon"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
</Button>
|
||||
) :
|
||||
isGenerating ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={onStop}
|
||||
>
|
||||
<StopCircleIcon className="w-4 h-4" />
|
||||
Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onClick={() => {
|
||||
// @hack: When submission is disabled, we still want to issue
|
||||
// a warning to the user as to why the submission is disabled.
|
||||
// onSubmit on the Button will not be called because of the
|
||||
// disabled prop, hence the call here.
|
||||
if (isSubmitDisabled) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant={isSubmitDisabled ? "outline" : "default"}
|
||||
size="sm"
|
||||
className="w-6 h-6"
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitDisabled}
|
||||
>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{(isSubmitDisabled && isSubmitDisabledReason === "no-repos-selected") && (
|
||||
<TooltipContent>
|
||||
<div className="flex flex-row items-center">
|
||||
<TriangleAlertIcon className="h-4 w-4 text-warning mr-1" />
|
||||
<span className="text-destructive">One or more repositories must be selected.</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{suggestionMode !== "none" && (
|
||||
<SuggestionBox
|
||||
ref={suggestionsBoxRef}
|
||||
selectedIndex={index}
|
||||
onInsertSuggestion={onInsertSuggestion}
|
||||
isLoading={isLoading}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DefaultElement = (props: RenderElementProps) => {
|
||||
return <p {...props.attributes}>{props.children}</p>
|
||||
}
|
||||
|
||||
const Leaf = (props: RenderLeafProps) => {
|
||||
return (
|
||||
<span
|
||||
{...props.attributes}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const MentionComponent = ({
|
||||
attributes,
|
||||
children,
|
||||
element: { data },
|
||||
}: RenderElementPropsFor<MentionElement>) => {
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
|
||||
if (data.type === 'file') {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
{...attributes}
|
||||
contentEditable={false}
|
||||
className={cn(
|
||||
"px-1.5 py-0.5 mr-1.5 mb-1 align-baseline inline-block rounded bg-muted text-xs font-mono",
|
||||
{
|
||||
"ring-2 ring-blue-300": selected && focused
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span contentEditable={false} className="flex flex-row items-center select-none">
|
||||
{/* @see: https://github.com/ianstormtaylor/slate/issues/3490 */}
|
||||
{IS_MAC ? (
|
||||
<Fragment>
|
||||
{children}
|
||||
<VscodeFileIcon fileName={data.name} className="w-3 h-3 mr-1" />
|
||||
{data.name}
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<VscodeFileIcon fileName={data.name} className="w-3 h-3 mr-1" />
|
||||
{data.name}
|
||||
{children}
|
||||
</Fragment>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span className="text-xs font-mono">
|
||||
<span className="font-medium">{data.repo.split('/').pop()}</span>/{data.path}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
'use client';
|
||||
|
||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { LanguageModelInfo } from "@/features/chat/types";
|
||||
import { RepositoryQuery } from "@/lib/types";
|
||||
import { AtSignIcon } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { ReactEditor, useSlate } from "slate-react";
|
||||
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
||||
import { LanguageModelSelector } from "./languageModelSelector";
|
||||
import { RepoSelector } from "./repoSelector";
|
||||
|
||||
export interface ChatBoxToolbarProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
selectedRepos: string[];
|
||||
onSelectedReposChange: (repos: string[]) => void;
|
||||
isRepoSelectorOpen: boolean;
|
||||
onRepoSelectorOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const ChatBoxToolbar = ({
|
||||
languageModels,
|
||||
repos,
|
||||
selectedRepos,
|
||||
onSelectedReposChange,
|
||||
isRepoSelectorOpen,
|
||||
onRepoSelectorOpenChanged,
|
||||
}: ChatBoxToolbarProps) => {
|
||||
const editor = useSlate();
|
||||
|
||||
const onAddContext = useCallback(() => {
|
||||
editor.insertText("@");
|
||||
ReactEditor.focus(editor);
|
||||
}, [editor]);
|
||||
|
||||
useHotkeys("alt+mod+p", (e) => {
|
||||
e.preventDefault();
|
||||
onAddContext();
|
||||
}, {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
description: "Add context",
|
||||
});
|
||||
|
||||
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-6 h-6 text-muted-foreground hover:text-primary"
|
||||
onClick={onAddContext}
|
||||
>
|
||||
<AtSignIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
className="flex flex-row items-center gap-2"
|
||||
>
|
||||
<KeyboardShortcutHint shortcut="⌥ ⌘ P" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<span>Add context</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="h-3 mx-1" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<RepoSelector
|
||||
className="bg-inherit w-fit h-6 min-h-6"
|
||||
repos={repos.map((repo) => repo.repoName)}
|
||||
selectedRepos={selectedRepos}
|
||||
onSelectedReposChange={onSelectedReposChange}
|
||||
isOpen={isRepoSelectorOpen}
|
||||
onOpenChanged={onRepoSelectorOpenChanged}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<span>Repositories to scope conversation to.</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{languageModels.length > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-3 ml-1 mr-2" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<LanguageModelSelector
|
||||
languageModels={languageModels}
|
||||
onSelectedModelChange={setSelectedLanguageModel}
|
||||
selectedModel={selectedLanguageModel}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<span>Selected language model</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { ChatBox } from "./chatBox";
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { LanguageModelInfo } from "@/features/chat/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Bot,
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ModelProviderLogo } from "./modelProviderLogo";
|
||||
|
||||
interface LanguageModelSelectorProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
selectedModel?: LanguageModelInfo;
|
||||
onSelectedModelChange: (model: LanguageModelInfo) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LanguageModelSelector = ({
|
||||
languageModels: _languageModels,
|
||||
selectedModel,
|
||||
onSelectedModelChange,
|
||||
className,
|
||||
}: LanguageModelSelectorProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const handleInputKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.key === "Enter") {
|
||||
setIsPopoverOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const selectModel = (model: LanguageModelInfo) => {
|
||||
onSelectedModelChange(model);
|
||||
setIsPopoverOpen(false);
|
||||
};
|
||||
|
||||
const handleTogglePopover = () => {
|
||||
setIsPopoverOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
// De-duplicate models
|
||||
const languageModels = useMemo(() => {
|
||||
return _languageModels.filter((model, selfIndex, selfArray) =>
|
||||
selfIndex === selfArray.findIndex((t) => t.model === model.model)
|
||||
);
|
||||
}, [_languageModels]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex p-1 rounded-md items-center justify-between bg-inherit h-6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between mx-auto max-w-64 overflow-hidden">
|
||||
{selectedModel ? (
|
||||
<ModelProviderLogo
|
||||
provider={selectedModel.provider}
|
||||
className="mr-1"
|
||||
/>
|
||||
) : (
|
||||
<Bot className="h-4 w-4 text-muted-foreground mr-1" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground mx-1 text-ellipsis overflow-hidden whitespace-nowrap",
|
||||
selectedModel ? "font-medium" : "font-normal"
|
||||
)}
|
||||
>
|
||||
{selectedModel ? (selectedModel.displayName ?? selectedModel.model) : "Select model"}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground ml-2" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languageModels
|
||||
.map((model, index) => {
|
||||
const isSelected = selectedModel?.model === model.model;
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${model.model}-${index}`}
|
||||
onSelect={() => {
|
||||
selectModel(model)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<ModelProviderLogo
|
||||
provider={model.provider}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>{model.displayName ?? model.model}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { LanguageModelProvider } from "../../types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import anthropicLogo from "@/public/anthropic.svg";
|
||||
import bedrockLogo from "@/public/bedrock.svg";
|
||||
import geminiLogo from "@/public/gemini.svg";
|
||||
import openaiLogo from "@/public/openai.svg";
|
||||
|
||||
interface ModelProviderLogoProps {
|
||||
provider: LanguageModelProvider;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ModelProviderLogo = ({
|
||||
provider,
|
||||
className,
|
||||
}: ModelProviderLogoProps) => {
|
||||
const { src, className: logoClassName } = useMemo(() => {
|
||||
switch (provider) {
|
||||
case 'amazon-bedrock':
|
||||
return {
|
||||
src: bedrockLogo,
|
||||
className: 'w-3.5 h-3.5 dark:invert'
|
||||
};
|
||||
case 'anthropic':
|
||||
return {
|
||||
src: anthropicLogo,
|
||||
className: 'dark:invert'
|
||||
};
|
||||
case 'openai':
|
||||
return {
|
||||
src: openaiLogo,
|
||||
className: 'dark:invert w-3.5 h-3.5'
|
||||
};
|
||||
case 'google-generative-ai':
|
||||
case 'google-vertex':
|
||||
return {
|
||||
src: geminiLogo,
|
||||
className: 'w-3.5 h-3.5'
|
||||
};
|
||||
case 'google-vertex-anthropic':
|
||||
return {
|
||||
src: anthropicLogo,
|
||||
className: 'dark:invert'
|
||||
};
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={src}
|
||||
alt={provider}
|
||||
className={cn(
|
||||
'w-4 h-4',
|
||||
logoClassName,
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
// Adapted from: web/src/components/ui/multi-select.tsx
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
BookMarkedIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
|
||||
interface RepoSelectorProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
repos: string[];
|
||||
selectedRepos: string[];
|
||||
onSelectedReposChange: (repos: string[]) => void;
|
||||
className?: string;
|
||||
isOpen: boolean;
|
||||
onOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const RepoSelector = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
RepoSelectorProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
repos,
|
||||
onSelectedReposChange,
|
||||
className,
|
||||
selectedRepos,
|
||||
isOpen,
|
||||
onOpenChanged,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const handleInputKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.key === "Enter") {
|
||||
onOpenChanged(true);
|
||||
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
||||
const newSelectedRepos = [...selectedRepos];
|
||||
newSelectedRepos.pop();
|
||||
onSelectedReposChange(newSelectedRepos);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRepo = (repo: string) => {
|
||||
const newSelectedValues = selectedRepos.includes(repo)
|
||||
? selectedRepos.filter((value) => value !== repo)
|
||||
: [...selectedRepos, repo];
|
||||
onSelectedReposChange(newSelectedValues);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onSelectedReposChange([]);
|
||||
};
|
||||
|
||||
const handleTogglePopover = () => {
|
||||
onOpenChanged(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onOpenChanged}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex p-1 rounded-md items-center justify-between bg-inherit h-6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full mx-auto">
|
||||
<BookMarkedIcon className="h-4 w-4 text-muted-foreground mr-1" />
|
||||
<span
|
||||
className={cn("text-sm text-muted-foreground mx-1 font-medium")}
|
||||
>
|
||||
{
|
||||
selectedRepos.length === 0 ? `Select a repo` :
|
||||
selectedRepos.length === 1 ? `${selectedRepos[0].split('/').pop()}` :
|
||||
`${selectedRepos.length} repo${selectedRepos.length === 1 ? '' : 's'}`
|
||||
}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground ml-2" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
onEscapeKeyDown={() => onOpenChanged(false)}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repos..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
|
||||
{repos.map((repo) => {
|
||||
const isSelected = selectedRepos.includes(repo);
|
||||
return (
|
||||
<CommandItem
|
||||
key={repo}
|
||||
onSelect={() => toggleRepo(repo)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<span>{repo}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedRepos.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandItem
|
||||
onSelect={handleClear}
|
||||
className="flex-1 justify-center cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</CommandItem>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
RepoSelector.displayName = "RepoSelector";
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
'use client';
|
||||
|
||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { forwardRef, useMemo } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { VscFiles } from "react-icons/vsc";
|
||||
import { FileSuggestion, RefineSuggestion, Suggestion } from "./types";
|
||||
|
||||
interface SuggestionBoxProps {
|
||||
selectedIndex: number;
|
||||
onInsertSuggestion: (suggestion: Suggestion) => void;
|
||||
isLoading: boolean;
|
||||
suggestions: Suggestion[];
|
||||
}
|
||||
|
||||
export const SuggestionBox = forwardRef<HTMLDivElement, SuggestionBoxProps>(({
|
||||
selectedIndex,
|
||||
onInsertSuggestion,
|
||||
isLoading,
|
||||
suggestions,
|
||||
}, ref) => {
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={ref}
|
||||
className="absolute z-10 top-0 left-0 bg-background border rounded-md p-1 w-[500px] overflow-hidden text-ellipsis"
|
||||
data-cy="mentions-portal"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="animate-pulse flex flex-col gap-2 px-1 py-0.5 w-full">
|
||||
{
|
||||
Array.from({ length: 10 }).map((_, index) => (
|
||||
<Skeleton key={index} className="h-4 w-full" />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
) :
|
||||
(suggestions.length === 0) ? (
|
||||
<div className="flex flex-col gap-2 px-1 py-0.5 w-full">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No results found
|
||||
</p>
|
||||
</div>
|
||||
) :
|
||||
(
|
||||
<div className="flex flex-col w-full">
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn("flex flex-row gap-2 w-full cursor-pointer rounded-md px-1 py-0.5 hover:bg-accent", {
|
||||
"bg-accent": i === selectedIndex,
|
||||
})}
|
||||
onClick={() => {
|
||||
onInsertSuggestion(suggestion);
|
||||
}}
|
||||
>
|
||||
{
|
||||
suggestion.type === 'file' && (
|
||||
<FileSuggestionListItem file={suggestion} />
|
||||
)
|
||||
}
|
||||
{
|
||||
suggestion.type === 'refine' && (
|
||||
<RefineSuggestionListItem refine={suggestion} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
});
|
||||
|
||||
SuggestionBox.displayName = 'SuggestionBox';
|
||||
|
||||
|
||||
const FileSuggestionListItem = ({ file }: { file: FileSuggestion }) => {
|
||||
return (
|
||||
<>
|
||||
<VscodeFileIcon fileName={file.name} className="mt-1" />
|
||||
<div className="flex flex-col w-full">
|
||||
<span className="text-sm font-medium">
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="font-medium">{file.repo.split('/').pop()}</span>/{file.path}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const RefineSuggestionListItem = ({ refine }: { refine: RefineSuggestion }) => {
|
||||
|
||||
const Icon = useMemo(() => {
|
||||
switch (refine.targetSuggestionMode) {
|
||||
case 'file':
|
||||
return VscFiles;
|
||||
}
|
||||
}, [refine.targetSuggestionMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon className="w-4 h-4 flex-shrink-0 mt-1" />
|
||||
<div className="flex flex-col w-full">
|
||||
<span className="text-sm font-medium">
|
||||
{refine.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{refine.description}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
24
packages/web/src/features/chat/components/chatBox/types.ts
Normal file
24
packages/web/src/features/chat/components/chatBox/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
export type SuggestionMode =
|
||||
"none" |
|
||||
"refine" |
|
||||
"file"
|
||||
;
|
||||
|
||||
export type RefineSuggestion = {
|
||||
type: 'refine';
|
||||
targetSuggestionMode: Exclude<SuggestionMode, 'none' | 'refine'>;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type FileSuggestion = {
|
||||
type: 'file';
|
||||
repo: string;
|
||||
path: string;
|
||||
name: string;
|
||||
language: string;
|
||||
revision: string;
|
||||
}
|
||||
|
||||
export type Suggestion = FileSuggestion | RefineSuggestion;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
'use client';
|
||||
|
||||
import { word } from "@/features/chat/utils";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Editor, Range } from "slate";
|
||||
import { SuggestionMode } from "./types";
|
||||
import { useSlate, useSlateSelection } from "slate-react";
|
||||
import { usePrevious } from "@uidotdev/usehooks";
|
||||
|
||||
|
||||
export const useSuggestionModeAndQuery = () => {
|
||||
const selection = useSlateSelection();
|
||||
const editor = useSlate();
|
||||
|
||||
const { suggestionQuery, suggestionMode, range } = useMemo<{
|
||||
suggestionQuery: string;
|
||||
suggestionMode: SuggestionMode;
|
||||
range: Range | null;
|
||||
}>(() => {
|
||||
if (!selection || !Range.isCollapsed(selection)) {
|
||||
return {
|
||||
suggestionMode: "none",
|
||||
suggestionQuery: '',
|
||||
range: null,
|
||||
};
|
||||
}
|
||||
|
||||
const range = word(editor, selection, {
|
||||
terminator: [' '],
|
||||
directions: 'both',
|
||||
});
|
||||
|
||||
if (!range) {
|
||||
return {
|
||||
suggestionMode: "none",
|
||||
suggestionQuery: '',
|
||||
range: null,
|
||||
};
|
||||
}
|
||||
|
||||
const text = Editor.string(editor, range);
|
||||
|
||||
let match: RegExpMatchArray | null = null;
|
||||
|
||||
// Refine mode.
|
||||
match = text.match(/^@$/);
|
||||
if (match) {
|
||||
return {
|
||||
suggestionMode: "refine",
|
||||
suggestionQuery: '',
|
||||
range,
|
||||
};
|
||||
}
|
||||
|
||||
// File mode.
|
||||
match = text.match(/^@file:(.*)$/);
|
||||
if (match) {
|
||||
return {
|
||||
suggestionMode: "file",
|
||||
suggestionQuery: match[1],
|
||||
range,
|
||||
};
|
||||
}
|
||||
|
||||
// If the user starts typing, fallback to file mode.
|
||||
// In the future, it would be nice to have a "all" mode that
|
||||
// searches across all mode types.
|
||||
match = text.match(/^@(.*)$/);
|
||||
if (match) {
|
||||
return {
|
||||
suggestionMode: "file",
|
||||
suggestionQuery: match[1],
|
||||
range,
|
||||
};
|
||||
}
|
||||
|
||||
// Default to none mode.
|
||||
return {
|
||||
suggestionMode: "none",
|
||||
suggestionQuery: '',
|
||||
range: null,
|
||||
};
|
||||
}, [editor, selection]);
|
||||
|
||||
// Debug logging.
|
||||
const previousSuggestionMode = usePrevious(suggestionMode);
|
||||
useEffect(() => {
|
||||
if (previousSuggestionMode !== suggestionMode) {
|
||||
console.debug(`Suggestion mode changed: ${previousSuggestionMode} -> ${suggestionMode}`);
|
||||
}
|
||||
}, [previousSuggestionMode, suggestionMode])
|
||||
|
||||
return {
|
||||
suggestionQuery,
|
||||
suggestionMode,
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
'use client';
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FileSuggestion, RefineSuggestion, Suggestion, SuggestionMode } from "./types";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { unwrapServiceError } from "@/lib/utils";
|
||||
import { search } from "@/app/api/(client)/client";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props {
|
||||
suggestionMode: SuggestionMode;
|
||||
suggestionQuery: string;
|
||||
selectedRepos: string[];
|
||||
}
|
||||
|
||||
const refineSuggestions: RefineSuggestion[] = [
|
||||
{
|
||||
type: 'refine',
|
||||
targetSuggestionMode: 'file',
|
||||
name: 'Files',
|
||||
description: 'Include a file in the agent\'s context window.',
|
||||
}
|
||||
]
|
||||
|
||||
export const useSuggestionsData = ({
|
||||
suggestionMode,
|
||||
suggestionQuery,
|
||||
selectedRepos,
|
||||
}: Props): { isLoading: boolean, suggestions: Suggestion[] } => {
|
||||
const domain = useDomain();
|
||||
|
||||
const { data: fileSuggestions, isLoading: _isLoadingFileSuggestions } = useQuery({
|
||||
queryKey: ["fileSuggestions-agentic", suggestionQuery, domain, selectedRepos],
|
||||
queryFn: () => {
|
||||
let query = `file:${suggestionQuery}`;
|
||||
if (selectedRepos.length > 0) {
|
||||
query += ` reposet:${selectedRepos.join(',')}`;
|
||||
}
|
||||
|
||||
return unwrapServiceError(search({
|
||||
query,
|
||||
matches: 10,
|
||||
contextLines: 1,
|
||||
}, domain))
|
||||
},
|
||||
select: (data): FileSuggestion[] => {
|
||||
return data.files.map((file) => {
|
||||
const path = file.fileName.text;
|
||||
const suggestion: FileSuggestion = {
|
||||
type: 'file',
|
||||
path,
|
||||
repo: file.repository,
|
||||
name: path.split('/').pop() ?? '',
|
||||
language: file.language,
|
||||
revision: 'HEAD', // @todo: make revision configurable.
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
});
|
||||
},
|
||||
enabled: suggestionMode === "file",
|
||||
});
|
||||
const isLoadingFiles = useMemo(() => suggestionMode === "file" && _isLoadingFileSuggestions, [_isLoadingFileSuggestions, suggestionMode]);
|
||||
|
||||
switch (suggestionMode) {
|
||||
case 'file':
|
||||
return {
|
||||
suggestions: fileSuggestions ?? [],
|
||||
isLoading: isLoadingFiles,
|
||||
}
|
||||
case 'refine':
|
||||
return {
|
||||
suggestions: refineSuggestions,
|
||||
isLoading: false,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
isLoading: false,
|
||||
suggestions: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
'use client';
|
||||
|
||||
import { useExtractTOCItems } from "../../useTOCItems";
|
||||
import { TableOfContents } from "./tableOfContents";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TableOfContentsIcon, ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MarkdownRenderer } from "./markdownRenderer";
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { CopyIconButton } from "@/app/[domain]/components/copyIconButton";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { convertLLMOutputToPortableMarkdown } from "../../utils";
|
||||
import { submitFeedback } from "../../actions";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { LangfuseWeb } from "langfuse";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
interface AnswerCardProps {
|
||||
answerText: string;
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
feedback?: 'like' | 'dislike' | undefined;
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
const langfuseWeb = (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined && env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY) ? new LangfuseWeb({
|
||||
publicKey: env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY,
|
||||
baseUrl: env.NEXT_PUBLIC_LANGFUSE_BASE_URL,
|
||||
}) : null;
|
||||
|
||||
export const AnswerCard = forwardRef<HTMLDivElement, AnswerCardProps>(({
|
||||
answerText,
|
||||
messageId,
|
||||
chatId,
|
||||
feedback: _feedback,
|
||||
traceId,
|
||||
}, forwardedRef) => {
|
||||
const markdownRendererRef = useRef<HTMLDivElement>(null);
|
||||
const { tocItems, activeId } = useExtractTOCItems({ target: markdownRendererRef.current });
|
||||
const [isTOCButtonToggled, setIsTOCButtonToggled] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const domain = useDomain();
|
||||
const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
|
||||
const [feedback, setFeedback] = useState<'like' | 'dislike' | undefined>(_feedback);
|
||||
const captureEvent = useCaptureEvent();
|
||||
|
||||
useImperativeHandle(
|
||||
forwardedRef,
|
||||
() => markdownRendererRef.current as HTMLDivElement
|
||||
);
|
||||
|
||||
const onCopyAnswer = useCallback(() => {
|
||||
const markdownText = convertLLMOutputToPortableMarkdown(answerText);
|
||||
navigator.clipboard.writeText(markdownText);
|
||||
toast({
|
||||
description: "✅ Copied to clipboard",
|
||||
});
|
||||
return true;
|
||||
}, [answerText, toast]);
|
||||
|
||||
const onFeedback = useCallback(async (feedbackType: 'like' | 'dislike') => {
|
||||
setIsSubmittingFeedback(true);
|
||||
|
||||
const response = await submitFeedback({
|
||||
chatId,
|
||||
messageId,
|
||||
feedbackType
|
||||
}, domain);
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
description: `❌ Failed to submit feedback: ${response.message}`,
|
||||
variant: "destructive"
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
description: `✅ Feedback submitted`,
|
||||
});
|
||||
setFeedback(feedbackType);
|
||||
captureEvent('wa_chat_feedback_submitted', {
|
||||
feedback: feedbackType,
|
||||
chatId,
|
||||
messageId,
|
||||
});
|
||||
|
||||
langfuseWeb?.score({
|
||||
traceId: traceId,
|
||||
name: 'user_feedback',
|
||||
value: feedbackType === 'like' ? 1 : 0,
|
||||
})
|
||||
}
|
||||
|
||||
setIsSubmittingFeedback(false);
|
||||
}, [chatId, messageId, domain, toast, captureEvent, traceId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row w-full relative scroll-mt-16">
|
||||
{(isTOCButtonToggled && tocItems.length > 0) && (
|
||||
<TableOfContents
|
||||
tocItems={tocItems}
|
||||
activeId={activeId}
|
||||
className="sticky top-0 h-fit max-w-44 py-2 mr-1.5"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col w-full bg-[#fcfcfc] dark:bg-[#0e1320] px-4 py-2 rounded-lg shadow-sm">
|
||||
<div className="flex flex-col z-10 bg-inherit py-2 sticky top-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="font-semibold text-muted-foreground">Answer</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CopyIconButton
|
||||
onCopy={onCopyAnswer}
|
||||
className="h-6 w-6 text-muted-foreground"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
>
|
||||
Copy answer
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{tocItems.length > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Toggle
|
||||
className="h-6 w-6 px-3 min-w-6 text-muted-foreground"
|
||||
pressed={isTOCButtonToggled}
|
||||
onPressedChange={setIsTOCButtonToggled}
|
||||
>
|
||||
<TableOfContentsIcon className="h-3 w-3" />
|
||||
</Toggle>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
>
|
||||
Toggle table of contents
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</div>
|
||||
<MarkdownRenderer
|
||||
ref={markdownRendererRef}
|
||||
content={answerText}
|
||||
// scroll-mt offsets the scroll position for headings to take account
|
||||
// of the sticky "answer" header.
|
||||
className="prose prose-sm max-w-none prose-headings:scroll-mt-14"
|
||||
/>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={feedback === 'like' ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={() => onFeedback('like')}
|
||||
disabled={isSubmittingFeedback || feedback !== undefined}
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={feedback === 'dislike' ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={() => onFeedback('dislike')}
|
||||
disabled={isSubmittingFeedback || feedback !== undefined}
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
AnswerCard.displayName = 'AnswerCard';
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
'use client';
|
||||
|
||||
import { useToast } from '@/components/hooks/use-toast';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { CustomSlateEditor } from '@/features/chat/customSlateEditor';
|
||||
import { AdditionalChatRequestParams, CustomEditor, LanguageModelInfo, SBChatMessage, Source } from '@/features/chat/types';
|
||||
import { createUIMessage, getAllMentionElements, resetEditor, slateContentToString } from '@/features/chat/utils';
|
||||
import { useDomain } from '@/hooks/useDomain';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { CreateUIMessage, DefaultChatTransport } from 'ai';
|
||||
import { ArrowDownIcon } from 'lucide-react';
|
||||
import { useNavigationGuard } from 'next-navigation-guard';
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Descendant } from 'slate';
|
||||
import { useMessagePairs } from '../../useMessagePairs';
|
||||
import { useSelectedLanguageModel } from '../../useSelectedLanguageModel';
|
||||
import { ChatBox } from '../chatBox';
|
||||
import { ChatBoxToolbar } from '../chatBox/chatBoxToolbar';
|
||||
import { ChatThreadListItem } from './chatThreadListItem';
|
||||
import { ErrorBanner } from './errorBanner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { usePrevious } from '@uidotdev/usehooks';
|
||||
import { RepositoryQuery } from '@/lib/types';
|
||||
|
||||
type ChatHistoryState = {
|
||||
scrollOffset?: number;
|
||||
}
|
||||
|
||||
interface ChatThreadProps {
|
||||
id?: string | undefined;
|
||||
initialMessages?: SBChatMessage[];
|
||||
inputMessage?: CreateUIMessage<SBChatMessage>;
|
||||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
selectedRepos: string[];
|
||||
onSelectedReposChange: (repos: string[]) => void;
|
||||
isChatReadonly: boolean;
|
||||
}
|
||||
|
||||
export const ChatThread = ({
|
||||
id: defaultChatId,
|
||||
initialMessages,
|
||||
inputMessage,
|
||||
languageModels,
|
||||
repos,
|
||||
selectedRepos,
|
||||
onSelectedReposChange,
|
||||
isChatReadonly,
|
||||
}: ChatThreadProps) => {
|
||||
const domain = useDomain();
|
||||
const [isErrorBannerVisible, setIsErrorBannerVisible] = useState(false);
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const latestMessagePairRef = useRef<HTMLDivElement>(null);
|
||||
const hasSubmittedInputMessage = useRef(false);
|
||||
const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const [isRepoSelectorOpen, setIsRepoSelectorOpen] = useState(false);
|
||||
|
||||
// Initial state is from attachments that exist in in the chat history.
|
||||
const [sources, setSources] = useState<Source[]>(
|
||||
initialMessages?.flatMap((message) =>
|
||||
message.parts
|
||||
.filter((part) => part.type === 'data-source')
|
||||
.map((part) => part.data)
|
||||
) ?? []
|
||||
);
|
||||
|
||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
});
|
||||
|
||||
const {
|
||||
messages,
|
||||
sendMessage: _sendMessage,
|
||||
error,
|
||||
status,
|
||||
stop,
|
||||
id: chatId,
|
||||
} = useChat<SBChatMessage>({
|
||||
id: defaultChatId,
|
||||
messages: initialMessages,
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
headers: {
|
||||
"X-Org-Domain": domain,
|
||||
}
|
||||
}),
|
||||
onData: (dataPart) => {
|
||||
// Keeps sources added by the assistant in sync.
|
||||
if (dataPart.type === 'data-source') {
|
||||
setSources((prev) => [...prev, dataPart.data]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sendMessage = useCallback((message: CreateUIMessage<SBChatMessage>) => {
|
||||
if (!selectedLanguageModel) {
|
||||
toast({
|
||||
description: "Failed to send message. No language model selected.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Keeps sources added by the user in sync.
|
||||
const sources = message.parts
|
||||
.filter((part) => part.type === 'data-source')
|
||||
.map((part) => part.data);
|
||||
setSources((prev) => [...prev, ...sources]);
|
||||
|
||||
_sendMessage(message, {
|
||||
body: {
|
||||
selectedRepos,
|
||||
languageModelId: selectedLanguageModel.model,
|
||||
} satisfies AdditionalChatRequestParams,
|
||||
});
|
||||
}, [_sendMessage, selectedLanguageModel, selectedRepos, toast]);
|
||||
|
||||
|
||||
const messagePairs = useMessagePairs(messages);
|
||||
|
||||
useNavigationGuard({
|
||||
enabled: status === "streaming" || status === "submitted",
|
||||
confirm: () => window.confirm("You have unsaved changes that will be lost.")
|
||||
});
|
||||
|
||||
// When the chat is finished, refresh the page to update the chat history.
|
||||
const prevStatus = usePrevious(status);
|
||||
useEffect(() => {
|
||||
const wasPending = prevStatus === "submitted" || prevStatus === "streaming";
|
||||
const isFinished = status === "error" || status === "ready";
|
||||
|
||||
if (wasPending && isFinished) {
|
||||
router.refresh();
|
||||
}
|
||||
}, [prevStatus, status, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputMessage || hasSubmittedInputMessage.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage(inputMessage);
|
||||
setIsAutoScrollEnabled(true);
|
||||
hasSubmittedInputMessage.current = true;
|
||||
}, [inputMessage, sendMessage]);
|
||||
|
||||
// Track scroll position changes.
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
|
||||
if (!scrollElement) return;
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const handleScroll = () => {
|
||||
const scrollOffset = scrollElement.scrollTop;
|
||||
|
||||
const threshold = 50; // pixels from bottom to consider "at bottom"
|
||||
const { scrollHeight, clientHeight } = scrollElement;
|
||||
const isAtBottom = scrollHeight - scrollOffset - clientHeight <= threshold;
|
||||
setIsAutoScrollEnabled(isAtBottom);
|
||||
|
||||
// Debounce the history state update
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
history.replaceState(
|
||||
{
|
||||
scrollOffset,
|
||||
} satisfies ChatHistoryState,
|
||||
'',
|
||||
window.location.href
|
||||
);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
scrollElement.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
scrollElement.removeEventListener('scroll', handleScroll);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
|
||||
if (!scrollElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollOffset } = (history.state ?? {}) as ChatHistoryState;
|
||||
scrollElement.scrollTo({
|
||||
top: scrollOffset ?? 0,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}, []);
|
||||
|
||||
// When messages are being streamed, scroll to the latest message
|
||||
// assuming auto scrolling is enabled.
|
||||
useEffect(() => {
|
||||
if (
|
||||
!latestMessagePairRef.current ||
|
||||
!isAutoScrollEnabled ||
|
||||
messages.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestMessagePairRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest',
|
||||
});
|
||||
|
||||
}, [isAutoScrollEnabled, messages]);
|
||||
|
||||
|
||||
// Keep the error state & banner visibility in sync.
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setIsErrorBannerVisible(true);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const onSubmit = useCallback((children: Descendant[], editor: CustomEditor) => {
|
||||
const text = slateContentToString(children);
|
||||
const mentions = getAllMentionElements(children);
|
||||
|
||||
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos);
|
||||
sendMessage(message);
|
||||
|
||||
setIsAutoScrollEnabled(true);
|
||||
|
||||
resetEditor(editor);
|
||||
}, [sendMessage, selectedRepos]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<ErrorBanner
|
||||
error={error}
|
||||
isVisible={isErrorBannerVisible}
|
||||
onClose={() => setIsErrorBannerVisible(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollArea
|
||||
ref={scrollAreaRef}
|
||||
className="flex flex-col h-full w-full p-4 overflow-hidden"
|
||||
>
|
||||
{
|
||||
messagePairs.length === 0 ? (
|
||||
<div className="flex items-center justify-center text-center h-full">
|
||||
<p className="text-muted-foreground">no messages</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{messagePairs.map(([userMessage, assistantMessage], index) => {
|
||||
const isLastPair = index === messagePairs.length - 1;
|
||||
const isStreaming = isLastPair && (status === "streaming" || status === "submitted");
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ChatThreadListItem
|
||||
chatId={chatId}
|
||||
userMessage={userMessage}
|
||||
assistantMessage={assistantMessage}
|
||||
isStreaming={isStreaming}
|
||||
sources={sources}
|
||||
ref={isLastPair ? latestMessagePairRef : undefined}
|
||||
/>
|
||||
{index !== messagePairs.length - 1 && (
|
||||
<Separator className="my-12" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(!isAutoScrollEnabled && status === "streaming") && (
|
||||
<div className="absolute bottom-5 left-0 right-0 h-10 flex flex-row items-center justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full animate-bounce-slow h-8 w-8"
|
||||
onClick={() => {
|
||||
latestMessagePairRef.current?.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'end',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</ScrollArea>
|
||||
{!isChatReadonly && (
|
||||
<div className="border rounded-md w-full max-w-3xl mx-auto mb-8 shadow-sm">
|
||||
<CustomSlateEditor>
|
||||
<ChatBox
|
||||
onSubmit={onSubmit}
|
||||
className="min-h-[80px]"
|
||||
preferredSuggestionsBoxPlacement="top-start"
|
||||
isGenerating={status === "streaming" || status === "submitted"}
|
||||
onStop={stop}
|
||||
languageModels={languageModels}
|
||||
selectedRepos={selectedRepos}
|
||||
onRepoSelectorOpenChanged={setIsRepoSelectorOpen}
|
||||
/>
|
||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||
<ChatBoxToolbar
|
||||
languageModels={languageModels}
|
||||
repos={repos}
|
||||
selectedRepos={selectedRepos}
|
||||
onSelectedReposChange={onSelectedReposChange}
|
||||
isRepoSelectorOpen={isRepoSelectorOpen}
|
||||
onRepoSelectorOpenChanged={setIsRepoSelectorOpen}
|
||||
/>
|
||||
</div>
|
||||
</CustomSlateEditor>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
'use client';
|
||||
|
||||
import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Brain, CheckCircle, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, Zap } from 'lucide-react';
|
||||
import { CSSProperties, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { ANSWER_TAG } from '../../constants';
|
||||
import { Reference, referenceSchema, SBChatMessage, SBChatMessageMetadata, Source } from "../../types";
|
||||
import { useExtractReferences } from '../../useExtractReferences';
|
||||
import { getAnswerPartFromAssistantMessage, groupMessageIntoSteps } from '../../utils';
|
||||
import { AnswerCard } from './answerCard';
|
||||
import { MarkdownRenderer, REFERENCE_PAYLOAD_ATTRIBUTE } from './markdownRenderer';
|
||||
import { ReferencedSourcesListView } from './referencedSourcesListView';
|
||||
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
|
||||
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
|
||||
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
|
||||
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
|
||||
|
||||
interface ChatThreadListItemProps {
|
||||
userMessage: SBChatMessage;
|
||||
assistantMessage?: SBChatMessage;
|
||||
isStreaming: boolean;
|
||||
sources: Source[];
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemProps>(({
|
||||
userMessage,
|
||||
assistantMessage,
|
||||
isStreaming,
|
||||
sources,
|
||||
chatId,
|
||||
}, ref) => {
|
||||
const leftPanelRef = useRef<HTMLDivElement>(null);
|
||||
const [leftPanelHeight, setLeftPanelHeight] = useState<number | null>(null);
|
||||
const markdownRendererRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [hoveredReference, setHoveredReference] = useState<Reference | undefined>(undefined);
|
||||
const [selectedReference, setSelectedReference] = useState<Reference | undefined>(undefined);
|
||||
const references = useExtractReferences(assistantMessage);
|
||||
const [isDetailsPanelExpanded, _setIsDetailsPanelExpanded] = useState(isStreaming);
|
||||
const hasAutoCollapsed = useRef(false);
|
||||
const userHasManuallyExpanded = useRef(false);
|
||||
|
||||
|
||||
const userQuestion = useMemo(() => {
|
||||
return userMessage.parts.length > 0 && userMessage.parts[0].type === 'text' ? userMessage.parts[0].text : '';
|
||||
}, [userMessage]);
|
||||
|
||||
const messageMetadata = useMemo((): SBChatMessageMetadata | undefined => {
|
||||
return assistantMessage?.metadata;
|
||||
}, [assistantMessage?.metadata]);
|
||||
|
||||
const answerPart = useMemo(() => {
|
||||
if (!assistantMessage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getAnswerPartFromAssistantMessage(assistantMessage, isStreaming);
|
||||
}, [assistantMessage, isStreaming]);
|
||||
|
||||
|
||||
const thinkingSteps = useMemo(() => {
|
||||
const steps = groupMessageIntoSteps(assistantMessage?.parts ?? []);
|
||||
// Filter out the answerPart and empty steps
|
||||
return steps.map(step => step.filter(part => part !== answerPart)).filter(step => step.length > 0);
|
||||
}, [answerPart, assistantMessage?.parts]);
|
||||
|
||||
// "thinking" is when the agent is generating output that is not the answer.
|
||||
const isThinking = useMemo(() => {
|
||||
return isStreaming && !answerPart
|
||||
}, [answerPart, isStreaming]);
|
||||
|
||||
|
||||
// Auto-collapse when answer first appears, but only once and respect user preference
|
||||
useEffect(() => {
|
||||
if (answerPart && !hasAutoCollapsed.current && !userHasManuallyExpanded.current) {
|
||||
_setIsDetailsPanelExpanded(false);
|
||||
hasAutoCollapsed.current = true;
|
||||
}
|
||||
}, [answerPart]);
|
||||
|
||||
const onExpandDetailsPanel = useCallback((expanded: boolean) => {
|
||||
_setIsDetailsPanelExpanded(expanded);
|
||||
// If user manually expands after auto-collapse, remember their preference
|
||||
if (expanded && hasAutoCollapsed.current) {
|
||||
userHasManuallyExpanded.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
// Measure answer content height for dynamic sizing
|
||||
useEffect(() => {
|
||||
if (!leftPanelRef.current || !answerPart) {
|
||||
setLeftPanelHeight(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setLeftPanelHeight(entry.contentRect.height);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(leftPanelRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [answerPart]);
|
||||
|
||||
const rightPanelStyle: CSSProperties = useMemo(() => {
|
||||
const maxHeight = 'calc(100vh - 215px)';
|
||||
|
||||
return {
|
||||
height: leftPanelHeight ? `min(${leftPanelHeight}px, ${maxHeight})` : maxHeight,
|
||||
};
|
||||
}, [leftPanelHeight]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!markdownRendererRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdownRenderer = markdownRendererRef.current;
|
||||
|
||||
const handleMouseOver = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.hasAttribute(REFERENCE_PAYLOAD_ATTRIBUTE)) {
|
||||
try {
|
||||
const jsonPayload = JSON.parse(decodeURIComponent(target.getAttribute(REFERENCE_PAYLOAD_ATTRIBUTE) ?? '{}'));
|
||||
const reference = referenceSchema.parse(jsonPayload);
|
||||
setHoveredReference(reference);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseOut = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.hasAttribute(REFERENCE_PAYLOAD_ATTRIBUTE)) {
|
||||
setHoveredReference(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.hasAttribute(REFERENCE_PAYLOAD_ATTRIBUTE)) {
|
||||
try {
|
||||
const jsonPayload = JSON.parse(decodeURIComponent(target.getAttribute(REFERENCE_PAYLOAD_ATTRIBUTE) ?? '{}'));
|
||||
const reference = referenceSchema.parse(jsonPayload);
|
||||
setSelectedReference(reference.id === selectedReference?.id ? undefined : reference);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
markdownRenderer.addEventListener('mouseover', handleMouseOver);
|
||||
markdownRenderer.addEventListener('mouseout', handleMouseOut);
|
||||
markdownRenderer.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
markdownRenderer.removeEventListener('mouseover', handleMouseOver);
|
||||
markdownRenderer.removeEventListener('mouseout', handleMouseOut);
|
||||
markdownRenderer.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, [answerPart, selectedReference?.id]); // Re-run when answerPart changes to ensure we catch new content
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedReference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceElement = document.getElementById(`user-content-${selectedReference.id}`);
|
||||
if (!referenceElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollIntoView(referenceElement, {
|
||||
behavior: 'smooth',
|
||||
scrollMode: 'if-needed',
|
||||
block: 'center',
|
||||
});
|
||||
|
||||
referenceElement.classList.add('chat-reference--selected');
|
||||
|
||||
return () => {
|
||||
referenceElement.classList.remove('chat-reference--selected');
|
||||
};
|
||||
}, [selectedReference]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hoveredReference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceElement = document.getElementById(`user-content-${hoveredReference.id}`);
|
||||
if (!referenceElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
referenceElement.classList.add('chat-reference--hover');
|
||||
|
||||
return () => {
|
||||
referenceElement.classList.remove('chat-reference--hover');
|
||||
};
|
||||
}, [hoveredReference]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col md:flex-row relative min-h-[calc(100vh-250px)]"
|
||||
ref={ref}
|
||||
>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
style={{
|
||||
height: 'auto',
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<ResizablePanel
|
||||
order={1}
|
||||
minSize={30}
|
||||
maxSize={70}
|
||||
defaultSize={50}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={leftPanelRef}
|
||||
className="py-4 h-full"
|
||||
>
|
||||
<div className="flex flex-row gap-2 mb-4">
|
||||
{isStreaming ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin flex-shrink-0 mt-1.5" />
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 text-green-700 flex-shrink-0 mt-1.5" />
|
||||
)}
|
||||
<MarkdownRenderer
|
||||
content={userQuestion.trim()}
|
||||
className="prose-p:m-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isThinking && (
|
||||
<div className="space-y-4 mb-4">
|
||||
<Skeleton className="h-4 max-w-32" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-3 max-w-72" />
|
||||
<Skeleton className="h-3 max-w-64" />
|
||||
<Skeleton className="h-3 max-w-56" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="mb-4">
|
||||
<Collapsible open={isDetailsPanelExpanded} onOpenChange={onExpandDetailsPanel}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardContent
|
||||
className={cn("p-3 cursor-pointer hover:bg-muted", {
|
||||
"rounded-lg": !isDetailsPanelExpanded,
|
||||
"rounded-t-lg": isDetailsPanelExpanded,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
<p className="flex items-center font-semibold text-muted-foreground text-sm">
|
||||
{isThinking ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-1 flex-shrink-0" />
|
||||
Thinking...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<InfoIcon className="w-4 h-4 mr-1 flex-shrink-0" />
|
||||
Details
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{!isStreaming && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
{messageMetadata?.modelName && (
|
||||
<div className="flex items-center text-xs">
|
||||
<Cpu className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
{messageMetadata?.modelName}
|
||||
</div>
|
||||
)}
|
||||
{messageMetadata?.totalTokens && (
|
||||
<div className="flex items-center text-xs">
|
||||
<Zap className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
{messageMetadata?.totalTokens} tokens
|
||||
</div>
|
||||
)}
|
||||
{messageMetadata?.totalResponseTimeMs && (
|
||||
<div className="flex items-center text-xs">
|
||||
<Clock className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
{messageMetadata?.totalResponseTimeMs / 1000} seconds
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center text-xs">
|
||||
<Brain className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
{`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDetailsPanelExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="mt-2 space-y-6">
|
||||
{thinkingSteps.length === 0 ? (
|
||||
isStreaming ? (
|
||||
<Skeleton className="h-24 w-full" />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No thinking steps</p>
|
||||
)
|
||||
) : thinkingSteps.map((step, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="border-l-2 pl-4 relative border-muted"
|
||||
>
|
||||
<div
|
||||
className={`absolute left-[-9px] top-1 w-4 h-4 rounded-full flex items-center justify-center bg-muted`}
|
||||
>
|
||||
<span
|
||||
className={`text-xs font-semibold`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
{step.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case 'reasoning':
|
||||
case 'text':
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
key={index}
|
||||
content={part.text}
|
||||
className="text-sm"
|
||||
/>
|
||||
)
|
||||
case 'tool-readFiles':
|
||||
return (
|
||||
<ReadFilesToolComponent
|
||||
key={index}
|
||||
part={part}
|
||||
/>
|
||||
)
|
||||
case 'tool-searchCode':
|
||||
return (
|
||||
<SearchCodeToolComponent
|
||||
key={index}
|
||||
part={part}
|
||||
/>
|
||||
)
|
||||
case 'tool-findSymbolDefinitions':
|
||||
return (
|
||||
<FindSymbolDefinitionsToolComponent
|
||||
key={index}
|
||||
part={part}
|
||||
/>
|
||||
)
|
||||
case 'tool-findSymbolReferences':
|
||||
return (
|
||||
<FindSymbolReferencesToolComponent
|
||||
key={index}
|
||||
part={part}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Answer section */}
|
||||
{(answerPart && assistantMessage) ? (
|
||||
<AnswerCard
|
||||
ref={markdownRendererRef}
|
||||
answerText={answerPart.text.replace(ANSWER_TAG, '').trim()}
|
||||
chatId={chatId}
|
||||
messageId={assistantMessage.id}
|
||||
feedback={messageMetadata?.feedback?.type}
|
||||
traceId={messageMetadata?.traceId}
|
||||
/>
|
||||
) : !isStreaming && (
|
||||
<p className="text-destructive">Error: No answer response was provided</p>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<AnimatedResizableHandle className='mx-4' />
|
||||
<ResizablePanel
|
||||
order={2}
|
||||
minSize={30}
|
||||
maxSize={70}
|
||||
defaultSize={50}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sticky top-0"
|
||||
>
|
||||
{references.length > 0 ? (
|
||||
<ReferencedSourcesListView
|
||||
references={references}
|
||||
sources={sources}
|
||||
hoveredReference={hoveredReference}
|
||||
selectedReference={selectedReference}
|
||||
onSelectedReferenceChanged={setSelectedReference}
|
||||
onHoveredReferenceChanged={setHoveredReference}
|
||||
style={rightPanelStyle}
|
||||
/>
|
||||
) : isStreaming ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton key={index} className="w-full h-48" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 text-center text-muted-foreground text-sm">
|
||||
No file references found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
ChatThreadListItem.displayName = 'ChatThreadListItem';
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
'use client';
|
||||
|
||||
import { LightweightCodeHighlighter } from '@/app/[domain]/components/lightweightCodeHighlighter';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from '@radix-ui/react-icons';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
interface CodeBlockComponentProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
const MAX_LINES_TO_DISPLAY = 14;
|
||||
|
||||
export const CodeBlock = ({
|
||||
code,
|
||||
language = "text",
|
||||
}: CodeBlockComponentProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const lineCount = useMemo(() => {
|
||||
return code.split('\n').length;
|
||||
}, [code]);
|
||||
|
||||
const isExpandButtonVisible = useMemo(() => {
|
||||
return lineCount > MAX_LINES_TO_DISPLAY;
|
||||
}, [lineCount]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-md border overflow-hidden not-prose my-4">
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-300 ease-in-out",
|
||||
{
|
||||
"max-h-[350px]": !isExpanded && isExpandButtonVisible, // Roughly 14 lines
|
||||
"max-h-none": isExpanded || !isExpandButtonVisible
|
||||
}
|
||||
)}
|
||||
>
|
||||
<LightweightCodeHighlighter
|
||||
language={language}
|
||||
lineNumbers={true}
|
||||
renderWhitespace={true}
|
||||
>
|
||||
{code}
|
||||
</LightweightCodeHighlighter>
|
||||
</div>
|
||||
{isExpandButtonVisible && (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="flex flex-row items-center justify-center w-full bg-accent py-1 cursor-pointer text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
>
|
||||
{isExpanded ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
|
||||
<span className="text-sm ml-1">{isExpanded ? 'Show less' : 'Show more'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { PiArrowsOutLineVertical, PiArrowLineUp, PiArrowLineDown } from "react-icons/pi";
|
||||
|
||||
interface CodeFoldingExpandButton {
|
||||
onExpand: (direction: 'up' | 'down') => void;
|
||||
hiddenLineCount: number;
|
||||
canExpandUp: boolean;
|
||||
canExpandDown: boolean;
|
||||
}
|
||||
|
||||
export const CodeFoldingExpandButton = ({
|
||||
onExpand,
|
||||
hiddenLineCount,
|
||||
canExpandUp,
|
||||
canExpandDown,
|
||||
}: CodeFoldingExpandButton) => {
|
||||
|
||||
const expandDirections = useMemo((): ('up' | 'down' | 'merged')[] => {
|
||||
if (canExpandUp && !canExpandDown) {
|
||||
return ['up'];
|
||||
}
|
||||
|
||||
if (!canExpandUp && canExpandDown) {
|
||||
return ['down'];
|
||||
}
|
||||
|
||||
if (hiddenLineCount < 20) {
|
||||
return ['merged'];
|
||||
}
|
||||
|
||||
return ['down', 'up'];
|
||||
}, [canExpandUp, canExpandDown, hiddenLineCount]);
|
||||
|
||||
const onClick = useCallback((direction: 'up' | 'down' | 'merged') => {
|
||||
if (direction === 'merged') {
|
||||
// default to expanding down
|
||||
onExpand('down');
|
||||
} else {
|
||||
onExpand(direction);
|
||||
}
|
||||
}, [onExpand]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{expandDirections.map((direction, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="py-[3px] px-1.5 bg-chat-reference hover:bg-chat-reference-hover cursor-pointer" onClick={() => onClick(direction)}
|
||||
>
|
||||
{direction === 'up' && <PiArrowLineUp className="w-4 h-4" />}
|
||||
{direction === 'down' && <PiArrowLineDown className="w-4 h-4" />}
|
||||
{direction === 'merged' && <PiArrowsOutLineVertical className="w-4 h-4" />}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue