feat(ai-analysis): 添加详情页智能分析入口
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
This commit is contained in:
107
apps/web/components/analysis/AnalysisContextDrawer.tsx
Normal file
107
apps/web/components/analysis/AnalysisContextDrawer.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import type { AnalysisRequest, AnalysisResponse } from '@ftb/shared';
|
||||
import { Loader2, Send, X } from 'lucide-react';
|
||||
import { requestAnalysis } from '@/lib/analysis-api';
|
||||
import { AnalysisResultBlock } from './AnalysisResultBlock';
|
||||
|
||||
export function AnalysisContextDrawer({
|
||||
open,
|
||||
title,
|
||||
context,
|
||||
permissions,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
title: string;
|
||||
context: NonNullable<AnalysisRequest['context']>;
|
||||
permissions: string[];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [question, setQuestion] = useState('');
|
||||
const [response, setResponse] = useState<AnalysisResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
async function ask(prompt: string) {
|
||||
const value = prompt.trim();
|
||||
if (!value || loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await requestAnalysis({ question: value, context }, permissions);
|
||||
setResponse(result);
|
||||
setQuestion('');
|
||||
} catch {
|
||||
setResponse({
|
||||
ok: false,
|
||||
code: 'AI_UNAVAILABLE',
|
||||
message: '业务分析暂不可用,请稍后再试。',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
void ask(question);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex justify-end bg-black/40">
|
||||
<aside className="flex h-full w-full max-w-2xl flex-col border-l border-white/60 bg-[#f8fafc]/90 shadow-2xl backdrop-blur-xl">
|
||||
<header className="flex h-14 shrink-0 items-center justify-between border-b border-white/70 px-5">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[13px] font-semibold text-[#0f172a]">{title}</p>
|
||||
<p className="mt-0.5 text-[11px] text-[#64748b]">只读取当前上下文和已有权限数据,安全范围以后端为准。</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="ml-3 rounded-full p-2 text-[#64748b] transition-colors hover:bg-white hover:text-[#0f172a]"
|
||||
title="关闭"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-5">
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="rounded-[28px] border border-white/70 bg-white/80 p-3 shadow-[0_18px_60px_rgba(15,23,42,0.08)] backdrop-blur-xl"
|
||||
>
|
||||
<textarea
|
||||
value={question}
|
||||
onChange={(event) => setQuestion(event.target.value)}
|
||||
rows={3}
|
||||
className="w-full resize-none bg-transparent px-2 py-2 text-[14px] leading-6 text-[#0f172a] outline-none placeholder:text-[#94a3b8]"
|
||||
placeholder="例如:这个版本风险怎么样?需求完成趋势怎么样?"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3 px-1">
|
||||
<span className="text-[11px] text-[#64748b]">不会创建、修改业务数据,也不会触发状态流转。</span>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !question.trim()}
|
||||
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-full bg-[#0f172a] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#1e293b] disabled:cursor-not-allowed disabled:bg-[#cbd5e1]"
|
||||
>
|
||||
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />}
|
||||
{loading ? '分析中' : '开始分析'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{response ? (
|
||||
<AnalysisResultBlock response={response} onAsk={(prompt) => void ask(prompt)} />
|
||||
) : (
|
||||
<div className="rounded-[28px] border border-dashed border-[#dbe3ef] bg-white/55 p-6 text-[13px] leading-7 text-[#64748b]">
|
||||
可以围绕当前页面提问,例如进度、风险、需求分布、成员负载或质量情况。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
apps/web/components/analysis/AnalysisEntryButton.tsx
Normal file
25
apps/web/components/analysis/AnalysisEntryButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { BarChart3, Sparkles } from 'lucide-react';
|
||||
|
||||
export function AnalysisEntryButton({
|
||||
onClick,
|
||||
compact = false,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const Icon = compact ? BarChart3 : Sparkles;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-full border border-[#dbe3ef] bg-white/75 px-3 text-[12px] font-medium text-[#334155] shadow-sm backdrop-blur transition-colors hover:bg-white"
|
||||
title="智能分析"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
智能分析
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user