doc: README add kb
@@ -16,6 +16,7 @@
|
||||
## 🌟 核心特性
|
||||
|
||||
- **🗣️ 自然语言转 SQL**: 用大白话提问!它能理解你的数据表结构,生成准确的 SQL,甚至在报错时进行自我纠正 (Self-correction)。
|
||||
- **📚 智能知识库检索 (RAG)**: 支持上传 Word、PPT、PDF 等多种格式文档,通过向量检索增强回答,让你的私有文档“开口说话”。
|
||||
- **📈 即时数据可视化**: 拒绝枯燥的生肉表格,根据数据特征自动生成交互式图表。
|
||||
- **🗂️ 动态多数据源**: 无缝连接 PostgreSQL、Supabase,以及本地 CSV/Excel 文件上传解析。
|
||||
- **🧠 灵活的模型接入**: 原生集成 LiteLLM,支持随插随用 OpenAI、DeepSeek、智谱、通义千问 (DashScope)、火山引擎或任何兼容的 LLM 提供商。
|
||||
@@ -27,17 +28,21 @@
|
||||
|
||||
## 📸 界面预览
|
||||
|
||||
<div align="center">
|
||||
<h3>对话式分析界面</h3>
|
||||
<img src="./examples/index.png" width="80%" />
|
||||
<div align="left">
|
||||
<h3>💬 对话式分析界面</h3>
|
||||
<img src="./docs/index.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>可定制仪表盘</h3>
|
||||
<img src="./examples/dashboard.png" width="80%" />
|
||||
<h3>📊 可定制仪表盘</h3>
|
||||
<img src="./docs/dashboard.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>智能产物预览 (Artifact)</h3>
|
||||
<img src="./examples/artifact.png" width="80%" />
|
||||
<h3>📚 智能知识库问答</h3>
|
||||
<img src="./docs/kb.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📦 智能产物预览 (Artifact)</h3>
|
||||
<img src="./docs/artifact.png" width="80%" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -16,6 +16,7 @@ Whether you're querying a massive Supabase/PostgreSQL database or just tossing i
|
||||
## 🌟 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.
|
||||
- **📚 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.
|
||||
- **🗂️ 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.
|
||||
@@ -27,17 +28,21 @@ Whether you're querying a massive Supabase/PostgreSQL database or just tossing i
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<div align="center">
|
||||
<h3>Chat Interface</h3>
|
||||
<img src="./examples/index.png" width="80%" />
|
||||
<div align="left">
|
||||
<h3>💬 Chat Interface</h3>
|
||||
<img src="./docs/index.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>Customizable Dashboard</h3>
|
||||
<img src="./examples/dashboard.png" width="80%" />
|
||||
<h3>📊 Customizable Dashboard</h3>
|
||||
<img src="./docs/dashboard.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>Artifact Preview</h3>
|
||||
<img src="./examples/artifact.png" width="80%" />
|
||||
<h3>📚 Smart Knowledge Base</h3>
|
||||
<img src="./docs/kb.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📦 Artifact Preview</h3>
|
||||
<img src="./docs/artifact.png" width="80%" />
|
||||
</div>
|
||||
|
||||
<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
|
||||
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||
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
|
||||
const [availableDataSources, setAvailableDataSources] = useState<{id: string, name: string}[]>([]);
|
||||
@@ -1120,49 +1133,6 @@ export function ChatInterface() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Hidden file input available in all states */}
|
||||
<input
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { api } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -14,17 +17,51 @@ import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
const { projects, currentProject, fetchProjects, setCurrentProject, addProject } = useProjectStore();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Model Selection State
|
||||
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||
const [modelOpen, setModelOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
if (!newProjectName.trim()) return;
|
||||
setIsSubmitting(true);
|
||||
@@ -88,6 +125,50 @@ export function ProjectSwitcher() {
|
||||
</DropdownMenuContent>
|
||||
</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}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
||||