doc: README add kb

This commit is contained in:
qixinbo
2026-03-29 21:04:56 +08:00
parent 5869c377a3
commit ae6340d429
10 changed files with 120 additions and 59 deletions
+12 -7
View File
@@ -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 />
+12 -7
View File
@@ -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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.
Binary file not shown.
+14 -44
View File
@@ -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
+82 -1
View File
@@ -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>