Showing preview only (982K chars total). Download the full file or copy to clipboard to get everything.
Repository: caioricciuti/duck-ui
Branch: main
Commit: 6cbc5df4ac5d
Files: 173
Total size: 925.3 KB
Directory structure:
gitextract_fb4zvf6o/
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ └── feature.yml
│ └── workflows/
│ ├── 2-docker-build.yml
│ └── ci.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .prettierignore
├── .prettierrc
├── Dockerfile
├── LICENSE.md
├── README.md
├── components.json
├── docker-compose.yml
├── eslint.config.js
├── index.html
├── inject-env.js
├── package.json
├── public/
│ └── databases/
│ ├── README.md
│ └── manifest.json
├── serve.json
├── src/
│ ├── components/
│ │ ├── charts/
│ │ │ ├── ChartVisualizationPro.tsx
│ │ │ ├── UPlotChart.tsx
│ │ │ └── tooltipPlugin.ts
│ │ ├── cloud/
│ │ │ ├── CloudBrowser.tsx
│ │ │ └── CloudConnectionModal.tsx
│ │ ├── command-palette/
│ │ │ └── CommandPalette.tsx
│ │ ├── common/
│ │ │ ├── FloatingActionButton.tsx
│ │ │ └── ImportOptionsPopover.tsx
│ │ ├── connection/
│ │ │ └── ConnectionsModal.tsx
│ │ ├── duck-brain/
│ │ │ ├── DuckBrainCodeBlock.tsx
│ │ │ ├── DuckBrainInput.tsx
│ │ │ ├── DuckBrainMessages.tsx
│ │ │ ├── DuckBrainPanel.tsx
│ │ │ ├── MarkdownContent.tsx
│ │ │ ├── ResultsArtifact.tsx
│ │ │ └── SchemaAutocomplete.tsx
│ │ ├── editor/
│ │ │ ├── SqlEditor.tsx
│ │ │ └── monacoConfig.ts
│ │ ├── explorer/
│ │ │ ├── ColumnNode.tsx
│ │ │ ├── DataExplorer.tsx
│ │ │ ├── FileImporter.tsx
│ │ │ └── TreeNode.tsx
│ │ ├── folders/
│ │ │ └── FolderBrowser.tsx
│ │ ├── layout/
│ │ │ ├── ConnectionSwitcher.tsx
│ │ │ ├── MobileNavDrawer.tsx
│ │ │ └── Sidebar.tsx
│ │ ├── notebook/
│ │ │ ├── MarkdownRenderer.tsx
│ │ │ ├── NotebookCell.tsx
│ │ │ ├── NotebookTab.tsx
│ │ │ └── NotebookToolbar.tsx
│ │ ├── profile/
│ │ │ ├── PasswordDialog.tsx
│ │ │ ├── ProfileAvatar.tsx
│ │ │ ├── ProfileEditor.tsx
│ │ │ └── ProfilePicker.tsx
│ │ ├── saved-queries/
│ │ │ ├── SaveQueryDialog.tsx
│ │ │ └── SavedQueriesPanel.tsx
│ │ ├── table/
│ │ │ ├── CellValueViewer.tsx
│ │ │ ├── ColumnStatsPanel.tsx
│ │ │ └── DuckUItable.tsx
│ │ ├── theme/
│ │ │ ├── mode-toggle.tsx
│ │ │ └── theme-provider.tsx
│ │ ├── ui/
│ │ │ ├── accordion.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── breadcrumb.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── command.tsx
│ │ │ ├── context-menu.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── drawer.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── menubar.tsx
│ │ │ ├── multi-select.tsx
│ │ │ ├── navigation-menu.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── resizable.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── table.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ └── tooltip.tsx
│ │ └── workspace/
│ │ ├── BrainTab.tsx
│ │ ├── ConnectionsTab.tsx
│ │ ├── ExplainPlanViewer.tsx
│ │ ├── HomeTab.tsx
│ │ ├── QueryHistory.tsx
│ │ ├── SettingsTab.tsx
│ │ ├── SortableTab.tsx
│ │ ├── SqlTab.tsx
│ │ └── WorkspaceTabs.tsx
│ ├── hooks/
│ │ └── useQueryFromURL.ts
│ ├── index.css
│ ├── lib/
│ │ ├── chartDataTransform.ts
│ │ ├── chartExport.ts
│ │ ├── chartUtils.ts
│ │ ├── cloudStorage/
│ │ │ ├── index.ts
│ │ │ └── testHttpfs.ts
│ │ ├── duckBrain/
│ │ │ ├── index.ts
│ │ │ ├── models.config.ts
│ │ │ ├── prompts/
│ │ │ │ └── text-to-sql.ts
│ │ │ ├── providers/
│ │ │ │ ├── anthropic.provider.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── openai.provider.ts
│ │ │ │ └── types.ts
│ │ │ ├── schemaFormatter.ts
│ │ │ ├── sqlParser.ts
│ │ │ ├── webllm.service.ts
│ │ │ └── webllm.worker.ts
│ │ ├── fileSystem/
│ │ │ └── index.ts
│ │ ├── sqlSanitize.ts
│ │ └── utils.ts
│ ├── main.tsx
│ ├── pages/
│ │ └── Home.tsx
│ ├── services/
│ │ ├── duckdb/
│ │ │ ├── __tests__/
│ │ │ │ ├── resultParser.test.ts
│ │ │ │ └── utils.test.ts
│ │ │ ├── externalConnection.ts
│ │ │ ├── index.ts
│ │ │ ├── opfsConnection.ts
│ │ │ ├── resultParser.ts
│ │ │ ├── schemaFetcher.ts
│ │ │ ├── utils.ts
│ │ │ └── wasmConnection.ts
│ │ └── persistence/
│ │ ├── __tests__/
│ │ │ ├── crypto.test.ts
│ │ │ └── migrations.test.ts
│ │ ├── crypto.ts
│ │ ├── fallback.ts
│ │ ├── index.ts
│ │ ├── migrations.ts
│ │ ├── repositories/
│ │ │ ├── aiConfigRepository.ts
│ │ │ ├── connectionRepository.ts
│ │ │ ├── index.ts
│ │ │ ├── profileRepository.ts
│ │ │ ├── queryHistoryRepository.ts
│ │ │ ├── savedQueryRepository.ts
│ │ │ ├── settingsRepository.ts
│ │ │ └── workspaceRepository.ts
│ │ └── systemDb.ts
│ ├── store/
│ │ ├── index.ts
│ │ ├── slices/
│ │ │ ├── connectionSlice.ts
│ │ │ ├── duckBrainSlice.ts
│ │ │ ├── duckdbSlice.ts
│ │ │ ├── fileSystemSlice.ts
│ │ │ ├── profileSlice.ts
│ │ │ ├── querySlice.ts
│ │ │ ├── schemaSlice.ts
│ │ │ └── tabSlice.ts
│ │ └── types.ts
│ ├── types/
│ │ └── filesystem.d.ts
│ └── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Ignore node_modules (local dependencies)
node_modules
# Ignore build artifacts
dist
# Ignore configuration files that shouldn't be shared
.env
.vscode
.idea
# Ignore miscellaneous system files and logs
.DS_Store
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
# Ignore Git-specific files
.git
.gitignore
/docs
================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: 🐞 Bug Report
description: Found something that doesn't work as expected?
body:
- type: dropdown
id: Environment
attributes:
label: Environment
description: How are you using Duck-UI?
options:
- NPM build
- Docker
- Other
validations:
required: true
- type: textarea
id: repro
attributes:
label: How did you encounter the bug?
description: How can this bug be reproduced? Please provide steps to reproduce.
placeholder: |-
1. Run Docker container...
2. npm run build...
3. Go to...
validations:
required: true
- type: textarea
id: expected
attributes:
label: What did you expect?
description: What it supposed to happen? What did you expect to see?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Result
description: What was the accual result?
validations:
required: true
- type: dropdown
id: Browser
attributes:
label: Browser
description: What browser are you using?
options:
- Chrome
- Firefox
- Edge
- Safari
- Brave
- Other
validations:
required: true
# browser version with instructions to check it
- type: textarea
id: browser-version
attributes:
label: Browser Version
description: What version of the browser are you using?
placeholder: e.g. 125.0.0
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of Duck-UI are you using?
placeholder: e.g. 1.5.0
validations:
required: true
- type: markdown
attributes:
value: |-
### All done, now, just submit the issue and I will do my best to take care of it! 🙏
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature.yml
================================================
name: 💡 Feature Request
description: Tell us about something Duck-UI doesn't do yet, but should!
body:
- type: textarea
id: idea
attributes:
label: Idea Statement
description: Which is the feature you would like to see implemented?
placeholder: |-
I want to be able to do anything I want, whenever I want. Because my ideas are the best.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Feature implementation brainstorm
description: All your ideas are welcome, let's brainstorm together.
placeholder: |-
Create the next big feature that will all our problems.
validations:
required: false
- type: markdown
attributes:
value: |-
## Thanks 🙏
validations:
required: false
================================================
FILE: .github/workflows/2-docker-build.yml
================================================
# .github/workflows/2-docker-build.yml
name: Build and Push Docker Image
on:
push:
branches:
- main
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches:
- main
paths-ignore:
- '**.md'
- 'docs/**'
jobs:
build:
runs-on: ubuntu-latest
if: |
(!contains(github.event.head_commit.message, 'docker-false') && !contains(github.event.head_commit.message, 'docs-only')) ||
contains(github.event.head_commit.message, 'docker-only')
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_PAT }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=sha,format=long
type=ref,event=pr
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
pull_request:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run format:check
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run typecheck
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run test
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
# Editor directories and files
.vscode/*
/vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
post.md
CLAUDE.md
================================================
FILE: .husky/pre-commit
================================================
bunx lint-staged
================================================
FILE: .prettierignore
================================================
dist
node_modules
bun.lockb
*.wasm
public
docs/.vitepress/cache
docs/.vitepress/dist
================================================
FILE: .prettierrc
================================================
{
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
================================================
FILE: Dockerfile
================================================
# Use an official Node runtime as a parent image with bun
FROM oven/bun:1-alpine AS build
ARG DUCK_UI_BASEPATH="/"
# Set the working directory
WORKDIR /app
# Copy package.json and bun.lockb (if exists)
COPY package.json bun.lockb* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Bundle app source inside Docker image
COPY . .
# Build the app
RUN bun run build
# Use a second stage to reduce image size
FROM oven/bun:1-alpine
# Set the working directory for the second stage
WORKDIR /app
# Copy the build directory from the first stage to the second stage
COPY --from=build /app/dist /app
# Copy the injection script and serve config (COOP/COEP headers for OPFS)
COPY inject-env.js /app/
COPY serve.json /app/
# Install just serve for serving the built app
RUN bun add serve
# Expose port 5522
EXPOSE 5522
# Define environment variables
ENV DUCK_UI_EXTERNAL_CONNECTION_NAME=""
ENV DUCK_UI_EXTERNAL_HOST=""
ENV DUCK_UI_EXTERNAL_PORT=""
ENV DUCK_UI_EXTERNAL_USER=""
ENV DUCK_UI_EXTERNAL_PASS=""
ENV DUCK_UI_EXTERNAL_DATABASE_NAME=""
# Create user and change ownership
RUN addgroup -S duck-group -g 1001 && adduser -S duck-user -u 1001 -G duck-group
RUN chown -R duck-user:duck-group /app
USER duck-user
# Run the injection script then serve using bunx
CMD bun inject-env.js && bunx serve -s -l 5522 -c serve.json
================================================
FILE: LICENSE.md
================================================
# License
## DUCK UI - Apache License 2.0
Copyright 2025 Caio Ricciuti
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## What does this mean?
The Apache License 2.0 is a permissive open source license that provides users with extensive freedom to use, modify, and distribute the software, while also offering robust legal protection through its patent grant and clear contribution terms.
### You are free to:
- ✅ Use the software commercially
- ✅ Modify the software
- ✅ Distribute the software
- ✅ Use the software for private use
- ✅ Sublicense the software
- ✅ Use patents claims of contributors to the code
### Under the following conditions:
- ℹ️ Include the original copyright notice
- ℹ️ Include a copy of the license
- ℹ️ State significant changes made to the software
- ℹ️ Include the NOTICE file (if present) with attribution notes
### With the understanding that:
- ⚠️ The software is provided "as is", without warranty of any kind
- ⚠️ The authors cannot be held liable for damages
- ⚠️ Trademark use is not granted except as required for describing the origin of the work
## Key Benefits of Apache 2.0
### Patent Protection
Unlike simpler licenses like MIT, Apache 2.0 includes an express patent grant from contributors to users. This means that if a contributor has patents that cover their contribution, they automatically grant you a license to use those patents.
### Clear Contribution Terms
The license explicitly states that any contributions are assumed to be under the same Apache 2.0 license unless otherwise specified, providing clarity for collaborative development.
### Compatibility
Apache 2.0 is compatible with many other open source licenses and is widely accepted in both open source and commercial contexts.
## Third-Party Licenses
CH-UI is built on top of several open-source projects. We'd like to acknowledge and give credit to these projects:
- [ClickHouse](https://github.com/ClickHouse/ClickHouse) - Apache 2.0 License
- [React](https://github.com/facebook/react) - MIT License
- [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) - MIT License
- [Zustand](https://github.com/pmndrs/zustand) - MIT License
- [Monaco Editor](https://github.com/microsoft/monaco-editor) - MIT License
- [Lucide Icons](https://github.com/lucide-icons/lucide) - ISC License
For the full text of these licenses, please visit the respective project repositories.
================================================
FILE: README.md
================================================
# <img src="./public/logo.png" alt="Duck-UI Logo" title="Duck-UI Logo" width="50"> Duck-UI
Duck-UI is a web-based interface for interacting with DuckDB, a high-performance analytical database system. This project leverages DuckDB's WebAssembly (WASM) capabilities to provide a seamless and efficient user experience directly in the browser.
# [Official Docs](https://duckui.com?utm_source=github&utm_medium=readme) 🚀
# [Demo](https://demo.duckui.com?utm_source=github&utm_medium=readme) 💻
## Features
- **SQL Editor**: Write and execute SQL queries with syntax highlighting and auto-completion.
- **Data Import**: Import data from CSV, JSON, Parquet, and Arrow files.
- **Data Explorer**: Browse and manage databases and tables.
- **Query History**: View and manage your recent SQL queries.
## Getting Started
### Docker (Recommended)
```bash
docker run -p 5522:5522 ghcr.io/caioricciuti/duck-ui:latest
```
Open your browser and navigate to `http://localhost:5522`.
### Environment Variables
You can customize Duck-UI behavior using environment variables:
```bash
# For external DuckDB connections
docker run -p 5522:5522 \
-e DUCK_UI_EXTERNAL_CONNECTION_NAME="My DuckDB Server" \
-e DUCK_UI_EXTERNAL_HOST="http://duckdb-server" \
-e DUCK_UI_EXTERNAL_PORT="8000" \
-e DUCK_UI_EXTERNAL_USER="username" \
-e DUCK_UI_EXTERNAL_PASS="password" \
-e DUCK_UI_EXTERNAL_DATABASE_NAME="my_database" \
-e DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS="true" \
ghcr.io/caioricciuti/duck-ui:latest
```
| Runtime Variable | Description | Default |
|----------|-------------|---------|
| `DUCK_UI_EXTERNAL_CONNECTION_NAME` | Name for the external connection | "" |
| `DUCK_UI_EXTERNAL_HOST` | Host URL for external DuckDB | "" |
| `DUCK_UI_EXTERNAL_PORT` | Port for external DuckDB | null |
| `DUCK_UI_EXTERNAL_USER` | Username for external connection | "" |
| `DUCK_UI_EXTERNAL_PASS` | Password for external connection | "" |
| `DUCK_UI_EXTERNAL_DATABASE_NAME` | Database name for external connection | "" |
| `DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS` | Allow unsigned extensions in DuckDB | false |
| `DUCK_UI_DUCKDB_WASM_USE_CDN` | Load DuckDB WASM from CDN (ignored when build-time `DUCK_UI_DUCKDB_WASM_CDN_ONLY=true`) | false |
| `DUCK_UI_DUCKDB_WASM_BASE_URL` | Custom CDN base URL (used when `DUCK_UI_DUCKDB_WASM_USE_CDN=true`) | auto jsDelivr |
| Build-time Variable | Description | Default |
|----------|-------------|---------|
| `DUCK_UI_DUCKDB_WASM_CDN_ONLY` | Build a CDN-only artifact (local DuckDB WASM assets are not bundled). | false |
When `DUCK_UI_DUCKDB_WASM_CDN_ONLY=true`, runtime `DUCK_UI_DUCKDB_WASM_USE_CDN=false` cannot switch back to local WASM.
### Prerequisites
- Node.js >= 20.x
- npm >= 10.x
### Installation
1. Clone the repository:
```bash
git clone https://github.com/caioricciuti/duck-ui.git
cd duck-ui
```
2. Install dependencies:
```bash
npm install
# or
yarn install
```
### Running the Application
1. Start the development server:
```bash
npm run dev
# or
yarn dev
```
2. Open your browser and navigate to `http://localhost:5173`.
### Building for Production
To create a production build, run:
```bash
npm run build
# or
yarn build
```
The output will be in the `dist` directory.
### Running with Docker
1. Build the Docker image:
```bash
docker build -t duck-ui .
```
2. Run the Docker container:
```bash
docker run -p 5522:5522 duck-ui
```
3. Open your browser and navigate to `http://localhost:5522`.
## Usage
### SQL Editor
- Write your SQL queries in the editor.
- Use `Cmd/Ctrl + Enter` to execute the query.
- View the results in the results pane.
### Data Import
- Click on the "Import Files" button to upload CSV, JSON, Parquet, or Arrow files.
- Configure the table name and import settings.
- For CSV files, you can customize import options:
- Header row detection
- Auto-detection of column types
- Delimiter specification
- Error handling (ignore errors, null padding for missing columns)
- View the imported data in the Data Explorer.
### Data Explorer
- Browse through the databases and tables.
- Preview table data and view table schemas.
- Delete tables if needed.
### Query History
- Access your recent queries from the Query History section.
- Copy queries to the clipboard or re-execute them.
### Theme Toggle
- Switch between light and dark themes using the theme toggle button.
### Keyboard Shortcuts
- `Cmd/Ctrl + B`: Expand/Shrink Sidebar
- `Cmd/Ctrl + K`: Open Search Bar
- `Cmd/Ctrl + Enter`: Run Query
- `Cmd/Ctrl + Shift + Enter`: Run highlighted query
## Contributing
Contributions are welcome! Please follow these steps to contribute:
1. Fork the repository.
2. Create a new branch (`git checkout -b feature/your-feature`).
3. Commit your changes (`git commit -m 'Add some feature'`).
4. Push to the branch (`git push origin feature/your-feature`).
5. Open a pull request.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) file for details.
## Acknowledgements
- [DuckDB](https://duckdb.org/)
- [React](https://reactjs.org/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Zustand](https://github.com/pmndrs/zustand)
- [Lucide Icons](https://lucide.dev/)
## Contact
For any inquiries or support, please contact [Caio Ricciuti](https://github.com/caioricciuti).
## Sponsors
This project is sponsored by:
### [Ibero Data](https://iberodata.es/)
<img src="https://iberodata.es/logo.png" alt="Ibero Data Logo" title="Ibero Data Logo" width="100">
### [qxip](https://qxip.net/?utm_source=duck-ui&utm_medium=sponsorship)
<img src="https://qxip.net/images/qxip.png" alt="qxip" title="qxip Logo" width="150">
<br/>
Want to be a sponsor? [Contact us](mailto:caio.ricciuti+sponsorship@outlook.com).
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
================================================
FILE: docker-compose.yml
================================================
services:
duck-ui:
image: ghcr.io/caioricciuti/duck-ui:latest
restart: always
ports:
- "${DUCK_UI_PORT:-5522}:5522"
environment:
# External connection (optional)
- DUCK_UI_EXTERNAL_CONNECTION_NAME=${DUCK_UI_EXTERNAL_CONNECTION_NAME:-}
- DUCK_UI_EXTERNAL_HOST=${DUCK_UI_EXTERNAL_HOST:-}
- DUCK_UI_EXTERNAL_PORT=${DUCK_UI_EXTERNAL_PORT:-}
- DUCK_UI_EXTERNAL_USER=${DUCK_UI_EXTERNAL_USER:-}
- DUCK_UI_EXTERNAL_PASS=${DUCK_UI_EXTERNAL_PASS:-}
- DUCK_UI_EXTERNAL_DATABASE_NAME=${DUCK_UI_EXTERNAL_DATABASE_NAME:-}
# DuckDB configuration
- DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS=${DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS:-false}
================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'docs'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
},
},
)
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo-padding.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Duck UI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: inject-env.js
================================================
const fs = require("fs");
const path = require("path");
const indexHtmlPath = path.join(__dirname, "index.html");
let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf8");
// Inject the environment variables
const envVars = {
DUCK_UI_EXTERNAL_CONNECTION_NAME:
process.env.DUCK_UI_EXTERNAL_CONNECTION_NAME || "",
DUCK_UI_EXTERNAL_HOST: process.env.DUCK_UI_EXTERNAL_HOST || "",
DUCK_UI_EXTERNAL_PORT: process.env.DUCK_UI_EXTERNAL_PORT || null,
DUCK_UI_EXTERNAL_USER: process.env.DUCK_UI_EXTERNAL_USER || "",
DUCK_UI_EXTERNAL_PASS: process.env.DUCK_UI_EXTERNAL_PASS || "",
DUCK_UI_EXTERNAL_DATABASE_NAME:
process.env.DUCK_UI_EXTERNAL_DATABASE_NAME || "",
// Add new configuration for DuckDB settings
DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS:
process.env.DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS === "true" || false,
DUCK_UI_DUCKDB_WASM_USE_CDN:
process.env.DUCK_UI_DUCKDB_WASM_USE_CDN === "true" || false,
DUCK_UI_DUCKDB_WASM_BASE_URL:
process.env.DUCK_UI_DUCKDB_WASM_BASE_URL || ""
};
const scriptContent = `
<script>
window.env = ${JSON.stringify(envVars)};
</script>
`;
// Insert the script just before the closing </head> tag
indexHtmlContent = indexHtmlContent.replace(
"</head>",
`${scriptContent}</head>`
);
fs.writeFileSync(indexHtmlPath, indexHtmlContent);
console.log("Environment variables injected successfully");
================================================
FILE: package.json
================================================
{
"name": "duck-ui",
"private": true,
"version": "0.0.39",
"release_date": "2026-04-13",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"",
"typecheck": "tsc --noEmit",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"prepare": "husky"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@duckdb/duckdb-wasm": "1.33.1-dev45.0",
"@hookform/resolvers": "^5.2.2",
"@mlc-ai/web-llm": "^0.2.82",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.23",
"@types/dompurify": "^3.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dompurify": "^3.3.3",
"exceljs": "^4.4.0",
"framer-motion": "^12.38.0",
"html2canvas": "^1.4.1",
"lucide-react": "^0.546.0",
"marked": "^17.0.6",
"monaco-editor": "^0.55.1",
"openai": "^6.34.0",
"papaparse": "^5.5.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-error-boundary": "^6.1.1",
"react-hook-form": "^7.72.1",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"react-router": "^7.14.0",
"sonner": "^2.0.7",
"sql-formatter": "^15.7.3",
"tailwind-merge": "^3.5.0",
"uplot": "^1.6.32",
"vaul": "^1.1.2",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.2",
"@types/papaparse": "^5.5.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"baseline-browser-mapping": "^2.10.18",
"esbuild": "^0.25.12",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^16.5.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.8.2",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.58.2",
"vite": "^7.3.2",
"vitest": "^4.1.4"
},
"description": "Duck-UI is a web-based interface for interacting with DuckDB, a high-performance analytical database system. This project leverages DuckDB's WebAssembly (WASM) capabilities to provide a seamless and efficient user experience directly in the browser.",
"main": "eslint.config.js",
"directories": {
"doc": "docs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/caioricciuti/duck-ui.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/caioricciuti/duck-ui/issues"
},
"homepage": "https://github.com/caioricciuti/duck-ui#readme",
"lint-staged": {
"src/**/*.{ts,tsx,css}": "prettier --write"
},
"overrides": {
"brace-expansion": "^2.0.3",
"picomatch": "^4.0.4",
"flatted": "^3.4.2"
}
}
================================================
FILE: public/databases/README.md
================================================
# Embedded Databases
This directory allows you to embed DuckDB database files (`.db`) that will be automatically loaded when DuckUI starts. This is perfect for:
- Deploying DuckUI with demo data
- Distributing pre-configured databases
- Creating self-contained analytical dashboards
## How to Add Your Database
1. **Place your `.db` file in this directory**
```
public/databases/my-database.db
```
2. **Register it in `manifest.json`**
```json
{
"databases": [
{
"name": "My Database",
"file": "my-database.db",
"description": "Description of your database",
"autoLoad": true
}
]
}
```
3. **Build and deploy**
```bash
bun run build
```
## Manifest Format
Each database entry in `manifest.json` supports:
- **`name`** (required): Display name in the UI
- **`file`** (required): Filename of the `.db` file in this directory
- **`description`** (optional): Description shown in the UI
- **`autoLoad`** (optional, default: `true`): Whether to load on startup
## Example
```json
{
"databases": [
{
"name": "Sales Demo",
"file": "sales-demo.db",
"description": "Sample sales data from 2023",
"autoLoad": true
},
{
"name": "Analytics",
"file": "analytics.db",
"description": "Web analytics data",
"autoLoad": false
}
]
}
```
## Notes
- Database files are fetched and loaded in the browser
- Large database files will increase initial load time
- All databases are loaded into memory (consider file sizes)
- You can attach/detach databases dynamically from the SQL editor
================================================
FILE: public/databases/manifest.json
================================================
{
"databases": []
}
================================================
FILE: serve.json
================================================
{
"headers": [
{
"source": "**/*",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }
]
}
]
}
================================================
FILE: src/components/charts/ChartVisualizationPro.tsx
================================================
/**
* Professional Chart Visualization Component
* Features: Auto-chart, live preview, multi-series, customization, export
* Powered by uPlot (canvas-based, lightweight)
*/
import React, { useState, useRef, useMemo, useEffect, useCallback } from "react";
import uPlot from "uplot";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { MultiSelect } from "@/components/ui/multi-select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import {
Download,
BarChart3,
LineChart,
PieChart as PieChartIcon,
ScatterChart,
AreaChart,
Settings2,
RotateCcw,
Layers,
TrendingUp,
CircleDot,
ArrowUpDown,
} from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "@/components/theme/theme-provider";
import { formatNumber, formatNumberWithSuffix, shortenLabel } from "@/lib/chartUtils";
import { transformData, isNumericColumn, suggestChartTypes } from "@/lib/chartDataTransform";
import { exportChartAsPNG } from "@/lib/chartExport";
import UPlotChart from "./UPlotChart";
import { tooltipPlugin } from "./tooltipPlugin";
import type { QueryResult, ChartConfig, ChartType, DataTransform } from "@/store";
interface ChartVisualizationProProps {
result: QueryResult;
chartConfig?: ChartConfig;
onConfigChange: (config: ChartConfig | undefined) => void;
}
// Enhanced color palette
const DEFAULT_COLORS = [
"#D99B43",
"#8B5CF6",
"#3B82F6",
"#10B981",
"#F59E0B",
"#EF4444",
"#EC4899",
"#6366F1",
"#14B8A6",
"#F97316",
];
// Chart type display labels with icons
const CHART_TYPE_INFO: Record<string, { label: string; icon: React.ElementType }> = {
bar: { label: "Bar", icon: BarChart3 },
grouped_bar: { label: "Grouped Bar", icon: Layers },
stacked_bar: { label: "Stacked Bar", icon: Layers },
line: { label: "Line", icon: LineChart },
area: { label: "Area", icon: AreaChart },
stacked_area: { label: "Stacked Area", icon: TrendingUp },
pie: { label: "Pie", icon: PieChartIcon },
donut: { label: "Donut", icon: CircleDot },
scatter: { label: "Scatter", icon: ScatterChart },
};
// ── SVG Pie/Donut Chart ──────────────────────────────────────────────────────
function PieChartDisplay({
data,
xKey,
yKey,
colors,
isDonut,
innerRadius = 0.45,
theme,
}: {
data: Record<string, unknown>[];
xKey: string;
yKey: string;
colors: string[];
isDonut: boolean;
innerRadius?: number;
theme: string;
}) {
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
const total = data.reduce((sum, row) => sum + (Number(row[yKey]) || 0), 0);
if (total === 0)
return (
<div className="flex items-center justify-center h-full text-muted-foreground">No data</div>
);
const slices: { label: string; value: number; pct: number; color: string }[] = [];
let cumAngle = -Math.PI / 2;
const arcs: {
d: string;
color: string;
midAngle: number;
pct: number;
label: string;
value: number;
}[] = [];
data.forEach((row, i) => {
const value = Number(row[yKey]) || 0;
const pct = value / total;
const angle = pct * 2 * Math.PI;
const startAngle = cumAngle;
const endAngle = cumAngle + angle;
const midAngle = startAngle + angle / 2;
const color = colors[i % colors.length];
slices.push({ label: String(row[xKey]), value, pct, color });
const outerR = 1;
const innerR = isDonut ? innerRadius : 0;
const largeArc = angle > Math.PI ? 1 : 0;
const x1 = Math.cos(startAngle) * outerR;
const y1 = Math.sin(startAngle) * outerR;
const x2 = Math.cos(endAngle) * outerR;
const y2 = Math.sin(endAngle) * outerR;
const ix1 = Math.cos(endAngle) * innerR;
const iy1 = Math.sin(endAngle) * innerR;
const ix2 = Math.cos(startAngle) * innerR;
const iy2 = Math.sin(startAngle) * innerR;
const d = isDonut
? `M ${x1} ${y1} A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2} L ${ix1} ${iy1} A ${innerR} ${innerR} 0 ${largeArc} 0 ${ix2} ${iy2} Z`
: `M 0 0 L ${x1} ${y1} A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2} ${y2} Z`;
arcs.push({ d, color, midAngle, pct, label: String(row[xKey]), value });
cumAngle = endAngle;
});
const strokeColor = theme === "dark" ? "#1a1a1a" : "#fff";
return (
<div className="flex items-center justify-center h-full gap-6">
{/* Chart */}
<div className="relative flex-shrink-0">
<svg
viewBox="-1.3 -1.3 2.6 2.6"
className="w-full max-w-[360px] max-h-[360px]"
style={{ minWidth: 200 }}
>
{arcs.map((arc, i) => {
const isHovered = hoveredIdx === i;
const tx = isHovered ? Math.cos(arc.midAngle) * 0.06 : 0;
const ty = isHovered ? Math.sin(arc.midAngle) * 0.06 : 0;
return (
<g key={i}>
<path
d={arc.d}
fill={arc.color}
stroke={strokeColor}
strokeWidth={0.02}
opacity={hoveredIdx !== null && !isHovered ? 0.4 : 1}
transform={`translate(${tx}, ${ty})`}
style={{ transition: "opacity 0.2s, transform 0.2s" }}
onMouseEnter={() => setHoveredIdx(i)}
onMouseLeave={() => setHoveredIdx(null)}
/>
{arc.pct >= 0.05 && (
<text
x={Math.cos(arc.midAngle) * (isDonut ? 0.72 : 0.6) + tx}
y={Math.sin(arc.midAngle) * (isDonut ? 0.72 : 0.6) + ty}
textAnchor="middle"
dominantBaseline="central"
fill="white"
fontSize="0.11"
fontWeight="600"
pointerEvents="none"
style={{ transition: "all 0.2s" }}
>
{`${(arc.pct * 100).toFixed(0)}%`}
</text>
)}
</g>
);
})}
{/* Donut center total */}
{isDonut && (
<g>
<text
x="0"
y="-0.06"
textAnchor="middle"
dominantBaseline="central"
fill={theme === "dark" ? "#999" : "#666"}
fontSize="0.12"
>
Total
</text>
<text
x="0"
y="0.12"
textAnchor="middle"
dominantBaseline="central"
fill={theme === "dark" ? "#e5e5e5" : "#1a1a1a"}
fontSize="0.18"
fontWeight="700"
>
{formatNumberWithSuffix(total)}
</text>
</g>
)}
</svg>
{/* Hover tooltip */}
{hoveredIdx !== null && (
<div className="absolute top-2 left-1/2 -translate-x-1/2 bg-popover border rounded-lg shadow-lg px-3 py-2 text-xs pointer-events-none z-10">
<div className="font-medium">{slices[hoveredIdx].label}</div>
<div className="text-muted-foreground">
{formatNumber(slices[hoveredIdx].value)} ({(slices[hoveredIdx].pct * 100).toFixed(1)}
%)
</div>
</div>
)}
</div>
{/* Legend */}
<div className="flex flex-col gap-1.5 text-xs max-h-[320px] overflow-y-auto pr-2">
{slices.map((s, i) => (
<div
key={i}
className="flex items-center gap-2 cursor-default"
onMouseEnter={() => setHoveredIdx(i)}
onMouseLeave={() => setHoveredIdx(null)}
>
<span
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
style={{ backgroundColor: s.color }}
/>
<span className="text-muted-foreground truncate max-w-[140px]" title={s.label}>
{s.label}
</span>
<span className="font-medium ml-auto tabular-nums pl-2">
{formatNumberWithSuffix(s.value)}
</span>
<span className="text-muted-foreground tabular-nums w-[3.5em] text-right">
{(s.pct * 100).toFixed(1)}%
</span>
</div>
))}
</div>
</div>
);
}
// ── XY Chart Legend ──────────────────────────────────────────────────────────
function ChartLegend({
series,
colors,
onToggle,
}: {
series: { label: string; show: boolean }[];
colors: string[];
onToggle: (idx: number) => void;
}) {
if (series.length <= 1) return null;
return (
<div className="flex flex-wrap gap-x-4 gap-y-1 justify-center py-1 text-xs">
{series.map((s, i) => (
<button
key={i}
className="flex items-center gap-1.5 cursor-pointer hover:opacity-80 transition-opacity"
style={{ opacity: s.show ? 1 : 0.35 }}
onClick={() => onToggle(i)}
type="button"
>
<span
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
style={{ backgroundColor: colors[i % colors.length] }}
/>
<span className="text-muted-foreground">{s.label}</span>
</button>
))}
</div>
);
}
// ── Main Component ───────────────────────────────────────────────────────────
export const ChartVisualizationPro: React.FC<ChartVisualizationProProps> = ({
result,
chartConfig,
onConfigChange,
}) => {
const { theme } = useTheme();
const chartRef = useRef<HTMLDivElement>(null);
const uPlotRef = useRef<uPlot | null>(null);
const [hiddenSeries, setHiddenSeries] = useState<Set<number>>(new Set());
// Get numeric columns for Y-axis
const numericColumns = useMemo(
() => result.columns.filter((col) => isNumericColumn(result.data, col)),
[result]
);
// Auto-chart: detect best config when no config exists
const autoDetect = useCallback((): ChartConfig => {
const xAxis =
result.columns.find((col) => !isNumericColumn(result.data, col)) || result.columns[0] || "";
const yCol = numericColumns[0] || result.columns[1] || "";
const suggested = suggestChartTypes(result, xAxis, yCol);
const type = (suggested[0] || "bar") as ChartType;
return {
type,
xAxis,
yAxis: yCol,
colors: DEFAULT_COLORS,
showGrid: true,
showValues: false,
smooth: false,
legend: { show: true, position: "bottom" },
};
}, [result, numericColumns]);
// On mount: auto-generate config if none provided
useEffect(() => {
if (!chartConfig && result.data.length > 0) {
onConfigChange(autoDetect());
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Live config update helper
const updateConfig = useCallback(
(updates: Partial<ChartConfig>) => {
const base = chartConfig || autoDetect();
onConfigChange({ ...base, ...updates });
},
[chartConfig, autoDetect, onConfigChange]
);
const updateTransform = useCallback(
(updates: Partial<DataTransform>) => {
const base = chartConfig || autoDetect();
onConfigChange({ ...base, transform: { ...base.transform, ...updates } });
},
[chartConfig, autoDetect, onConfigChange]
);
// Current effective config
const config = chartConfig || autoDetect();
// Selected Y columns (from series or single yAxis)
const selectedYColumns = useMemo(() => {
if (config.series?.length) return config.series.map((s) => s.column);
if (config.yAxis) return [config.yAxis];
return [];
}, [config]);
const handleYColumnsChange = useCallback(
(cols: string[]) => {
const series = cols.map((col, idx) => ({
column: col,
label: col,
color: DEFAULT_COLORS[idx % DEFAULT_COLORS.length],
}));
updateConfig({
series: series.length > 1 ? series : undefined,
yAxis: series.length === 1 ? cols[0] : undefined,
});
},
[updateConfig]
);
// Transform data based on configuration
const transformedData = useMemo(() => {
return transformData(result, config.transform, config.xAxis, config.yAxis || config.series);
}, [result, config.transform, config.xAxis, config.yAxis, config.series]);
const handleExportPNG = async () => {
if (!chartRef.current) {
toast.error("No chart to export");
return;
}
try {
const fileName = `chart-${Date.now()}.png`;
const bg = theme === "dark" ? "#1a1a1a" : "#ffffff";
await exportChartAsPNG(chartRef.current, fileName, bg);
toast.success("Chart exported as PNG");
} catch (error) {
toast.error(
`Failed to export chart: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
};
// ── Build uPlot options + data ──────────────────────────────────────────────
const { uPlotOptions, uPlotData, seriesInfo } = useMemo(() => {
if (!config.xAxis || (!config.yAxis && (!config.series || config.series.length === 0))) {
return { uPlotOptions: null, uPlotData: null, seriesInfo: [] };
}
const chartData = transformedData.map((row) => {
const newRow: Record<string, unknown> = { ...row };
if (config.yAxis) newRow[config.yAxis] = Number(row[config.yAxis]) || 0;
if (config.series) {
config.series.forEach((s) => {
newRow[s.column] = Number(row[s.column]) || 0;
});
}
return newRow;
});
const yKeys: string[] =
config.series?.map((s) => s.column) ?? (config.yAxis ? [config.yAxis] : []);
const colors = config.colors || DEFAULT_COLORS;
const isDark = theme === "dark";
const xs = chartData.map((_, i) => i);
const seriesData = yKeys.map((key) =>
chartData.map((row) => (Number(row[key]) || 0) as number)
);
const isStacked = config.type === "stacked_bar" || config.type === "stacked_area";
const stackedData = isStacked
? seriesData.reduce<number[][]>((acc, curr) => {
if (acc.length === 0) return [curr];
const prev = acc[acc.length - 1];
acc.push(curr.map((v, i) => v + prev[i]));
return acc;
}, [])
: seriesData;
const finalSeriesData = isStacked ? stackedData : seriesData;
const isBarType = ["bar", "stacked_bar", "grouped_bar"].includes(config.type);
const isAreaType = ["area", "stacked_area"].includes(config.type);
const isLineType = config.type === "line";
const isScatter = config.type === "scatter";
const useSmooth = config.smooth && (isLineType || isAreaType);
const xLabels = chartData.map((row) => shortenLabel(String(row[config.xAxis])));
const barsBuilder = isBarType
? uPlot.paths.bars!({
size: [0.6, 100],
radius: 0.2,
gap: yKeys.length > 1 && config.type === "grouped_bar" ? 2 : 0,
})
: undefined;
const splineBuilder = useSmooth ? uPlot.paths.spline!() : undefined;
// Build series config
const sInfo: { label: string; color: string }[] = [];
const uSeries: uPlot.Series[] = [
{ label: config.xAxis },
...yKeys.map((key, i) => {
const color = config.series?.[i]?.color || colors[i % colors.length];
const seriesLabel = config.series?.[i]?.label || key;
sInfo.push({ label: seriesLabel, color });
const s: uPlot.Series = {
label: seriesLabel,
stroke: color,
width: isBarType ? 0 : 2,
fill: isBarType || isAreaType ? color + (isAreaType ? "66" : "cc") : undefined,
points: { show: isScatter, size: isScatter ? 8 : 4 },
paths: isBarType
? barsBuilder
: useSmooth
? splineBuilder
: isScatter
? () => null
: undefined,
};
if (config.type === "grouped_bar" && yKeys.length > 1) {
const groupBars = uPlot.paths.bars!({
size: [0.6 / yKeys.length, 100],
radius: 0.2,
align: i === 0 ? -1 : i === yKeys.length - 1 ? 1 : 0,
});
s.paths = groupBars;
}
return s;
}),
];
const needsRotation = chartData.length > 10;
const maxLabelLen = Math.max(...xLabels.map((l) => l.length), 1);
const xAxisSize = needsRotation ? Math.min(120, 40 + maxLabelLen * 5) : 60;
const uAxes: uPlot.Axis[] = [
{
stroke: isDark ? "#888" : "#666",
grid: { stroke: isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)", width: 1 },
ticks: { stroke: isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.15)", width: 1 },
values: (_u: uPlot, vals: number[]) => vals.map((v) => xLabels[v] ?? ""),
gap: 8,
size: xAxisSize,
font: "12px system-ui, sans-serif",
labelFont: "12px system-ui, sans-serif",
rotate: needsRotation ? -45 : 0,
},
{
stroke: isDark ? "#888" : "#666",
grid: config.showGrid
? {
stroke: isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)",
width: 1,
dash: [4, 4],
}
: { show: false },
ticks: { stroke: isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.15)", width: 1 },
values: (_u: uPlot, vals: number[]) => vals.map((v) => formatNumberWithSuffix(v)),
gap: 8,
size: 70,
font: "12px system-ui, sans-serif",
labelFont: "12px system-ui, sans-serif",
},
];
// Data labels hook for bar charts
const hooks: uPlot.Plugin["hooks"] = {};
if (config.showValues && isBarType) {
hooks.draw = [
(u: uPlot) => {
const ctx = u.ctx;
ctx.save();
ctx.font = "10px system-ui, sans-serif";
ctx.fillStyle = isDark ? "#ccc" : "#333";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
for (let si = 1; si < u.series.length; si++) {
if (!u.series[si].show) continue;
for (let di = 0; di < u.data[si].length; di++) {
const val = u.data[si][di];
if (val == null) continue;
const cx = u.valToPos(u.data[0][di]!, "x", true);
const cy = u.valToPos(val, "y", true);
ctx.fillText(formatNumberWithSuffix(val as number), cx, cy - 4);
}
}
ctx.restore();
},
];
}
const opts: Omit<uPlot.Options, "width" | "height"> = {
scales: { x: { time: false } },
series: uSeries,
axes: uAxes,
cursor: {
drag: { x: false, y: false },
points: {
size: 6,
fill: (u: uPlot, i: number) =>
(typeof u.series[i].stroke === "function"
? (u.series[i].stroke as (self: uPlot, seriesIdx: number) => string)(u, i)
: u.series[i].stroke) as string,
stroke: "transparent",
width: 0,
},
},
legend: { show: false },
plugins: [tooltipPlugin(xLabels, { stacked: isStacked }), { hooks }],
padding: [16, 16, 8, 0],
};
const data: uPlot.AlignedData = isStacked
? ([xs, ...finalSeriesData.reverse()] as uPlot.AlignedData)
: ([xs, ...finalSeriesData] as uPlot.AlignedData);
return { uPlotOptions: opts, uPlotData: data, seriesInfo: sInfo };
}, [config, transformedData, theme]);
const legendState = useMemo(
() => seriesInfo.map((s, i) => ({ label: s.label, show: !hiddenSeries.has(i) })),
[seriesInfo, hiddenSeries]
);
const handleLegendToggle = useCallback((idx: number) => {
setHiddenSeries((prev) => {
const next = new Set(prev);
if (next.has(idx)) {
next.delete(idx);
} else {
next.add(idx);
}
// Toggle series visibility on the uPlot instance
if (uPlotRef.current) {
uPlotRef.current.setSeries(idx + 1, { show: !next.has(idx) });
}
return next;
});
}, []);
// ── Render ──────────────────────────────────────────────────────────────────
const renderChart = () => {
if (config.type === "pie" || config.type === "donut") {
const chartData = transformedData.map((row) => ({
...row,
[config.xAxis]: String(row[config.xAxis]),
[config.yAxis || config.series?.[0]?.column || ""]:
Number(row[config.yAxis || config.series?.[0]?.column || ""]) || 0,
}));
return (
<PieChartDisplay
data={chartData}
xKey={config.xAxis}
yKey={config.yAxis || config.series?.[0]?.column || ""}
colors={config.colors || DEFAULT_COLORS}
isDonut={config.type === "donut"}
innerRadius={config.innerRadius ? config.innerRadius / 120 : 0.45}
theme={theme}
/>
);
}
if (!uPlotOptions || !uPlotData) return null;
return (
<div className="flex flex-col h-full">
<div className="flex-1 min-h-0">
<UPlotChart
options={uPlotOptions}
data={uPlotData}
className="w-full h-full"
onInit={(u) => {
uPlotRef.current = u;
}}
/>
</div>
<ChartLegend
series={legendState}
colors={seriesInfo.map((s) => s.color)}
onToggle={handleLegendToggle}
/>
</div>
);
};
if (!result || !result.data || result.data.length === 0) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-2">
<div className="text-muted-foreground text-sm">No data available for visualization</div>
</div>
</div>
);
}
const isLineOrArea = ["line", "area", "stacked_area"].includes(config.type);
return (
<div className="flex flex-col h-full">
{/* Compact Toolbar */}
<div className="flex flex-wrap items-center gap-2 px-4 py-2 border-b bg-muted/30">
{/* Chart Type */}
<Select
value={config.type}
onValueChange={(value) => updateConfig({ type: value as ChartType })}
>
<SelectTrigger className="w-[150px] shrink-0 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(CHART_TYPE_INFO).map(([value, info]) => {
const Icon = info.icon;
return (
<SelectItem key={value} value={value}>
<span className="flex items-center gap-2">
<Icon className="h-3.5 w-3.5 opacity-60" />
{info.label}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{/* X-Axis */}
<Select value={config.xAxis} onValueChange={(value) => updateConfig({ xAxis: value })}>
<SelectTrigger className="w-[140px] shrink-0 h-8 text-xs">
<span className="text-muted-foreground mr-1">X:</span>
<SelectValue placeholder="Column" />
</SelectTrigger>
<SelectContent>
{result.columns.map((col) => (
<SelectItem key={col} value={col}>
{col}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Y-Axis */}
<div className="w-[200px] shrink-0">
<MultiSelect
options={numericColumns.map((col, idx) => ({
label: col,
value: col,
color: selectedYColumns.includes(col)
? DEFAULT_COLORS[selectedYColumns.indexOf(col) % DEFAULT_COLORS.length]
: DEFAULT_COLORS[idx % DEFAULT_COLORS.length],
}))}
selected={selectedYColumns}
onChange={handleYColumnsChange}
placeholder="Values..."
className="h-8 text-xs"
/>
</div>
{/* Settings Popover */}
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Settings2 className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px]" align="end">
<div className="space-y-4">
<h4 className="font-medium text-sm">Chart Settings</h4>
{/* Sort */}
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground flex items-center gap-1">
<ArrowUpDown className="h-3 w-3" /> Sort by
</label>
<div className="flex gap-1.5">
<Select
value={config.transform?.sortBy || "__none__"}
onValueChange={(v) =>
updateTransform({ sortBy: v === "__none__" ? undefined : v })
}
>
<SelectTrigger className="h-8 text-xs flex-1">
<SelectValue placeholder="None" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">None</SelectItem>
{result.columns.map((col) => (
<SelectItem key={col} value={col}>
{col}
</SelectItem>
))}
</SelectContent>
</Select>
{config.transform?.sortBy && (
<Select
value={config.transform?.sortOrder || "asc"}
onValueChange={(v) => updateTransform({ sortOrder: v as "asc" | "desc" })}
>
<SelectTrigger className="h-8 text-xs w-[80px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc">Asc</SelectItem>
<SelectItem value="desc">Desc</SelectItem>
</SelectContent>
</Select>
)}
</div>
</div>
{/* Limit */}
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground">Limit rows</label>
<Input
type="number"
min={0}
placeholder="No limit"
className="h-8 text-xs"
value={config.transform?.limit ?? ""}
onChange={(e) => {
const v = e.target.value ? parseInt(e.target.value, 10) : undefined;
updateTransform({ limit: v });
}}
/>
</div>
{/* Aggregation */}
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground">Aggregation</label>
<Select
value={config.transform?.aggregation || "none"}
onValueChange={(v) =>
updateTransform({
aggregation: v as "sum" | "avg" | "count" | "min" | "max" | "none",
groupBy: v !== "none" ? config.xAxis : undefined,
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="sum">Sum</SelectItem>
<SelectItem value="avg">Average</SelectItem>
<SelectItem value="count">Count</SelectItem>
<SelectItem value="min">Min</SelectItem>
<SelectItem value="max">Max</SelectItem>
</SelectContent>
</Select>
</div>
{/* Toggles */}
<div className="space-y-3 pt-1 border-t">
<div className="flex items-center justify-between">
<label className="text-xs">Show values</label>
<Switch
checked={config.showValues ?? false}
onCheckedChange={(v) => updateConfig({ showValues: v })}
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs">Show grid</label>
<Switch
checked={config.showGrid ?? true}
onCheckedChange={(v) => updateConfig({ showGrid: v })}
/>
</div>
{isLineOrArea && (
<div className="flex items-center justify-between">
<label className="text-xs">Smooth lines</label>
<Switch
checked={config.smooth ?? false}
onCheckedChange={(v) => updateConfig({ smooth: v })}
/>
</div>
)}
</div>
</div>
</PopoverContent>
</Popover>
{/* Export */}
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleExportPNG}>
<Download className="h-4 w-4" />
</Button>
{/* Clear / Reset */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {
onConfigChange(undefined);
// Will auto-detect on next render
setTimeout(() => onConfigChange(autoDetect()), 0);
}}
title="Reset chart"
>
<RotateCcw className="h-3.5 w-3.5" />
</Button>
</div>
{/* Chart Display */}
<div className="flex-1 min-h-0 p-2" ref={chartRef}>
{renderChart()}
</div>
</div>
);
};
export default React.memo(ChartVisualizationPro);
================================================
FILE: src/components/charts/UPlotChart.tsx
================================================
import { useRef, useEffect, useCallback } from "react";
import uPlot from "uplot";
import "uplot/dist/uPlot.min.css";
interface UPlotChartProps {
options: Omit<uPlot.Options, "width" | "height">;
data: uPlot.AlignedData;
className?: string;
onInit?: (chart: uPlot) => void;
}
export default function UPlotChart({ options, data, className, onInit }: UPlotChartProps) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<uPlot | null>(null);
const create = useCallback(() => {
if (!containerRef.current) return;
const el = containerRef.current;
const { width, height } = el.getBoundingClientRect();
if (width === 0 || height === 0) return;
chartRef.current?.destroy();
const chart = new uPlot({ ...options, width, height }, data, el);
chartRef.current = chart;
onInit?.(chart);
}, [options, data, onInit]);
useEffect(() => {
create();
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver((entries) => {
const { width: w, height: h } = entries[0].contentRect;
if (w > 0 && h > 0) {
chartRef.current?.setSize({ width: w, height: h });
}
});
ro.observe(el);
return () => {
ro.disconnect();
chartRef.current?.destroy();
chartRef.current = null;
};
}, [create]);
return <div ref={containerRef} className={className} style={{ minHeight: 0 }} />;
}
================================================
FILE: src/components/charts/tooltipPlugin.ts
================================================
import uPlot from "uplot";
import { formatNumberWithSuffix } from "@/lib/chartUtils";
function escapeHtml(str: string): string {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
export interface TooltipPluginOptions {
stacked?: boolean;
}
export function tooltipPlugin(xLabels: string[], opts?: TooltipPluginOptions): uPlot.Plugin {
let tooltip: HTMLDivElement;
let over: HTMLElement;
const isStacked = opts?.stacked ?? false;
return {
hooks: {
init(u: uPlot) {
over = u.over;
tooltip = document.createElement("div");
tooltip.className = "uplot-tooltip";
tooltip.style.display = "none";
tooltip.style.position = "absolute";
tooltip.style.pointerEvents = "none";
tooltip.style.zIndex = "100";
over.appendChild(tooltip);
over.addEventListener("mouseenter", () => {
tooltip.style.display = "block";
});
over.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
});
},
setSize() {
// bounds recalculated in setCursor via over.clientWidth/Height
},
setCursor(u: uPlot) {
const { idx, left, top } = u.cursor;
if (idx == null || left == null || top == null) {
tooltip.style.display = "none";
return;
}
const xLabel = escapeHtml(xLabels[idx] ?? String(u.data[0][idx]));
let html = `<div class="uplot-tooltip-title">${xLabel}</div>`;
let total = 0;
const rows: { label: string; color: string; val: number | null }[] = [];
for (let i = 1; i < u.series.length; i++) {
const s = u.series[i];
if (!s.show) continue;
const rawVal = u.data[i][idx];
const val = rawVal != null ? (rawVal as number) : null;
const color =
typeof s.stroke === "function"
? (s.stroke as (self: uPlot, seriesIdx: number) => string)(u, i)
: s.stroke;
const safeLabel = typeof s.label === "string" ? escapeHtml(s.label) : "";
const safeColor = typeof color === "string" ? escapeHtml(color) : "";
if (val != null) total += val;
rows.push({ label: safeLabel, color: safeColor, val });
}
for (const row of rows) {
const formatted = row.val != null ? formatNumberWithSuffix(row.val) : "\u2014";
const pct =
isStacked && row.val != null && total > 0
? ` <span class="uplot-tooltip-pct">(${((row.val / total) * 100).toFixed(1)}%)</span>`
: "";
html += `<div class="uplot-tooltip-row">
<span class="uplot-tooltip-dot" style="background:${row.color}"></span>
<span class="uplot-tooltip-label">${row.label}</span>
<span class="uplot-tooltip-value">${formatted}${pct}</span>
</div>`;
}
if (isStacked && rows.length > 1) {
html += `<div class="uplot-tooltip-total">
<span class="uplot-tooltip-label">Total</span>
<span class="uplot-tooltip-value">${formatNumberWithSuffix(total)}</span>
</div>`;
}
tooltip.innerHTML = html;
const tooltipW = tooltip.offsetWidth;
const tooltipH = tooltip.offsetHeight;
const overW = over.clientWidth;
const overH = over.clientHeight;
const pad = 12;
let posLeft = left + pad;
let posTop = top - tooltipH / 2;
if (posLeft + tooltipW > overW) posLeft = left - tooltipW - pad;
if (posTop < 0) posTop = 0;
if (posTop + tooltipH > overH) posTop = overH - tooltipH;
tooltip.style.left = posLeft + "px";
tooltip.style.top = posTop + "px";
tooltip.style.display = "block";
},
},
};
}
================================================
FILE: src/components/cloud/CloudBrowser.tsx
================================================
/**
* Cloud Browser Component
* Displays cloud storage connections and allows browsing/importing files
*/
import { useState, useEffect } from "react";
import { useDuckStore } from "@/store";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Cloud,
CloudOff,
Plus,
MoreVertical,
Trash2,
Link,
Unlink,
AlertCircle,
Loader2,
} from "lucide-react";
import { CloudConnectionModal } from "./CloudConnectionModal";
import type { CloudConnection } from "@/store";
// Provider icons/colors
const PROVIDER_CONFIG = {
s3: { label: "S3", color: "text-orange-500", bgColor: "bg-orange-500/10" },
gcs: { label: "GCS", color: "text-blue-500", bgColor: "bg-blue-500/10" },
azure: { label: "Azure", color: "text-cyan-500", bgColor: "bg-cyan-500/10" },
};
interface CloudConnectionItemProps {
connection: CloudConnection;
onConnect: () => void;
onDisconnect: () => void;
onRemove: () => void;
}
function CloudConnectionItem({
connection,
onConnect,
onDisconnect,
onRemove,
}: CloudConnectionItemProps) {
const [isLoading, setIsLoading] = useState(false);
const config = PROVIDER_CONFIG[connection.type];
const handleConnect = async () => {
setIsLoading(true);
try {
await onConnect();
} finally {
setIsLoading(false);
}
};
const handleDisconnect = async () => {
setIsLoading(true);
try {
await onDisconnect();
} finally {
setIsLoading(false);
}
};
return (
<div className="group flex items-center justify-between py-1.5 px-2 rounded hover:bg-muted/50">
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className={`p-1 rounded ${config.bgColor}`}>
<Cloud className={`h-3.5 w-3.5 ${config.color}`} />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="text-sm truncate">{connection.name}</span>
<span className={`text-[10px] px-1 py-0.5 rounded ${config.bgColor} ${config.color}`}>
{config.label}
</span>
{connection.isConnected && <span className="w-1.5 h-1.5 rounded-full bg-green-500" />}
</div>
{connection.lastError && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 text-destructive">
<AlertCircle className="h-3 w-3" />
<span className="text-xs truncate">Error</span>
</div>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">{connection.lastError}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 opacity-0 group-hover:opacity-100">
{isLoading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<MoreVertical className="h-3.5 w-3.5" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{connection.isConnected ? (
<DropdownMenuItem onClick={handleDisconnect}>
<Unlink className="h-4 w-4 mr-2" />
Disconnect
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={handleConnect}>
<Link className="h-4 w-4 mr-2" />
Connect
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onRemove} className="text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
export default function CloudBrowser() {
const [isModalOpen, setIsModalOpen] = useState(false);
const cloudConnections = useDuckStore((s) => s.cloudConnections);
const cloudSupportStatus = useDuckStore((s) => s.cloudSupportStatus);
const isCloudStorageInitialized = useDuckStore((s) => s.isCloudStorageInitialized);
const initCloudStorage = useDuckStore((s) => s.initCloudStorage);
const connectCloudStorage = useDuckStore((s) => s.connectCloudStorage);
const disconnectCloudStorage = useDuckStore((s) => s.disconnectCloudStorage);
const removeCloudConnection = useDuckStore((s) => s.removeCloudConnection);
// Initialize cloud storage on mount
useEffect(() => {
if (!isCloudStorageInitialized) {
initCloudStorage();
}
}, [isCloudStorageInitialized, initCloudStorage]);
const handleConnect = async (id: string) => {
await connectCloudStorage(id);
};
const handleDisconnect = async (id: string) => {
await disconnectCloudStorage(id);
};
const handleRemove = async (id: string) => {
await removeCloudConnection(id);
};
// Show warning if cloud storage is not supported
const showWarning = cloudSupportStatus && !cloudSupportStatus.httpfsAvailable;
return (
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between px-2">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Cloud
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-5 w-5"
onClick={() => setIsModalOpen(true)}
>
<Plus className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Add Cloud Connection</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Warning Banner */}
{showWarning && (
<div className="mx-2 p-2 rounded bg-yellow-500/10 border border-yellow-500/20">
<div className="flex items-start gap-2">
<CloudOff className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
<p className="text-xs text-yellow-600 dark:text-yellow-400">
Cloud storage has limited support in browsers. Consider using HTTPS URLs instead.
</p>
</div>
</div>
)}
{/* Connections List */}
{cloudConnections.length === 0 ? (
<div className="px-2 py-4 text-center">
<Cloud className="h-8 w-8 mx-auto text-muted-foreground/30 mb-2" />
<p className="text-xs text-muted-foreground">No cloud connections</p>
<Button
variant="link"
size="sm"
className="text-xs h-auto p-0 mt-1"
onClick={() => setIsModalOpen(true)}
>
Add your first connection
</Button>
</div>
) : (
<div className="space-y-0.5">
{cloudConnections.map((conn) => (
<CloudConnectionItem
key={conn.id}
connection={conn}
onConnect={() => handleConnect(conn.id)}
onDisconnect={() => handleDisconnect(conn.id)}
onRemove={() => handleRemove(conn.id)}
/>
))}
</div>
)}
{/* Add Connection Modal */}
<CloudConnectionModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
);
}
================================================
FILE: src/components/cloud/CloudConnectionModal.tsx
================================================
/**
* Cloud Connection Modal
* Form for adding/editing S3, GCS, and Azure cloud storage connections
*/
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Loader2, Cloud, AlertTriangle } from "lucide-react";
import { useDuckStore } from "@/store";
import type { CloudConnection } from "@/store";
interface CloudConnectionModalProps {
isOpen: boolean;
onClose: () => void;
existingConnection?: CloudConnection;
}
// Form validation schema
const cloudConnectionSchema = z
.object({
name: z.string().min(1, "Name is required"),
type: z.enum(["s3", "gcs", "azure"]),
// S3 fields
bucket: z.string().optional(),
region: z.string().optional(),
accessKeyId: z.string().optional(),
secretAccessKey: z.string().optional(),
endpoint: z.string().optional(),
// GCS fields
hmacKeyId: z.string().optional(),
hmacSecret: z.string().optional(),
// Azure fields
accountName: z.string().optional(),
accountKey: z.string().optional(),
containerName: z.string().optional(),
})
.refine(
(data) => {
// Validate required fields based on type
if (data.type === "s3") {
return data.bucket && data.accessKeyId && data.secretAccessKey;
}
if (data.type === "gcs") {
return data.bucket && data.hmacKeyId && data.hmacSecret;
}
if (data.type === "azure") {
return data.containerName && data.accountName && data.accountKey;
}
return true;
},
{
message: "Please fill in all required fields for the selected provider",
}
);
type CloudConnectionFormData = z.infer<typeof cloudConnectionSchema>;
export function CloudConnectionModal({
isOpen,
onClose,
existingConnection,
}: CloudConnectionModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; error?: string } | null>(null);
const addCloudConnection = useDuckStore((s) => s.addCloudConnection);
const cloudSupportStatus = useDuckStore((s) => s.cloudSupportStatus);
const form = useForm<CloudConnectionFormData>({
resolver: zodResolver(cloudConnectionSchema),
defaultValues: {
name: existingConnection?.name || "",
type: existingConnection?.type || "s3",
bucket: existingConnection?.bucket || "",
region: existingConnection?.region || "us-east-1",
accessKeyId: existingConnection?.accessKeyId || "",
secretAccessKey: existingConnection?.secretAccessKey || "",
endpoint: existingConnection?.endpoint || "",
hmacKeyId: existingConnection?.hmacKeyId || "",
hmacSecret: existingConnection?.hmacSecret || "",
accountName: existingConnection?.accountName || "",
accountKey: existingConnection?.accountKey || "",
containerName: existingConnection?.containerName || "",
},
});
const selectedType = form.watch("type");
const onSubmit = async (data: CloudConnectionFormData) => {
setIsSubmitting(true);
try {
await addCloudConnection({
name: data.name,
type: data.type,
bucket: data.bucket,
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
endpoint: data.endpoint,
hmacKeyId: data.hmacKeyId,
hmacSecret: data.hmacSecret,
accountName: data.accountName,
accountKey: data.accountKey,
containerName: data.containerName,
});
onClose();
} finally {
setIsSubmitting(false);
}
};
const handleTest = async () => {
setIsTesting(true);
setTestResult(null);
// For testing, we'd need to add the connection first, test it, then remove if failed
// For now, show a message about the support status
setTimeout(() => {
if (!cloudSupportStatus?.httpfsAvailable) {
setTestResult({
success: false,
error:
"Cloud storage (httpfs) is not available in this browser. Direct S3/GCS/Azure access may not work due to CORS restrictions.",
});
} else if (!cloudSupportStatus?.secretsSupported) {
setTestResult({
success: false,
error: "DuckDB secrets are not supported. Cloud storage authentication may not work.",
});
} else {
setTestResult({
success: true,
});
}
setIsTesting(false);
}, 500);
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Cloud className="h-5 w-5" />
{existingConnection ? "Edit" : "Add"} Cloud Connection
</DialogTitle>
<DialogDescription>
Connect to cloud storage (S3, Google Cloud Storage, or Azure Blob Storage)
</DialogDescription>
</DialogHeader>
{/* Support Status Warning */}
{cloudSupportStatus && !cloudSupportStatus.httpfsAvailable && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Cloud storage support is limited in this browser. The httpfs extension is not
available. You may need to use HTTPS URLs directly instead of s3:// or gcs://
protocols.
</AlertDescription>
</Alert>
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* Connection Name */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Connection Name</FormLabel>
<FormControl>
<Input placeholder="My S3 Bucket" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Provider Type */}
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Provider</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="s3">Amazon S3 / S3-Compatible</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
<SelectItem value="azure">Azure Blob Storage</SelectItem>
</SelectContent>
</Select>
<FormDescription>
S3-compatible includes MinIO, Cloudflare R2, DigitalOcean Spaces
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* S3 Fields */}
{selectedType === "s3" && (
<>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket Name *</FormLabel>
<FormControl>
<Input placeholder="my-bucket" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="region"
render={({ field }) => (
<FormItem>
<FormLabel>Region</FormLabel>
<FormControl>
<Input placeholder="us-east-1" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>Access Key ID *</FormLabel>
<FormControl>
<Input placeholder="AKIAIOSFODNN7EXAMPLE" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>Secret Access Key *</FormLabel>
<FormControl>
<Input type="password" placeholder="wJalrXUtn..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Endpoint (Optional)</FormLabel>
<FormControl>
<Input placeholder="https://minio.example.com" {...field} />
</FormControl>
<FormDescription>For S3-compatible services like MinIO or R2</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{/* GCS Fields */}
{selectedType === "gcs" && (
<>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket Name *</FormLabel>
<FormControl>
<Input placeholder="my-gcs-bucket" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hmacKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>HMAC Key ID *</FormLabel>
<FormControl>
<Input placeholder="GOOGTS7C7FUP..." {...field} />
</FormControl>
<FormDescription>
Generate HMAC keys in GCP Console under Cloud Storage Settings
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hmacSecret"
render={({ field }) => (
<FormItem>
<FormLabel>HMAC Secret *</FormLabel>
<FormControl>
<Input type="password" placeholder="..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{/* Azure Fields */}
{selectedType === "azure" && (
<>
<FormField
control={form.control}
name="containerName"
render={({ field }) => (
<FormItem>
<FormLabel>Container Name *</FormLabel>
<FormControl>
<Input placeholder="my-container" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accountName"
render={({ field }) => (
<FormItem>
<FormLabel>Storage Account Name *</FormLabel>
<FormControl>
<Input placeholder="mystorageaccount" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accountKey"
render={({ field }) => (
<FormItem>
<FormLabel>Account Key *</FormLabel>
<FormControl>
<Input type="password" placeholder="..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{/* Test Result */}
{testResult && (
<Alert variant={testResult.success ? "default" : "destructive"}>
<AlertDescription>
{testResult.success ? "Cloud storage support is available!" : testResult.error}
</AlertDescription>
</Alert>
)}
<DialogFooter className="gap-2">
<Button type="button" variant="outline" onClick={handleTest} disabled={isTesting}>
{isTesting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Test Support
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{existingConnection ? "Update" : "Add"} Connection
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
================================================
FILE: src/components/command-palette/CommandPalette.tsx
================================================
import { useEffect, useState, useMemo } from "react";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { useDuckStore } from "@/store";
import { useTheme } from "@/components/theme/theme-provider";
import {
Terminal,
Home,
Cable,
Brain,
Settings,
Sun,
Moon,
Database,
Table,
Bookmark,
} from "lucide-react";
import type { EditorTabType } from "@/store";
import {
getSavedQueries,
type SavedQuery,
} from "@/services/persistence/repositories/savedQueryRepository";
export default function CommandPalette() {
const [open, setOpen] = useState(false);
const { theme, setTheme } = useTheme();
const tabs = useDuckStore((s) => s.tabs);
const activeTabId = useDuckStore((s) => s.activeTabId);
const createTab = useDuckStore((s) => s.createTab);
const setActiveTab = useDuckStore((s) => s.setActiveTab);
const toggleBrainPanel = useDuckStore((s) => s.toggleBrainPanel);
const databases = useDuckStore((s) => s.databases);
const connectionList = useDuckStore((s) => s.connectionList);
const currentConnection = useDuckStore((s) => s.currentConnection);
const setCurrentConnection = useDuckStore((s) => s.setCurrentConnection);
const currentProfileId = useDuckStore((s) => s.currentProfileId);
const savedQueriesVersion = useDuckStore((s) => s.savedQueriesVersion);
const [savedQueries, setSavedQueries] = useState<SavedQuery[]>([]);
// Load saved queries when palette opens
useEffect(() => {
if (open && currentProfileId) {
getSavedQueries(currentProfileId)
.then(setSavedQueries)
.catch(() => {});
}
}, [open, currentProfileId, savedQueriesVersion]);
// Keyboard shortcut
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((o) => !o);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
const openOrFocusTab = (type: EditorTabType, title: string) => {
const existing = tabs.find((t) => t.type === type);
if (existing) {
setActiveTab(existing.id);
} else {
createTab(type, "", title);
}
setOpen(false);
};
const openTabs = useMemo(() => tabs.filter((t) => t.id !== activeTabId), [tabs, activeTabId]);
const tableEntries = useMemo(() => {
const entries: { database: string; table: string }[] = [];
for (const db of databases) {
for (const table of db.tables) {
entries.push({ database: db.name, table: table.name });
}
}
return entries;
}, [databases]);
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Search commands, tabs, tables..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{/* Quick Actions */}
<CommandGroup heading="Quick Actions">
<CommandItem
onSelect={() => {
createTab("sql", "");
setOpen(false);
}}
>
<Terminal className="mr-2 h-4 w-4" />
New SQL Tab
</CommandItem>
<CommandItem onSelect={() => openOrFocusTab("home", "Home")}>
<Home className="mr-2 h-4 w-4" />
Home
</CommandItem>
<CommandItem onSelect={() => openOrFocusTab("connections", "Connections")}>
<Cable className="mr-2 h-4 w-4" />
Connections
</CommandItem>
<CommandItem onSelect={() => openOrFocusTab("brain", "Duck Brain")}>
<Brain className="mr-2 h-4 w-4" />
Duck Brain
</CommandItem>
<CommandItem onSelect={() => openOrFocusTab("settings", "Settings")}>
<Settings className="mr-2 h-4 w-4" />
Settings
</CommandItem>
<CommandItem
onSelect={() => {
toggleBrainPanel();
setOpen(false);
}}
>
<Brain className="mr-2 h-4 w-4" />
Toggle AI Panel
</CommandItem>
<CommandItem
onSelect={() => {
setTheme(theme === "dark" ? "light" : "dark");
setOpen(false);
}}
>
{theme === "dark" ? (
<Sun className="mr-2 h-4 w-4" />
) : (
<Moon className="mr-2 h-4 w-4" />
)}
{theme === "dark" ? "Light Theme" : "Dark Theme"}
</CommandItem>
</CommandGroup>
{/* Open Tabs */}
{openTabs.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading="Open Tabs">
{openTabs.map((tab) => (
<CommandItem
key={tab.id}
onSelect={() => {
setActiveTab(tab.id);
setOpen(false);
}}
>
<Terminal className="mr-2 h-4 w-4" />
{tab.title || tab.type}
<CommandShortcut>{tab.type}</CommandShortcut>
</CommandItem>
))}
</CommandGroup>
</>
)}
{/* Connections */}
{connectionList.connections.length > 1 && (
<>
<CommandSeparator />
<CommandGroup heading="Connections">
{connectionList.connections
.filter((c) => c.id !== currentConnection?.id)
.map((conn) => (
<CommandItem
key={conn.id}
onSelect={async () => {
await setCurrentConnection(conn.id);
setOpen(false);
}}
>
<Database className="mr-2 h-4 w-4" />
Switch to {conn.name}
<CommandShortcut>{conn.scope}</CommandShortcut>
</CommandItem>
))}
</CommandGroup>
</>
)}
{/* Saved Queries */}
{savedQueries.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading="Saved Queries">
{savedQueries.map((query) => (
<CommandItem
key={query.id}
onSelect={() => {
createTab("sql", query.sql_text, query.name);
setOpen(false);
}}
>
<Bookmark className="mr-2 h-4 w-4" />
{query.name}
</CommandItem>
))}
</CommandGroup>
</>
)}
{/* Tables */}
{tableEntries.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading="Tables">
{tableEntries.map(({ database, table }) => (
<CommandItem
key={`${database}.${table}`}
onSelect={() => {
const query = `SELECT * FROM "${database}"."${table}" LIMIT 100`;
createTab("sql", query, table);
setOpen(false);
}}
>
<Table className="mr-2 h-4 w-4" />
{database}.{table}
<CommandShortcut>SELECT</CommandShortcut>
</CommandItem>
))}
</CommandGroup>
</>
)}
</CommandList>
</CommandDialog>
);
}
================================================
FILE: src/components/common/FloatingActionButton.tsx
================================================
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
interface FloatingActionButtonProps {
onClick: () => void;
icon: LucideIcon;
label: string;
disabled?: boolean;
className?: string;
variant?: "default" | "outline" | "secondary";
}
export const FloatingActionButton: React.FC<FloatingActionButtonProps> = ({
onClick,
icon: Icon,
label,
disabled = false,
className,
variant = "default",
}) => {
return (
<Button
onClick={onClick}
disabled={disabled}
variant={variant}
size="lg"
className={cn(
// Base styles
"fixed bottom-6 right-6 z-50",
"h-14 px-6 rounded-full shadow-lg",
"flex items-center gap-2",
// Mobile-first (show by default)
"md:hidden",
// Animation
"transition-all duration-200",
"hover:scale-105 active:scale-95",
// Shadow
"shadow-[0_8px_16px_rgba(0,0,0,0.15)]",
"dark:shadow-[0_8px_16px_rgba(0,0,0,0.3)]",
className
)}
aria-label={label}
>
<Icon className="h-5 w-5" />
<span className="font-medium">{label}</span>
</Button>
);
};
export default FloatingActionButton;
================================================
FILE: src/components/common/ImportOptionsPopover.tsx
================================================
import React, { useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Import, Table, Link2 } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ImportOptions {
tableName: string;
importMode: "table" | "view";
}
interface ImportOptionsPopoverProps {
fileName: string;
onImport: (options: ImportOptions) => void;
children: React.ReactNode;
disabled?: boolean;
}
/**
* Generate a valid table name from a filename
*/
function generateTableName(fileName: string): string {
return fileName
.replace(/\.[^.]+$/, "") // Remove extension
.replace(/[^a-zA-Z0-9_]/g, "_") // Replace special chars with underscore
.replace(/^[0-9]/, "_$&") // Ensure doesn't start with number
.replace(/_+/g, "_") // Collapse multiple underscores
.replace(/^_|_$/g, ""); // Trim leading/trailing underscores
}
const ImportOptionsPopover: React.FC<ImportOptionsPopoverProps> = ({
fileName,
onImport,
children,
disabled = false,
}) => {
const [open, setOpen] = useState(false);
const [tableName, setTableName] = useState(generateTableName(fileName));
const [importMode, setImportMode] = useState<"table" | "view">("table");
const handleImport = () => {
if (!tableName.trim()) return;
onImport({ tableName: tableName.trim(), importMode });
setOpen(false);
};
const handleOpenChange = (isOpen: boolean) => {
if (isOpen) {
// Reset to defaults when opening
setTableName(generateTableName(fileName));
setImportMode("table");
}
setOpen(isOpen);
};
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild disabled={disabled}>
{children}
</PopoverTrigger>
<PopoverContent className="w-72" align="start" side="right">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium text-sm">Import Options</h4>
<p className="text-xs text-muted-foreground truncate" title={fileName}>
{fileName}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="tableName" className="text-xs">
Name
</Label>
<Input
id="tableName"
value={tableName}
onChange={(e) => setTableName(e.target.value)}
placeholder="table_name"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">Mode</Label>
<div className="flex gap-1">
<Button
type="button"
variant={importMode === "table" ? "default" : "outline"}
size="sm"
className={cn("flex-1 gap-1.5 text-xs", importMode === "table" && "bg-primary")}
onClick={() => setImportMode("table")}
>
<Table className="h-3.5 w-3.5" />
Table
</Button>
<Button
type="button"
variant={importMode === "view" ? "default" : "outline"}
size="sm"
className={cn("flex-1 gap-1.5 text-xs", importMode === "view" && "bg-primary")}
onClick={() => setImportMode("view")}
>
<Link2 className="h-3.5 w-3.5" />
View
</Button>
</div>
<p className="text-[10px] text-muted-foreground">
{importMode === "table"
? "Copies data into DuckDB (faster queries)"
: "Links to file (fresh data, less memory)"}
</p>
</div>
<Button
onClick={handleImport}
disabled={!tableName.trim()}
size="sm"
className="w-full gap-1.5"
>
<Import className="h-3.5 w-3.5" />
{importMode === "table" ? "Import" : "Link"}
</Button>
</div>
</PopoverContent>
</Popover>
);
};
export default ImportOptionsPopover;
================================================
FILE: src/components/connection/ConnectionsModal.tsx
================================================
// ConnectionManager.tsx
import React from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetFooter,
} from "@/components/ui/sheet";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { useDuckStore } from "@/store";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Info } from "lucide-react";
const scopeEnum = z.enum(["External", "OPFS"]);
const nameSchema = z
.string()
.min(2, {
message: "Connection name must be at least 2 characters.",
})
.max(30, {
message: "Connection name must not exceed 30 characters.",
});
const opfsSchema = z.object({
name: nameSchema,
scope: z.literal(scopeEnum.enum.OPFS),
path: z.string().min(1, {
message: "Path is required.",
}),
});
const externalSchema = z.object({
name: nameSchema,
scope: z.literal(scopeEnum.enum.External),
host: z.string().url({
message: "Host must be a valid URL.",
}),
port: z
.string()
.refine((val) => !isNaN(parseInt(val, 10)) || val === "", {
message: "Port must be a number.",
})
.optional(),
database: z.string().optional(),
user: z.string().optional(),
password: z.string().optional(),
authMode: z.enum(["none", "password", "api_key"]).optional(),
apiKey: z.string().optional(),
});
const connectionSchema = z.discriminatedUnion("scope", [opfsSchema, externalSchema]);
type ConnectionFormValues = z.infer<typeof connectionSchema>;
interface ConnectionManagerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (values: ConnectionFormValues) => Promise<void>;
initialValues?: ConnectionFormValues;
isEditMode?: boolean;
}
const ConnectionManager: React.FC<ConnectionManagerProps> = ({
open,
onOpenChange,
onSubmit,
initialValues,
isEditMode = false,
}) => {
const form = useForm<ConnectionFormValues>({
resolver: zodResolver(connectionSchema),
defaultValues: initialValues || {
name: "Local DuckDB",
scope: "External" as const,
host: "http://localhost:9999",
port: "",
database: "",
user: "",
password: "",
authMode: "none" as const,
apiKey: "",
},
mode: "onChange",
});
const currentScope = form.watch("scope");
const isLoadingExternalConnection = useDuckStore((s) => s.isLoadingExternalConnection);
const handleSubmit = async (values: ConnectionFormValues) => {
await onSubmit(values);
form.reset();
onOpenChange(false);
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-full sm:w-[450px] overflow-y-auto">
<SheetHeader>
<SheetTitle>{isEditMode ? "Edit Connection" : "Add New Connection"}</SheetTitle>
<SheetDescription>
{isEditMode
? "Modify existing connection details."
: "Connect to a DuckDB instance or browser storage."}
</SheetDescription>
</SheetHeader>
<div className="mt-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Connection Name</FormLabel>
<FormControl>
<Input placeholder="My Database" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="scope"
render={({ field }) => (
<FormItem>
<FormLabel>Connection Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="External">DuckDB HTTP Server</SelectItem>
<SelectItem value="OPFS">Browser Storage (OPFS)</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* External Connection Fields */}
{currentScope === "External" && (
<>
<Alert className="bg-muted/50">
<Info className="h-4 w-4" />
<AlertDescription className="text-xs space-y-1">
<p>Start HTTP server in DuckDB:</p>
<pre className="bg-background px-2 py-1 rounded text-[10px] leading-relaxed">
{`INSTALL httpserver FROM community;
LOAD httpserver;
SELECT httpserve_start('0.0.0.0', 9999, '');`}
</pre>
</AlertDescription>
</Alert>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host URL</FormLabel>
<FormControl>
<Input
placeholder="http://localhost:9999"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormDescription>Full URL including protocol (http/https)</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="database"
render={({ field }) => (
<FormItem>
<FormLabel>Database (optional)</FormLabel>
<FormControl>
<Input placeholder="my_database" {...field} value={field.value ?? ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="authMode"
render={({ field }) => (
<FormItem>
<FormLabel>Authentication</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select auth mode" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="password">Username/Password</SelectItem>
<SelectItem value="api_key">API Key</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{form.watch("authMode") === "password" && (
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="user"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="user" {...field} value={field.value ?? ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="********"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{form.watch("authMode") === "api_key" && (
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter API key"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
{/* OPFS Fields */}
{currentScope === "OPFS" && (
<>
<Alert className="bg-muted/50">
<Info className="h-4 w-4" />
<AlertDescription className="text-xs">
Data persists in your browser across sessions.
</AlertDescription>
</Alert>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Database File</FormLabel>
<FormControl>
<Input placeholder="my_data.db" {...field} value={field.value ?? ""} />
</FormControl>
<FormDescription>
Filename for your database (e.g., data.db)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<SheetFooter className="pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoadingExternalConnection}
>
Cancel
</Button>
<Button type="submit" disabled={isLoadingExternalConnection}>
{isLoadingExternalConnection
? "Connecting..."
: isEditMode
? "Update"
: "Connect"}
</Button>
</SheetFooter>
</form>
</Form>
</div>
</SheetContent>
</Sheet>
);
};
export default ConnectionManager;
================================================
FILE: src/components/duck-brain/DuckBrainCodeBlock.tsx
================================================
import React from "react";
import { Copy, Play, FileInput, Check, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import type { QueryResultArtifact } from "@/store";
import ResultsArtifact from "./ResultsArtifact";
interface DuckBrainCodeBlockProps {
sql: string;
messageId: string;
queryResult?: QueryResultArtifact;
onExecute?: (messageId: string, sql: string) => void;
onInsert?: (sql: string) => void;
className?: string;
}
const DuckBrainCodeBlock: React.FC<DuckBrainCodeBlockProps> = ({
sql,
messageId,
queryResult,
onExecute,
onInsert,
className,
}) => {
const [copied, setCopied] = React.useState(false);
const isRunning = queryResult?.status === "running";
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(sql);
setCopied(true);
toast.success("SQL copied to clipboard");
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error("Failed to copy");
}
};
const handleExecute = () => {
if (onExecute && !isRunning) {
onExecute(messageId, sql);
}
};
const handleRetry = () => {
handleExecute();
};
return (
<div className={cn("space-y-2", className)}>
{/* SQL Code Block */}
<div className="rounded-lg overflow-hidden border bg-muted/30">
{/* SQL Code */}
<pre className="p-3 text-xs overflow-x-auto">
<code className="text-foreground font-mono whitespace-pre-wrap break-all">{sql}</code>
</pre>
{/* Actions */}
<div className="flex items-center gap-1 p-2 border-t bg-muted/50">
<Button variant="ghost" size="sm" onClick={handleCopy} className="h-7 text-xs gap-1">
{copied ? (
<>
<Check className="h-3 w-3" />
Copied
</>
) : (
<>
<Copy className="h-3 w-3" />
Copy
</>
)}
</Button>
{onInsert && (
<Button
variant="ghost"
size="sm"
onClick={() => onInsert(sql)}
className="h-7 text-xs gap-1"
>
<FileInput className="h-3 w-3" />
Insert
</Button>
)}
{onExecute && (
<Button
variant="ghost"
size="sm"
onClick={handleExecute}
disabled={isRunning}
className="h-7 text-xs gap-1 text-primary"
>
{isRunning ? (
<>
<Loader2 className="h-3 w-3 animate-spin" />
Running...
</>
) : (
<>
<Play className="h-3 w-3" />
Run
</>
)}
</Button>
)}
</div>
</div>
{/* Results Artifact */}
{queryResult && queryResult.status !== "pending" && (
<ResultsArtifact queryResult={queryResult} onRetry={handleRetry} />
)}
</div>
);
};
export default DuckBrainCodeBlock;
================================================
FILE: src/components/duck-brain/DuckBrainInput.tsx
================================================
import React, { useState, useRef, useEffect, useCallback } from "react";
import { Send, Square, Loader2, Table2, Columns } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import type { DatabaseInfo } from "@/store";
import SchemaAutocomplete, {
buildSchemaSuggestions,
type SchemaSuggestion,
} from "./SchemaAutocomplete";
// Render text with styled @ mentions
const renderWithMentions = (text: string) => {
const mentionRegex = /@([\w.]+)/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
let match;
while ((match = mentionRegex.exec(text)) !== null) {
// Add text before the mention
if (match.index > lastIndex) {
parts.push(
<span key={`text-${lastIndex}`} className="whitespace-pre-wrap">
{text.slice(lastIndex, match.index)}
</span>
);
}
// Add the mention as a styled pill
const mention = match[1];
const isColumn = mention.includes(".");
parts.push(
<span
key={`mention-${match.index}`}
className="inline-flex items-center gap-0.5 px-1 py-px rounded bg-primary/20 text-primary text-xs font-medium align-baseline"
>
{isColumn ? <Columns className="h-3 w-3" /> : <Table2 className="h-3 w-3" />}
{mention}
</span>
);
lastIndex = match.index + match[0].length;
}
// Add remaining text
if (lastIndex < text.length) {
parts.push(
<span key={`text-${lastIndex}`} className="whitespace-pre-wrap">
{text.slice(lastIndex)}
</span>
);
}
return parts.length > 0 ? parts : text;
};
interface DuckBrainInputProps {
onSend: (message: string) => void;
onAbort?: () => void;
isGenerating: boolean;
disabled?: boolean;
placeholder?: string;
databases?: DatabaseInfo[];
className?: string;
}
const DuckBrainInput: React.FC<DuckBrainInputProps> = ({
onSend,
onAbort,
isGenerating,
disabled = false,
placeholder = "Ask Duck Brain to write SQL... (@ for tables)",
databases = [],
className,
}) => {
const [input, setInput] = useState("");
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const [suggestions, setSuggestions] = useState<SchemaSuggestion[]>([]);
const [activeIndex, setActiveIndex] = useState(0);
const [mentionStart, setMentionStart] = useState<number | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLFormElement>(null);
// Auto-resize textarea
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
}
}, [input]);
// Detect @ mentions and filter suggestions
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const cursorPos = e.target.selectionStart || 0;
setInput(value);
// Find the @ before cursor
const textBeforeCursor = value.slice(0, cursorPos);
const lastAtIndex = textBeforeCursor.lastIndexOf("@");
if (lastAtIndex !== -1) {
// Check if there's a space between @ and cursor (mention ended)
const textAfterAt = textBeforeCursor.slice(lastAtIndex + 1);
const hasSpace = /\s/.test(textAfterAt);
if (!hasSpace) {
// Active mention - show suggestions
setMentionStart(lastAtIndex);
const filter = textAfterAt;
const filtered = buildSchemaSuggestions(databases, filter);
setSuggestions(filtered);
setIsAutocompleteOpen(filtered.length > 0);
setActiveIndex(0);
return;
}
}
// No active mention
setIsAutocompleteOpen(false);
setMentionStart(null);
},
[databases]
);
// Insert selected suggestion
const insertSuggestion = useCallback(
(suggestion: SchemaSuggestion) => {
if (mentionStart === null) return;
const before = input.slice(0, mentionStart);
const cursorPos = textareaRef.current?.selectionStart || input.length;
const after = input.slice(cursorPos);
// Insert with @ prefix so it renders as a pill
const insertText = `@${suggestion.fullPath}`;
const newInput = `${before}${insertText} ${after}`;
setInput(newInput);
setIsAutocompleteOpen(false);
setMentionStart(null);
// Focus and set cursor position
setTimeout(() => {
const newCursorPos = before.length + insertText.length + 1;
textareaRef.current?.focus();
textareaRef.current?.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
},
[input, mentionStart]
);
const handleKeyDown = (e: React.KeyboardEvent) => {
// Handle autocomplete navigation
if (isAutocompleteOpen && suggestions.length > 0) {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setActiveIndex((prev) => (prev + 1) % suggestions.length);
return;
case "ArrowUp":
e.preventDefault();
setActiveIndex((prev) => (prev === 0 ? suggestions.length - 1 : prev - 1));
return;
case "Tab":
case "Enter":
if (suggestions[activeIndex]) {
e.preventDefault();
insertSuggestion(suggestions[activeIndex]);
return;
}
break;
case "Escape":
e.preventDefault();
setIsAutocompleteOpen(false);
return;
}
}
// Regular Enter to send (if not in autocomplete)
if (e.key === "Enter" && !e.shiftKey && !isAutocompleteOpen) {
e.preventDefault();
handleSubmit();
}
};
const handleSubmit = (e?: React.FormEvent) => {
e?.preventDefault();
if (!input.trim() || isGenerating || disabled) return;
onSend(input.trim());
setInput("");
setIsAutocompleteOpen(false);
};
// Close autocomplete when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsAutocompleteOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Check if input has any @ mentions to show overlay
const hasMentions = /@[\w.]+/.test(input);
return (
<form ref={containerRef} onSubmit={handleSubmit} className={cn("relative", className)}>
{/* Visual overlay for styled mentions */}
{hasMentions && (
<div
className="absolute inset-0 pointer-events-none px-3 py-[9px] text-sm overflow-hidden"
aria-hidden="true"
>
<div className="whitespace-pre-wrap break-words leading-normal">
{renderWithMentions(input)}
</div>
</div>
)}
<Textarea
ref={textareaRef}
value={input}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled || isGenerating}
className={cn(
"min-h-[44px] max-h-[120px] resize-none pr-12",
"text-sm placeholder:text-muted-foreground/60",
// Make text transparent when we have mentions to show overlay
hasMentions && "text-transparent caret-foreground"
)}
rows={1}
/>
{/* Schema Autocomplete Popover */}
<SchemaAutocomplete
isOpen={isAutocompleteOpen}
suggestions={suggestions}
activeIndex={activeIndex}
onSelect={insertSuggestion}
position={{ top: 50, left: 0 }}
/>
<div className="absolute right-2 bottom-2">
{isGenerating ? (
<Button
type="button"
variant="ghost"
size="icon"
onClick={onAbort}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<Square className="h-4 w-4" />
</Button>
) : (
<Button
type="submit"
variant="ghost"
size="icon"
disabled={!input.trim() || disabled}
className="h-8 w-8"
>
{disabled ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
</Button>
)}
</div>
</form>
);
};
export default DuckBrainInput;
================================================
FILE: src/components/duck-brain/DuckBrainMessages.tsx
================================================
import React, { useRef, useEffect } from "react";
import { User, Bot, Table2, Columns } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { DuckBrainMessage } from "@/store";
import DuckBrainCodeBlock from "./DuckBrainCodeBlock";
import MarkdownContent from "./MarkdownContent";
// Render @ mentions as styled pills
const renderMentions = (content: string) => {
// Match @table_name or @table.column patterns
const mentionRegex = /@([\w.]+)/g;
const parts: (string | React.ReactNode)[] = [];
let lastIndex = 0;
let match;
while ((match = mentionRegex.exec(content)) !== null) {
// Add text before the mention
if (match.index > lastIndex) {
parts.push(content.slice(lastIndex, match.index));
}
// Add the mention as a pill
const mention = match[1];
const isColumn = mention.includes(".");
parts.push(
<span
key={`${match.index}-${mention}`}
className="inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 rounded-md bg-primary/20 text-primary-foreground text-xs font-medium"
>
{isColumn ? <Columns className="h-3 w-3" /> : <Table2 className="h-3 w-3" />}
{mention}
</span>
);
lastIndex = match.index + match[0].length;
}
// Add remaining text
if (lastIndex < content.length) {
parts.push(content.slice(lastIndex));
}
return parts.length > 0 ? parts : content;
};
interface DuckBrainMessagesProps {
messages: DuckBrainMessage[];
streamingContent: string;
isGenerating: boolean;
onExecuteSQL?: (messageId: string, sql: string) => void;
onInsertSQL?: (sql: string) => void;
className?: string;
}
const DuckBrainMessages: React.FC<DuckBrainMessagesProps> = ({
messages,
streamingContent,
isGenerating,
onExecuteSQL,
onInsertSQL,
className,
}) => {
const scrollRef = useRef<HTMLDivElement>(null);
const bottomRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom on new messages
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, streamingContent]);
const formatTime = (date: Date) => {
return new Date(date).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
};
if (messages.length === 0 && !isGenerating) {
return (
<div className={cn("flex-1 flex items-center justify-center p-4", className)}>
<div className="text-center text-muted-foreground">
<Bot className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p className="text-sm font-medium">Hi! I'm Duck Brain</p>
<p className="text-xs mt-1">Ask me to write SQL queries for your data</p>
</div>
</div>
);
}
return (
<ScrollArea className={cn("flex-1", className)} ref={scrollRef}>
<div className="p-4 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={cn("flex gap-3", message.role === "user" ? "justify-end" : "justify-start")}
>
{message.role === "assistant" && (
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="h-4 w-4 text-primary" />
</div>
)}
<div
className={cn(
"max-w-[85%] space-y-2",
message.role === "user" ? "items-end" : "items-start"
)}
>
<div
className={cn(
"rounded-2xl px-4 py-2",
message.role === "user"
? "bg-primary text-primary-foreground rounded-br-sm"
: "bg-muted rounded-bl-sm"
)}
>
{message.role === "user" ? (
<p className="text-sm whitespace-pre-wrap">{renderMentions(message.content)}</p>
) : (
<MarkdownContent content={message.content} skipCodeBlocks={!!message.sql} />
)}
</div>
{/* SQL Code Block for assistant messages with extracted SQL */}
{message.role === "assistant" && message.sql && (
<DuckBrainCodeBlock
sql={message.sql}
messageId={message.id}
queryResult={message.queryResult}
onExecute={onExecuteSQL}
onInsert={onInsertSQL}
/>
)}
<span className="text-[10px] text-muted-foreground px-1">
{formatTime(message.timestamp)}
</span>
</div>
{message.role === "user" && (
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-muted flex items-center justify-center">
<User className="h-4 w-4" />
</div>
)}
</div>
))}
{/* Streaming response */}
{isGenerating && streamingContent && (
<div className="flex gap-3 justify-start">
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="h-4 w-4 text-primary animate-pulse" />
</div>
<div className="max-w-[85%]">
<div className="rounded-2xl rounded-bl-sm bg-muted px-4 py-2">
<p className="text-sm whitespace-pre-wrap">
{streamingContent}
<span className="inline-block w-1 h-4 bg-primary ml-1 animate-pulse" />
</p>
</div>
</div>
</div>
)}
{/* Loading indicator */}
{isGenerating && !streamingContent && (
<div className="flex gap-3 justify-start">
<div className="flex-shrink-0 w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="h-4 w-4 text-primary" />
</div>
<div className="rounded-2xl rounded-bl-sm bg-muted px-4 py-2">
<div className="flex gap-1">
<span className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce" />
<span className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce [animation-delay:150ms]" />
<span className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce [animation-delay:300ms]" />
</div>
</div>
</div>
)}
<div ref={bottomRef} />
</div>
</ScrollArea>
);
};
export default DuckBrainMessages;
================================================
FILE: src/components/duck-brain/DuckBrainPanel.tsx
================================================
import React, { useCallback, useMemo } from "react";
import { Brain, X, Loader2, AlertCircle, Download, Trash2, RefreshCw, Cloud } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useDuckStore, type AIProviderType } from "@/store";
import { AVAILABLE_MODELS, DEFAULT_MODEL } from "@/lib/duckBrain";
import { OPENAI_MODELS, ANTHROPIC_MODELS } from "@/lib/duckBrain/providers/types";
import DuckBrainMessages from "./DuckBrainMessages";
import DuckBrainInput from "./DuckBrainInput";
import { toast } from "sonner";
interface DuckBrainPanelProps {
tabId: string;
}
const DuckBrainPanel: React.FC<DuckBrainPanelProps> = React.memo(({ tabId }) => {
const duckBrain = useDuckStore((s) => s.duckBrain);
const databases = useDuckStore((s) => s.databases);
const toggleBrainPanel = useDuckStore((s) => s.toggleBrainPanel);
const initializeDuckBrain = useDuckStore((s) => s.initializeDuckBrain);
const generateSQL = useDuckStore((s) => s.generateSQL);
const abortGeneration = useDuckStore((s) => s.abortGeneration);
const clearBrainMessages = useDuckStore((s) => s.clearBrainMessages);
const executeQueryInChat = useDuckStore((s) => s.executeQueryInChat);
const updateTabQuery = useDuckStore((s) => s.updateTabQuery);
const setAIProvider = useDuckStore((s) => s.setAIProvider);
const {
modelStatus,
downloadProgress,
downloadStatus,
isWebGPUSupported,
error,
messages,
isGenerating,
streamingContent,
aiProvider = "webllm",
providerConfigs = {},
} = duckBrain;
// Get the display name for the current provider/model
const providerDisplayInfo = useMemo(() => {
if (aiProvider === "openai") {
const config = providerConfigs.openai;
if (config?.apiKey) {
const model = OPENAI_MODELS.find((m) => m.id === config.modelId);
return { name: model?.name || "GPT-4o Mini", isCloud: true };
}
} else if (aiProvider === "anthropic") {
const config = providerConfigs.anthropic;
if (config?.apiKey) {
const model = ANTHROPIC_MODELS.find((m) => m.id === config.modelId);
return { name: model?.name || "Claude Sonnet 4", isCloud: true };
}
} else if (aiProvider === "openai-compatible") {
const config = providerConfigs["openai-compatible"];
if (config?.baseUrl && config?.modelId) {
return { name: config.modelId, isCloud: true };
}
}
// Default to local model
const localModel = AVAILABLE_MODELS.find((m) => m.id === duckBrain.currentModel);
return { name: localModel?.displayName || "Local Model", isCloud: false };
}, [aiProvider, providerConfigs, duckBrain.currentModel]);
// Build list of available providers for selector
const availableProviders = useMemo(() => {
const providers: { value: AIProviderType; label: string }[] = [];
// Add WebLLM if model is loaded or loading
if (modelStatus === "ready" || modelStatus === "downloading" || modelStatus === "loading") {
const localModel = AVAILABLE_MODELS.find((m) => m.id === duckBrain.currentModel);
providers.push({
value: "webllm",
label: localModel?.displayName || "Local Model",
});
}
// Add OpenAI if configured
if (providerConfigs.openai?.apiKey) {
const model = OPENAI_MODELS.find((m) => m.id === providerConfigs.openai?.modelId);
providers.push({
value: "openai",
label: model?.name || "GPT-4o Mini",
});
}
// Add Anthropic if configured
if (providerConfigs.anthropic?.apiKey) {
const model = ANTHROPIC_MODELS.find((m) => m.id === providerConfigs.anthropic?.modelId);
providers.push({
value: "anthropic",
label: model?.name || "Claude Sonnet 4",
});
}
// Add OpenAI-Compatible if configured
if (
providerConfigs["openai-compatible"]?.baseUrl &&
providerConfigs["openai-compatible"]?.modelId
) {
providers.push({
value: "openai-compatible",
label: providerConfigs["openai-compatible"].modelId,
});
}
return providers;
}, [modelStatus, duckBrain.currentModel, providerConfigs]);
const handleSend = useCallback(
async (message: string) => {
await generateSQL(message);
},
[generateSQL]
);
const handleExecuteSQL = useCallback(
async (messageId: string, sql: string) => {
try {
const result = await executeQueryInChat(messageId, sql);
if (result) {
toast.success(`Query returned ${result.rowCount} rows`);
}
} catch {
// Error is already handled in the store and shown in ResultsArtifact
}
},
[executeQueryInChat]
);
const handleInsertSQL = useCallback(
(sql: string) => {
updateTabQuery(tabId, sql);
toast.success("SQL inserted into editor");
},
[updateTabQuery, tabId]
);
const handleInitialize = useCallback(async () => {
await initializeDuckBrain(DEFAULT_MODEL.id);
}, [initializeDuckBrain]);
// Render WebGPU not supported state
if (isWebGPUSupported === false) {
return (
<div className="flex flex-col h-full border-l bg-background">
<Header onClose={toggleBrainPanel} />
<div className="flex-1 flex items-center justify-center p-4">
<Alert variant="destructive" className="max-w-sm">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="mt-2">
<p className="font-medium">WebGPU Not Supported</p>
<p className="text-xs mt-1">
Duck Brain requires WebGPU for local AI processing. Please use Chrome 113+ or Edge
113+.
</p>
</AlertDescription>
</Alert>
</div>
</div>
);
}
// Check if external provider is configured
const hasExternalProvider =
(aiProvider === "openai" && providerConfigs.openai?.apiKey) ||
(aiProvider === "anthropic" && providerConfigs.anthropic?.apiKey) ||
(aiProvider === "openai-compatible" &&
providerConfigs["openai-compatible"]?.baseUrl &&
providerConfigs["openai-compatible"]?.modelId);
// Render idle state (not initialized) - only for WebLLM without external provider
if ((modelStatus === "idle" || modelStatus === "checking") && !hasExternalProvider) {
return (
<div className="flex flex-col h-full border-l bg-background">
<Header onClose={toggleBrainPanel} />
<div className="flex-1 flex items-center justify-center p-4">
<div className="text-center max-w-sm">
<Brain className="h-12 w-12 mx-auto mb-4 text-primary" />
<h3 className="font-semibold mb-2">Initialize Duck Brain</h3>
<p className="text-sm text-muted-foreground mb-4">
Download an AI model to enable natural language to SQL conversion. This runs 100%
locally in your browser.
</p>
<div className="space-y-2 text-xs text-muted-foreground mb-4">
<p>
<strong>Model:</strong> {DEFAULT_MODEL.displayName}
</p>
<p>
<strong>Size:</strong> {DEFAULT_MODEL.size}
</p>
<p>First load downloads the model. Future loads use cache.</p>
</div>
<Button onClick={handleInitialize} className="gap-2">
<Download className="h-4 w-4" />
Load AI Model
</Button>
</div>
</div>
</div>
);
}
// Render downloading/loading state
if (modelStatus === "downloading" || modelStatus === "loading") {
return (
<div className="flex flex-col h-full border-l bg-background">
<Header onClose={toggleBrainPanel} />
<div className="flex-1 flex items-center justify-center p-4">
<div className="text-center max-w-sm w-full">
<Loader2 className="h-8 w-8 mx-auto mb-4 animate-spin text-primary" />
<h3 className="font-semibold mb-2">
{modelStatus === "downloading" ? "Downloading Model..." : "Loading Model..."}
</h3>
<Progress value={downloadProgress} className="mb-2" />
<p className="text-xs text-muted-foreground">
{downloadStatus || `${downloadProgress}%`}
</p>
</div>
</div>
</div>
);
}
// Render error state
if (modelStatus === "error") {
return (
<div className="flex flex-col h-full border-l bg-background">
<Header onClose={toggleBrainPanel} />
<div className="flex-1 flex items-center justify-center p-4">
<div className="text-center max-w-sm">
<AlertCircle className="h-12 w-12 mx-auto mb-4 text-destructive" />
<h3 className="font-semibold mb-2">Failed to Load Model</h3>
<p className="text-sm text-muted-foreground mb-4">
{error || "An unknown error occurred"}
</p>
<Button onClick={handleInitialize} variant="outline" className="gap-2">
<RefreshCw className="h-4 w-4" />
Try Again
</Button>
</div>
</div>
</div>
);
}
// Render ready state with chat interface
return (
<div className="flex flex-col h-full border-l bg-background">
<Header
onClose={toggleBrainPanel}
onClear={clearBrainMessages}
showClear={messages.length > 0}
/>
{/* Status Badge & Provider Selector */}
<div className="px-3 py-2 border-b">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-green-500/10 text-green-600 text-xs">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 mr-1.5" />
Ready
</Badge>
{/* Provider selector - show when multiple providers available */}
{availableProviders.length > 1 ? (
<Select
value={aiProvider}
onValueChange={(value) => setAIProvider(value as AIProviderType)}
>
<SelectTrigger className="h-6 w-auto gap-1 px-2 text-xs border-0 bg-transparent">
<div className="flex items-center gap-1">
{providerDisplayInfo.isCloud && <Cloud className="h-3 w-3" />}
<SelectValue />
</div>
</SelectTrigger>
<SelectContent>
{availableProviders.map((p) => (
<SelectItem key={p.value} value={p.value} className="text-xs">
{p.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
{providerDisplayInfo.isCloud && <Cloud className="h-3 w-3" />}
<span>{providerDisplayInfo.name}</span>
</div>
)}
</div>
</div>
{/* Messages */}
<DuckBrainMessages
messages={messages}
streamingContent={streamingContent}
isGenerating={isGenerating}
onExecuteSQL={handleExecuteSQL}
onInsertSQL={handleInsertSQL}
className="flex-1"
/>
{/* Input */}
<div className="p-3 border-t">
<DuckBrainInput
onSend={handleSend}
onAbort={abortGeneration}
isGenerating={isGenerating}
disabled={modelStatus !== "ready" && !hasExternalProvider}
databases={databases}
placeholder="Ask Duck Brain... (@ for tables)"
/>
</div>
</div>
);
});
// Header component
interface HeaderProps {
onClose: () => void;
onClear?: () => void;
showClear?: boolean;
}
const Header: React.FC<HeaderProps> = ({ onClose, onClear, showClear }) => (
<div className="flex items-center justify-between px-3 py-2 border-b">
<div className="flex items-center gap-2">
<Brain className="h-5 w-5 text-primary" />
<span className="font-semibold text-sm">Duck Brain</span>
</div>
<div className="flex items-center gap-1">
{showClear && onClear && (
<Button variant="ghost" size="icon" onClick={onClear} className="h-7 w-7">
<Trash2 className="h-4 w-4" />
</Button>
)}
<Button variant="ghost" size="icon" onClick={onClose} className="h-7 w-7">
<X className="h-4 w-4" />
</Button>
</div>
</div>
);
export default DuckBrainPanel;
================================================
FILE: src/components/duck-brain/MarkdownContent.tsx
================================================
import React from "react";
import ReactMarkdown, { Components } from "react-markdown";
import { cn } from "@/lib/utils";
interface MarkdownContentProps {
content: string;
skipCodeBlocks?: boolean;
className?: string;
}
/**
* Renders markdown content with custom styling.
* When skipCodeBlocks is true, removes ```sql...``` blocks since they're handled separately.
*/
const MarkdownContent: React.FC<MarkdownContentProps> = ({
content,
skipCodeBlocks = false,
className,
}) => {
// Remove ALL code blocks if they're handled separately by DuckBrainCodeBlock
// Uses two passes to catch all variations:
// 1. Fenced blocks with language identifier and newline: ```sql\n...\n```
// 2. Fallback for any remaining code blocks: ```...```
const processedContent = skipCodeBlocks
? content
.replace(/```\w*\n[\s\S]*?```/g, "") // Code blocks with newline after lang
.replace(/```[\s\S]*?```/g, "") // Any remaining code blocks
.replace(/\n{3,}/g, "\n\n") // Collapse multiple blank lines
.trim()
: content;
// If nothing left after removing code blocks, don't render
if (!processedContent) {
return null;
}
const components: Components = {
// Inline code styling
code: ({ className: codeClassName, children, ...props }) => {
// Check if this is a code block (has language class) vs inline code
const isCodeBlock = codeClassName?.includes("language-");
if (isCodeBlock) {
return (
<pre className="bg-muted p-3 rounded-md overflow-x-auto my-2">
<code className={cn("text-sm font-mono", codeClassName)} {...props}>
{children}
</code>
</pre>
);
}
// Inline code
return (
<code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
{children}
</code>
);
},
// Paragraph styling
p: ({ children }) => <p className="mb-2 last:mb-0 leading-relaxed">{children}</p>,
// Strong/bold
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
// Emphasis/italic
em: ({ children }) => <em className="italic">{children}</em>,
// Unordered list
ul: ({ children }) => <ul className="list-disc pl-4 mb-2 space-y-1">{children}</ul>,
// Ordered list
ol: ({ children }) => <ol className="list-decimal pl-4 mb-2 space-y-1">{children}</ol>,
// List item
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
// Links
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline hover:no-underline"
>
{children}
</a>
),
// Blockquote
blockquote: ({ children }) => (
<blockquote className="border-l-2 border-muted-foreground/30 pl-3 italic my-2">
{children}
</blockquote>
),
// Headings (rarely used in chat but good to have)
h1: ({ children }) => <h1 className="text-lg font-bold mt-3 mb-2">{children}</h1>,
h2: ({ children }) => <h2 className="text-base font-bold mt-3 mb-2">{children}</h2>,
h3: ({ children }) => <h3 className="text-sm font-bold mt-2 mb-1">{children}</h3>,
// Horizontal rule
hr: () => <hr className="my-3 border-muted-foreground/20" />,
};
return (
<div className={cn("text-sm prose-sm max-w-none", className)}>
<ReactMarkdown components={components}>{processedContent}</ReactMarkdown>
</div>
);
};
export default MarkdownContent;
================================================
FILE: src/components/duck-brain/ResultsArtifact.tsx
================================================
import React, { useState } from "react";
import { Loader2, AlertCircle, ChevronDown, ChevronUp, Table2, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import type { QueryResultArtifact } from "@/store";
interface ResultsArtifactProps {
queryResult: QueryResultArtifact;
onRetry?: () => void;
className?: string;
}
const MAX_INLINE_ROWS = 5;
const MAX_INLINE_COLUMNS = 6;
const ResultsArtifact: React.FC<ResultsArtifactProps> = ({ queryResult, onRetry, className }) => {
const [isExpanded, setIsExpanded] = useState(false);
const { status, data, error } = queryResult;
// Running state
if (status === "running") {
return (
<div
className={cn(
"flex items-center gap-2 p-3 rounded-lg border border-border bg-muted/30",
className
)}
>
<Loader2 className="h-4 w-4 animate-spin text-primary" />
<span className="text-sm text-muted-foreground">Executing query...</span>
</div>
);
}
// Error state
if (status === "error") {
return (
<div
className={cn("p-3 rounded-lg border border-destructive/50 bg-destructive/5", className)}
>
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-destructive mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-destructive">Query failed</p>
<p className="text-xs text-muted-foreground mt-1 break-words">
{error || "Unknown error occurred"}
</p>
</div>
{onRetry && (
<Button variant="ghost" size="sm" onClick={onRetry} className="flex-shrink-0 h-7 px-2">
<RotateCcw className="h-3 w-3 mr-1" />
Retry
</Button>
)}
</div>
</div>
);
}
// Pending state - shouldn't normally show, but handle it
if (status === "pending" || !data) {
return null;
}
// Success state - show results
const { columns, columnTypes, data: rows, rowCount } = data;
const hasMoreRows = rowCount > MAX_INLINE_ROWS;
const hasMoreColumns = columns.length > MAX_INLINE_COLUMNS;
const displayRows = isExpanded ? rows.slice(0, 50) : rows.slice(0, MAX_INLINE_ROWS);
const displayColumns = columns.slice(0, MAX_INLINE_COLUMNS);
return (
<div className={cn("rounded-lg border border-border overflow-hidden bg-card", className)}>
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 bg-muted/50 border-b">
<div className="flex items-center gap-2">
<Table2 className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium">Results</span>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{rowCount.toLocaleString()} row{rowCount !== 1 ? "s" : ""}
</Badge>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b bg-muted/30">
{displayColumns.map((col, i) => (
<th
key={col}
className="px-3 py-1.5 text-left font-medium text-muted-foreground whitespace-nowrap"
>
<div className="flex flex-col">
<span>{col}</span>
<span className="text-[10px] font-normal opacity-60">{columnTypes[i]}</span>
</div>
</th>
))}
{hasMoreColumns && (
<th className="px-3 py-1.5 text-left font-medium text-muted-foreground">
<span className="text-[10px]">+{columns.length - MAX_INLINE_COLUMNS} more</span>
</th>
)}
</tr>
</thead>
<tbody>
{displayRows.map((row, rowIdx) => (
<tr key={rowIdx} className="border-b last:border-0 hover:bg-muted/20">
{displayColumns.map((col) => (
<td
key={col}
className="px-3 py-1.5 whitespace-nowrap max-w-[200px] truncate"
title={String(row[col] ?? "")}
>
{formatCellValue(row[col])}
</td>
))}
{hasMoreColumns && <td className="px-3 py-1.5 text-muted-foreground">...</td>}
</tr>
))}
</tbody>
</table>
</div>
{/* Expand/Collapse */}
{hasMoreRows && (
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<CollapsibleTrigger asChild>
<button className="w-full px-3 py-1.5 text-xs text-center text-muted-foreground hover:text-foreground hover:bg-muted/50 flex items-center justify-center gap-1 border-t">
{isExpanded ? (
<>
<ChevronUp className="h-3 w-3" />
Show less
</>
) : (
<>
<ChevronDown className="h-3 w-3" />
Show more ({Math.min(50, rowCount) - MAX_INLINE_ROWS} more rows)
</>
)}
</button>
</CollapsibleTrigger>
</Collapsible>
)}
</div>
);
};
// Helper to format cell values for display
function formatCellValue(value: unknown): string {
if (value === null || value === undefined) {
return "NULL";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "object") {
return JSON.stringify(value);
}
if (typeof value === "number") {
// Format numbers nicely
if (Number.isInteger(value)) {
return value.toLocaleString();
}
return value.toLocaleString(undefined, { maximumFractionDigits: 4 });
}
return String(value);
}
export default ResultsArtifact;
================================================
FILE: src/components/duck-brain/SchemaAutocomplete.tsx
================================================
import React, { useEffect, useRef } from "react";
import { Table2, Columns3 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { DatabaseInfo } from "@/store";
export interface SchemaSuggestion {
type: "table" | "column";
name: string;
fullPath: string;
tableName?: string;
columnType?: string;
rowCount?: number;
}
interface SchemaAutocompleteProps {
isOpen: boolean;
suggestions: SchemaSuggestion[];
activeIndex: number;
onSelect: (suggestion: SchemaSuggestion) => void;
position: { top: number; left: number };
className?: string;
}
/**
* Builds suggestions from database schema
*/
export function buildSchemaSuggestions(
databases: DatabaseInfo[],
filter: string = ""
): SchemaSuggestion[] {
const suggestions: SchemaSuggestion[] = [];
const lowerFilter = filter.toLowerCase();
// Check if filter contains a dot (table.column)
const dotIndex = filter.indexOf(".");
const tableFilter = dotIndex > 0 ? filter.slice(0, dotIndex).toLowerCase() : null;
const columnFilter = dotIndex > 0 ? filter.slice(dotIndex + 1).toLowerCase() : null;
for (const db of databases) {
for (const table of db.tables) {
const tableName = db.name === "memory" ? table.name : `${db.name}.${table.name}`;
// If filtering for columns of a specific table
if (tableFilter) {
if (table.name.toLowerCase() === tableFilter || tableName.toLowerCase() === tableFilter) {
// Show columns for this table
for (const col of table.columns) {
if (!columnFilter || col.name.toLowerCase().startsWith(columnFilter)) {
suggestions.push({
type: "column",
name: col.name,
fullPath: `${table.name}.${col.name}`,
tableName: table.name,
columnType: col.type,
});
}
}
}
} else {
// Show tables matching filter
if (!lowerFilter || table.name.toLowerCase().startsWith(lowerFilter)) {
suggestions.push({
type: "table",
name: table.name,
fullPath: tableName,
rowCount: table.rowCount,
});
}
}
}
}
// Sort: tables first, then columns, alphabetically
return suggestions
.sort((a, b) => {
if (a.type !== b.type) return a.type === "table" ? -1 : 1;
return a.name.localeCompare(b.name);
})
.slice(0, 10); // Limit to 10 suggestions
}
const SchemaAutocomplete: React.FC<SchemaAutocompleteProps> = ({
isOpen,
suggestions,
activeIndex,
onSelect,
position,
className,
}) => {
const listRef = useRef<HTMLDivElement>(null);
// Scroll active item into view
useEffect(() => {
if (listRef.current && activeIndex >= 0) {
const activeItem = listRef.current.children[activeIndex] as HTMLElement;
activeItem?.scrollIntoView({ block: "nearest" });
}
}, [activeIndex]);
if (!isOpen || suggestions.length === 0) {
return null;
}
return (
<div
className={cn(
"absolute z-50 w-64 max-h-48 overflow-auto",
"bg-popover border rounded-md shadow-lg",
className
)}
style={{ bottom: position.top, left: position.left }}
>
<div ref={listRef} className="py-1">
{suggestions.map((suggestion, index) => (
<button
key={`${suggestion.type}-${suggestion.fullPath}`}
type="button"
onClick={() => onSelect(suggestion)}
className={cn(
"w-full px-3 py-1.5 text-left text-sm flex items-center gap-2",
"hover:bg-accent",
index === activeIndex && "bg-accent"
)}
>
{suggestion.type === "table" ? (
<Table2 className="h-3.5 w-3.5 text-primary flex-shrink-0" />
) : (
<Columns3 className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<span className="font-medium truncate block">{suggestion.name}</span>
{suggestion.type === "table" && (
<span className="text-[10px] text-muted-foreground">
{suggestion.rowCount ? `${suggestion.rowCount.toLocaleString()} rows` : "table"}
</span>
)}
{suggestion.type === "column" && suggestion.columnType && (
<span className="text-[10px] text-muted-foreground">{suggestion.columnType}</span>
)}
</div>
</button>
))}
</div>
<div className="px-3 py-1.5 text-[10px] text-muted-foreground border-t bg-muted/30">
<kbd className="px-1 rounded bg-muted">↑↓</kbd> navigate
<span className="mx-1.5">·</span>
<kbd className="px-1 rounded bg-muted">Tab</kbd> select
<span className="mx-1.5">·</span>
<kbd className="px-1 rounded bg-muted">Esc</kbd> close
</div>
</div>
);
};
export default SchemaAutocomplete;
================================================
FILE: src/components/editor/SqlEditor.tsx
================================================
import React, { useRef, useEffect, useState, useCallback } from "react";
import {
Play,
Loader2,
Lightbulb,
Command,
Edit,
Share2,
Brain,
Bookmark,
ListTree,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useDuckStore } from "@/store";
import { useTheme } from "../theme/theme-provider";
import { cn } from "@/lib/utils";
import { createEditor, useMonacoConfig, type EditorInstance } from "./monacoConfig";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import FloatingActionButton from "@/components/common/FloatingActionButton";
import { copyQueryURL } from "@/hooks/useQueryFromURL";
import SaveQueryDialog from "@/components/saved-queries/SaveQueryDialog";
import { ExplainPlanViewer } from "@/components/workspace/ExplainPlanViewer";
interface SqlEditorProps {
tabId: string;
title: string;
className?: string;
}
const SqlEditor: React.FC<SqlEditorProps> = ({ tabId, title, className }) => {
const editorRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<EditorInstance | null>(null);
const { theme } = useTheme();
const tabs = useDuckStore((s) => s.tabs);
const executeQuery = useDuckStore((s) => s.executeQuery);
const isExecuting = useDuckStore((s) => !!s.executingTabs[tabId]);
const updateTabTitle = useDuckStore((s) => s.updateTabTitle);
const toggleBrainPanel = useDuckStore((s) => s.toggleBrainPanel);
const duckBrain = useDuckStore((s) => s.duckBrain);
const currentProfileId = useDuckStore((s) => s.currentProfileId);
const monacoConfig = useMonacoConfig(theme);
const currentTab = tabs.find((tab) => tab.id === tabId);
const currentContent =
currentTab?.type === "sql" && typeof currentTab.content === "string" ? currentTab.content : "";
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title);
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const [explainOpen, setExplainOpen] = useState(false);
const [explainText, setExplainText] = useState("");
// Stable callback for query execution
const stableExecuteCallback = useCallback(
async (query: string, queryTabId: string) => {
await executeQuery(query, queryTabId);
},
[executeQuery] // Add executeQuery as a dependency
);
// Editor initialization effect
useEffect(() => {
if (!editorRef.current) return;
// Initialize editor with stable configuration
editorInstanceRef.current = createEditor(
editorRef.current,
monacoConfig,
currentContent,
tabId,
stableExecuteCallback
);
// Cleanup function
return () => {
if (editorInstanceRef.current) {
editorInstanceRef.current.dispose();
editorInstanceRef.current = null;
}
};
}, [tabId, monacoConfig, stableExecuteCallback]); // Keep stableExecuteCallback
// Content sync effect
useEffect(() => {
const editor = editorInstanceRef.current?.editor;
if (editor && currentContent !== editor.getValue()) {
const position = editor.getPosition();
editor.setValue(currentContent);
if (position) {
editor.setPosition(position);
}
}
}, [currentContent]); // Only depend on currentContent
const handleExecuteQuery = async () => {
const editor = editorInstanceRef.current?.editor;
if (!editor || isExecuting) return;
const query = editor.getValue().trim();
if (!query) return;
try {
await executeQuery(query, tabId);
} catch (error) {
console.error("Query execution failed:", error);
toast.error("Query execution failed");
}
};
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCurrentTitle(e.target.value);
};
const handleTitleSubmit = () => {
if (currentTitle.trim()) {
updateTabTitle(tabId, currentTitle);
setIsEditingTitle(false);
toast.success(`Tab title updated to ${currentTitle}`);
} else {
setCurrentTitle(title);
setIsEditingTitle(false);
toast.error("Title cannot be empty");
}
};
const handleTitleEdit = () => {
setIsEditingTitle(true);
};
const handleExplainQuery = async () => {
const editor = editorInstanceRef.current?.editor;
if (!editor || isExecuting) return;
const query = editor.getValue().trim();
if (!query) return;
try {
// Run without tabId so the result is returned without overwriting the tab's data
const result = await executeQuery(`EXPLAIN ANALYZE ${query}`);
if (result && result.data?.length > 0) {
// DuckDB returns rows with explain_key / explain_value — the analyzed_plan row has the full plan
const planRow = result.data.find((row) => row["explain_key"] === "analyzed_plan");
const planText = planRow
? String(planRow["explain_value"])
: result.data.map((row) => String(row["explain_value"] ?? "")).join("\n");
setExplainText(planText);
setExplainOpen(true);
}
} catch (error) {
console.error("Explain failed:", error);
toast.error("Explain query failed");
}
};
const handleShareQuery = async () => {
const editor = editorInstanceRef.current?.editor;
if (!editor) return;
const query = editor.getValue().trim();
if (!query) {
toast.error("No query to share");
return;
}
const success = await copyQueryURL(query, false);
if (success) {
toast.success("Query URL copied to clipboard");
} else {
toast.error("Failed to copy URL");
}
};
return (
<div className={cn("flex flex-col h-full relative", className)}>
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 border-b">
{/* Title (always visible) */}
<div className="flex items-center gap-2 flex-1 min-w-0">
{isEditingTitle ? (
<Input
className="text-sm font-medium truncate max-w-[200px]"
value={currentTitle}
onChange={handleTitleChange}
onBlur={handleTitleSubmit}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleTitleSubmit();
} else if (e.key === "Escape") {
setCurrentTitle(title);
setIsEditingTitle(false);
}
}}
autoFocus
/>
) : (
<div className="flex items-center gap-2">
<span className="text-lg font-medium truncate text-sm">{currentTitle}</span>
<Button
variant="ghost"
size="icon"
onClick={handleTitleEdit}
className="group-hover:opacity-100 transition-opacity hidden md:flex"
aria-label="Edit tab title"
>
<Edit className="h-4 w-4" />
</Button>
</div>
)}
</div>
{/* Desktop Actions */}
<div className="hidden md:flex items-center gap-4">
<div className="flex gap-2 text-sm text-muted-foreground">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="hover:bg-muted/50 p-2 rounded-md transition-colors">
<Lightbulb className="h-5 w-5 text-yellow-500/70 hover:text-yellow-500 transition-colors" />
</TooltipTrigger>
<TooltipContent side="bottom" className="w-72 p-0" sideOffset={5}>
<div className="bg-card px-3 py-2 rounded-t-sm border-b">
<h4 className="font-medium flex items-center gap-2">
<Command className="h-4 w-4" />
SQL Editor Shortcuts
</h4>
</div>
<div className="p-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">Run Query</span>
<Badge variant="secondary" className="font-mono text-xs">
Ctrl + Enter
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Run Selected</span>
<Badge variant="secondary" className="font-mono text-xs">
Ctrl + Shift + Enter
</Badge>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Button onClick={handleShareQuery} variant="ghost" size="icon" className="h-9 w-9">
<Share2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Copy shareable URL</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Button
onClick={() => setSaveDialogOpen(true)}
variant="ghost"
size="icon"
className="h-9 w-9"
disabled={!currentContent.trim() || !currentProfileId}
>
<Bookmark className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Save Query</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Button
onClick={toggleBrainPanel}
variant={duckBrain.isPanelOpen ? "secondary" : "ghost"}
size="icon"
className="h-9 w-9"
>
<Brain className={cn("h-4 w-4", duckBrain.isPanelOpen && "text-primary")} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{duckBrain.isPanelOpen ? "Close Duck Brain" : "Open Duck Brain"}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Button
onClick={handleExplainQuery}
disabled={isExecuting}
variant="ghost"
size="icon"
className="h-9 w-9"
>
<ListTree className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Explain Plan</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
onClick={handleExecuteQuery}
disabled={isExecuting}
variant="outline"
className="flex items-center gap-2 min-w-[100px]"
>
{isExecuting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
{isExecuting ? "Running..." : "Run Query"}
</Button>
</div>
</div>
{/* Editor */}
<div className="flex-1 relative">
<div ref={editorRef} className="h-full w-full absolute inset-0" />
</div>
{/* Mobile FAB */}
<FloatingActionButton
onClick={handleExecuteQuery}
icon={isExecuting ? Loader2 : Play}
label={isExecuting ? "Running..." : "Run"}
disabled={isExecuting}
className={isExecuting ? "animate-pulse" : ""}
/>
<SaveQueryDialog
open={saveDialogOpen}
onOpenChange={setSaveDialogOpen}
defaultName={currentTitle}
sqlText={currentContent}
/>
<ExplainPlanViewer
open={explainOpen}
onOpenChange={setExplainOpen}
explainText={explainText}
/>
</div>
);
};
export default SqlEditor;
================================================
FILE: src/components/editor/monacoConfig.ts
================================================
// monacoConfig.ts
import * as monaco from "monaco-editor";
import { useDuckStore } from "@/store";
import { useMemo } from "react";
import type { editor } from "monaco-editor";
import { toast } from "sonner";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import { format } from "sql-formatter";
import { sqlEscapeString } from "@/lib/sqlSanitize";
// Types
export interface EditorInstance {
editor: editor.IStandaloneCodeEditor;
dispose: () => void;
}
interface EditorConfig {
language: string;
theme: string;
automaticLayout: boolean;
tabSize: number;
minimap: { enabled: boolean };
padding: { top: number };
suggestOnTriggerCharacters: boolean;
quickSuggestions: boolean;
wordBasedSuggestions: boolean;
fontSize: number;
lineNumbers: "on" | "off" | "relative";
scrollBeyondLastLine: boolean;
cursorBlinking: "blink" | "smooth" | "phase" | "expand" | "solid";
matchBrackets: "always" | "never" | "near";
rulers: number[];
}
// Worker configuration
self.MonacoEnvironment = {
getWorker(_workerId: string) {
return new editorWorker();
},
};
// Create editor instance
export const createEditor = (
container: HTMLElement,
config: EditorConfig,
initialContent: string,
tabId: string,
executeQueryFn: (query: string, tabId: string) => Promise<void>
): EditorInstance => {
const editor = monaco.editor.create(container, {
...config,
value: initialContent,
wordBasedSuggestions: config.wordBasedSuggestions ? "allDocuments" : "off",
bracketPairColorization: { enabled: true },
guides: { bracketPairs: true, indentation: true },
renderWhitespace: "selection",
smoothScrolling: true,
cursorSmoothCaretAnimation: "on",
formatOnPaste: true,
formatOnType: true,
snippetSuggestions: "inline",
suggest: {
preview: true,
showMethods: true,
showFunctions: true,
showVariables: true,
showWords: true,
showColors: true,
},
});
// Add commands
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, async () => {
const query = editor.getValue().trim();
if (!query) {
toast.error("Please enter a query to execute");
return;
}
try {
await executeQueryFn(query, tabId);
} catch (err) {
toast.error(
`Query execution failed: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
});
editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.KeyF, () => {
const formatAction = editor.getAction("editor.action.formatDocument");
formatAction?.run();
});
// Add context menu actions
editor.addAction({
id: "execute-selection",
label: "Execute Selected Query",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter],
contextMenuGroupId: "navigation",
run: async (ed) => {
const selection = ed.getSelection();
const selectedText = selection ? ed.getModel()?.getValueInRange(selection) : "";
if (selectedText?.trim()) {
try {
await executeQueryFn(selectedText.trim(), tabId);
} catch (err) {
toast.error(
`Query execution failed: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
}
},
});
editor.addAction({
id: "format-sql",
label: "Format SQL",
keybindings: [monaco.KeyMod.Alt | monaco.KeyCode.KeyF],
contextMenuGroupId: "modification",
run: (ed) => {
const text = ed.getValue();
try {
const formatted = format(text, {
language: "sql",
keywordCase: "upper",
indentStyle: "standard",
linesBetweenQueries: 2,
});
ed.setValue(formatted);
} catch {
toast.error("Failed to format SQL");
}
},
});
// Setup content change listener with debounce
let timeoutId: number;
const disposable = editor.onDidChangeModelContent(() => {
clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
const newValue = editor.getValue();
useDuckStore.getState().updateTabQuery(tabId, newValue);
}, 300);
});
return {
editor,
dispose: () => {
clearTimeout(timeoutId);
disposable.dispose();
editor.dispose();
},
};
};
// Create a lightweight editor for notebook cells (no auto-save to tab query)
export const createCellEditor = (
container: HTMLElement,
config: EditorConfig,
initialContent: string,
executeQueryFn: () => Promise<void>,
onContentChange: (value: string) => void
): EditorInstance => {
const editor = monaco.editor.create(container, {
...config,
value: initialContent,
wordBasedSuggestions: config.wordBasedSuggestions ? "allDocuments" : "off",
bracketPairColorization: { enabled: true },
guides: { bracketPairs: true, indentation: true },
renderWhitespace: "selection",
smoothScrolling: true,
cursorSmoothCaretAnimation: "on",
formatOnPaste: true,
formatOnType: true,
snippetSuggestions: "inline",
suggest: {
preview: true,
showMethods: true,
showFunctions: true,
showVariables: true,
showWords: true,
showColors: true,
},
});
// Ctrl/Cmd+Enter to run cell
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, async () => {
const query = editor.getValue().trim();
if (!query) return;
try {
await executeQueryFn();
} catch (err) {
toast.error(
`Query execution failed: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
});
// Shift+Enter to run cell (Jupyter-style)
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, async () => {
const query = editor.getValue().trim();
if (!query) return;
try {
await executeQueryFn();
} catch (err) {
toast.error(
`Query execution failed: ${err instanceof Error ? err.message : "Unknown error"}`
);
}
});
editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.KeyF, () => {
const formatAction = editor.getAction("editor.action.formatDocument");
formatAction?.run();
});
editor.addAction({
id: "format-sql",
label: "Format SQL",
keybindings: [monaco.KeyMod.Alt | monaco.KeyCode.KeyF],
contextMenuGroupId: "modification",
run: (ed) => {
const text = ed.getValue();
try {
const formatted = format(text, {
language: "sql",
keywordCase: "upper",
indentStyle: "standard",
linesBetweenQueries: 2,
});
ed.setValue(formatted);
} catch {
toast.error("Failed to format SQL");
}
},
});
// Content change listener — calls provided callback instead of updateTabQuery
let timeoutId: number;
const disposable = editor.onDidChangeModelContent(() => {
clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
onContentChange(editor.getValue());
}, 300);
});
return {
editor,
dispose: () => {
clearTimeout(timeoutId);
disposable.dispose();
editor.dispose();
},
};
};
// Enhanced config hook with better defaults
export const useMonacoConfig = (theme: string): EditorConfig => {
return useMemo(
() => ({
language: "sql",
theme: theme === "dark" ? "vs-dark" : "vs",
automaticLayout: true,
tabSize: 2,
minimap: { enabled: false },
padding: { top: 10 },
suggestOnTriggerCharacters: true,
quickSuggestions: true,
wordBasedSuggestions: false,
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
cursorBlinking: "blink",
matchBrackets: "always",
rulers: [],
}),
[theme]
);
};
// Register SQL formatting provider
monaco.languages.registerDocumentFormattingEditProvider("sql", {
provideDocumentFormattingEdits: (model) => {
try {
const formatted = format(model.getValue(), {
language: "sql",
keywordCase: "upper",
indentStyle: "standard",
linesBetweenQueries: 2,
});
return [
{
range: model.getFullModelRange(),
text: formatted,
},
];
} catch (err) {
console.error("SQL formatting failed:", err);
return [];
}
},
});
// Adapt to use the WASM autocompletion
interface AutocompleteItem {
suggestion: string;
}
const queryNative = async <T>(
connection: { query: (sql: string) => Promise<{ toArray: () => unknown[] }> },
query: string
): Promise<T[]> => {
const results = await connection.query(query);
return results.toArray().map((row: unknown) => row as T);
};
monaco.languages.registerCompletionItemProvider("sql", {
triggerCharacters: [" ", ".", "(", ","],
async provideCompletionItems(model, position) {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
const textInRange = model.getValueInRange({
startColumn: 0,
endColumn: position.column,
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
});
// Get the connection and ensure it's valid
const { connection } = useDuckStore.getState();
if (!connection) {
console.warn("No database connection available for autocompletion.");
return { suggestions: [] };
}
try {
const escapedText = sqlEscapeString(textInRange);
const query = `select suggestion from sql_auto_complete('${escapedText}')`;
const items: AutocompleteItem[] = await queryNative<AutocompleteItem>(connection, query);
const suggestions = items.map((item) => {
return {
label: String(item.suggestion),
kind: monaco.languages.CompletionItemKind.Field,
insertText: String(item.suggestion),
range,
};
});
return { suggestions };
} catch (error) {
console.error("Autocompletion query failed:", error);
return { suggestions: [] };
}
},
});
// Export everything needed
export default {
createEditor,
useMonacoConfig,
};
================================================
FILE: src/components/explorer/ColumnNode.tsx
================================================
import React, { useState } from "react";
import { ChevronRight, ChevronDown, Hash, Type, Calendar, ToggleLeft } from "lucide-react";
import { type ColumnStats } from "@/store";
interface ColumnNodeProps {
stats: ColumnStats;
}
const getTypeIcon = (type: string) => {
const upperType = type.toUpperCase();
if (
upperType.includes("INT") ||
upperType.includes("DOUBLE") ||
upperType.includes("FLOAT") ||
upperType.includes("DECIMAL")
) {
return <Hash className="w-3 h-3" />;
} else if (upperType.includes("DATE") || upperType.includes("TIME")) {
return <Calendar className="w-3 h-3" />;
} else if (upperType.includes("BOOL")) {
return <ToggleLeft className="w-3 h-3" />;
}
return <Type className="w-3 h-3" />;
};
const getTypeColor = (type: string) => {
const upperType = type.toUpperCase();
if (
upperType.includes("INT") ||
upperType.includes("DOUBLE") ||
upperType.includes("FLOAT") ||
upperType.includes("DECIMAL")
) {
return "text-purple-500 bg-purple-500/10";
} else if (upperType.includes("DATE") || upperType.includes("TIME")) {
return "text-green-500 bg-green-500/10";
} else if (upperType.includes("BOOL")) {
return "text-yellow-500 bg-yellow-500/10";
}
return "text-blue-500 bg-blue-500/10";
};
const getFillColor = (percentage: number) => {
if (percentage >= 90) return "bg-green-500";
if (percentage >= 50) return "bg-yellow-500";
return "bg-red-500";
};
export const ColumnNode: React.FC<ColumnNodeProps> = ({ stats }) => {
const [isExpanded, setIsExpanded] = useState(false);
// Safe parsing function that handles both string and number types
const parseValue = (value: string | number): number => {
if (typeof value === "number") return value;
if (typeof value === "string") {
// Remove quotes if present
const cleaned = value.replace(/"/g, "");
return parseFloat(cleaned) || 0;
}
return 0;
};
const nullPercentage = parseValue(stats.null_percentage);
const fillPercentage = 100 - nullPercentage;
const uniqueCount = stats.approx_unique ? parseValue(stats.approx_unique) : 0;
const totalCount = parseValue(stats.count);
const isNumeric =
stats.column_type.toUpperCase().includes("INT") ||
stats.column_type.toUpperCase().includes("DOUBLE") ||
stats.column_type.toUpperCase().includes("FLOAT") ||
stats.column_type.toUpperCase().includes("DECIMAL");
return (
<div className="ml-8">
<div
className="flex items-center py-1.5 px-2 hover:bg-secondary/50 rounded-md cursor-pointer group"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex-1 flex items-center gap-2 min-w-0">
{isExpanded ? (
<ChevronDown className="w-3 h-3 flex-shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="w-3 h-3 flex-shrink-0 text-muted-foreground" />
)}
<div className={`flex-shrink-0 p-1 rounded ${getTypeColor(stats.column_type)}`}>
{getTypeIcon(stats.column_type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-medium truncate">{stats.column_name}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono">
{stats.column_type}
</span>
</div>
{!isExpanded && (
<div className="flex items-center gap-2 mt-1">
<div className="flex-1 max-w-[120px]">
<div className="flex items-center gap-1">
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div
className={`h-full ${getFillColor(fillPercentage)} transition-all`}
style={{ width: `${fillPercentage}%` }}
/>
</div>
<span className="text-[9px] text-muted-foreground font-mono">
{fillPercentage.toFixed(0)}%
</span>
</div>
</div>
<span className="text-[10px] text-muted-foreground">
{uniqueCount.toLocaleString()} unique
</span>
</div>
)}
</div>
</div>
</div>
{isExpanded && (
<div className="ml-6 mt-1 mb-2 p-2 bg-muted/30 rounded-md space-y-2">
{/* Fill Percentage */}
<div className="space-y-1">
<div className="flex items-center justify-between text-[10px]">
<span className="text-muted-foreground">Data Fill</span>
<span className="font-medium">{fillPercentage.toFixed(1)}%</span>
</div>
<div className="h-2 bg-background rounded-full overflow-hidden">
<div
className={`h-full ${getFillColor(fillPercentage)} transition-all`}
style={{ width: `${fillPercentage}%` }}
/>
</div>
</div>
{/* Basic Stats */}
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-[10px]">
<div className="flex justify-between">
<span className="text-muted-foreground">Total:</span>
<span className="font-mono">{totalCount.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Unique:</span>
<span className="font-mono">{uniqueCount.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Nulls:</span>
<span className="font-mono">{((nullPercentage / 100) * totalCount).toFixed(0)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Cardinality:</span>
<span className="font-mono">
{isNaN((uniqueCount / totalCount) * 100)
? "0.0"
: ((uniqueCount / totalCount) * 100).toFixed(1)}
%
</span>
</div>
</div>
{/* Numeric Stats */}
{isNumeric && stats.avg && (
<>
<div className="border-t border-border/50 my-1" />
<div className="space-y-1 text-[10px]">
<div className="flex justify-between">
<span className="text-muted-foreground">Min:</span>
<span className="font-mono">{parseValue(stats.min!).toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Max:</span>
<span className="font-mono">{parseValue(stats.max!).toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Avg:</span>
<span className="font-mono">
{parseValue(stats.avg).toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
</span>
</div>
{stats.std && (
<div className="flex justify-between">
<span className="text-muted-foreground">Std Dev:</span>
<span className="font-mono">
{parseValue(stats.std).toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
</span>
</div>
)}
</div>
{stats.q25 && stats.q50 && stats.q75 && (
<>
<div className="border-t border-border/50 my-1" />
<div className="space-y-1 text-[10px]">
<div className="text-[9px] text-muted-foreground font-medium mb-1">
Quartiles
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Q1 (25%):</span>
<span className="font-mono text-[9px]">
{parseValue(stats.q25).toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Q2 (50%):</span>
<span className="font-mono text-[9px]">
{parseValue(stats.q50).toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Q3 (75%):</span>
<span className="font-mono text-[9px]">
{parseValue(stats.q75).toLocaleString()}
</span>
</div>
</div>
</>
)}
</>
)}
{/* String Stats */}
{!isNumeric && stats.min && stats.max && (
<>
<div className="border-t border-border/50 my-1" />
<div className="space-y-1 text-[10px]">
<div className="flex justify-between gap-2">
<span className="text-muted-foreground flex-shrink-0">Min:</span>
<span className="font-mono truncate text-right" title={stats.min}>
{stats.min}
</span>
</div>
<div className="flex justify-between gap-2">
<span className="text-muted-foreground flex-shrink-0">Max:</span>
<span className="font-mono truncate text-right" title={stats.max}>
{stats.max}
</span>
</div>
</div>
</>
)}
</div>
)}
</div>
);
};
export default ColumnNode;
================================================
FILE: src/components/explorer/DataExplorer.tsx
================================================
import React, { useState, useCallback, lazy, Suspense } from "react";
import { useDuckStore } from "@/store";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Database, EllipsisVertical, FileUp, Plus, RefreshCw, Server } from "lucide-react";
const FileImporter = lazy(() => import("./FileImporter"));
import TreeNode, { TreeNodeData } from "./TreeNode";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import FolderBrowser from "@/components/folders/FolderBrowser";
import CloudBrowser from "@/components/cloud/CloudBrowser";
import { type FileEntry, fileSystemService } from "@/lib/fileSystem";
import { type ImportOptions } from "@/components/common/ImportOptionsPopover";
import { toast } from "sonner";
export default function DataExplorer() {
const [isSheetOpen, setIsSheetOpen] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const databases = useDuckStore((s) => s.databases);
const isLoading = useDuckStore((s) => s.isLoading);
const currentConnection = useDuckStore((s) => s.currentConnection);
const importFile = useDuckStore((s) => s.importFile);
const fetchDatabasesAndTablesInfo = useDuckStore((s) => s.fetchDatabasesAndTablesInfo);
const isFileSystemSupported = useDuckStore((s) => s.isFileSystemSupported);
const schemaFetchError = useDuckStore((s) => s.schemaFetchError);
const isLoadingDbTablesFetch = useDuckStore((s) => s.isLoadingDbTablesFetch);
const [searchTerm, setSearchTerm] = useState("");
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};
// Handle file import from mounted folder
const handleFolderFileImport = useCallback(
async (folderId: string, file: FileEntry, options: ImportOptions) => {
const { tableName, importMode } = options;
const modeLabel = importMode === "view" ? "Linking" : "Importing";
const resultLabel = importMode === "view" ? "view" : "table";
try {
toast.loading(`${modeLabel} ${file.name}...`, { id: "folder-import" });
// Read file from folder
const fileData = await fileSystemService.readFile(folderId, file.path);
const buffer = await fileData.arrayBuffer();
// Determine file type from extension
const ext = file.extension.replace(".", "").toLowerCase();
let fileType = ext;
if (ext === "jsonl" || ext === "ndjson") fileType = "json";
await importFile(file.name, buffer, tableName, fileType, undefined, { importMode });
await fetchDatabasesAndTablesInfo();
toast.success(`Created ${resultLabel} "${tableName}" from "${file.name}"`, {
id: "folder-import",
});
} catch (error) {
console.error("Failed to import file:", error);
toast.error(
`Failed to import: ${error instanceof Error ? error.message : "Unknown error"}`,
{ id: "folder-import" }
);
}
},
[importFile, fetchDatabasesAndTablesInfo]
);
const buildTreeData = () => {
const treeData: TreeNodeData[] = databases.map((db) => ({
name: db.name,
type: "database",
children: db.tables.map((table) => ({
name: table.name,
type: "table",
})),
}));
return treeData;
};
const treeData = buildTreeData();
const isExternal = currentConnection?.scope === "External";
const handleDragOver = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.types.includes("Files") && !isExternal) {
setIsDraggingOver(true);
}
},
[isExternal]
);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only set false if we're leaving the container (not entering a child)
if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDraggingOver(false);
}
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(false);
if (isExternal || !e.dataTransfer.files.length) return;
setIsSheetOpen(true);
},
[isExternal]
);
return (
<Card
className="h-full overflow-hidden border-none relative"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{isDraggingOver && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-primary/10 border-2 border-dashed border-primary rounded-lg pointer-events-none">
<div className="text-center">
<FileUp className="h-10 w-10 text-primary mx-auto mb-2" />
<p className="text-sm font-medium text-primary">Drop files to import</p>
</div>
</div>
)}
{isLoading && (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Loading databases...</p>
</div>
)}
<CardHeader className="p-2 border-b">
<div className="flex items-center justify-between">
gitextract_fb4zvf6o/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ └── feature.yml │ └── workflows/ │ ├── 2-docker-build.yml │ └── ci.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE.md ├── README.md ├── components.json ├── docker-compose.yml ├── eslint.config.js ├── index.html ├── inject-env.js ├── package.json ├── public/ │ └── databases/ │ ├── README.md │ └── manifest.json ├── serve.json ├── src/ │ ├── components/ │ │ ├── charts/ │ │ │ ├── ChartVisualizationPro.tsx │ │ │ ├── UPlotChart.tsx │ │ │ └── tooltipPlugin.ts │ │ ├── cloud/ │ │ │ ├── CloudBrowser.tsx │ │ │ └── CloudConnectionModal.tsx │ │ ├── command-palette/ │ │ │ └── CommandPalette.tsx │ │ ├── common/ │ │ │ ├── FloatingActionButton.tsx │ │ │ └── ImportOptionsPopover.tsx │ │ ├── connection/ │ │ │ └── ConnectionsModal.tsx │ │ ├── duck-brain/ │ │ │ ├── DuckBrainCodeBlock.tsx │ │ │ ├── DuckBrainInput.tsx │ │ │ ├── DuckBrainMessages.tsx │ │ │ ├── DuckBrainPanel.tsx │ │ │ ├── MarkdownContent.tsx │ │ │ ├── ResultsArtifact.tsx │ │ │ └── SchemaAutocomplete.tsx │ │ ├── editor/ │ │ │ ├── SqlEditor.tsx │ │ │ └── monacoConfig.ts │ │ ├── explorer/ │ │ │ ├── ColumnNode.tsx │ │ │ ├── DataExplorer.tsx │ │ │ ├── FileImporter.tsx │ │ │ └── TreeNode.tsx │ │ ├── folders/ │ │ │ └── FolderBrowser.tsx │ │ ├── layout/ │ │ │ ├── ConnectionSwitcher.tsx │ │ │ ├── MobileNavDrawer.tsx │ │ │ └── Sidebar.tsx │ │ ├── notebook/ │ │ │ ├── MarkdownRenderer.tsx │ │ │ ├── NotebookCell.tsx │ │ │ ├── NotebookTab.tsx │ │ │ └── NotebookToolbar.tsx │ │ ├── profile/ │ │ │ ├── PasswordDialog.tsx │ │ │ ├── ProfileAvatar.tsx │ │ │ ├── ProfileEditor.tsx │ │ │ └── ProfilePicker.tsx │ │ ├── saved-queries/ │ │ │ ├── SaveQueryDialog.tsx │ │ │ └── SavedQueriesPanel.tsx │ │ ├── table/ │ │ │ ├── CellValueViewer.tsx │ │ │ ├── ColumnStatsPanel.tsx │ │ │ └── DuckUItable.tsx │ │ ├── theme/ │ │ │ ├── mode-toggle.tsx │ │ │ └── theme-provider.tsx │ │ ├── ui/ │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── multi-select.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ └── tooltip.tsx │ │ └── workspace/ │ │ ├── BrainTab.tsx │ │ ├── ConnectionsTab.tsx │ │ ├── ExplainPlanViewer.tsx │ │ ├── HomeTab.tsx │ │ ├── QueryHistory.tsx │ │ ├── SettingsTab.tsx │ │ ├── SortableTab.tsx │ │ ├── SqlTab.tsx │ │ └── WorkspaceTabs.tsx │ ├── hooks/ │ │ └── useQueryFromURL.ts │ ├── index.css │ ├── lib/ │ │ ├── chartDataTransform.ts │ │ ├── chartExport.ts │ │ ├── chartUtils.ts │ │ ├── cloudStorage/ │ │ │ ├── index.ts │ │ │ └── testHttpfs.ts │ │ ├── duckBrain/ │ │ │ ├── index.ts │ │ │ ├── models.config.ts │ │ │ ├── prompts/ │ │ │ │ └── text-to-sql.ts │ │ │ ├── providers/ │ │ │ │ ├── anthropic.provider.ts │ │ │ │ ├── index.ts │ │ │ │ ├── openai.provider.ts │ │ │ │ └── types.ts │ │ │ ├── schemaFormatter.ts │ │ │ ├── sqlParser.ts │ │ │ ├── webllm.service.ts │ │ │ └── webllm.worker.ts │ │ ├── fileSystem/ │ │ │ └── index.ts │ │ ├── sqlSanitize.ts │ │ └── utils.ts │ ├── main.tsx │ ├── pages/ │ │ └── Home.tsx │ ├── services/ │ │ ├── duckdb/ │ │ │ ├── __tests__/ │ │ │ │ ├── resultParser.test.ts │ │ │ │ └── utils.test.ts │ │ │ ├── externalConnection.ts │ │ │ ├── index.ts │ │ │ ├── opfsConnection.ts │ │ │ ├── resultParser.ts │ │ │ ├── schemaFetcher.ts │ │ │ ├── utils.ts │ │ │ └── wasmConnection.ts │ │ └── persistence/ │ │ ├── __tests__/ │ │ │ ├── crypto.test.ts │ │ │ └── migrations.test.ts │ │ ├── crypto.ts │ │ ├── fallback.ts │ │ ├── index.ts │ │ ├── migrations.ts │ │ ├── repositories/ │ │ │ ├── aiConfigRepository.ts │ │ │ ├── connectionRepository.ts │ │ │ ├── index.ts │ │ │ ├── profileRepository.ts │ │ │ ├── queryHistoryRepository.ts │ │ │ ├── savedQueryRepository.ts │ │ │ ├── settingsRepository.ts │ │ │ └── workspaceRepository.ts │ │ └── systemDb.ts │ ├── store/ │ │ ├── index.ts │ │ ├── slices/ │ │ │ ├── connectionSlice.ts │ │ │ ├── duckBrainSlice.ts │ │ │ ├── duckdbSlice.ts │ │ │ ├── fileSystemSlice.ts │ │ │ ├── profileSlice.ts │ │ │ ├── querySlice.ts │ │ │ ├── schemaSlice.ts │ │ │ └── tabSlice.ts │ │ └── types.ts │ ├── types/ │ │ └── filesystem.d.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts
SYMBOL INDEX (409 symbols across 92 files)
FILE: src/components/charts/ChartVisualizationPro.tsx
type ChartVisualizationProProps (line 44) | interface ChartVisualizationProProps {
constant DEFAULT_COLORS (line 51) | const DEFAULT_COLORS = [
constant CHART_TYPE_INFO (line 65) | const CHART_TYPE_INFO: Record<string, { label: string; icon: React.Eleme...
function PieChartDisplay (line 79) | function PieChartDisplay({
function ChartLegend (line 262) | function ChartLegend({
FILE: src/components/charts/UPlotChart.tsx
type UPlotChartProps (line 5) | interface UPlotChartProps {
function UPlotChart (line 12) | function UPlotChart({ options, data, className, onInit }: UPlotChartProp...
FILE: src/components/charts/tooltipPlugin.ts
function escapeHtml (line 4) | function escapeHtml(str: string): string {
type TooltipPluginOptions (line 13) | interface TooltipPluginOptions {
function tooltipPlugin (line 17) | function tooltipPlugin(xLabels: string[], opts?: TooltipPluginOptions): ...
FILE: src/components/cloud/CloudBrowser.tsx
constant PROVIDER_CONFIG (line 32) | const PROVIDER_CONFIG = {
type CloudConnectionItemProps (line 38) | interface CloudConnectionItemProps {
function CloudConnectionItem (line 45) | function CloudConnectionItem({
function CloudBrowser (line 137) | function CloudBrowser() {
FILE: src/components/cloud/CloudConnectionModal.tsx
type CloudConnectionModalProps (line 41) | interface CloudConnectionModalProps {
type CloudConnectionFormData (line 85) | type CloudConnectionFormData = z.infer<typeof cloudConnectionSchema>;
function CloudConnectionModal (line 87) | function CloudConnectionModal({
FILE: src/components/command-palette/CommandPalette.tsx
function CommandPalette (line 32) | function CommandPalette() {
FILE: src/components/common/FloatingActionButton.tsx
type FloatingActionButtonProps (line 5) | interface FloatingActionButtonProps {
FILE: src/components/common/ImportOptionsPopover.tsx
type ImportOptions (line 9) | interface ImportOptions {
type ImportOptionsPopoverProps (line 14) | interface ImportOptionsPopoverProps {
function generateTableName (line 24) | function generateTableName(fileName: string): string {
FILE: src/components/connection/ConnectionsModal.tsx
type ConnectionFormValues (line 75) | type ConnectionFormValues = z.infer<typeof connectionSchema>;
type ConnectionManagerProps (line 77) | interface ConnectionManagerProps {
FILE: src/components/duck-brain/DuckBrainCodeBlock.tsx
type DuckBrainCodeBlockProps (line 9) | interface DuckBrainCodeBlockProps {
FILE: src/components/duck-brain/DuckBrainInput.tsx
type DuckBrainInputProps (line 57) | interface DuckBrainInputProps {
FILE: src/components/duck-brain/DuckBrainMessages.tsx
type DuckBrainMessagesProps (line 47) | interface DuckBrainMessagesProps {
FILE: src/components/duck-brain/DuckBrainPanel.tsx
type DuckBrainPanelProps (line 21) | interface DuckBrainPanelProps {
type HeaderProps (line 328) | interface HeaderProps {
FILE: src/components/duck-brain/MarkdownContent.tsx
type MarkdownContentProps (line 5) | interface MarkdownContentProps {
FILE: src/components/duck-brain/ResultsArtifact.tsx
type ResultsArtifactProps (line 9) | interface ResultsArtifactProps {
constant MAX_INLINE_ROWS (line 15) | const MAX_INLINE_ROWS = 5;
constant MAX_INLINE_COLUMNS (line 16) | const MAX_INLINE_COLUMNS = 6;
function formatCellValue (line 154) | function formatCellValue(value: unknown): string {
FILE: src/components/duck-brain/SchemaAutocomplete.tsx
type SchemaSuggestion (line 6) | interface SchemaSuggestion {
type SchemaAutocompleteProps (line 15) | interface SchemaAutocompleteProps {
function buildSchemaSuggestions (line 27) | function buildSchemaSuggestions(
FILE: src/components/editor/SqlEditor.tsx
type SqlEditorProps (line 27) | interface SqlEditorProps {
FILE: src/components/editor/monacoConfig.ts
type EditorInstance (line 12) | interface EditorInstance {
type EditorConfig (line 17) | interface EditorConfig {
method getWorker (line 37) | getWorker(_workerId: string) {
type AutocompleteItem (line 306) | interface AutocompleteItem {
method provideCompletionItems (line 319) | async provideCompletionItems(model, position) {
FILE: src/components/explorer/ColumnNode.tsx
type ColumnNodeProps (line 5) | interface ColumnNodeProps {
FILE: src/components/explorer/DataExplorer.tsx
function DataExplorer (line 24) | function DataExplorer() {
FILE: src/components/explorer/FileImporter.tsx
constant ACCEPTED_FILE_TYPES (line 50) | const ACCEPTED_FILE_TYPES = {
constant MAX_FILE_SIZE (line 58) | const MAX_FILE_SIZE = 3 * 1024 * 1024 * 1024;
constant SUPPORTED_FILE_EXTENSIONS (line 59) | const SUPPORTED_FILE_EXTENSIONS = [
constant MAX_CONCURRENT_UPLOADS (line 69) | const MAX_CONCURRENT_UPLOADS = 3;
constant PREVIEW_ROW_LIMIT (line 70) | const PREVIEW_ROW_LIMIT = 20;
constant DUCKDB_TYPES (line 73) | const DUCKDB_TYPES = [
type FileExtension (line 87) | type FileExtension = (typeof SUPPORTED_FILE_EXTENSIONS)[number];
type FileWithPreview (line 89) | interface FileWithPreview extends File {
type UploadError (line 93) | interface UploadError {
type FileImportState (line 100) | interface FileImportState {
type CsvImportOptions (line 108) | interface CsvImportOptions {
type SchemaColumn (line 126) | interface SchemaColumn {
type FileImporterProps (line 133) | interface FileImporterProps {
type FileDetailsProps (line 139) | interface FileDetailsProps {
FILE: src/components/explorer/TreeNode.tsx
type TreeNodeData (line 31) | interface TreeNodeData {
type TreeNodeProps (line 38) | interface TreeNodeProps {
FILE: src/components/folders/FolderBrowser.tsx
type FolderBrowserProps (line 36) | interface FolderBrowserProps {
type FileNodeProps (line 76) | interface FileNodeProps {
type MountedFolderNodeProps (line 265) | interface MountedFolderNodeProps {
FILE: src/components/layout/ConnectionSwitcher.tsx
type ConnectionSwitcherProps (line 16) | interface ConnectionSwitcherProps {
function ConnectionSwitcher (line 20) | function ConnectionSwitcher({ className }: ConnectionSwitcherProps) {
FILE: src/components/layout/Sidebar.tsx
type SidebarProps (line 43) | interface SidebarProps {
function Sidebar (line 48) | function Sidebar({ isExplorerOpen, onToggleExplorer }: SidebarProps) {
FILE: src/components/notebook/MarkdownRenderer.tsx
type MarkdownRendererProps (line 5) | interface MarkdownRendererProps {
function MarkdownRenderer (line 15) | function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
FILE: src/components/notebook/NotebookCell.tsx
type NotebookCellProps (line 49) | interface NotebookCellProps {
function NotebookCellComponent (line 59) | function NotebookCellComponent({
function CellResults (line 371) | function CellResults({
FILE: src/components/notebook/NotebookTab.tsx
type NotebookTabProps (line 8) | interface NotebookTabProps {
function NotebookTab (line 12) | function NotebookTab({ tabId }: NotebookTabProps) {
FILE: src/components/notebook/NotebookToolbar.tsx
type NotebookToolbarProps (line 10) | interface NotebookToolbarProps {
function NotebookToolbar (line 17) | function NotebookToolbar({
FILE: src/components/profile/PasswordDialog.tsx
type PasswordDialogProps (line 18) | interface PasswordDialogProps {
function PasswordDialog (line 25) | function PasswordDialog({
FILE: src/components/profile/ProfileAvatar.tsx
type ProfileAvatarProps (line 13) | interface ProfileAvatarProps {
function ProfileAvatar (line 19) | function ProfileAvatar({ avatarEmoji, size = "md", className }: ProfileA...
FILE: src/components/profile/ProfileEditor.tsx
constant AVATAR_OPTIONS (line 9) | const AVATAR_OPTIONS = [
type ProfileEditorProps (line 33) | interface ProfileEditorProps {
function ProfileEditor (line 40) | function ProfileEditor({
FILE: src/components/profile/ProfilePicker.tsx
type ProfilePickerProps (line 22) | interface ProfilePickerProps {
function ProfilePicker (line 28) | function ProfilePicker({
FILE: src/components/saved-queries/SaveQueryDialog.tsx
type SaveQueryDialogProps (line 19) | interface SaveQueryDialogProps {
function SaveQueryDialog (line 26) | function SaveQueryDialog({
FILE: src/components/saved-queries/SavedQueriesPanel.tsx
type SavedQueriesPanelProps (line 25) | interface SavedQueriesPanelProps {
function SavedQueriesPanel (line 29) | function SavedQueriesPanel({ onClose }: SavedQueriesPanelProps) {
FILE: src/components/table/CellValueViewer.tsx
type CellValueViewerProps (line 7) | interface CellValueViewerProps {
FILE: src/components/table/ColumnStatsPanel.tsx
type DataRow (line 7) | type DataRow = Record<string, unknown>;
type ColumnStats (line 9) | interface ColumnStats {
type ColumnStatsPanelProps (line 21) | interface ColumnStatsPanelProps {
FILE: src/components/table/DuckUItable.tsx
type DataRow (line 62) | type DataRow = Record<string, any>;
type CellPosition (line 65) | type CellPosition = { row: number; col: string };
type ContextMenuPosition (line 66) | type ContextMenuPosition = { x: number; y: number };
type DuckTableProps (line 69) | interface DuckTableProps {
constant DEFAULT_COLUMN_WIDTH (line 86) | const DEFAULT_COLUMN_WIDTH = 150;
constant MIN_COLUMN_WIDTH (line 87) | const MIN_COLUMN_WIDTH = 50;
constant MAX_COLUMN_WIDTH (line 88) | const MAX_COLUMN_WIDTH = 1000;
constant DEFAULT_MAX_AUTO_WIDTH (line 89) | const DEFAULT_MAX_AUTO_WIDTH = 250;
constant DEFAULT_MIN_AUTO_WIDTH (line 90) | const DEFAULT_MIN_AUTO_WIDTH = 80;
constant DEFAULT_SAMPLE_SIZE (line 91) | const DEFAULT_SAMPLE_SIZE = 100;
FILE: src/components/theme/mode-toggle.tsx
function ModeToggle (line 12) | function ModeToggle() {
FILE: src/components/theme/theme-provider.tsx
type Theme (line 3) | type Theme = "dark" | "light" | "system";
type ThemeProviderProps (line 5) | type ThemeProviderProps = {
type ThemeProviderState (line 11) | type ThemeProviderState = {
function ThemeProvider (line 23) | function ThemeProvider({
FILE: src/components/ui/badge.tsx
type BadgeProps (line 25) | interface BadgeProps
function Badge (line 28) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: src/components/ui/button.tsx
type ButtonProps (line 33) | interface ButtonProps
FILE: src/components/ui/form.tsx
type FormFieldContextValue (line 18) | type FormFieldContextValue<
type FormItemContextValue (line 63) | type FormItemContextValue = {
FILE: src/components/ui/multi-select.tsx
type MultiSelectOption (line 16) | interface MultiSelectOption {
type MultiSelectProps (line 22) | interface MultiSelectProps {
function MultiSelect (line 31) | function MultiSelect({
FILE: src/components/ui/pagination.tsx
type PaginationLinkProps (line 29) | type PaginationLinkProps = {
FILE: src/components/ui/sheet.tsx
type SheetContentProps (line 52) | interface SheetContentProps
FILE: src/components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivE...
FILE: src/components/ui/sonner.tsx
type ToasterProps (line 4) | type ToasterProps = React.ComponentProps<typeof Sonner>;
FILE: src/components/workspace/ConnectionsTab.tsx
type ConnectionFormValues (line 63) | type ConnectionFormValues = z.infer<typeof connectionSchema>;
FILE: src/components/workspace/ExplainPlanViewer.tsx
type ExplainPlanViewerProps (line 11) | interface ExplainPlanViewerProps {
constant OPERATOR_COLORS (line 17) | const OPERATOR_COLORS: Record<string, string> = {
function colorizeOperators (line 37) | function colorizeOperators(text: string): React.ReactNode[] {
function ExplainPlanViewer (line 56) | function ExplainPlanViewer({ open, onOpenChange, explainText }: ExplainP...
FILE: src/components/workspace/QueryHistory.tsx
type QueryHistoryProps (line 39) | interface QueryHistoryProps {
FILE: src/components/workspace/SettingsTab.tsx
function SettingsTab (line 38) | function SettingsTab() {
FILE: src/components/workspace/SortableTab.tsx
type Tab (line 9) | interface Tab {
type SortableTabProps (line 16) | interface SortableTabProps {
FILE: src/components/workspace/SqlTab.tsx
type SqlTabProps (line 33) | interface SqlTabProps {
FILE: src/components/workspace/WorkspaceTabs.tsx
function WorkspaceTabs (line 72) | function WorkspaceTabs() {
FILE: src/hooks/useQueryFromURL.ts
function useQueryFromURL (line 10) | function useQueryFromURL() {
function generateQueryURL (line 60) | function generateQueryURL(query: string, autoExecute = false): string {
function copyQueryURL (line 73) | async function copyQueryURL(query: string, autoExecute = false): Promise...
FILE: src/lib/chartDataTransform.ts
type TransformedData (line 9) | type TransformedData = Record<string, unknown>[];
function adjustColorBrightness (line 260) | function adjustColorBrightness(hex: string, percent: number): string {
FILE: src/lib/cloudStorage/index.ts
type CloudProviderType (line 11) | type CloudProviderType = "s3" | "gcs" | "azure";
type CloudConnection (line 14) | interface CloudConnection {
type CloudFile (line 43) | interface CloudFile {
type CloudSupportStatus (line 53) | interface CloudSupportStatus {
constant DB_NAME (line 62) | const DB_NAME = "duck-ui-cloud";
constant STORE_NAME (line 63) | const STORE_NAME = "cloud-connections";
constant DB_VERSION (line 64) | const DB_VERSION = 1;
function openDatabase (line 66) | async function openDatabase(): Promise<IDBDatabase> {
class CloudStorageService (line 85) | class CloudStorageService {
method init (line 94) | async init(): Promise<void> {
method checkCloudSupport (line 114) | async checkCloudSupport(): Promise<CloudSupportStatus> {
method getSupportStatus (line 176) | getSupportStatus(): CloudSupportStatus | null {
method loadPersistedConnections (line 183) | private async loadPersistedConnections(): Promise<void> {
method persistConnection (line 207) | private async persistConnection(conn: CloudConnection): Promise<void> {
method removePersistedConnection (line 233) | private async removePersistedConnection(id: string): Promise<void> {
method addConnection (line 249) | async addConnection(
method updateConnection (line 270) | async updateConnection(
method removeConnection (line 287) | async removeConnection(id: string): Promise<void> {
method getConnections (line 301) | getConnections(): CloudConnection[] {
method getConnection (line 308) | getConnection(id: string): CloudConnection | undefined {
method connect (line 315) | async connect(id: string): Promise<boolean> {
method disconnect (line 384) | async disconnect(id: string): Promise<void> {
method testConnection (line 404) | async testConnection(id: string): Promise<{ success: boolean; error?: ...
method getUriPrefix (line 446) | getUriPrefix(id: string): string | null {
FILE: src/lib/cloudStorage/testHttpfs.ts
type HttpfsTestResult (line 11) | interface HttpfsTestResult {
function testHttpfsSupport (line 18) | async function testHttpfsSupport(): Promise<HttpfsTestResult[]> {
FILE: src/lib/duckBrain/models.config.ts
type ModelConfig (line 2) | interface ModelConfig {
constant AVAILABLE_MODELS (line 10) | const AVAILABLE_MODELS: ModelConfig[] = [
constant DEFAULT_MODEL (line 34) | const DEFAULT_MODEL = AVAILABLE_MODELS[0];
FILE: src/lib/duckBrain/prompts/text-to-sql.ts
constant TEXT_TO_SQL_SYSTEM_PROMPT (line 4) | const TEXT_TO_SQL_SYSTEM_PROMPT = `You are Duck Brain, a DuckDB SQL quer...
function buildResultsContext (line 28) | function buildResultsContext(messages: DuckBrainMessage[]): string {
constant DUCKDB_FEW_SHOT_EXAMPLES (line 59) | const DUCKDB_FEW_SHOT_EXAMPLES: ChatCompletionMessageParam[] = [
function buildTextToSQLMessages (line 102) | function buildTextToSQLMessages(
FILE: src/lib/duckBrain/providers/anthropic.provider.ts
class AnthropicProvider (line 14) | class AnthropicProvider implements AIProvider {
method initialize (line 24) | async initialize(config: ProviderConfig): Promise<void> {
method generateStreaming (line 48) | async generateStreaming(
method generateText (line 152) | async generateText(
method abort (line 192) | abort(): void {
method cleanup (line 196) | async cleanup(): Promise<void> {
method getStatus (line 202) | getStatus(): ProviderStatus {
method isReady (line 211) | isReady(): boolean {
FILE: src/lib/duckBrain/providers/index.ts
function createProvider (line 12) | function createProvider(type: AIProviderType): AIProvider {
function testProviderConnection (line 32) | async function testProviderConnection(
FILE: src/lib/duckBrain/providers/openai.provider.ts
class OpenAIProvider (line 14) | class OpenAIProvider implements AIProvider {
method initialize (line 25) | async initialize(config: ProviderConfig): Promise<void> {
method generateStreaming (line 86) | async generateStreaming(
method generateText (line 175) | async generateText(
method abort (line 215) | abort(): void {
method cleanup (line 219) | async cleanup(): Promise<void> {
method getStatus (line 225) | getStatus(): ProviderStatus {
method isReady (line 234) | isReady(): boolean {
FILE: src/lib/duckBrain/providers/types.ts
type AIProviderType (line 3) | type AIProviderType = "webllm" | "openai" | "anthropic" | "gemini" | "op...
type ProviderConfig (line 5) | interface ProviderConfig {
type StreamCallbacks (line 11) | interface StreamCallbacks {
type GenerationOptions (line 17) | interface GenerationOptions {
type ProviderStatus (line 23) | interface ProviderStatus {
type AIProvider (line 34) | interface AIProvider {
type ModelOption (line 83) | interface ModelOption {
constant OPENAI_MODELS (line 90) | const OPENAI_MODELS: ModelOption[] = [
constant ANTHROPIC_MODELS (line 117) | const ANTHROPIC_MODELS: ModelOption[] = [
FILE: src/lib/duckBrain/schemaFormatter.ts
type SchemaContext (line 3) | interface SchemaContext {
function formatSchemaForContext (line 13) | function formatSchemaForContext(
function getSchemaSummary (line 86) | function getSchemaSummary(databases: DatabaseInfo[]): string {
FILE: src/lib/duckBrain/sqlParser.ts
type ParsedSQLResult (line 1) | interface ParsedSQLResult {
constant SQL_KEYWORDS (line 7) | const SQL_KEYWORDS = [
function extractSQLFromResponse (line 29) | function extractSQLFromResponse(response: string): ParsedSQLResult {
function formatSQLForDisplay (line 150) | function formatSQLForDisplay(sql: string): string {
FILE: src/lib/duckBrain/webllm.service.ts
type GPUAdapter (line 10) | interface GPUAdapter {
type GPUInterface (line 14) | interface GPUInterface {
type Navigator (line 19) | interface Navigator {
type ModelStatus (line 24) | type ModelStatus = "idle" | "checking" | "downloading" | "loading" | "re...
type DuckBrainServiceState (line 26) | interface DuckBrainServiceState {
type StreamCallbacks (line 35) | interface StreamCallbacks {
type StateListener (line 41) | type StateListener = (state: DuckBrainServiceState) => void;
class DuckBrainService (line 47) | class DuckBrainService {
method constructor (line 62) | constructor() {
method checkWebGPUSupport (line 70) | async checkWebGPUSupport(): Promise<boolean> {
method updateState (line 106) | private updateState(partial: Partial<DuckBrainServiceState>) {
method subscribe (line 114) | subscribe(listener: StateListener): () => void {
method getState (line 124) | getState(): DuckBrainServiceState {
method initialize (line 145) | async initialize(modelId: string = DEFAULT_MODEL.id): Promise<void> {
method generateStreaming (line 204) | async generateStreaming(
method generate (line 249) | async generate(
method abort (line 269) | abort(): void {
method cleanup (line 277) | async cleanup(): Promise<void> {
method isReady (line 305) | isReady(): boolean {
FILE: src/lib/fileSystem/index.ts
type FileEntry (line 8) | interface FileEntry {
type FolderEntry (line 18) | interface FolderEntry {
type FSEntry (line 26) | type FSEntry = FileEntry | FolderEntry;
type MountedFolder (line 28) | interface MountedFolder {
constant SUPPORTED_EXTENSIONS (line 37) | const SUPPORTED_EXTENSIONS = [
function isFileSystemAccessSupported (line 54) | function isFileSystemAccessSupported(): boolean {
constant DB_NAME (line 59) | const DB_NAME = "duck-ui-filesystem";
constant STORE_NAME (line 60) | const STORE_NAME = "folder-handles";
constant DB_VERSION (line 61) | const DB_VERSION = 1;
function openDatabase (line 66) | async function openDatabase(): Promise<IDBDatabase> {
function verifyPermission (line 85) | async function verifyPermission(
function getExtension (line 107) | function getExtension(filename: string): string {
class FileSystemService (line 115) | class FileSystemService {
method init (line 123) | async init(): Promise<void> {
method loadPersistedFolders (line 145) | private async loadPersistedFolders(): Promise<void> {
method persistFolder (line 170) | private async persistFolder(folder: MountedFolder): Promise<void> {
method removePersistedFolder (line 186) | private async removePersistedFolder(id: string): Promise<void> {
method mountFolder (line 202) | async mountFolder(): Promise<MountedFolder> {
method unmountFolder (line 233) | async unmountFolder(id: string): Promise<void> {
method getMountedFolders (line 241) | getMountedFolders(): MountedFolder[] {
method getFolder (line 248) | getFolder(id: string): MountedFolder | undefined {
method requestPermission (line 255) | async requestPermission(id: string): Promise<boolean> {
method checkAllPermissions (line 267) | async checkAllPermissions(): Promise<Map<string, boolean>> {
method listFiles (line 288) | async listFiles(
method readDirectory (line 315) | private async readDirectory(
method readFile (line 383) | async readFile(folderId: string, filePath: string): Promise<File> {
method readFileBuffer (line 415) | async readFileBuffer(folderId: string, filePath: string): Promise<Arra...
method requestWritePermission (line 423) | async requestWritePermission(id: string): Promise<boolean> {
method saveFile (line 435) | async saveFile(
method getFolderHandle (line 483) | getFolderHandle(id: string): FileSystemDirectoryHandle | undefined {
FILE: src/lib/sqlSanitize.ts
function sqlEscapeString (line 2) | function sqlEscapeString(value: string): string {
function sqlEscapeIdentifier (line 7) | function sqlEscapeIdentifier(name: string): string {
FILE: src/lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
function generateUUID (line 14) | function generateUUID(): string {
FILE: src/main.tsx
type LoadingScreenProps (line 24) | interface LoadingScreenProps {
type AppInitializerProps (line 28) | interface AppInitializerProps {
function boot (line 79) | async function boot() {
function migrateFromLocalStorage (line 148) | async function migrateFromLocalStorage(profileId: string): Promise<void> {
FILE: src/pages/Home.tsx
function Home (line 11) | function Home() {
FILE: src/services/duckdb/opfsConnection.ts
function normalizeOpfsPath (line 10) | function normalizeOpfsPath(path: string): string {
FILE: src/services/persistence/__tests__/migrations.test.ts
function createMockConnection (line 5) | function createMockConnection() {
FILE: src/services/persistence/crypto.ts
constant KEY_DB_NAME (line 9) | const KEY_DB_NAME = "duck-ui-keys";
constant KEY_STORE_NAME (line 10) | const KEY_STORE_NAME = "encryption-keys";
constant KEY_DB_VERSION (line 11) | const KEY_DB_VERSION = 1;
constant ALGORITHM (line 13) | const ALGORITHM = "AES-GCM";
constant KEY_LENGTH (line 14) | const KEY_LENGTH = 256;
constant IV_LENGTH (line 15) | const IV_LENGTH = 12;
constant SALT_LENGTH (line 16) | const SALT_LENGTH = 16;
constant PBKDF2_ITERATIONS (line 17) | const PBKDF2_ITERATIONS = 100_000;
function uint8ArrayToBase64 (line 21) | function uint8ArrayToBase64(bytes: Uint8Array): string {
function base64ToUint8Array (line 29) | function base64ToUint8Array(base64: string): Uint8Array {
function openKeyDatabase (line 40) | function openKeyDatabase(): Promise<IDBDatabase> {
function storeKeyForProfile (line 54) | async function storeKeyForProfile(
function loadKeyForProfile (line 84) | async function loadKeyForProfile(
function deleteKeyForProfile (line 117) | async function deleteKeyForProfile(profileId: string): Promise<void> {
function generateEncryptionKey (line 140) | async function generateEncryptionKey(): Promise<CryptoKey> {
function generateSalt (line 147) | function generateSalt(): Uint8Array {
function deriveKeyFromPassword (line 151) | async function deriveKeyFromPassword(
function encrypt (line 179) | async function encrypt(plaintext: string, key: CryptoKey): Promise<strin...
function decrypt (line 194) | async function decrypt(encoded: string, key: CryptoKey): Promise<string> {
function exportKey (line 204) | async function exportKey(key: CryptoKey): Promise<string> {
function importKey (line 209) | async function importKey(exported: string): Promise<CryptoKey> {
FILE: src/services/persistence/fallback.ts
constant FALLBACK_DB_NAME (line 7) | const FALLBACK_DB_NAME = "duck-ui-persistence";
constant FALLBACK_DB_VERSION (line 8) | const FALLBACK_DB_VERSION = 1;
constant STORES (line 10) | const STORES = [
function openFallbackDb (line 23) | function openFallbackDb(): Promise<IDBDatabase> {
type StoreName (line 52) | type StoreName = (typeof STORES)[number];
function fallbackPut (line 54) | async function fallbackPut(
function fallbackGet (line 68) | async function fallbackGet(store: StoreName, key: IDBValidKey): Promise<...
function fallbackGetAll (line 79) | async function fallbackGetAll(store: StoreName): Promise<unknown[]> {
function fallbackDelete (line 90) | async function fallbackDelete(store: StoreName, key: IDBValidKey): Promi...
function fallbackClear (line 101) | async function fallbackClear(store: StoreName): Promise<void> {
FILE: src/services/persistence/migrations.ts
type Migration (line 8) | interface Migration {
constant MIGRATIONS (line 14) | const MIGRATIONS: Migration[] = [
function runMigrations (line 95) | async function runMigrations(conn: duckdb.AsyncDuckDBConnection): Promis...
FILE: src/services/persistence/repositories/aiConfigRepository.ts
type AIProviderConfig (line 6) | interface AIProviderConfig {
type AIConversation (line 13) | interface AIConversation {
function saveProviderConfig (line 23) | async function saveProviderConfig(
function getProviderConfigs (line 53) | async function getProviderConfigs(
function saveConversation (line 97) | async function saveConversation(
function getConversations (line 137) | async function getConversations(profileId: string): Promise<AIConversati...
FILE: src/services/persistence/repositories/connectionRepository.ts
type SavedConnection (line 6) | interface SavedConnection {
type ConnectionInput (line 17) | interface ConnectionInput {
function saveConnection (line 25) | async function saveConnection(
function getConnections (line 70) | async function getConnections(
function deleteConnection (line 132) | async function deleteConnection(id: string): Promise<void> {
FILE: src/services/persistence/repositories/profileRepository.ts
type Profile (line 5) | interface Profile {
function createProfile (line 15) | async function createProfile(
function getProfile (line 53) | async function getProfile(id: string): Promise<Profile | null> {
function listProfiles (line 75) | async function listProfiles(): Promise<Profile[]> {
function updateProfile (line 99) | async function updateProfile(
function deleteProfile (line 128) | async function deleteProfile(id: string): Promise<void> {
function updateLastActive (line 155) | async function updateLastActive(id: string): Promise<void> {
FILE: src/services/persistence/repositories/queryHistoryRepository.ts
type HistoryEntry (line 5) | interface HistoryEntry {
function addHistoryEntry (line 16) | async function addHistoryEntry(
function getHistory (line 53) | async function getHistory(
function clearHistory (line 85) | async function clearHistory(profileId: string): Promise<void> {
function getHistoryCount (line 99) | async function getHistoryCount(profileId: string): Promise<number> {
FILE: src/services/persistence/repositories/savedQueryRepository.ts
type SavedQuery (line 5) | interface SavedQuery {
function saveQuery (line 17) | async function saveQuery(
function getSavedQueries (line 50) | async function getSavedQueries(profileId: string, folder?: string): Prom...
function updateSavedQuery (line 79) | async function updateSavedQuery(
function deleteSavedQuery (line 110) | async function deleteSavedQuery(id: string): Promise<void> {
FILE: src/services/persistence/repositories/settingsRepository.ts
type SettingRecord (line 4) | interface SettingRecord {
function getSetting (line 11) | async function getSetting(
function setSetting (line 33) | async function setSetting(
function getSettingsByCategory (line 50) | async function getSettingsByCategory(
function deleteSetting (line 77) | async function deleteSetting(
FILE: src/services/persistence/repositories/workspaceRepository.ts
type WorkspaceState (line 4) | interface WorkspaceState {
function saveWorkspace (line 13) | async function saveWorkspace(
function loadWorkspace (line 42) | async function loadWorkspace(profileId: string): Promise<WorkspaceState ...
FILE: src/services/persistence/systemDb.ts
function sqlEscape (line 15) | function sqlEscape(value: string): string {
function sqlQuote (line 20) | function sqlQuote(value: string): string {
function sqlIdentifier (line 25) | function sqlIdentifier(name: string): string {
function isOpfsAvailable (line 37) | async function isOpfsAvailable(): Promise<boolean> {
function initializeSystemDb (line 54) | function initializeSystemDb(): Promise<void> {
function doInitialize (line 61) | async function doInitialize(): Promise<void> {
function getSystemConnection (line 70) | function getSystemConnection(): AsyncDuckDBConnection {
function isUsingOpfs (line 78) | function isUsingOpfs(): boolean {
function isSystemDbInitialized (line 85) | function isSystemDbInitialized(): boolean {
function closeSystemDb (line 92) | async function closeSystemDb(): Promise<void> {
FILE: src/store/index.ts
function persistWorkspaceState (line 47) | async function persistWorkspaceState(state: DuckStoreState): Promise<voi...
function startAutoSave (line 117) | function startAutoSave(): void {
FILE: src/store/slices/duckdbSlice.ts
constant DEFAULT_DUCKDB_MEMORY_LIMIT_MB (line 7) | const DEFAULT_DUCKDB_MEMORY_LIMIT_MB = 4096;
FILE: src/store/slices/profileSlice.ts
constant VERIFY_TOKEN_PLAINTEXT (line 38) | const VERIFY_TOKEN_PLAINTEXT = "duck-ui-profile-verify";
function mapProfile (line 280) | function mapProfile(p: {
FILE: src/store/slices/tabSlice.ts
function parseNotebookCells (line 6) | function parseNotebookCells(tab: EditorTab): NotebookCell[] {
function serializeCells (line 15) | function serializeCells(cells: NotebookCell[]): string {
function createDefaultCell (line 21) | function createDefaultCell(type: "sql" | "markdown" = "sql"): NotebookCe...
function updateNotebookContent (line 25) | function updateNotebookContent(
FILE: src/store/types.ts
type Window (line 9) | interface Window {
type CurrentConnection (line 28) | interface CurrentConnection {
type ConnectionProvider (line 43) | interface ConnectionProvider {
type ConnectionList (line 58) | interface ConnectionList {
type ColumnInfo (line 66) | interface ColumnInfo {
type ColumnStats (line 72) | interface ColumnStats {
type TableInfo (line 87) | interface TableInfo {
type DatabaseInfo (line 96) | interface DatabaseInfo {
type QueryResult (line 105) | interface QueryResult {
type QueryHistoryItem (line 113) | interface QueryHistoryItem {
type QueryResultArtifact (line 120) | interface QueryResultArtifact {
type ExternalQueryResponse (line 127) | interface ExternalQueryResponse {
type AIProviderType (line 137) | type AIProviderType = "webllm" | "openai" | "anthropic" | "openai-compat...
type ProviderConfigs (line 139) | interface ProviderConfigs {
type DuckBrainMessage (line 145) | interface DuckBrainMessage {
type MountedFolderInfo (line 158) | interface MountedFolderInfo {
type EditorTabType (line 169) | type EditorTabType = "sql" | "notebook" | "home" | "brain" | "connection...
type NotebookCell (line 171) | interface NotebookCell {
type ChartType (line 180) | type ChartType =
type AggregationType (line 198) | type AggregationType = "sum" | "avg" | "count" | "min" | "max" | "none";
type SortOrder (line 199) | type SortOrder = "asc" | "desc" | "none";
type AxisScale (line 200) | type AxisScale = "linear" | "log";
type SeriesConfig (line 202) | interface SeriesConfig {
type AxisConfig (line 211) | interface AxisConfig {
type LegendConfig (line 221) | interface LegendConfig {
type AnnotationConfig (line 227) | interface AnnotationConfig {
type DataTransform (line 237) | interface DataTransform {
type ChartConfig (line 246) | interface ChartConfig {
type EditorTab (line 269) | interface EditorTab {
type DuckdbSlice (line 282) | interface DuckdbSlice {
type ConnectionSlice (line 298) | interface ConnectionSlice {
type QuerySlice (line 310) | interface QuerySlice {
type SchemaSlice (line 320) | interface SchemaSlice {
type TabSlice (line 339) | interface TabSlice {
type DuckBrainSlice (line 368) | interface DuckBrainSlice {
type FileSystemSlice (line 402) | interface FileSystemSlice {
type Profile (line 427) | interface Profile {
type ProfileSlice (line 436) | interface ProfileSlice {
type DuckStoreState (line 457) | type DuckStoreState = DuckdbSlice &
FILE: src/types/filesystem.d.ts
type FileSystemHandlePermissionDescriptor (line 6) | interface FileSystemHandlePermissionDescriptor {
type FileSystemHandle (line 10) | interface FileSystemHandle {
type FileSystemFileHandle (line 18) | interface FileSystemFileHandle extends FileSystemHandle {
type FileSystemDirectoryHandle (line 24) | interface FileSystemDirectoryHandle extends FileSystemHandle {
type FileSystemGetDirectoryOptions (line 41) | interface FileSystemGetDirectoryOptions {
type FileSystemGetFileOptions (line 45) | interface FileSystemGetFileOptions {
type FileSystemRemoveOptions (line 49) | interface FileSystemRemoveOptions {
type FileSystemCreateWritableOptions (line 53) | interface FileSystemCreateWritableOptions {
type FileSystemWritableFileStream (line 57) | interface FileSystemWritableFileStream extends WritableStream {
type FileSystemWriteChunkType (line 63) | type FileSystemWriteChunkType = ArrayBuffer | ArrayBufferView | Blob | s...
type WriteParams (line 65) | interface WriteParams {
type OpenFilePickerOptions (line 72) | interface OpenFilePickerOptions {
type SaveFilePickerOptions (line 79) | interface SaveFilePickerOptions {
type DirectoryPickerOptions (line 86) | interface DirectoryPickerOptions {
type FilePickerAcceptType (line 92) | interface FilePickerAcceptType {
type WellKnownDirectory (line 97) | type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music...
type Window (line 99) | interface Window {
Condensed preview — 173 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,002K chars).
[
{
"path": ".dockerignore",
"chars": 330,
"preview": "# Ignore node_modules (local dependencies)\nnode_modules\n\n# Ignore build artifacts\ndist\n\n# Ignore configuration files tha"
},
{
"path": ".github/ISSUE_TEMPLATE/bug.yml",
"chars": 1925,
"preview": "name: 🐞 Bug Report\ndescription: Found something that doesn't work as expected?\nbody:\n - type: dropdown\n id: Environm"
},
{
"path": ".github/ISSUE_TEMPLATE/feature.yml",
"chars": 819,
"preview": "name: 💡 Feature Request\ndescription: Tell us about something Duck-UI doesn't do yet, but should!\nbody:\n - type: textare"
},
{
"path": ".github/workflows/2-docker-build.yml",
"chars": 1764,
"preview": "# .github/workflows/2-docker-build.yml\nname: Build and Push Docker Image\n\non:\n push:\n branches:\n - main\n pat"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1426,
"preview": "name: CI\n\non:\n push:\n branches: [main]\n paths-ignore:\n - \"**.md\"\n - \"docs/**\"\n pull_request:\n branc"
},
{
"path": ".gitignore",
"chars": 330,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": ".husky/pre-commit",
"chars": 17,
"preview": "bunx lint-staged\n"
},
{
"path": ".prettierignore",
"chars": 85,
"preview": "dist\nnode_modules\nbun.lockb\n*.wasm\npublic\ndocs/.vitepress/cache\ndocs/.vitepress/dist\n"
},
{
"path": ".prettierrc",
"chars": 181,
"preview": "{\n \"semi\": true,\n \"singleQuote\": false,\n \"trailingComma\": \"es5\",\n \"tabWidth\": 2,\n \"printWidth\": 100,\n \"bracketSpac"
},
{
"path": "Dockerfile",
"chars": 1339,
"preview": "# Use an official Node runtime as a parent image with bun\nFROM oven/bun:1-alpine AS build\n\nARG DUCK_UI_BASEPATH=\"/\"\n\n# S"
},
{
"path": "LICENSE.md",
"chars": 2882,
"preview": "# License\n\n## DUCK UI - Apache License 2.0\n\nCopyright 2025 Caio Ricciuti \n\nLicensed under the Apache License, Version 2."
},
{
"path": "README.md",
"chars": 5819,
"preview": "# <img src=\"./public/logo.png\" alt=\"Duck-UI Logo\" title=\"Duck-UI Logo\" width=\"50\"> Duck-UI\n\nDuck-UI is a web-based inter"
},
{
"path": "components.json",
"chars": 442,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"default\",\n \"rsc\": false,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "docker-compose.yml",
"chars": 688,
"preview": "services:\n duck-ui:\n image: ghcr.io/caioricciuti/duck-ui:latest\n restart: always\n ports:\n - \"${DUCK_UI_PO"
},
{
"path": "eslint.config.js",
"chars": 883,
"preview": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reac"
},
{
"path": "index.html",
"chars": 360,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/png\" href=\"/logo"
},
{
"path": "inject-env.js",
"chars": 1366,
"preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst indexHtmlPath = path.join(__dirname, \"index.html\");\nlet i"
},
{
"path": "package.json",
"chars": 4088,
"preview": "{\n \"name\": \"duck-ui\",\n \"private\": true,\n \"version\": \"0.0.39\",\n \"release_date\": \"2026-04-13\",\n \"type\": \"module\",\n \""
},
{
"path": "public/databases/README.md",
"chars": 1635,
"preview": "# Embedded Databases\n\nThis directory allows you to embed DuckDB database files (`.db`) that will be automatically loaded"
},
{
"path": "public/databases/manifest.json",
"chars": 22,
"preview": "{\n \"databases\": []\n}\n"
},
{
"path": "serve.json",
"chars": 236,
"preview": "{\n \"headers\": [\n {\n \"source\": \"**/*\",\n \"headers\": [\n { \"key\": \"Cross-Origin-Opener-Policy\", \"value\""
},
{
"path": "src/components/charts/ChartVisualizationPro.tsx",
"chars": 30104,
"preview": "/**\n * Professional Chart Visualization Component\n * Features: Auto-chart, live preview, multi-series, customization, ex"
},
{
"path": "src/components/charts/UPlotChart.tsx",
"chars": 1430,
"preview": "import { useRef, useEffect, useCallback } from \"react\";\nimport uPlot from \"uplot\";\nimport \"uplot/dist/uPlot.min.css\";\n\ni"
},
{
"path": "src/components/charts/tooltipPlugin.ts",
"chars": 3907,
"preview": "import uPlot from \"uplot\";\nimport { formatNumberWithSuffix } from \"@/lib/chartUtils\";\n\nfunction escapeHtml(str: string):"
},
{
"path": "src/components/cloud/CloudBrowser.tsx",
"chars": 7761,
"preview": "/**\n * Cloud Browser Component\n * Displays cloud storage connections and allows browsing/importing files\n */\n\nimport { u"
},
{
"path": "src/components/cloud/CloudConnectionModal.tsx",
"chars": 14927,
"preview": "/**\n * Cloud Connection Modal\n * Form for adding/editing S3, GCS, and Azure cloud storage connections\n */\n\nimport { useS"
},
{
"path": "src/components/command-palette/CommandPalette.tsx",
"chars": 7674,
"preview": "import { useEffect, useState, useMemo } from \"react\";\nimport {\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n Comman"
},
{
"path": "src/components/common/FloatingActionButton.tsx",
"chars": 1258,
"preview": "import { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { LucideIcon } from \"lucide-re"
},
{
"path": "src/components/common/ImportOptionsPopover.tsx",
"chars": 4275,
"preview": "import React, { useState } from \"react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popove"
},
{
"path": "src/components/connection/ConnectionsModal.tsx",
"chars": 12180,
"preview": "// ConnectionManager.tsx\nimport React from \"react\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,"
},
{
"path": "src/components/duck-brain/DuckBrainCodeBlock.tsx",
"chars": 3218,
"preview": "import React from \"react\";\nimport { Copy, Play, FileInput, Check, Loader2 } from \"lucide-react\";\nimport { Button } from "
},
{
"path": "src/components/duck-brain/DuckBrainInput.tsx",
"chars": 8538,
"preview": "import React, { useState, useRef, useEffect, useCallback } from \"react\";\nimport { Send, Square, Loader2, Table2, Columns"
},
{
"path": "src/components/duck-brain/DuckBrainMessages.tsx",
"chars": 6625,
"preview": "import React, { useRef, useEffect } from \"react\";\nimport { User, Bot, Table2, Columns } from \"lucide-react\";\nimport { Sc"
},
{
"path": "src/components/duck-brain/DuckBrainPanel.tsx",
"chars": 12777,
"preview": "import React, { useCallback, useMemo } from \"react\";\nimport { Brain, X, Loader2, AlertCircle, Download, Trash2, RefreshC"
},
{
"path": "src/components/duck-brain/MarkdownContent.tsx",
"chars": 3568,
"preview": "import React from \"react\";\nimport ReactMarkdown, { Components } from \"react-markdown\";\nimport { cn } from \"@/lib/utils\";"
},
{
"path": "src/components/duck-brain/ResultsArtifact.tsx",
"chars": 6127,
"preview": "import React, { useState } from \"react\";\nimport { Loader2, AlertCircle, ChevronDown, ChevronUp, Table2, RotateCcw } from"
},
{
"path": "src/components/duck-brain/SchemaAutocomplete.tsx",
"chars": 5033,
"preview": "import React, { useEffect, useRef } from \"react\";\nimport { Table2, Columns3 } from \"lucide-react\";\nimport { cn } from \"@"
},
{
"path": "src/components/editor/SqlEditor.tsx",
"chars": 12536,
"preview": "import React, { useRef, useEffect, useState, useCallback } from \"react\";\nimport {\n Play,\n Loader2,\n Lightbulb,\n Comm"
},
{
"path": "src/components/editor/monacoConfig.ts",
"chars": 10229,
"preview": "// monacoConfig.ts\nimport * as monaco from \"monaco-editor\";\nimport { useDuckStore } from \"@/store\";\nimport { useMemo } f"
},
{
"path": "src/components/explorer/ColumnNode.tsx",
"chars": 10190,
"preview": "import React, { useState } from \"react\";\nimport { ChevronRight, ChevronDown, Hash, Type, Calendar, ToggleLeft } from \"lu"
},
{
"path": "src/components/explorer/DataExplorer.tsx",
"chars": 10911,
"preview": "import React, { useState, useCallback, lazy, Suspense } from \"react\";\nimport { useDuckStore } from \"@/store\";\nimport { C"
},
{
"path": "src/components/explorer/FileImporter.tsx",
"chars": 73041,
"preview": "import React, { useCallback, useState, useMemo, useRef, useEffect } from \"react\";\nimport { format } from \"date-fns\";\nimp"
},
{
"path": "src/components/explorer/TreeNode.tsx",
"chars": 10140,
"preview": "// TreeNode.tsx\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport {\n ChevronRight,\n Che"
},
{
"path": "src/components/folders/FolderBrowser.tsx",
"chars": 15344,
"preview": "import React, { useEffect, useState, useCallback } from \"react\";\nimport {\n FolderOpen,\n FolderPlus,\n ChevronRight,\n "
},
{
"path": "src/components/layout/ConnectionSwitcher.tsx",
"chars": 4625,
"preview": "import * as React from \"react\";\nimport { ChevronDown, Circle, Loader2, Cable } from \"lucide-react\";\nimport {\n DropdownM"
},
{
"path": "src/components/layout/MobileNavDrawer.tsx",
"chars": 4701,
"preview": "import { useState } from \"react\";\nimport { Menu, Github, BookText, Cable, Sun, Moon, Brain } from \"lucide-react\";\nimport"
},
{
"path": "src/components/layout/Sidebar.tsx",
"chars": 16563,
"preview": "/**\n * Sidebar Component\n * Minimalist icon-only sidebar with tooltips\n */\n\nimport { useState } from \"react\";\nimport {\n "
},
{
"path": "src/components/notebook/MarkdownRenderer.tsx",
"chars": 758,
"preview": "import { useMemo } from \"react\";\nimport { marked } from \"marked\";\nimport DOMPurify from \"dompurify\";\n\ninterface Markdown"
},
{
"path": "src/components/notebook/NotebookCell.tsx",
"chars": 13461,
"preview": "import React, { useRef, useEffect, useState, useCallback, useMemo } from \"react\";\nimport {\n Play,\n Loader2,\n Trash2,\n"
},
{
"path": "src/components/notebook/NotebookTab.tsx",
"chars": 3931,
"preview": "import { useState, useCallback, useRef, useMemo } from \"react\";\nimport { useDuckStore } from \"@/store\";\nimport type { No"
},
{
"path": "src/components/notebook/NotebookToolbar.tsx",
"chars": 1725,
"preview": "import { Play, Loader2, Plus, Code, Type } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport "
},
{
"path": "src/components/profile/PasswordDialog.tsx",
"chars": 3061,
"preview": "import { useState } from \"react\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeade"
},
{
"path": "src/components/profile/ProfileAvatar.tsx",
"chars": 936,
"preview": "import { useTheme } from \"@/components/theme/theme-provider\";\nimport { cn } from \"@/lib/utils\";\nimport Logo from \"/logo."
},
{
"path": "src/components/profile/ProfileEditor.tsx",
"chars": 5369,
"preview": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/u"
},
{
"path": "src/components/profile/ProfilePicker.tsx",
"chars": 5330,
"preview": "import { useState } from \"react\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { Lock, UserPlus, Loader2 } fro"
},
{
"path": "src/components/saved-queries/SaveQueryDialog.tsx",
"chars": 3369,
"preview": "import { useState, useEffect } from \"react\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n "
},
{
"path": "src/components/saved-queries/SavedQueriesPanel.tsx",
"chars": 7567,
"preview": "import { useState, useEffect, useRef } from \"react\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { MoreVertic"
},
{
"path": "src/components/table/CellValueViewer.tsx",
"chars": 5561,
"preview": "import React, { useState, useMemo } from \"react\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components"
},
{
"path": "src/components/table/ColumnStatsPanel.tsx",
"chars": 9400,
"preview": "import React, { useMemo } from \"react\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";"
},
{
"path": "src/components/table/DuckUItable.tsx",
"chars": 65277,
"preview": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\nimport {\n flexRender,\n getCoreRowModel,\n getFilt"
},
{
"path": "src/components/theme/mode-toggle.tsx",
"chars": 1159,
"preview": "import { Moon, Sun } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n D"
},
{
"path": "src/components/theme/theme-provider.tsx",
"chars": 1592,
"preview": "import { createContext, useContext, useEffect, useState } from \"react\";\n\ntype Theme = \"dark\" | \"light\" | \"system\";\n\ntype"
},
{
"path": "src/components/ui/accordion.tsx",
"chars": 1975,
"preview": "import * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } "
},
{
"path": "src/components/ui/alert-dialog.tsx",
"chars": 4350,
"preview": "import * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } fro"
},
{
"path": "src/components/ui/alert.tsx",
"chars": 1573,
"preview": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \""
},
{
"path": "src/components/ui/aspect-ratio.tsx",
"chars": 158,
"preview": "\"use client\";\n\nimport * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\";\n\nconst AspectRatio = AspectRatioPri"
},
{
"path": "src/components/ui/badge.tsx",
"chars": 1110,
"preview": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \""
},
{
"path": "src/components/ui/breadcrumb.tsx",
"chars": 2745,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } fro"
},
{
"path": "src/components/ui/button.tsx",
"chars": 1848,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
},
{
"path": "src/components/ui/card.tsx",
"chars": 1823,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<HTMLDivElement, React."
},
{
"path": "src/components/ui/checkbox.tsx",
"chars": 1067,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { C"
},
{
"path": "src/components/ui/collapsible.tsx",
"chars": 320,
"preview": "import * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\n\nconst Collapsible = CollapsiblePrimitive.Root;\n\nco"
},
{
"path": "src/components/ui/command.tsx",
"chars": 4898,
"preview": "import * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { Command as CommandPr"
},
{
"path": "src/components/ui/context-menu.tsx",
"chars": 7240,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimp"
},
{
"path": "src/components/ui/dialog.tsx",
"chars": 3799,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } f"
},
{
"path": "src/components/ui/drawer.tsx",
"chars": 2972,
"preview": "import * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/lib/utils\";\n\nco"
},
{
"path": "src/components/ui/dropdown-menu.tsx",
"chars": 7417,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\ni"
},
{
"path": "src/components/ui/form.tsx",
"chars": 4101,
"preview": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui"
},
{
"path": "src/components/ui/hover-card.tsx",
"chars": 1192,
"preview": "import * as React from \"react\";\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\";\n\nimport { cn } from \"@"
},
{
"path": "src/components/ui/input.tsx",
"chars": 797,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Input = React.forwardRef<HTMLInputElement, Rea"
},
{
"path": "src/components/ui/label.tsx",
"chars": 715,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, ty"
},
{
"path": "src/components/ui/menubar.tsx",
"chars": 7914,
"preview": "import * as React from \"react\";\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\";\nimport { Check, ChevronRigh"
},
{
"path": "src/components/ui/multi-select.tsx",
"chars": 4902,
"preview": "import * as React from \"react\";\nimport { X, Check, ChevronsUpDown } from \"lucide-react\";\nimport { Badge } from \"@/compon"
},
{
"path": "src/components/ui/navigation-menu.tsx",
"chars": 5027,
"preview": "import * as React from \"react\";\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\nimport { cva"
},
{
"path": "src/components/ui/pagination.tsx",
"chars": 2736,
"preview": "import * as React from \"react\";\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from \"lucide-react\";\n\nimport { cn }"
},
{
"path": "src/components/ui/popover.tsx",
"chars": 1238,
"preview": "import * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/lib/"
},
{
"path": "src/components/ui/progress.tsx",
"chars": 780,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\n\nimport { "
},
{
"path": "src/components/ui/radio-group.tsx",
"chars": 1446,
"preview": "import * as React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } fr"
},
{
"path": "src/components/ui/resizable.tsx",
"chars": 1714,
"preview": "\"use client\";\n\nimport { GripVertical } from \"lucide-react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\""
},
{
"path": "src/components/ui/scroll-area.tsx",
"chars": 1634,
"preview": "import * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from "
},
{
"path": "src/components/ui/select.tsx",
"chars": 5614,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check"
},
{
"path": "src/components/ui/separator.tsx",
"chars": 722,
"preview": "import * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/"
},
{
"path": "src/components/ui/sheet.tsx",
"chars": 4232,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva, t"
},
{
"path": "src/components/ui/skeleton.tsx",
"chars": 234,
"preview": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n "
},
{
"path": "src/components/ui/sonner.tsx",
"chars": 885,
"preview": "import { useTheme } from \"@/components/theme/theme-provider\";\nimport { Toaster as Sonner } from \"sonner\";\n\ntype ToasterP"
},
{
"path": "src/components/ui/switch.tsx",
"chars": 1160,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn "
},
{
"path": "src/components/ui/table.tsx",
"chars": 2721,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Table = React.forwardRef<HTMLTableElement, Rea"
},
{
"path": "src/components/ui/tabs.tsx",
"chars": 1894,
"preview": "import * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\""
},
{
"path": "src/components/ui/textarea.tsx",
"chars": 715,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Textarea = React.forwardRef<HTMLTextAreaElemen"
},
{
"path": "src/components/ui/tooltip.tsx",
"chars": 1169,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn"
},
{
"path": "src/components/workspace/BrainTab.tsx",
"chars": 25652,
"preview": "import { useState, useEffect } from \"react\";\nimport { useDuckStore, type AIProviderType } from \"@/store\";\nimport { Butto"
},
{
"path": "src/components/workspace/ConnectionsTab.tsx",
"chars": 12444,
"preview": "import { useState } from \"react\";\nimport { useDuckStore, ConnectionProvider } from \"@/store\";\nimport { generateUUID } fr"
},
{
"path": "src/components/workspace/ExplainPlanViewer.tsx",
"chars": 2266,
"preview": "import { useMemo } from \"react\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport {\n Sheet,\n SheetCont"
},
{
"path": "src/components/workspace/HomeTab.tsx",
"chars": 17659,
"preview": "import { useState, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardHeader,"
},
{
"path": "src/components/workspace/QueryHistory.tsx",
"chars": 6849,
"preview": "import React, { useState } from \"react\";\nimport { useDuckStore, QueryHistoryItem } from \"@/store\";\nimport { Button } fro"
},
{
"path": "src/components/workspace/SettingsTab.tsx",
"chars": 13691,
"preview": "import { useEffect, useState } from \"react\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/t"
},
{
"path": "src/components/workspace/SortableTab.tsx",
"chars": 3980,
"preview": "import React from \"react\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { useSortable } from \"@dnd-kit/sortable\";\nim"
},
{
"path": "src/components/workspace/SqlTab.tsx",
"chars": 6177,
"preview": "// src/components/workspace/SqlTab.tsx\nimport React from \"react\";\nimport { useDuckStore } from \"@/store\";\nimport SqlEdit"
},
{
"path": "src/components/workspace/WorkspaceTabs.tsx",
"chars": 8149,
"preview": "// src/components/workspace/WorkspaceTabs.tsx\nimport { useMemo, useEffect, lazy, Suspense } from \"react\";\nimport { Tabs,"
},
{
"path": "src/hooks/useQueryFromURL.ts",
"chars": 2552,
"preview": "import { useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { useDuckStore } from "
},
{
"path": "src/index.css",
"chars": 8275,
"preview": "@import \"tailwindcss\";\n@plugin \"@tailwindcss/typography\";\n\n:root {\n --background: oklch(1 0 0);\n --foreground: oklch(0"
},
{
"path": "src/lib/chartDataTransform.ts",
"chars": 9267,
"preview": "/**\n * Advanced data transformation utilities for charting\n * Handles aggregations, grouping, sorting, and filtering\n */"
},
{
"path": "src/lib/chartExport.ts",
"chars": 8443,
"preview": "/**\n * Chart export utilities for PNG, SVG formats\n * Uses html2canvas and SVG manipulation for high-quality exports\n */"
},
{
"path": "src/lib/chartUtils.ts",
"chars": 3200,
"preview": "/**\n * Format a number with thousand separators\n * @param value - The number to format\n * @returns Formatted string (e.g"
},
{
"path": "src/lib/cloudStorage/index.ts",
"chars": 12431,
"preview": "/**\n * Cloud Storage Service\n * Manages connections to S3, Google Cloud Storage, and Azure Blob Storage\n */\n\nimport { us"
},
{
"path": "src/lib/cloudStorage/testHttpfs.ts",
"chars": 6260,
"preview": "/**\n * HTTPFS Feasibility Test Utility\n * Run these tests in browser console to check what works in DuckDB-WASM\n *\n * Us"
},
{
"path": "src/lib/duckBrain/index.ts",
"chars": 692,
"preview": "// Duck Brain - Local AI Data Analyst for DuckUI\n// Powered by WebLLM (in-browser LLM inference)\n\nexport { duckBrainServ"
},
{
"path": "src/lib/duckBrain/models.config.ts",
"chars": 866,
"preview": "// Model configurations for Duck Brain\nexport interface ModelConfig {\n id: string;\n displayName: string;\n size: strin"
},
{
"path": "src/lib/duckBrain/prompts/text-to-sql.ts",
"chars": 4218,
"preview": "import type { ChatCompletionMessageParam } from \"@mlc-ai/web-llm\";\nimport type { DuckBrainMessage } from \"@/store\";\n\nexp"
},
{
"path": "src/lib/duckBrain/providers/anthropic.provider.ts",
"chars": 6431,
"preview": "import type { ChatCompletionMessageParam } from \"@mlc-ai/web-llm\";\nimport type {\n AIProvider,\n ProviderConfig,\n Strea"
},
{
"path": "src/lib/duckBrain/providers/index.ts",
"chars": 1510,
"preview": "export * from \"./types\";\nexport { OpenAIProvider } from \"./openai.provider\";\nexport { AnthropicProvider } from \"./anthro"
},
{
"path": "src/lib/duckBrain/providers/openai.provider.ts",
"chars": 6839,
"preview": "import type { ChatCompletionMessageParam } from \"@mlc-ai/web-llm\";\nimport type {\n AIProvider,\n ProviderConfig,\n Strea"
},
{
"path": "src/lib/duckBrain/providers/types.ts",
"chars": 2844,
"preview": "import type { ChatCompletionMessageParam } from \"@mlc-ai/web-llm\";\n\nexport type AIProviderType = \"webllm\" | \"openai\" | \""
},
{
"path": "src/lib/duckBrain/schemaFormatter.ts",
"chars": 2644,
"preview": "import type { DatabaseInfo } from \"@/store\";\n\nexport interface SchemaContext {\n formatted: string;\n tableCount: number"
},
{
"path": "src/lib/duckBrain/sqlParser.ts",
"chars": 5300,
"preview": "export interface ParsedSQLResult {\n sql: string | null;\n confidence: number;\n issues: string[];\n}\n\nconst SQL_KEYWORDS"
},
{
"path": "src/lib/duckBrain/webllm.service.ts",
"chars": 7903,
"preview": "import {\n CreateWebWorkerMLCEngine,\n WebWorkerMLCEngine,\n InitProgressReport,\n ChatCompletionMessageParam,\n} from \"@"
},
{
"path": "src/lib/duckBrain/webllm.worker.ts",
"chars": 233,
"preview": "import { WebWorkerMLCEngineHandler } from \"@mlc-ai/web-llm\";\n\n// Create handler for WebLLM engine in Web Worker\nconst ha"
},
{
"path": "src/lib/fileSystem/index.ts",
"chars": 12525,
"preview": "/**\n * File System Access API Service\n * Provides persistent folder access across browser sessions\n */\nimport { generate"
},
{
"path": "src/lib/sqlSanitize.ts",
"chars": 364,
"preview": "/** Escape a string value for safe use in SQL single-quoted literals */\nexport function sqlEscapeString(value: string): "
},
{
"path": "src/lib/utils.ts",
"chars": 1985,
"preview": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: C"
},
{
"path": "src/main.tsx",
"chars": 11195,
"preview": "import \"./index.css\";\nimport { StrictMode, useEffect, useRef, useState } from \"react\";\nimport { createRoot } from \"react"
},
{
"path": "src/pages/Home.tsx",
"chars": 3161,
"preview": "import { useState } from \"react\";\nimport { Menu } from \"lucide-react\";\nimport DataExplorer from \"@/components/explorer/D"
},
{
"path": "src/services/duckdb/__tests__/resultParser.test.ts",
"chars": 4278,
"preview": "import { describe, it, expect } from \"vitest\";\nimport { rawResultToJSON } from \"../resultParser\";\n\ndescribe(\"rawResultTo"
},
{
"path": "src/services/duckdb/__tests__/utils.test.ts",
"chars": 3422,
"preview": "import { describe, it, expect, vi } from \"vitest\";\nimport { retryWithBackoff, updateHistory } from \"../utils\";\n\n// Mock "
},
{
"path": "src/services/duckdb/externalConnection.ts",
"chars": 6937,
"preview": "import { rawResultToJSON } from \"./resultParser\";\nimport { sqlEscapeIdentifier, sqlEscapeString } from \"@/lib/sqlSanitiz"
},
{
"path": "src/services/duckdb/index.ts",
"chars": 617,
"preview": "// DuckDB Service Layer\n// Extracted from the monolithic store for testability and modularity.\n\nexport { rawResultToJSON"
},
{
"path": "src/services/duckdb/opfsConnection.ts",
"chars": 5213,
"preview": "import * as duckdb from \"@duckdb/duckdb-wasm\";\nimport { retryWithBackoff, validateConnection } from \"./utils\";\nimport { "
},
{
"path": "src/services/duckdb/resultParser.ts",
"chars": 8500,
"preview": "import type { QueryResult, ExternalQueryResponse } from \"@/store/types\";\n\n/**\n * Converts a raw result (from an external"
},
{
"path": "src/services/duckdb/schemaFetcher.ts",
"chars": 2112,
"preview": "import * as duckdb from \"@duckdb/duckdb-wasm\";\nimport { sqlEscapeIdentifier, sqlEscapeString } from \"@/lib/sqlSanitize\";"
},
{
"path": "src/services/duckdb/utils.ts",
"chars": 1708,
"preview": "import * as duckdb from \"@duckdb/duckdb-wasm\";\nimport { generateUUID } from \"@/lib/utils\";\nimport type { QueryHistoryIte"
},
{
"path": "src/services/duckdb/wasmConnection.ts",
"chars": 6716,
"preview": "import * as duckdb from \"@duckdb/duckdb-wasm\";\nimport { sqlEscapeString, sqlEscapeIdentifier } from \"@/lib/sqlSanitize\";"
},
{
"path": "src/services/persistence/__tests__/crypto.test.ts",
"chars": 6775,
"preview": "import { describe, it, expect } from \"vitest\";\nimport {\n generateEncryptionKey,\n generateSalt,\n deriveKeyFromPassword"
},
{
"path": "src/services/persistence/__tests__/migrations.test.ts",
"chars": 5139,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { describe, it, expect, vi } from \"vitest\";\nimport { runM"
},
{
"path": "src/services/persistence/crypto.ts",
"chars": 6411,
"preview": "/**\n * Web Crypto API helpers for encrypting sensitive data at rest.\n * Uses AES-256-GCM for encryption and PBKDF2 for p"
},
{
"path": "src/services/persistence/fallback.ts",
"chars": 3686,
"preview": "/**\n * IndexedDB fallback for browsers without OPFS support (e.g., Firefox).\n * Implements the same data access patterns"
},
{
"path": "src/services/persistence/index.ts",
"chars": 440,
"preview": "export {\n initializeSystemDb,\n getSystemConnection,\n closeSystemDb,\n isOpfsAvailable,\n isUsingOpfs,\n isSystemDbIni"
},
{
"path": "src/services/persistence/migrations.ts",
"chars": 4088,
"preview": "/**\n * Schema migration runner for the system database.\n * Tracks applied versions in `schema_version` table and runs pe"
},
{
"path": "src/services/persistence/repositories/aiConfigRepository.ts",
"chars": 5395,
"preview": "import { generateUUID } from \"@/lib/utils\";\nimport { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nim"
},
{
"path": "src/services/persistence/repositories/connectionRepository.ts",
"chars": 4089,
"preview": "import { generateUUID } from \"@/lib/utils\";\nimport { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nim"
},
{
"path": "src/services/persistence/repositories/index.ts",
"chars": 274,
"preview": "export * from \"./profileRepository\";\nexport * from \"./settingsRepository\";\nexport * from \"./connectionRepository\";\nexpor"
},
{
"path": "src/services/persistence/repositories/profileRepository.ts",
"chars": 5812,
"preview": "import { generateUUID } from \"@/lib/utils\";\nimport { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nim"
},
{
"path": "src/services/persistence/repositories/queryHistoryRepository.ts",
"chars": 3793,
"preview": "import { generateUUID } from \"@/lib/utils\";\nimport { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nim"
},
{
"path": "src/services/persistence/repositories/savedQueryRepository.ts",
"chars": 4297,
"preview": "import { generateUUID } from \"@/lib/utils\";\nimport { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nim"
},
{
"path": "src/services/persistence/repositories/settingsRepository.ts",
"chars": 2709,
"preview": "import { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nimport { fallbackPut, fallbackGet, fallbackGet"
},
{
"path": "src/services/persistence/repositories/workspaceRepository.ts",
"chars": 2259,
"preview": "import { isUsingOpfs, getSystemConnection, sqlQuote } from \"../systemDb\";\nimport { fallbackPut, fallbackGet } from \"../f"
},
{
"path": "src/services/persistence/systemDb.ts",
"chars": 2745,
"preview": "/**\n * System Database Service\n *\n * Manages persistence for profiles, settings, connections, query history,\n * and othe"
},
{
"path": "src/store/index.ts",
"chars": 5035,
"preview": "import { create, type StateCreator } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { createDuckd"
},
{
"path": "src/store/slices/connectionSlice.ts",
"chars": 8034,
"preview": "import type { StateCreator } from \"zustand\";\nimport { toast } from \"sonner\";\nimport {\n testExternalConnection,\n testOP"
},
{
"path": "src/store/slices/duckBrainSlice.ts",
"chars": 13552,
"preview": "import type { StateCreator } from \"zustand\";\nimport { toast } from \"sonner\";\nimport { generateUUID } from \"@/lib/utils\";"
},
{
"path": "src/store/slices/duckdbSlice.ts",
"chars": 4871,
"preview": "import type { StateCreator } from \"zustand\";\nimport { initializeWasmConnection } from \"@/services/duckdb\";\nimport type {"
},
{
"path": "src/store/slices/fileSystemSlice.ts",
"chars": 6245,
"preview": "import type { StateCreator } from \"zustand\";\nimport { toast } from \"sonner\";\nimport { cloudStorageService } from \"@/lib/"
},
{
"path": "src/store/slices/profileSlice.ts",
"chars": 9213,
"preview": "import { StateCreator } from \"zustand\";\nimport type { DuckStoreState, ProfileSlice, Profile } from \"../types\";\nimport {\n"
},
{
"path": "src/store/slices/querySlice.ts",
"chars": 4520,
"preview": "import type { StateCreator } from \"zustand\";\nimport {\n executeExternalQuery,\n resultToJSON,\n validateConnection,\n up"
},
{
"path": "src/store/slices/schemaSlice.ts",
"chars": 7323,
"preview": "import type { StateCreator } from \"zustand\";\nimport { toast } from \"sonner\";\nimport {\n executeExternalQuery,\n resultTo"
},
{
"path": "src/store/slices/tabSlice.ts",
"chars": 6656,
"preview": "import type { StateCreator } from \"zustand\";\nimport { toast } from \"sonner\";\nimport { generateUUID } from \"@/lib/utils\";"
},
{
"path": "src/store/types.ts",
"chars": 11920,
"preview": "import * as duckdb from \"@duckdb/duckdb-wasm\";\nimport type { CloudConnection, CloudSupportStatus } from \"@/lib/cloudStor"
},
{
"path": "src/types/filesystem.d.ts",
"chars": 3282,
"preview": "/**\n * File System Access API type declarations\n * These APIs are available in Chrome 86+ and Edge 86+\n */\n\ninterface Fi"
},
{
"path": "src/vite-env.d.ts",
"chars": 188,
"preview": "/// <reference types=\"vite/client\" />\n\ndeclare const __DUCK_UI_VERSION__: string;\ndeclare const __DUCK_UI_RELEASE_DATE__"
},
{
"path": "tsconfig.app.json",
"chars": 772,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n },\n\n \"tsBuildInfoFile\": \"./n"
},
{
"path": "tsconfig.json",
"chars": 213,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n },\n \"files\": [],\n \"refere"
},
{
"path": "tsconfig.node.json",
"chars": 593,
"preview": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n \"target\": \"ES2022\","
},
{
"path": "vite.config.ts",
"chars": 1733,
"preview": "import { defineConfig, loadEnv } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport path from 'path';\nimport "
},
{
"path": "vitest.config.ts",
"chars": 283,
"preview": "import { defineConfig } from \"vitest/config\";\nimport path from \"path\";\n\nexport default defineConfig({\n resolve: {\n a"
}
]
About this extraction
This page contains the full source code of the caioricciuti/duck-ui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 173 files (925.3 KB), approximately 226.1k tokens, and a symbol index with 409 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.