feat:前端修改(我认为ok)
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronRight, Database, Send, Sparkles } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Nl2SqlConsole } from "@/components/admin/sections/nl2sql-console";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
ErrorState,
|
||||
LoadingBlock,
|
||||
Select,
|
||||
cn,
|
||||
useToast,
|
||||
} from "@/components/admin/ui";
|
||||
import { errMessage } from "@/lib/admin-api";
|
||||
import { nl2sqlApi, type Nl2SqlQueryResult } from "@/lib/nl2sql-api";
|
||||
|
||||
/** 示例问题,避免员工面对空白输入框不知道怎么问。 */
|
||||
const SAMPLES = [
|
||||
"近一个月成交额排名前十的客户有哪些?",
|
||||
"统计各风险等级客户的数量",
|
||||
"查询本月新增签约客户名单",
|
||||
];
|
||||
|
||||
/**
|
||||
* NL2SQL 功能入口。
|
||||
* 主界面只做「自然语言问数」这一件事;权限与运维面板默认折叠在下方,
|
||||
* 需要排查问题时再展开,避免员工被配置项淹没。
|
||||
*/
|
||||
export function Nl2SqlSection() {
|
||||
const toast = useToast();
|
||||
const [question, setQuestion] = useState("");
|
||||
const [maxRows, setMaxRows] = useState(200);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<Nl2SqlQueryResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showConsole, setShowConsole] = useState(false);
|
||||
|
||||
const run = async (text: string) => {
|
||||
const value = text.trim();
|
||||
if (!value || loading) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
try {
|
||||
const data = await nl2sqlApi.query({
|
||||
question: value,
|
||||
max_rows: maxRows,
|
||||
});
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setError(errMessage(err, "问数失败,请改写问题后重试"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = result?.columns ?? [];
|
||||
const rows = result?.rows ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<Card
|
||||
title="智能问数"
|
||||
description="用自然语言提问,系统自动生成 SQL 并返回结果(受当前账号的数据权限约束)"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<textarea
|
||||
value={question}
|
||||
onChange={(event) => setQuestion(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
void run(question);
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
placeholder="例如:近一个月成交额排名前十的客户有哪些?"
|
||||
className="w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm leading-6 text-slate-700 focus:border-indigo-400 focus:outline-none"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{SAMPLES.map((sample) => (
|
||||
<button
|
||||
key={sample}
|
||||
onClick={() => setQuestion(sample)}
|
||||
className="rounded-full border border-slate-200 px-3 py-1 text-xs text-slate-500 transition-colors hover:border-indigo-300 hover:text-indigo-600"
|
||||
>
|
||||
{sample}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">返回上限</span>
|
||||
<Select
|
||||
value={String(maxRows)}
|
||||
onChange={(event) => setMaxRows(Number(event.target.value))}
|
||||
className="w-24"
|
||||
>
|
||||
<option value="50">50</option>
|
||||
<option value="200">200</option>
|
||||
<option value="1000">1000</option>
|
||||
</Select>
|
||||
<Button variant="primary" loading={loading} onClick={() => void run(question)}>
|
||||
<Send className="h-4 w-4" />
|
||||
开始问数
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-400">快捷键 Ctrl / ⌘ + Enter 直接提交</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{loading && <LoadingBlock text="正在生成 SQL 并执行查询..." />}
|
||||
{error && <ErrorState message={error} onRetry={() => void run(question)} />}
|
||||
|
||||
{result && !loading && (
|
||||
<Card
|
||||
title="查询结果"
|
||||
description={
|
||||
result.summary ??
|
||||
result.answer ??
|
||||
`共 ${result.row_count ?? rows.length} 行${result.truncated ? "(已截断)" : ""}`
|
||||
}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
{result.elapsed_ms != null && (
|
||||
<Badge tone="info">耗时 {result.elapsed_ms} ms</Badge>
|
||||
)}
|
||||
{result.query_id && (
|
||||
<span className="font-mono text-xs text-slate-400">{result.query_id}</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{rows.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-slate-400">查询未返回数据</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-lg border border-slate-200">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-slate-50 text-xs text-slate-500">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th key={column} className="whitespace-nowrap px-3 py-2 font-medium">
|
||||
{column}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{rows.map((row, index) => (
|
||||
<tr key={index} className="hover:bg-slate-50">
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={column}
|
||||
className="whitespace-nowrap px-3 py-2 text-slate-700"
|
||||
>
|
||||
{formatCell(row[column])}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl border border-slate-200 bg-white">
|
||||
<button
|
||||
onClick={() => setShowConsole(!showConsole)}
|
||||
className="flex w-full items-center gap-2 px-5 py-3.5 text-left transition-colors hover:bg-slate-50"
|
||||
>
|
||||
{showConsole ? (
|
||||
<ChevronDown className="h-4 w-4 text-slate-400" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-slate-400" />
|
||||
)}
|
||||
<Database className="h-4 w-4 text-slate-400" />
|
||||
<span className="text-sm font-semibold text-slate-800">权限与运维面板</span>
|
||||
<span className="ml-auto flex items-center gap-2 text-xs text-slate-400">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
只读展示 · 排查问题时展开
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showConsole && (
|
||||
<div className={cn("border-t border-slate-100 p-5")}>
|
||||
<Nl2SqlConsole />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatCell(value: unknown): string {
|
||||
if (value === null || value === undefined) return "-";
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
Reference in New Issue
Block a user