feat(asset-ai): add contextual analysis and unmet-demand feedback
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchAuthenticated } from "../../../auth/api-client";
|
||||
import {
|
||||
GAP_REASONS,
|
||||
GAP_STATUSES,
|
||||
type DemandGap,
|
||||
type GapStatus,
|
||||
} from "../../../shared/assets/assistant";
|
||||
|
||||
export function DemandGaps({ close }: { close: () => void }) {
|
||||
const [items, setItems] = useState<DemandGap[]>([]),
|
||||
[all, setAll] = useState(false),
|
||||
[canManage, setCanManage] = useState(false);
|
||||
const [error, setError] = useState(""),
|
||||
[loading, setLoading] = useState(true),
|
||||
[revision, setRevision] = useState(0);
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
void fetchAuthenticated(
|
||||
`/api/asset-assistant/gaps?scope=${all ? "all" : "mine"}`,
|
||||
{ signal: controller.signal },
|
||||
)
|
||||
.then(async (r) => {
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error(data.error);
|
||||
return data;
|
||||
})
|
||||
.then((data) => {
|
||||
setItems(data.items);
|
||||
setCanManage(data.canManage);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!controller.signal.aborted) setError(e.message || "读取失败。");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [all, revision]);
|
||||
const update = async (id: number, status: GapStatus) => {
|
||||
setError("");
|
||||
try {
|
||||
const r = await fetchAuthenticated(`/api/asset-assistant/gaps/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
if (!r.ok) throw new Error("状态保存失败。");
|
||||
setRevision((x) => x + 1);
|
||||
} catch {
|
||||
setError("状态保存失败,请重试。");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<section className="aq-gap-panel" aria-label="待完善需求">
|
||||
<div className="aq-card-top">
|
||||
<div>
|
||||
<h3>待完善需求</h3>
|
||||
<p>保留90天 · 记录未完成的问题与反馈,用于完善分析能力</p>
|
||||
</div>
|
||||
<button onClick={close}>返回问数</button>
|
||||
</div>
|
||||
<div className="aq-result-toolbar">
|
||||
{canManage && (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={all}
|
||||
onChange={(e) => setAll(e.target.checked)}
|
||||
/>{" "}
|
||||
查看全部用户需求
|
||||
</label>
|
||||
)}
|
||||
<button onClick={() => setRevision((x) => x + 1)} disabled={loading}>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<p role="alert" className="aq-error">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{loading ? (
|
||||
<p role="status">正在读取需求记录…</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{items.length}项需求 · 累计
|
||||
{items.reduce((n, v) => n + v.occurrences, 0)}次(最近200项)
|
||||
</p>
|
||||
{items.length === 0 && !error && (
|
||||
<p className="aq-no-data">暂无未满足需求</p>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<article key={item.id} className="aq-gap-item">
|
||||
<strong>{item.demand}</strong>
|
||||
<p>
|
||||
{GAP_REASONS[item.reason]} · {item.occurrences}次 · 最近{" "}
|
||||
{new Date(item.lastSeen).toLocaleString("zh-CN")}
|
||||
</p>
|
||||
{canManage ? (
|
||||
<label>
|
||||
处理状态{" "}
|
||||
<select
|
||||
value={item.status}
|
||||
onChange={(e) =>
|
||||
void update(item.id, e.target.value as GapStatus)
|
||||
}
|
||||
>
|
||||
{Object.entries(GAP_STATUSES).map(([id, label]) => (
|
||||
<option key={id} value={id}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : (
|
||||
<span>{GAP_STATUSES[item.status]}</span>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user