Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
108 lines
4.1 KiB
TypeScript
108 lines
4.1 KiB
TypeScript
'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>
|
||
);
|
||
}
|