Files
ftb-project-management/apps/web/components/analysis/AnalysisContextDrawer.tsx
1e6fb0c7aa
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
feat(ai-analysis): 添加详情页智能分析入口
2026-07-09 09:53:34 +08:00

108 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}