doc: README add kb
@@ -16,6 +16,7 @@
|
|||||||
## 🌟 核心特性
|
## 🌟 核心特性
|
||||||
|
|
||||||
- **🗣️ 自然语言转 SQL**: 用大白话提问!它能理解你的数据表结构,生成准确的 SQL,甚至在报错时进行自我纠正 (Self-correction)。
|
- **🗣️ 自然语言转 SQL**: 用大白话提问!它能理解你的数据表结构,生成准确的 SQL,甚至在报错时进行自我纠正 (Self-correction)。
|
||||||
|
- **📚 智能知识库检索 (RAG)**: 支持上传 Word、PPT、PDF 等多种格式文档,通过向量检索增强回答,让你的私有文档“开口说话”。
|
||||||
- **📈 即时数据可视化**: 拒绝枯燥的生肉表格,根据数据特征自动生成交互式图表。
|
- **📈 即时数据可视化**: 拒绝枯燥的生肉表格,根据数据特征自动生成交互式图表。
|
||||||
- **🗂️ 动态多数据源**: 无缝连接 PostgreSQL、Supabase,以及本地 CSV/Excel 文件上传解析。
|
- **🗂️ 动态多数据源**: 无缝连接 PostgreSQL、Supabase,以及本地 CSV/Excel 文件上传解析。
|
||||||
- **🧠 灵活的模型接入**: 原生集成 LiteLLM,支持随插随用 OpenAI、DeepSeek、智谱、通义千问 (DashScope)、火山引擎或任何兼容的 LLM 提供商。
|
- **🧠 灵活的模型接入**: 原生集成 LiteLLM,支持随插随用 OpenAI、DeepSeek、智谱、通义千问 (DashScope)、火山引擎或任何兼容的 LLM 提供商。
|
||||||
@@ -27,17 +28,21 @@
|
|||||||
|
|
||||||
## 📸 界面预览
|
## 📸 界面预览
|
||||||
|
|
||||||
<div align="center">
|
<div align="left">
|
||||||
<h3>对话式分析界面</h3>
|
<h3>💬 对话式分析界面</h3>
|
||||||
<img src="./examples/index.png" width="80%" />
|
<img src="./docs/index.png" width="80%" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<h3>可定制仪表盘</h3>
|
<h3>📊 可定制仪表盘</h3>
|
||||||
<img src="./examples/dashboard.png" width="80%" />
|
<img src="./docs/dashboard.png" width="80%" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<h3>智能产物预览 (Artifact)</h3>
|
<h3>📚 智能知识库问答</h3>
|
||||||
<img src="./examples/artifact.png" width="80%" />
|
<img src="./docs/kb.png" width="80%" />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<h3>📦 智能产物预览 (Artifact)</h3>
|
||||||
|
<img src="./docs/artifact.png" width="80%" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Whether you're querying a massive Supabase/PostgreSQL database or just tossing i
|
|||||||
## 🌟 Key Features
|
## 🌟 Key Features
|
||||||
|
|
||||||
- **🗣️ Chat to SQL**: Ask questions in plain English (or Chinese!). DataClaw understands your schema, generates accurate SQL, and self-corrects if things go sideways.
|
- **🗣️ Chat to SQL**: Ask questions in plain English (or Chinese!). DataClaw understands your schema, generates accurate SQL, and self-corrects if things go sideways.
|
||||||
|
- **📚 Smart Knowledge Base (RAG)**: Support uploading Word, PPT, PDF and other document formats. Enhance answers through vector retrieval, making your private documents "speak".
|
||||||
- **📈 Instant Visualizations**: Returns not just raw tables, but auto-generated interactive charts tailored to your data's shape.
|
- **📈 Instant Visualizations**: Returns not just raw tables, but auto-generated interactive charts tailored to your data's shape.
|
||||||
- **🗂️ Multi-Source Ready**: Connects seamlessly to PostgreSQL, Supabase, and local CSV/Excel uploads.
|
- **🗂️ Multi-Source Ready**: Connects seamlessly to PostgreSQL, Supabase, and local CSV/Excel uploads.
|
||||||
- **🧠 Bring Your Own LLM**: Native integration with LiteLLM. Plug in OpenAI, DeepSeek, Zhipu, DashScope, Volcengine, or any compatible provider.
|
- **🧠 Bring Your Own LLM**: Native integration with LiteLLM. Plug in OpenAI, DeepSeek, Zhipu, DashScope, Volcengine, or any compatible provider.
|
||||||
@@ -27,17 +28,21 @@ Whether you're querying a massive Supabase/PostgreSQL database or just tossing i
|
|||||||
|
|
||||||
## 📸 Screenshots
|
## 📸 Screenshots
|
||||||
|
|
||||||
<div align="center">
|
<div align="left">
|
||||||
<h3>Chat Interface</h3>
|
<h3>💬 Chat Interface</h3>
|
||||||
<img src="./examples/index.png" width="80%" />
|
<img src="./docs/index.png" width="80%" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<h3>Customizable Dashboard</h3>
|
<h3>📊 Customizable Dashboard</h3>
|
||||||
<img src="./examples/dashboard.png" width="80%" />
|
<img src="./docs/dashboard.png" width="80%" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<h3>Artifact Preview</h3>
|
<h3>📚 Smart Knowledge Base</h3>
|
||||||
<img src="./examples/artifact.png" width="80%" />
|
<img src="./docs/kb.png" width="80%" />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<h3>📦 Artifact Preview</h3>
|
||||||
|
<img src="./docs/artifact.png" width="80%" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 398 KiB After Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 467 KiB |
@@ -328,7 +328,20 @@ export function ChatInterface() {
|
|||||||
// Model selection state
|
// Model selection state
|
||||||
const [models, setModels] = useState<ModelConfig[]>([]);
|
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||||
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||||
const [modelOpen, setModelOpen] = useState(false);
|
|
||||||
|
// Listen for model changes from the ProjectSwitcher
|
||||||
|
useEffect(() => {
|
||||||
|
const handleModelChange = (e: Event) => {
|
||||||
|
const customEvent = e as CustomEvent<string>;
|
||||||
|
if (customEvent.detail) {
|
||||||
|
setSelectedModelId(customEvent.detail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('nanobot:model-changed', handleModelChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('nanobot:model-changed', handleModelChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Data Source selection state
|
// Data Source selection state
|
||||||
const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]);
|
const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]);
|
||||||
@@ -1120,49 +1133,6 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-background relative">
|
<div className="flex flex-col h-full bg-background relative">
|
||||||
{/* Header with Model Selection */}
|
|
||||||
<div className="px-4 py-3 flex items-center justify-between border-b border-border bg-background/50 backdrop-blur-md sticky top-0 z-20">
|
|
||||||
<Popover open={modelOpen} onOpenChange={setModelOpen}>
|
|
||||||
<PopoverTrigger className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-muted transition-colors group">
|
|
||||||
<span className="font-semibold text-foreground">
|
|
||||||
{selectedModelId ? models.find(m => m.id === selectedModelId)?.name || 'DataClaw' : 'DataClaw'}
|
|
||||||
</span>
|
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground group-hover:text-muted-foreground transition-colors" />
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[280px] p-0" align="start">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder={t('searchModel')} />
|
|
||||||
<CommandList className="max-h-[300px]">
|
|
||||||
<CommandEmpty>{t('modelNotFound')}</CommandEmpty>
|
|
||||||
<CommandGroup heading={t('availableModels')}>
|
|
||||||
{models.map((model) => (
|
|
||||||
<CommandItem
|
|
||||||
key={model.id}
|
|
||||||
onSelect={() => {
|
|
||||||
setSelectedModelId(model.id);
|
|
||||||
setModelOpen(false);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 py-2.5 cursor-pointer"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium text-foreground">{model.name || model.model}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{model.provider}</span>
|
|
||||||
</div>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"ml-auto h-4 w-4",
|
|
||||||
selectedModelId === model.id ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
{/* Hidden file input available in all states */}
|
{/* Hidden file input available in all states */}
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ChevronDown, Plus, Folder } from 'lucide-react';
|
import { ChevronDown, Plus, Folder, Check } from 'lucide-react';
|
||||||
import { useProjectStore } from '@/store/projectStore';
|
import { useProjectStore } from '@/store/projectStore';
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -14,17 +17,51 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||||
|
|
||||||
|
interface ModelConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
model: string;
|
||||||
|
provider: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function ProjectSwitcher() {
|
export function ProjectSwitcher() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { projects, currentProject, fetchProjects, setCurrentProject, addProject } = useProjectStore();
|
const { projects, currentProject, fetchProjects, setCurrentProject, addProject } = useProjectStore();
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [newProjectName, setNewProjectName] = useState('');
|
const [newProjectName, setNewProjectName] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Model Selection State
|
||||||
|
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||||
|
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||||
|
const [modelOpen, setModelOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProjects();
|
fetchProjects();
|
||||||
}, [fetchProjects]);
|
}, [fetchProjects]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchModels = async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.get<ModelConfig[]>("/api/v1/llm");
|
||||||
|
setModels(data);
|
||||||
|
const active = data.find(m => m.is_active);
|
||||||
|
if (active) {
|
||||||
|
setSelectedModelId(active.id);
|
||||||
|
} else if (data.length > 0) {
|
||||||
|
setSelectedModelId(data[0].id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch models", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchModels();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCreateProject = async () => {
|
const handleCreateProject = async () => {
|
||||||
if (!newProjectName.trim()) return;
|
if (!newProjectName.trim()) return;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@@ -88,6 +125,50 @@ export function ProjectSwitcher() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<div className="h-4 w-px bg-border mx-1" />
|
||||||
|
|
||||||
|
<Popover open={modelOpen} onOpenChange={setModelOpen}>
|
||||||
|
<PopoverTrigger className="flex items-center gap-1 px-2 py-1 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors group">
|
||||||
|
<span className="font-semibold text-[14px]">
|
||||||
|
{selectedModelId ? (models.find(m => m.id === selectedModelId)?.name || models.find(m => m.id === selectedModelId)?.model || 'DataClaw') : 'DataClaw'}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50 group-hover:opacity-100 transition-colors" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[280px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={t('searchModel')} />
|
||||||
|
<CommandList className="max-h-[300px]">
|
||||||
|
<CommandEmpty>{t('modelNotFound')}</CommandEmpty>
|
||||||
|
<CommandGroup heading={t('availableModels')}>
|
||||||
|
{models.map((model) => (
|
||||||
|
<CommandItem
|
||||||
|
key={model.id}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedModelId(model.id);
|
||||||
|
setModelOpen(false);
|
||||||
|
// Fire custom event to notify ChatInterface if needed
|
||||||
|
window.dispatchEvent(new CustomEvent("nanobot:model-changed", { detail: model.id }));
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 py-2.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-foreground">{model.name || model.model}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{model.provider}</span>
|
||||||
|
</div>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
selectedModelId === model.id ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||